PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / pages / ViewCustomer.tsx

ViewCustomer.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.10, at resources/js/pages/ViewCustomer.tsx

882 lines 31.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * View Customer Page
3 * Display customer details with dynamic data from API
4 */
5
6 import React, { useMemo } from "react";
7 import { useQuery } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Mail,
11 Phone,
12 MapPin,
13 Calendar,
14 Users,
15 DollarSign,
16 FileText,
17 Edit,
18 Award,
19 Globe,
20 User,
21 AlertCircle,
22 } from "lucide-react";
23 import { __ } from "../lib/i18n";
24 import { apiService } from "../lib/api-client";
25 import { formatDate as formatDateUtil } from "../lib/dateFormat";
26 import { usePermissions } from "../hooks/usePermissions";
27 import { Button } from "../components/ui/button";
28 import { PageHeader } from "../components/common/PageHeader";
29 import {
30 Card,
31 CardContent,
32 CardHeader,
33 CardTitle,
34 } from "../components/ui/card";
35 import { ConditionalRender } from "../components/ui/conditional-render";
36 import { CustomerSummaryCard } from "../components/ai/CustomerSummaryCard";
37 import {
38 Table,
39 TableBody,
40 TableCell,
41 TableHead,
42 TableHeader,
43 TableRow,
44 } from "../components/ui/table";
45 import { Skeleton } from "../components/ui/skeleton";
46 import { formatYatraMoney } from "../lib/currency-display";
47
48 interface Customer {
49 id: number;
50 user_id: number;
51 first_name: string;
52 last_name: string;
53 name: string;
54 email: string;
55 phone: string;
56 secondary_phone?: string;
57 country: string;
58 city?: string;
59 state?: string;
60 address?: string;
61 postal_code?: string;
62 nationality?: string;
63 date_of_birth?: string;
64 gender?: string;
65 emergency_name?: string;
66 emergency_phone?: string;
67 emergency_relationship?: string;
68 dietary_requirements?: string;
69 medical_conditions?: string;
70 special_needs?: string;
71 newsletter_optin: boolean;
72 marketing_optin: boolean;
73 total_bookings: number;
74 total_spent: number;
75 total_travelers: number;
76 loyalty_tier: string;
77 loyalty_points: number;
78 status: string;
79 notes?: string;
80 created_at: string;
81 last_booking_date?: string;
82 last_travel_date?: string;
83 }
84
85 interface Booking {
86 id: number;
87 reference: string;
88 trip_id: number;
89 trip_title: string;
90 trip_slug?: string;
91 travel_date: string;
92 travelers_count: number;
93 total_amount: number;
94 currency: string;
95 status: string;
96 payment_status: string;
97 created_at: string;
98 }
99
100 const ViewCustomer: React.FC = () => {
101 const { can, isPro } = usePermissions();
102
103 const baseAdminUrl = (window as any).yatraAdmin?.adminUrl || "";
104
105 // Get customer id from URL
106 const customerId = useMemo(() => {
107 const params = new URLSearchParams(window.location.search);
108 return params.get("id") ? parseInt(params.get("id") || "0") : null;
109 }, []);
110
111 // Fetch customer data from API
112 const {
113 data: customer,
114 isLoading,
115 error,
116 } = useQuery<Customer>({
117 queryKey: ["customer", customerId],
118 queryFn: async () => {
119 if (!customerId) throw new Error("No customer ID");
120 const response = await apiService.getCustomer(customerId);
121 const data = (response as any)?.data ?? response;
122
123 // Normalize nested emergency_contact structure to flat fields expected by the UI
124 const emergency = (data as any).emergency_contact || {};
125
126 return {
127 ...data,
128 emergency_name: (data as any).emergency_name ?? emergency.name ?? null,
129 emergency_phone:
130 (data as any).emergency_phone ?? emergency.phone ?? null,
131 emergency_relationship:
132 (data as any).emergency_relationship ??
133 emergency.relationship ??
134 null,
135 } as Customer;
136 },
137 enabled: !!customerId && can("yatra_view_customers"),
138 });
139
140 // Fetch customer bookings
141 const { data: bookings, isLoading: isLoadingBookings } = useQuery<Booking[]>({
142 queryKey: ["customer-bookings", customerId],
143 queryFn: async () => {
144 if (!customerId) return [];
145 const json = await apiService.getCustomerBookings(customerId);
146 // API returns an object with data property; ensure we always return an array
147 if (Array.isArray(json)) {
148 return json;
149 }
150 if (json && Array.isArray(json.data)) {
151 return json.data;
152 }
153 return [];
154 },
155 enabled: !!customerId && can("yatra_view_bookings"),
156 });
157
158 const formatDate = (dateString: string | null | undefined) => {
159 return formatDateUtil(dateString);
160 };
161
162 const formatShortDate = (dateString: string | null | undefined) => {
163 return formatDateUtil(dateString);
164 };
165
166 const formatPrice = (price: number, currencyCode: string = "USD") =>
167 formatYatraMoney(Number(price) || 0, currencyCode, {
168 zeroAsUnknown: false,
169 });
170
171 const getStatusBadge = (status: string) => {
172 const statusMap: Record<string, { className: string; label: string }> = {
173 active: {
174 className:
175 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
176 label: __("Active", "yatra"),
177 },
178 inactive: {
179 className:
180 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
181 label: __("Inactive", "yatra"),
182 },
183 blocked: {
184 className:
185 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
186 label: __("Blocked", "yatra"),
187 },
188 };
189
190 const statusInfo = statusMap[status] || {
191 className:
192 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
193 label: status,
194 };
195
196 return (
197 <span
198 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
199 >
200 {statusInfo.label}
201 </span>
202 );
203 };
204
205 const getBookingStatusBadge = (status: string) => {
206 const statusMap: Record<string, { className: string; label: string }> = {
207 confirmed: {
208 className:
209 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
210 label: __("Confirmed", "yatra"),
211 },
212 completed: {
213 className:
214 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
215 label: __("Completed", "yatra"),
216 },
217 pending: {
218 className:
219 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
220 label: __("Pending", "yatra"),
221 },
222 cancelled: {
223 className:
224 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
225 label: __("Cancelled", "yatra"),
226 },
227 };
228
229 const statusInfo = statusMap[status] || {
230 className:
231 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
232 label: status,
233 };
234
235 return (
236 <span
237 className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${statusInfo.className}`}
238 >
239 {statusInfo.label}
240 </span>
241 );
242 };
243
244 const getLoyaltyBadge = (tier: string) => {
245 const tierMap: Record<string, { className: string; icon: string }> = {
246 bronze: {
247 className:
248 "bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400",
249 icon: "🥉",
250 },
251 silver: {
252 className:
253 "bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-300",
254 icon: "🥈",
255 },
256 gold: {
257 className:
258 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
259 icon: "🥇",
260 },
261 platinum: {
262 className:
263 "bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400",
264 icon: "💎",
265 },
266 };
267
268 const tierInfo = tierMap[tier] || tierMap["bronze"];
269
270 return (
271 <span
272 className={`inline-flex items-center gap-1 px-3 py-1 rounded-md text-sm font-medium ${tierInfo.className}`}
273 >
274 <span>{tierInfo.icon}</span>
275 <span className="capitalize">{tier}</span>
276 </span>
277 );
278 };
279
280 const handleBack = () => {
281 window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers`;
282 };
283
284 const handleEdit = () => {
285 window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers&action=edit&id=${customerId}`;
286 };
287
288 const handleViewBooking = (bookingId: number) => {
289 window.location.href = `${baseAdminUrl}?page=yatra&subpage=bookings&action=view&id=${bookingId}`;
290 };
291
292 // Skeleton loader
293 const renderSkeleton = () => (
294 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
295 <div className="lg:col-span-2 space-y-3">
296 <Card>
297 <CardHeader className="pb-2">
298 <div className="flex items-center justify-between">
299 <Skeleton className="h-5 w-40" />
300 <Skeleton className="h-6 w-16 rounded" />
301 </div>
302 </CardHeader>
303 <CardContent className="space-y-4">
304 <Skeleton className="h-6 w-48" />
305 <div className="grid grid-cols-2 gap-4">
306 <div className="space-y-2">
307 <Skeleton className="h-3 w-20" />
308 <Skeleton className="h-4 w-36" />
309 </div>
310 <div className="space-y-2">
311 <Skeleton className="h-3 w-20" />
312 <Skeleton className="h-4 w-28" />
313 </div>
314 </div>
315 <div className="space-y-2">
316 <Skeleton className="h-3 w-16" />
317 <Skeleton className="h-4 w-64" />
318 </div>
319 </CardContent>
320 </Card>
321 </div>
322 <div className="space-y-3">
323 <Card>
324 <CardHeader className="pb-2">
325 <Skeleton className="h-5 w-24" />
326 </CardHeader>
327 <CardContent className="space-y-3">
328 <div className="space-y-2">
329 <Skeleton className="h-3 w-20" />
330 <Skeleton className="h-8 w-12" />
331 </div>
332 <div className="space-y-2">
333 <Skeleton className="h-3 w-20" />
334 <Skeleton className="h-8 w-20" />
335 </div>
336 </CardContent>
337 </Card>
338 </div>
339 </div>
340 );
341
342 if (isLoading) {
343 return (
344 <div className="space-y-3">
345 <PageHeader
346 title={__("Customer Details", "yatra")}
347 description={__("Loading...", "yatra")}
348 actions={
349 <Button
350 variant="outline"
351 onClick={handleBack}
352 className="flex items-center gap-2"
353 >
354 <ArrowLeft className="w-4 h-4" />
355 {__("Back", "yatra")}
356 </Button>
357 }
358 />
359 {renderSkeleton()}
360 </div>
361 );
362 }
363
364 if (error || !customer) {
365 return (
366 <div className="space-y-3">
367 <PageHeader
368 title={__("Customer Not Found", "yatra")}
369 description={__(
370 "The customer you are looking for does not exist",
371 "yatra",
372 )}
373 actions={
374 <Button
375 variant="outline"
376 onClick={handleBack}
377 className="flex items-center gap-2"
378 >
379 <ArrowLeft className="w-4 h-4" />
380 {__("Back to Customers", "yatra")}
381 </Button>
382 }
383 />
384 <Card>
385 <CardContent className="p-8 text-center">
386 <AlertCircle className="w-12 h-12 mx-auto mb-3 text-red-400" />
387 <p className="text-red-500">
388 {__("Error loading customer or customer not found", "yatra")}
389 </p>
390 </CardContent>
391 </Card>
392 </div>
393 );
394 }
395
396 return (
397 <div className="space-y-3">
398 <PageHeader
399 title={__("Customer Details", "yatra")}
400 description={__("View complete customer information", "yatra")}
401 actions={
402 <div className="flex gap-2">
403 <ConditionalRender capability="yatra_edit_customers">
404 <Button onClick={handleEdit} className="flex items-center gap-2">
405 <Edit className="w-4 h-4" />
406 {__("Edit Customer", "yatra")}
407 </Button>
408 </ConditionalRender>
409 <Button
410 variant="outline"
411 onClick={handleBack}
412 className="flex items-center gap-2"
413 >
414 <ArrowLeft className="w-4 h-4" />
415 {__("Back", "yatra")}
416 </Button>
417 </div>
418 }
419 />
420
421 <ConditionalRender capability="yatra_view_customers">
422 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
423 {/* Main Content */}
424 <div className="lg:col-span-2 space-y-3">
425 {/* AI Customer Summary — operator-triggered 3-line
426 snapshot. Self-gates on AI eligibility + module
427 state, so renders nothing in non-eligible tiers. */}
428 {customer?.id && <CustomerSummaryCard customerId={customer.id} />}
429
430 {/* Customer Overview */}
431 <Card>
432 <CardHeader className="pb-2">
433 <div className="flex items-center justify-between">
434 <CardTitle className="text-base">
435 {__("Customer Overview", "yatra")}
436 </CardTitle>
437 <div className="flex gap-2">
438 {getStatusBadge(customer.status)}
439 {getLoyaltyBadge(customer.loyalty_tier || "bronze")}
440 </div>
441 </div>
442 </CardHeader>
443 <CardContent className="space-y-4">
444 <div>
445 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
446 <User className="w-3 h-3" />
447 {__("Full Name", "yatra")}
448 </div>
449 <div className="text-lg font-semibold text-gray-900 dark:text-white">
450 <a
451 href={`${baseAdminUrl}?page=yatra&subpage=customers&action=view&id=${customer.id}`}
452 className="hover:underline text-primary-600 dark:text-primary-400"
453 >
454 {customer.name ||
455 `${customer.first_name} ${customer.last_name}`}
456 </a>
457 </div>
458 </div>
459
460 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
461 <div>
462 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
463 <Mail className="w-3 h-3" />
464 {__("Email Address", "yatra")}
465 </div>
466 <div className="text-sm text-gray-900 dark:text-white">
467 {customer.email}
468 </div>
469 </div>
470 {customer.phone && (
471 <div>
472 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
473 <Phone className="w-3 h-3" />
474 {__("Phone Number", "yatra")}
475 </div>
476 <div className="text-sm text-gray-900 dark:text-white">
477 {customer.phone}
478 {customer.secondary_phone && (
479 <span className="text-gray-500 ml-2">
480 / {customer.secondary_phone}
481 </span>
482 )}
483 </div>
484 </div>
485 )}
486 </div>
487
488 {customer.address && (
489 <div>
490 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
491 <MapPin className="w-3 h-3" />
492 {__("Address", "yatra")}
493 </div>
494 <div className="text-sm text-gray-900 dark:text-white">
495 {customer.address}
496 {customer.city && `, ${customer.city}`}
497 {customer.state && `, ${customer.state}`}
498 {customer.postal_code && ` ${customer.postal_code}`}
499 </div>
500 </div>
501 )}
502
503 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
504 {customer.country && (
505 <div>
506 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
507 <Globe className="w-3 h-3" />
508 {__("Country", "yatra")}
509 </div>
510 <div className="text-sm text-gray-900 dark:text-white">
511 {customer.country}
512 </div>
513 </div>
514 )}
515 {customer.nationality && (
516 <div>
517 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
518 {__("Nationality", "yatra")}
519 </div>
520 <div className="text-sm text-gray-900 dark:text-white">
521 {customer.nationality}
522 </div>
523 </div>
524 )}
525 {customer.date_of_birth && (
526 <div>
527 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
528 {__("Date of Birth", "yatra")}
529 </div>
530 <div className="text-sm text-gray-900 dark:text-white">
531 {formatShortDate(customer.date_of_birth)}
532 </div>
533 </div>
534 )}
535 </div>
536 </CardContent>
537 </Card>
538
539 {/* Emergency Contact */}
540 {customer.emergency_name && (
541 <Card>
542 <CardHeader className="pb-2">
543 <CardTitle className="text-base flex items-center gap-2">
544 <AlertCircle className="w-4 h-4 text-red-500" />
545 {__("Emergency Contact", "yatra")}
546 </CardTitle>
547 </CardHeader>
548 <CardContent>
549 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
550 <div>
551 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
552 {__("Name", "yatra")}
553 </div>
554 <div className="text-sm text-gray-900 dark:text-white">
555 {customer.emergency_name}
556 </div>
557 </div>
558 {customer.emergency_phone && (
559 <div>
560 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
561 {__("Phone", "yatra")}
562 </div>
563 <div className="text-sm text-gray-900 dark:text-white">
564 {customer.emergency_phone}
565 </div>
566 </div>
567 )}
568 {customer.emergency_relationship && (
569 <div>
570 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
571 {__("Relationship", "yatra")}
572 </div>
573 <div className="text-sm text-gray-900 dark:text-white">
574 {customer.emergency_relationship}
575 </div>
576 </div>
577 )}
578 </div>
579 </CardContent>
580 </Card>
581 )}
582
583 {/* Recent Bookings */}
584 <Card>
585 <CardHeader className="pb-2">
586 <CardTitle className="text-base">
587 {__("Booking History", "yatra")}
588 </CardTitle>
589 </CardHeader>
590 <CardContent className="p-0">
591 {isLoadingBookings ? (
592 <div className="p-4 space-y-2">
593 {[...Array(3)].map((_, i) => (
594 <div key={i} className="flex gap-4">
595 <Skeleton className="h-4 w-24" />
596 <Skeleton className="h-4 w-40" />
597 <Skeleton className="h-4 w-20" />
598 <Skeleton className="h-4 w-16" />
599 </div>
600 ))}
601 </div>
602 ) : !bookings || bookings.length === 0 ? (
603 <div className="p-8 text-center text-gray-500 dark:text-gray-400">
604 {__("No bookings yet", "yatra")}
605 </div>
606 ) : (
607 <Table>
608 <TableHeader>
609 <TableRow>
610 <TableHead>{__("Reference", "yatra")}</TableHead>
611 <TableHead>{__("Trip", "yatra")}</TableHead>
612 <TableHead>{__("Travel Date", "yatra")}</TableHead>
613 <TableHead>{__("Amount", "yatra")}</TableHead>
614 <TableHead>{__("Status", "yatra")}</TableHead>
615 <TableHead className="text-right">
616 {__("Actions", "yatra")}
617 </TableHead>
618 </TableRow>
619 </TableHeader>
620 <TableBody>
621 {bookings.map((booking) => (
622 <TableRow key={booking.id}>
623 <TableCell className="font-medium text-gray-900 dark:text-white">
624 {booking.reference}
625 </TableCell>
626 <TableCell className="text-gray-900 dark:text-white">
627 {booking.trip_title}
628 </TableCell>
629 <TableCell className="text-gray-500 dark:text-gray-400 text-sm">
630 {formatShortDate(booking.travel_date)}
631 </TableCell>
632 <TableCell className="font-medium">
633 {formatPrice(
634 booking.total_amount,
635 booking.currency,
636 )}
637 </TableCell>
638 <TableCell>
639 {getBookingStatusBadge(booking.status)}
640 </TableCell>
641 <TableCell className="text-right">
642 <Button
643 variant="ghost"
644 size="sm"
645 onClick={() => handleViewBooking(booking.id)}
646 className="h-8"
647 >
648 {__("View", "yatra")}
649 </Button>
650 </TableCell>
651 </TableRow>
652 ))}
653 </TableBody>
654 </Table>
655 )}
656 </CardContent>
657 </Card>
658
659 {/* Notes */}
660 {customer.notes && (
661 <Card>
662 <CardHeader className="pb-2">
663 <CardTitle className="text-base flex items-center gap-2">
664 <FileText className="w-4 h-4" />
665 {__("Internal Notes", "yatra")}
666 </CardTitle>
667 </CardHeader>
668 <CardContent>
669 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
670 {customer.notes}
671 </p>
672 </CardContent>
673 </Card>
674 )}
675
676 {/* Special Requirements */}
677 {(customer.dietary_requirements ||
678 customer.medical_conditions ||
679 customer.special_needs) && (
680 <Card>
681 <CardHeader className="pb-2">
682 <CardTitle className="text-base">
683 {__("Special Requirements", "yatra")}
684 </CardTitle>
685 </CardHeader>
686 <CardContent className="space-y-3">
687 {customer.dietary_requirements && (
688 <div>
689 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
690 {__("Dietary Requirements", "yatra")}
691 </div>
692 <div className="text-sm text-gray-900 dark:text-white">
693 {customer.dietary_requirements}
694 </div>
695 </div>
696 )}
697 {customer.medical_conditions && (
698 <div>
699 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
700 {__("Medical Conditions", "yatra")}
701 </div>
702 <div className="text-sm text-gray-900 dark:text-white">
703 {customer.medical_conditions}
704 </div>
705 </div>
706 )}
707 {customer.special_needs && (
708 <div>
709 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
710 {__("Special Needs", "yatra")}
711 </div>
712 <div className="text-sm text-gray-900 dark:text-white">
713 {customer.special_needs}
714 </div>
715 </div>
716 )}
717 </CardContent>
718 </Card>
719 )}
720 </div>
721
722 {/* Sidebar */}
723 <div className="space-y-3">
724 {/* Statistics */}
725 {isPro && (
726 <Card>
727 <CardHeader className="pb-2">
728 <CardTitle className="text-base">
729 {__("Statistics", "yatra")}
730 </CardTitle>
731 </CardHeader>
732 <CardContent className="space-y-3">
733 <div>
734 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
735 <Users className="w-3 h-3" />
736 {__("Total Bookings", "yatra")}
737 </div>
738 <div className="text-2xl font-semibold text-gray-900 dark:text-white">
739 {customer.total_bookings || 0}
740 </div>
741 </div>
742 <div>
743 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
744 <DollarSign className="w-3 h-3" />
745 {__("Total Spent", "yatra")}
746 </div>
747 <div className="text-2xl font-semibold text-gray-900 dark:text-white">
748 {formatPrice(customer.total_spent || 0)}
749 </div>
750 </div>
751 <div>
752 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
753 <Users className="w-3 h-3" />
754 {__("Total Travelers", "yatra")}
755 </div>
756 <div className="text-2xl font-semibold text-gray-900 dark:text-white">
757 {customer.total_travelers || 0}
758 </div>
759 </div>
760 </CardContent>
761 </Card>
762 )}
763
764 {/* Loyalty */}
765 <Card>
766 <CardHeader className="pb-2">
767 <CardTitle className="text-base flex items-center gap-2">
768 <Award className="w-4 h-4" />
769 {__("Loyalty", "yatra")}
770 </CardTitle>
771 </CardHeader>
772 <CardContent className="space-y-3">
773 <div>
774 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
775 {__("Current Tier", "yatra")}
776 </div>
777 <div>
778 {getLoyaltyBadge(customer.loyalty_tier || "bronze")}
779 </div>
780 </div>
781 <div>
782 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
783 {__("Points", "yatra")}
784 </div>
785 <div className="text-xl font-semibold text-gray-900 dark:text-white">
786 {customer.loyalty_points || 0}
787 </div>
788 </div>
789 </CardContent>
790 </Card>
791
792 {/* Timeline */}
793 <Card>
794 <CardHeader className="pb-2">
795 <CardTitle className="text-base">
796 {__("Timeline", "yatra")}
797 </CardTitle>
798 </CardHeader>
799 <CardContent className="space-y-3">
800 <div>
801 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
802 <Calendar className="w-3 h-3" />
803 {__("Registered", "yatra")}
804 </div>
805 <div className="text-sm text-gray-900 dark:text-white">
806 {formatDate(customer.created_at)}
807 </div>
808 </div>
809 {customer.last_booking_date && (
810 <div>
811 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
812 {__("Last Booking", "yatra")}
813 </div>
814 <div className="text-sm text-gray-900 dark:text-white">
815 {formatShortDate(customer.last_booking_date)}
816 </div>
817 </div>
818 )}
819 {customer.last_travel_date && (
820 <div>
821 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
822 {__("Last Travel", "yatra")}
823 </div>
824 <div className="text-sm text-gray-900 dark:text-white">
825 {formatShortDate(customer.last_travel_date)}
826 </div>
827 </div>
828 )}
829 </CardContent>
830 </Card>
831
832 {/* Preferences */}
833 <Card>
834 <CardHeader className="pb-2">
835 <CardTitle className="text-base">
836 {__("Preferences", "yatra")}
837 </CardTitle>
838 </CardHeader>
839 <CardContent className="space-y-2">
840 <div className="flex items-center justify-between text-sm">
841 <span className="text-gray-600 dark:text-gray-400">
842 {__("Newsletter", "yatra")}
843 </span>
844 <span
845 className={
846 customer.newsletter_optin
847 ? "text-green-600"
848 : "text-gray-400"
849 }
850 >
851 {customer.newsletter_optin
852 ? __("Subscribed", "yatra")
853 : __("Not subscribed", "yatra")}
854 </span>
855 </div>
856 <div className="flex items-center justify-between text-sm">
857 <span className="text-gray-600 dark:text-gray-400">
858 {__("Marketing", "yatra")}
859 </span>
860 <span
861 className={
862 customer.marketing_optin
863 ? "text-green-600"
864 : "text-gray-400"
865 }
866 >
867 {customer.marketing_optin
868 ? __("Opted in", "yatra")
869 : __("Opted out", "yatra")}
870 </span>
871 </div>
872 </CardContent>
873 </Card>
874 </div>
875 </div>
876 </ConditionalRender>
877 </div>
878 );
879 };
880
881 export default ViewCustomer;
882