PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.16 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 All 84 releases
yatra / resources / js / pages / ViewCustomer.tsx

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

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