PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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.4, at resources/js/pages/ViewCustomer.tsx

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