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

882 lines 31.5 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 { getCurrencySymbol, getCurrency } from "../data/currencies";
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 const symbol = getCurrencySymbol(currencyCode);
167 const currencyData = getCurrency(currencyCode);
168 const decimals = currencyData?.decimalDigits ?? 2;
169
170 return `${symbol}${new Intl.NumberFormat(undefined, {
171 minimumFractionDigits: decimals,
172 maximumFractionDigits: decimals,
173 }).format(price)}`;
174 };
175
176 const getStatusBadge = (status: string) => {
177 const statusMap: Record<string, { className: string; label: string }> = {
178 active: {
179 className:
180 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
181 label: __("Active", "yatra"),
182 },
183 inactive: {
184 className:
185 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
186 label: __("Inactive", "yatra"),
187 },
188 blocked: {
189 className:
190 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
191 label: __("Blocked", "yatra"),
192 },
193 };
194
195 const statusInfo = statusMap[status] || {
196 className:
197 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
198 label: status,
199 };
200
201 return (
202 <span
203 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
204 >
205 {statusInfo.label}
206 </span>
207 );
208 };
209
210 const getBookingStatusBadge = (status: string) => {
211 const statusMap: Record<string, { className: string; label: string }> = {
212 confirmed: {
213 className:
214 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
215 label: __("Confirmed", "yatra"),
216 },
217 completed: {
218 className:
219 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
220 label: __("Completed", "yatra"),
221 },
222 pending: {
223 className:
224 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
225 label: __("Pending", "yatra"),
226 },
227 cancelled: {
228 className:
229 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
230 label: __("Cancelled", "yatra"),
231 },
232 };
233
234 const statusInfo = statusMap[status] || {
235 className:
236 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
237 label: status,
238 };
239
240 return (
241 <span
242 className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${statusInfo.className}`}
243 >
244 {statusInfo.label}
245 </span>
246 );
247 };
248
249 const getLoyaltyBadge = (tier: string) => {
250 const tierMap: Record<string, { className: string; icon: string }> = {
251 bronze: {
252 className:
253 "bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400",
254 icon: "🥉",
255 },
256 silver: {
257 className:
258 "bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-300",
259 icon: "🥈",
260 },
261 gold: {
262 className:
263 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
264 icon: "🥇",
265 },
266 platinum: {
267 className:
268 "bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400",
269 icon: "💎",
270 },
271 };
272
273 const tierInfo = tierMap[tier] || tierMap["bronze"];
274
275 return (
276 <span
277 className={`inline-flex items-center gap-1 px-3 py-1 rounded-md text-sm font-medium ${tierInfo.className}`}
278 >
279 <span>{tierInfo.icon}</span>
280 <span className="capitalize">{tier}</span>
281 </span>
282 );
283 };
284
285 const handleBack = () => {
286 window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers`;
287 };
288
289 const handleEdit = () => {
290 window.location.href = `${baseAdminUrl}?page=yatra&subpage=customers&action=edit&id=${customerId}`;
291 };
292
293 const handleViewBooking = (bookingId: number) => {
294 window.location.href = `${baseAdminUrl}?page=yatra&subpage=bookings&action=view&id=${bookingId}`;
295 };
296
297 // Skeleton loader
298 const renderSkeleton = () => (
299 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
300 <div className="lg:col-span-2 space-y-3">
301 <Card>
302 <CardHeader className="pb-2">
303 <div className="flex items-center justify-between">
304 <Skeleton className="h-5 w-40" />
305 <Skeleton className="h-6 w-16 rounded" />
306 </div>
307 </CardHeader>
308 <CardContent className="space-y-4">
309 <Skeleton className="h-6 w-48" />
310 <div className="grid grid-cols-2 gap-4">
311 <div className="space-y-2">
312 <Skeleton className="h-3 w-20" />
313 <Skeleton className="h-4 w-36" />
314 </div>
315 <div className="space-y-2">
316 <Skeleton className="h-3 w-20" />
317 <Skeleton className="h-4 w-28" />
318 </div>
319 </div>
320 <div className="space-y-2">
321 <Skeleton className="h-3 w-16" />
322 <Skeleton className="h-4 w-64" />
323 </div>
324 </CardContent>
325 </Card>
326 </div>
327 <div className="space-y-3">
328 <Card>
329 <CardHeader className="pb-2">
330 <Skeleton className="h-5 w-24" />
331 </CardHeader>
332 <CardContent className="space-y-3">
333 <div className="space-y-2">
334 <Skeleton className="h-3 w-20" />
335 <Skeleton className="h-8 w-12" />
336 </div>
337 <div className="space-y-2">
338 <Skeleton className="h-3 w-20" />
339 <Skeleton className="h-8 w-20" />
340 </div>
341 </CardContent>
342 </Card>
343 </div>
344 </div>
345 );
346
347 if (isLoading) {
348 return (
349 <div className="space-y-3">
350 <PageHeader
351 title={__("Customer Details", "yatra")}
352 description={__("Loading...", "yatra")}
353 actions={
354 <Button
355 variant="outline"
356 onClick={handleBack}
357 className="flex items-center gap-2"
358 >
359 <ArrowLeft className="w-4 h-4" />
360 {__("Back", "yatra")}
361 </Button>
362 }
363 />
364 {renderSkeleton()}
365 </div>
366 );
367 }
368
369 if (error || !customer) {
370 return (
371 <div className="space-y-3">
372 <PageHeader
373 title={__("Customer Not Found", "yatra")}
374 description={__(
375 "The customer you are looking for does not exist",
376 "yatra",
377 )}
378 actions={
379 <Button
380 variant="outline"
381 onClick={handleBack}
382 className="flex items-center gap-2"
383 >
384 <ArrowLeft className="w-4 h-4" />
385 {__("Back to Customers", "yatra")}
386 </Button>
387 }
388 />
389 <Card>
390 <CardContent className="p-8 text-center">
391 <AlertCircle className="w-12 h-12 mx-auto mb-3 text-red-400" />
392 <p className="text-red-500">
393 {__("Error loading customer or customer not found", "yatra")}
394 </p>
395 </CardContent>
396 </Card>
397 </div>
398 );
399 }
400
401 return (
402 <div className="space-y-3">
403 <PageHeader
404 title={__("Customer Details", "yatra")}
405 description={__("View complete customer information", "yatra")}
406 actions={
407 <div className="flex gap-2">
408 <ConditionalRender capability="yatra_edit_bookings">
409 <Button onClick={handleEdit} className="flex items-center gap-2">
410 <Edit className="w-4 h-4" />
411 {__("Edit Customer", "yatra")}
412 </Button>
413 </ConditionalRender>
414 <Button
415 variant="outline"
416 onClick={handleBack}
417 className="flex items-center gap-2"
418 >
419 <ArrowLeft className="w-4 h-4" />
420 {__("Back", "yatra")}
421 </Button>
422 </div>
423 }
424 />
425
426 <ConditionalRender capability="yatra_view_bookings">
427 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
428 {/* Main Content */}
429 <div className="lg:col-span-2 space-y-3">
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