PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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 / ViewBooking.tsx

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

1,599 lines 61.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * View Booking Page
3 * Display booking details in a clean, minimal SaaS-style design
4 */
5
6 import React, { useMemo } from "react";
7 import { useQuery } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Mail,
11 Phone,
12 Calendar,
13 Users,
14 DollarSign,
15 CreditCard,
16 FileText,
17 AlertCircle,
18 FileSignature,
19 CheckCircle,
20 Clock,
21 Send,
22 } from "lucide-react";
23 import { apiClient, apiService } from "../lib/api-client";
24 import { __ } from "../lib/i18n";
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 { Skeleton } from "../components/ui/skeleton";
37 import { getCurrencySymbol, getCurrency } from "../data/currencies";
38
39 interface GoogleCalendarSyncInfo {
40 synced: boolean;
41 calendar_id: string | null;
42 event_id: string | null;
43 event_type: string | null;
44 sync_status: string | null;
45 error_message: string | null;
46 last_synced_at: string | null;
47 }
48
49 interface FormFieldConfig {
50 id: string;
51 type: string;
52 label: string;
53 enabled: boolean;
54 order: number;
55 section?: string;
56 }
57
58 interface FormSectionConfig {
59 title: string;
60 enabled: boolean;
61 fields: FormFieldConfig[];
62 }
63
64 interface BookingFormConfig {
65 contact_form: FormSectionConfig;
66 emergency_contact_form: FormSectionConfig;
67 traveler_form: FormSectionConfig;
68 }
69
70 const ViewBooking: React.FC = () => {
71 const { can } = usePermissions();
72
73 // Get booking id from URL
74 const bookingId = useMemo(() => {
75 const params = new URLSearchParams(window.location.search);
76 return params.get("id") ? parseInt(params.get("id") || "0") : null;
77 }, []);
78
79 // Fetch booking form configuration for dynamic field labels
80 const { data: formConfig } = useQuery<BookingFormConfig>({
81 queryKey: ["booking-form-config"],
82 queryFn: async () => {
83 const response = await apiService.getSettings();
84 return (
85 response?.data?.booking_form_config ||
86 response?.booking_form_config ||
87 null
88 );
89 },
90 });
91
92 // Get enabled traveler fields from config
93 const travelerFields = useMemo(() => {
94 if (!formConfig?.traveler_form?.fields) return [];
95 return formConfig.traveler_form.fields
96 .filter((field) => field.enabled)
97 .sort((a, b) => a.order - b.order);
98 }, [formConfig]);
99
100 // Get enabled emergency contact fields
101 const emergencyFields = useMemo(() => {
102 if (!formConfig?.emergency_contact_form?.fields) return [];
103 return formConfig.emergency_contact_form.fields
104 .filter((field) => field.enabled)
105 .sort((a, b) => a.order - b.order);
106 }, [formConfig]);
107
108 // Helper to get field label by ID
109 const getFieldLabel = (
110 fieldId: string,
111 fields: FormFieldConfig[],
112 ): string => {
113 const field = fields.find((f) => f.id === fieldId);
114 return (
115 field?.label ||
116 fieldId.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
117 );
118 };
119
120 // Fetch booking data from API
121 const {
122 data: booking,
123 isLoading,
124 error,
125 } = useQuery({
126 queryKey: ["booking", bookingId],
127 queryFn: async () => {
128 if (!bookingId) return null;
129
130 const result = await apiService.getBooking(bookingId);
131
132 if (!result) {
133 throw new Error("Failed to fetch booking");
134 }
135
136 // Handle both wrapped { success, data } and direct data response formats
137 const data = (result as any)?.data ?? result;
138
139 if (data && data.id) {
140 const contact = (data as any).contact || {};
141 return {
142 id: data.id,
143 booking_number: data.reference,
144 customer_name:
145 data.customer_name ||
146 (data.contact?.first_name && data.contact?.last_name
147 ? `${data.contact.first_name} ${data.contact.last_name}`.trim()
148 : `${data.contact_first_name || ""} ${data.contact_last_name || ""}`.trim()) ||
149 "N/A",
150 customer_email:
151 data.customer_email || data.contact_email || contact.email || "",
152 customer_phone:
153 data.customer_phone || data.contact_phone || contact.phone || "",
154 customer_country: data.contact_country || contact.country || "",
155 trip_id: data.trip_id,
156 trip_title: data.trip_title || `Trip #${data.trip_id}`,
157 trip_image: data.trip_image,
158 trip_price: data.total_amount / (data.travelers_count || 1),
159 booking_date: data.created_at,
160 travel_date: data.travel_date,
161 travelers: data.travelers_count,
162 travelers_data: data.travelers || [],
163 total_amount: data.total_amount,
164 amount_paid: data.amount_paid || 0,
165 amount_due: data.amount_due || 0,
166 discount_amount: data.discount_amount || 0,
167 discount_code: data.discount_code || null,
168 currency: data.currency || "USD",
169 // Tax fields
170 subtotal: data.subtotal,
171 tax_amount: data.tax_amount,
172 tax_rate: data.tax_rate,
173 tax_inclusive: data.tax_inclusive,
174 tax_details: data.tax_details,
175 tax_breakdown: data.tax_breakdown,
176 taxable_amount: data.taxable_amount,
177 // Itinerary costs
178 itinerary_costs: data.itinerary_costs,
179 itinerary_costs_total: data.itinerary_costs_total,
180 // End tax fields
181 payment_status: data.payment_status,
182 booking_status: data.status,
183 payment_method: data.payment_gateway,
184 payment_date: data.payment_date,
185 notes: data.special_requests,
186 internal_notes: data.internal_notes,
187 emergency_contact: data.emergency_contact,
188 contact_data: data.contact_data,
189 payments: data.payments || [],
190 created_at: data.created_at,
191 updated_at: data.updated_at,
192 confirmed_at: data.confirmed_at,
193 completed_at: data.completed_at,
194 cancelled_at: data.cancelled_at,
195 cancellation_reason: data.cancellation_reason,
196 trip_details: data.trip_details,
197 google_calendar: data.google_calendar as
198 | GoogleCalendarSyncInfo
199 | undefined,
200 };
201 }
202
203 return null;
204 },
205 enabled: !!bookingId && can("yatra_view_bookings"),
206 });
207
208 // Fetch consent status for this booking (only if Pro is active)
209 const isPro = !!(window as any).yatraAdmin?.isPro;
210 const { data: consentStatus } = useQuery({
211 queryKey: ["booking-consent-status", bookingId],
212 queryFn: async () => {
213 if (!bookingId) return null;
214 const response = await apiClient.get(
215 `/bookings/${bookingId}/consent-status`,
216 );
217 return response?.data || null;
218 },
219 enabled: !!bookingId && isPro,
220 });
221
222 const formatDate = (dateString: string) => {
223 return formatDateUtil(dateString);
224 };
225
226 const formatPrice = (price: number, currencyCode: string = "USD") => {
227 // Always format the price, even if 0 - don't show "Contact for pricing" for bookings
228 const numPrice = Number(price) || 0;
229 const symbol = getCurrencySymbol(currencyCode);
230 const currencyData = getCurrency(currencyCode);
231 const decimals = currencyData?.decimalDigits ?? 2;
232
233 const formatted = new Intl.NumberFormat(undefined, {
234 minimumFractionDigits: decimals,
235 maximumFractionDigits: decimals,
236 }).format(numPrice);
237
238 return `${symbol}${formatted}`;
239 };
240
241 // Country code to name mapping
242 const COUNTRY_NAMES: Record<string, string> = {
243 AF: "Afghanistan",
244 AL: "Albania",
245 DZ: "Algeria",
246 AD: "Andorra",
247 AO: "Angola",
248 AG: "Antigua and Barbuda",
249 AR: "Argentina",
250 AM: "Armenia",
251 AU: "Australia",
252 AT: "Austria",
253 AZ: "Azerbaijan",
254 BS: "Bahamas",
255 BH: "Bahrain",
256 BD: "Bangladesh",
257 BB: "Barbados",
258 BY: "Belarus",
259 BE: "Belgium",
260 BZ: "Belize",
261 BJ: "Benin",
262 BT: "Bhutan",
263 BO: "Bolivia",
264 BA: "Bosnia and Herzegovina",
265 BW: "Botswana",
266 BR: "Brazil",
267 BN: "Brunei",
268 BG: "Bulgaria",
269 BF: "Burkina Faso",
270 BI: "Burundi",
271 KH: "Cambodia",
272 CM: "Cameroon",
273 CA: "Canada",
274 CV: "Cape Verde",
275 CF: "Central African Republic",
276 TD: "Chad",
277 CL: "Chile",
278 CN: "China",
279 CO: "Colombia",
280 KM: "Comoros",
281 CG: "Congo",
282 CD: "DR Congo",
283 CR: "Costa Rica",
284 CI: "Ivory Coast",
285 HR: "Croatia",
286 CU: "Cuba",
287 CY: "Cyprus",
288 CZ: "Czech Republic",
289 DK: "Denmark",
290 DJ: "Djibouti",
291 DM: "Dominica",
292 DO: "Dominican Republic",
293 EC: "Ecuador",
294 EG: "Egypt",
295 SV: "El Salvador",
296 GQ: "Equatorial Guinea",
297 ER: "Eritrea",
298 EE: "Estonia",
299 SZ: "Eswatini",
300 ET: "Ethiopia",
301 FJ: "Fiji",
302 FI: "Finland",
303 FR: "France",
304 GA: "Gabon",
305 GM: "Gambia",
306 GE: "Georgia",
307 DE: "Germany",
308 GH: "Ghana",
309 GR: "Greece",
310 GD: "Grenada",
311 GT: "Guatemala",
312 GN: "Guinea",
313 GW: "Guinea-Bissau",
314 GY: "Guyana",
315 HT: "Haiti",
316 HN: "Honduras",
317 HU: "Hungary",
318 IS: "Iceland",
319 IN: "India",
320 ID: "Indonesia",
321 IR: "Iran",
322 IQ: "Iraq",
323 IE: "Ireland",
324 IL: "Israel",
325 IT: "Italy",
326 JM: "Jamaica",
327 JP: "Japan",
328 JO: "Jordan",
329 KZ: "Kazakhstan",
330 KE: "Kenya",
331 KI: "Kiribati",
332 KP: "North Korea",
333 KR: "South Korea",
334 KW: "Kuwait",
335 KG: "Kyrgyzstan",
336 LA: "Laos",
337 LV: "Latvia",
338 LB: "Lebanon",
339 LS: "Lesotho",
340 LR: "Liberia",
341 LY: "Libya",
342 LI: "Liechtenstein",
343 LT: "Lithuania",
344 LU: "Luxembourg",
345 MG: "Madagascar",
346 MW: "Malawi",
347 MY: "Malaysia",
348 MV: "Maldives",
349 ML: "Mali",
350 MT: "Malta",
351 MH: "Marshall Islands",
352 MR: "Mauritania",
353 MU: "Mauritius",
354 MX: "Mexico",
355 FM: "Micronesia",
356 MD: "Moldova",
357 MC: "Monaco",
358 MN: "Mongolia",
359 ME: "Montenegro",
360 MA: "Morocco",
361 MZ: "Mozambique",
362 MM: "Myanmar",
363 NA: "Namibia",
364 NR: "Nauru",
365 NP: "Nepal",
366 NL: "Netherlands",
367 NZ: "New Zealand",
368 NI: "Nicaragua",
369 NE: "Niger",
370 NG: "Nigeria",
371 MK: "North Macedonia",
372 NO: "Norway",
373 OM: "Oman",
374 PK: "Pakistan",
375 PW: "Palau",
376 PS: "Palestine",
377 PA: "Panama",
378 PG: "Papua New Guinea",
379 PY: "Paraguay",
380 PE: "Peru",
381 PH: "Philippines",
382 PL: "Poland",
383 PT: "Portugal",
384 QA: "Qatar",
385 RO: "Romania",
386 RU: "Russia",
387 RW: "Rwanda",
388 KN: "Saint Kitts and Nevis",
389 LC: "Saint Lucia",
390 VC: "Saint Vincent and the Grenadines",
391 WS: "Samoa",
392 SM: "San Marino",
393 ST: "Sao Tome and Principe",
394 SA: "Saudi Arabia",
395 SN: "Senegal",
396 RS: "Serbia",
397 SC: "Seychelles",
398 SL: "Sierra Leone",
399 SG: "Singapore",
400 SK: "Slovakia",
401 SI: "Slovenia",
402 SB: "Solomon Islands",
403 SO: "Somalia",
404 ZA: "South Africa",
405 SS: "South Sudan",
406 ES: "Spain",
407 LK: "Sri Lanka",
408 SD: "Sudan",
409 SR: "Suriname",
410 SE: "Sweden",
411 CH: "Switzerland",
412 SY: "Syria",
413 TW: "Taiwan",
414 TJ: "Tajikistan",
415 TZ: "Tanzania",
416 TH: "Thailand",
417 TL: "Timor-Leste",
418 TG: "Togo",
419 TO: "Tonga",
420 TT: "Trinidad and Tobago",
421 TN: "Tunisia",
422 TR: "Turkey",
423 TM: "Turkmenistan",
424 TV: "Tuvalu",
425 UG: "Uganda",
426 UA: "Ukraine",
427 AE: "United Arab Emirates",
428 GB: "United Kingdom",
429 US: "United States",
430 UY: "Uruguay",
431 UZ: "Uzbekistan",
432 VU: "Vanuatu",
433 VA: "Vatican City",
434 VE: "Venezuela",
435 VN: "Vietnam",
436 YE: "Yemen",
437 ZM: "Zambia",
438 ZW: "Zimbabwe",
439 };
440
441 const getCountryName = (code: string): string => {
442 if (!code) return "";
443 const upperCode = code.toUpperCase();
444 return COUNTRY_NAMES[upperCode] || code;
445 };
446
447 const getBookingStatusBadge = (status: string) => {
448 const statusMap: Record<string, { className: string; label: string }> = {
449 confirmed: {
450 className:
451 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
452 label: __("Confirmed", "yatra"),
453 },
454 pending: {
455 className:
456 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
457 label: __("Pending", "yatra"),
458 },
459 cancelled: {
460 className:
461 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
462 label: __("Cancelled", "yatra"),
463 },
464 completed: {
465 className:
466 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
467 label: __("Completed", "yatra"),
468 },
469 };
470
471 const statusInfo = statusMap[status] || {
472 className:
473 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
474 label: status,
475 };
476
477 return (
478 <span
479 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
480 >
481 {statusInfo.label}
482 </span>
483 );
484 };
485
486 const getPaymentStatusBadge = (status: string) => {
487 const statusMap: Record<string, { className: string; label: string }> = {
488 paid: {
489 className:
490 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
491 label: __("Paid", "yatra"),
492 },
493 pending: {
494 className:
495 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
496 label: __("Pending", "yatra"),
497 },
498 partial: {
499 className:
500 "bg-orange-100 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400",
501 label: __("Partial", "yatra"),
502 },
503 refunded: {
504 className:
505 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
506 label: __("Refunded", "yatra"),
507 },
508 };
509
510 const statusInfo = statusMap[status] || {
511 className:
512 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
513 label: status,
514 };
515
516 return (
517 <span
518 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
519 >
520 {statusInfo.label}
521 </span>
522 );
523 };
524
525 const handleBack = () => {
526 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
527 };
528
529 const handleEdit = () => {
530 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=edit&id=${bookingId}`;
531 };
532
533 if (isLoading) {
534 return (
535 <div className="space-y-3">
536 {/* Header Skeleton */}
537 <div className="flex items-center justify-between">
538 <div className="space-y-2">
539 <Skeleton className="h-8 w-48" />
540 <Skeleton className="h-4 w-64" />
541 </div>
542 <div className="flex gap-2">
543 <Skeleton className="h-10 w-28" />
544 <Skeleton className="h-10 w-20" />
545 </div>
546 </div>
547
548 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
549 {/* Main Content Skeleton */}
550 <div className="lg:col-span-2 space-y-3">
551 {/* Booking Overview Card */}
552 <Card>
553 <CardHeader className="pb-2">
554 <div className="flex items-center justify-between">
555 <Skeleton className="h-5 w-36" />
556 <div className="flex items-center gap-2">
557 <Skeleton className="h-6 w-20 rounded-md" />
558 <Skeleton className="h-6 w-16 rounded-md" />
559 </div>
560 </div>
561 </CardHeader>
562 <CardContent className="space-y-4">
563 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
564 {[...Array(6)].map((_, i) => (
565 <div key={i} className="space-y-1">
566 <Skeleton className="h-3 w-24" />
567 <Skeleton className="h-5 w-32" />
568 </div>
569 ))}
570 </div>
571 </CardContent>
572 </Card>
573
574 {/* Customer Information Card */}
575 <Card>
576 <CardHeader className="pb-2">
577 <Skeleton className="h-5 w-40" />
578 </CardHeader>
579 <CardContent className="space-y-3">
580 <Skeleton className="h-5 w-36" />
581 <div className="flex items-center gap-4">
582 <Skeleton className="h-4 w-48" />
583 <Skeleton className="h-4 w-32" />
584 </div>
585 </CardContent>
586 </Card>
587
588 {/* Notes Card */}
589 <Card>
590 <CardHeader className="pb-2">
591 <Skeleton className="h-5 w-16" />
592 </CardHeader>
593 <CardContent>
594 <Skeleton className="h-20 w-full" />
595 </CardContent>
596 </Card>
597 </div>
598
599 {/* Sidebar Skeleton */}
600 <div className="space-y-3">
601 {/* Payment Information Card */}
602 <Card>
603 <CardHeader className="pb-2">
604 <Skeleton className="h-5 w-36" />
605 </CardHeader>
606 <CardContent className="space-y-3">
607 <div className="space-y-1">
608 <Skeleton className="h-3 w-24" />
609 <Skeleton className="h-6 w-16 rounded-md" />
610 </div>
611 <div className="space-y-1">
612 <Skeleton className="h-3 w-28" />
613 <Skeleton className="h-4 w-24" />
614 </div>
615 <div className="space-y-1">
616 <Skeleton className="h-3 w-32" />
617 <Skeleton className="h-4 w-20" />
618 </div>
619 <div className="space-y-1">
620 <Skeleton className="h-3 w-24" />
621 <Skeleton className="h-6 w-28" />
622 </div>
623 </CardContent>
624 </Card>
625
626 {/* Timeline Card */}
627 <Card>
628 <CardHeader className="pb-2">
629 <Skeleton className="h-5 w-20" />
630 </CardHeader>
631 <CardContent className="space-y-3">
632 <div className="space-y-1">
633 <Skeleton className="h-3 w-16" />
634 <Skeleton className="h-4 w-40" />
635 </div>
636 <div className="space-y-1">
637 <Skeleton className="h-3 w-24" />
638 <Skeleton className="h-4 w-40" />
639 </div>
640 </CardContent>
641 </Card>
642 </div>
643 </div>
644 </div>
645 );
646 }
647
648 if (error || !booking) {
649 return (
650 <div className="space-y-3">
651 <PageHeader
652 title={__("Booking Not Found", "yatra")}
653 description={__(
654 "The booking you are looking for does not exist",
655 "yatra",
656 )}
657 actions={
658 <Button
659 variant="outline"
660 onClick={handleBack}
661 className="flex items-center gap-2"
662 >
663 <ArrowLeft className="w-4 h-4" />
664 {__("Back to Bookings", "yatra")}
665 </Button>
666 }
667 />
668 <Card>
669 <CardContent className="p-8 text-center text-red-500">
670 {__("Error loading booking or booking not found", "yatra")}
671 </CardContent>
672 </Card>
673 </div>
674 );
675 }
676
677 return (
678 <div className="space-y-3">
679 <PageHeader
680 title={__("Booking Details", "yatra")}
681 description={__("View complete booking information", "yatra")}
682 actions={
683 <div className="flex gap-2">
684 <ConditionalRender capability="yatra_edit_bookings">
685 <Button onClick={handleEdit} className="flex items-center gap-2">
686 {__("Edit Booking", "yatra")}
687 </Button>
688 </ConditionalRender>
689 <Button
690 variant="outline"
691 onClick={handleBack}
692 className="flex items-center gap-2"
693 >
694 <ArrowLeft className="w-4 h-4" />
695 {__("Back", "yatra")}
696 </Button>
697 </div>
698 }
699 />
700
701 <ConditionalRender capability="yatra_view_bookings">
702 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
703 {/* Main Content */}
704 <div className="lg:col-span-2 space-y-3">
705 {/* Booking Overview */}
706 <Card>
707 <CardHeader className="pb-2">
708 <div className="flex items-center justify-between">
709 <CardTitle className="text-base">
710 {__("Booking Overview", "yatra")}
711 </CardTitle>
712 <div className="flex items-center gap-2">
713 {getBookingStatusBadge(booking.booking_status)}
714 {getPaymentStatusBadge(booking.payment_status)}
715 </div>
716 </div>
717 </CardHeader>
718 <CardContent className="space-y-4">
719 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
720 <div>
721 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
722 {__("Booking Number", "yatra")}
723 </div>
724 <div className="text-lg font-semibold text-gray-900 dark:text-white">
725 {booking.booking_number}
726 </div>
727 </div>
728 <div>
729 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
730 {__("Trip", "yatra")}
731 </div>
732 <div className="text-lg font-semibold text-gray-900 dark:text-white">
733 {booking.trip_title}
734 </div>
735 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
736 {__("Trip ID", "yatra")}: #{booking.trip_id}
737 </div>
738 </div>
739 <div>
740 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
741 <Calendar className="w-3 h-3" />
742 {__("Booking Date", "yatra")}
743 </div>
744 <div className="text-sm text-gray-900 dark:text-white">
745 {formatDate(booking.booking_date)}
746 </div>
747 </div>
748 <div>
749 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
750 <Calendar className="w-3 h-3" />
751 {__("Travel Date", "yatra")}
752 </div>
753 <div className="text-sm text-gray-900 dark:text-white">
754 {formatDate(booking.travel_date)}
755 </div>
756 </div>
757 <div>
758 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
759 <Users className="w-3 h-3" />
760 {__("Number of Travelers", "yatra")}
761 </div>
762 <div className="text-sm text-gray-900 dark:text-white">
763 {booking.travelers}{" "}
764 {booking.travelers === 1
765 ? __("Traveler", "yatra")
766 : __("Travelers", "yatra")}
767 </div>
768 </div>
769 </div>
770
771 {/* Payment Summary with Gross/Net and Tax Breakdown */}
772 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
773 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
774 {__("Payment Summary", "yatra")}
775 </h4>
776 <div className="space-y-2">
777 {/* Gross Total */}
778 <div className="flex justify-between text-sm">
779 <span className="font-bold text-gray-900 dark:text-white">
780 {__("Gross Total", "yatra")}
781 </span>
782 <span className="text-gray-900 dark:text-white">
783 {formatPrice(
784 (booking as any).subtotal || booking.total_amount,
785 booking.currency,
786 )}
787 </span>
788 </div>
789
790 {/* Itinerary Costs */}
791 {(() => {
792 const itineraryCosts = (booking as any).itinerary_costs;
793 const itineraryCostsTotal = parseFloat(
794 (booking as any).itinerary_costs_total || 0,
795 );
796
797 if (itineraryCostsTotal > 0) {
798 let costs = [];
799
800 // Try to parse itinerary_costs if it's a string
801 if (typeof itineraryCosts === "string") {
802 try {
803 costs = JSON.parse(itineraryCosts);
804 } catch (e) {
805 console.error(
806 "Failed to parse itinerary_costs:",
807 e,
808 );
809 }
810 } else if (Array.isArray(itineraryCosts)) {
811 costs = itineraryCosts;
812 }
813
814 if (costs.length > 0) {
815 return (
816 <>
817 <div className="flex justify-between text-sm font-medium">
818 <span className="text-gray-600 dark:text-gray-400">
819 {__("Itinerary Costs", "yatra")}
820 </span>
821 </div>
822 {costs.map((cost: any, index: number) => (
823 <div
824 key={index}
825 className="flex justify-between text-sm ml-4"
826 >
827 <span className="text-gray-600 dark:text-gray-400">
828 {cost.name} (
829 {cost.price_per === "person"
830 ? "per person"
831 : "per group"}
832 )
833 </span>
834 <span className="text-gray-900 dark:text-white">
835 {formatPrice(
836 cost.total_cost,
837 booking.currency,
838 )}
839 </span>
840 </div>
841 ))}
842 <div className="flex justify-between text-sm font-medium">
843 <span className="text-gray-600 dark:text-gray-400">
844 {__("Itinerary Costs Total", "yatra")}
845 </span>
846 <span className="text-gray-900 dark:text-white">
847 {formatPrice(
848 itineraryCostsTotal,
849 booking.currency,
850 )}
851 </span>
852 </div>
853 </>
854 );
855 } else if (itineraryCostsTotal > 0) {
856 // Show total even if individual items aren't available
857 return (
858 <div className="flex justify-between text-sm">
859 <span className="text-gray-600 dark:text-gray-400">
860 {__("Itinerary Costs", "yatra")}
861 </span>
862 <span className="text-gray-900 dark:text-white">
863 {formatPrice(
864 itineraryCostsTotal,
865 booking.currency,
866 )}
867 </span>
868 </div>
869 );
870 }
871 }
872
873 return null;
874 })()}
875
876 {/* Taxable Amount (SubTotal) - Displayed after all costs are added */}
877 {((booking as any).tax_amount > 0 ||
878 (booking as any).tax_breakdown?.length > 0) && (
879 <div className="flex justify-between text-sm font-medium border-t border-gray-200 dark:border-gray-700 pt-2">
880 <span className="font-bold text-gray-900 dark:text-white">
881 {__("Subtotal (Taxable Amount)", "yatra")}
882 </span>
883 <span className="text-gray-900 dark:text-white">
884 {formatPrice(
885 (booking as any).taxable_amount > 0
886 ? (booking as any).taxable_amount
887 : (booking as any).subtotal ||
888 booking.total_amount,
889 booking.currency,
890 )}
891 </span>
892 </div>
893 )}
894
895 {/* Discount if any */}
896 {booking.discount_amount > 0 && (
897 <div className="flex justify-between text-sm">
898 <span className="text-gray-600 dark:text-gray-400">
899 {__("Discount", "yatra")}
900 </span>
901 <span className="text-green-600 dark:text-green-400">
902 -
903 {formatPrice(
904 booking.discount_amount,
905 booking.currency,
906 )}
907 </span>
908 </div>
909 )}
910
911 {/* Tax Breakdown */}
912 {(parseFloat((booking as any).tax_amount) > 0 ||
913 (booking as any).tax_breakdown?.length > 0) && (
914 <>
915 {(() => {
916 // Use tax_breakdown if available (already parsed by backend)
917 const taxBreakdown = (booking as any).tax_breakdown;
918
919 if (
920 Array.isArray(taxBreakdown) &&
921 taxBreakdown.length > 0
922 ) {
923 return taxBreakdown.map(
924 (tax: any, index: number) => (
925 <div
926 key={index}
927 className="flex justify-between text-sm"
928 >
929 <span className="text-gray-600 dark:text-gray-400">
930 {tax.name} ({tax.rate}%)
931 </span>
932 <span className="text-gray-900 dark:text-white">
933 {formatPrice(tax.amount, booking.currency)}
934 </span>
935 </div>
936 ),
937 );
938 }
939
940 // Fallback: try parsing tax_details JSON string
941 const taxDetails = (booking as any).tax_details;
942 if (taxDetails) {
943 try {
944 const taxes =
945 typeof taxDetails === "string"
946 ? JSON.parse(taxDetails)
947 : taxDetails;
948
949 if (Array.isArray(taxes) && taxes.length > 0) {
950 return taxes.map((tax: any, index: number) => (
951 <div
952 key={index}
953 className="flex justify-between text-sm"
954 >
955 <span className="text-gray-600 dark:text-gray-400">
956 {tax.name} ({tax.rate}%)
957 </span>
958 <span className="text-gray-900 dark:text-white">
959 {formatPrice(
960 tax.amount,
961 booking.currency,
962 )}
963 </span>
964 </div>
965 ));
966 }
967 } catch (e) {
968 console.error("Failed to parse tax_details:", e);
969 }
970 }
971
972 // Final fallback: show single tax line
973 if ((booking as any).tax_amount > 0) {
974 return (
975 <div className="flex justify-between text-sm">
976 <span className="text-gray-600 dark:text-gray-400">
977 {__("Tax", "yatra")} (
978 {(booking as any).tax_rate}%)
979 </span>
980 <span className="text-gray-900 dark:text-white">
981 {formatPrice(
982 (booking as any).tax_amount,
983 booking.currency,
984 )}
985 </span>
986 </div>
987 );
988 }
989
990 return null;
991 })()}
992 </>
993 )}
994
995 {/* Net Amount (Total with tax) */}
996 <div className="flex justify-between text-base font-semibold pt-2 border-t border-gray-200 dark:border-gray-700">
997 <span className="text-gray-900 dark:text-white">
998 {__("Net Amount", "yatra")}
999 </span>
1000 <span className="text-gray-900 dark:text-white">
1001 {formatPrice(
1002 booking.total_amount || 0,
1003 booking.currency,
1004 )}
1005 </span>
1006 </div>
1007 </div>
1008 </div>
1009 </CardContent>
1010 </Card>
1011
1012 {/* Customer Information */}
1013 <Card>
1014 <CardHeader className="pb-2">
1015 <CardTitle className="text-base">
1016 {__("Customer Information", "yatra")}
1017 </CardTitle>
1018 </CardHeader>
1019 <CardContent className="space-y-3">
1020 <div>
1021 <div className="text-sm font-medium text-gray-900 dark:text-white mb-1">
1022 {booking.customer_name}
1023 </div>
1024 <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
1025 <div className="flex items-center gap-1.5">
1026 <Mail className="w-4 h-4" />
1027 {booking.customer_email}
1028 </div>
1029 {booking.customer_phone && (
1030 <div className="flex items-center gap-1.5">
1031 <Phone className="w-4 h-4" />
1032 {booking.customer_phone}
1033 </div>
1034 )}
1035 </div>
1036 </div>
1037 </CardContent>
1038 </Card>
1039
1040 {/* Travelers Information - Dynamic Fields */}
1041 {booking.travelers_data && booking.travelers_data.length > 0 && (
1042 <Card>
1043 <CardHeader className="pb-2">
1044 <CardTitle className="text-base flex items-center gap-2">
1045 <Users className="w-4 h-4" />
1046 {formConfig?.traveler_form?.title ||
1047 __("Travelers Information", "yatra")}
1048 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1049 ({booking.travelers_data.length}{" "}
1050 {booking.travelers_data.length === 1
1051 ? __("traveler", "yatra")
1052 : __("travelers", "yatra")}
1053 )
1054 </span>
1055 </CardTitle>
1056 </CardHeader>
1057 <CardContent className="space-y-4">
1058 {booking.travelers_data.map(
1059 (traveler: any, index: number) => {
1060 // Extract fields from traveler - handle both flat structure and nested fields property
1061 const travelerFieldsData = traveler.fields || traveler;
1062 const systemFields = [
1063 "id",
1064 "booking_id",
1065 "traveller_index",
1066 "is_lead",
1067 "created_at",
1068 "updated_at",
1069 "fields",
1070 ];
1071
1072 // Get all non-empty fields from the traveler data, excluding system fields
1073 const travelerEntries = Object.entries(
1074 travelerFieldsData,
1075 ).filter(([key, value]) => {
1076 // Exclude system fields
1077 if (systemFields.includes(key)) return false;
1078 // Exclude empty values
1079 if (
1080 !value ||
1081 (typeof value === "string" && value.trim() === "")
1082 )
1083 return false;
1084 // Exclude objects (they should be in fields)
1085 if (typeof value === "object" && !Array.isArray(value))
1086 return false;
1087 return true;
1088 });
1089
1090 // Get name from fields or direct properties
1091 const firstName =
1092 travelerFieldsData.first_name ||
1093 traveler.first_name ||
1094 "";
1095 const lastName =
1096 travelerFieldsData.last_name ||
1097 traveler.last_name ||
1098 "";
1099
1100 return (
1101 <div
1102 key={index}
1103 className={`p-4 rounded-lg ${index === 0 ? "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800" : "bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700"}`}
1104 >
1105 <div className="flex items-center justify-between mb-3">
1106 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1107 {index === 0
1108 ? __("Lead Traveler", "yatra")
1109 : `${__("Traveler", "yatra")} ${index + 1}`}
1110 {/* Show name if available */}
1111 {(firstName || lastName) && (
1112 <span className="font-normal text-gray-500 dark:text-gray-400 ml-2">
1113 -{" "}
1114 {[firstName, lastName]
1115 .filter(Boolean)
1116 .join(" ")}
1117 </span>
1118 )}
1119 </h4>
1120 {index === 0 && (
1121 <span className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 px-2 py-0.5 rounded">
1122 {__("Primary Contact", "yatra")}
1123 </span>
1124 )}
1125 </div>
1126
1127 {/* Dynamic Fields Display */}
1128 <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
1129 {travelerEntries.map(([fieldId, fieldValue]) => {
1130 // Skip first_name and last_name as they're shown in header
1131 if (
1132 fieldId === "first_name" ||
1133 fieldId === "last_name"
1134 )
1135 return null;
1136
1137 const fieldConfig = travelerFields.find(
1138 (f) => f.id === fieldId,
1139 );
1140 const label =
1141 fieldConfig?.label ||
1142 getFieldLabel(fieldId, travelerFields);
1143 const isLongField =
1144 fieldConfig?.type === "textarea" ||
1145 String(fieldValue).length > 50;
1146
1147 // Format date fields
1148 let displayValue = String(fieldValue);
1149 if (
1150 fieldConfig?.type === "date" ||
1151 fieldId.includes("date") ||
1152 fieldId.includes("expiry")
1153 ) {
1154 try {
1155 displayValue = new Date(
1156 fieldValue as string,
1157 ).toLocaleDateString();
1158 } catch {
1159 displayValue = String(fieldValue);
1160 }
1161 }
1162
1163 // Format country/nationality fields - convert code to full name
1164 if (
1165 (fieldId === "nationality" ||
1166 fieldId === "country") &&
1167 fieldValue &&
1168 typeof fieldValue === "string" &&
1169 fieldValue.length === 2
1170 ) {
1171 displayValue = getCountryName(fieldValue);
1172 }
1173
1174 return (
1175 <div
1176 key={fieldId}
1177 className={
1178 isLongField
1179 ? "col-span-2 md:col-span-3"
1180 : ""
1181 }
1182 >
1183 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1184 {label}
1185 </div>
1186 <div
1187 className={`text-sm text-gray-900 dark:text-white ${fieldId === "passport" ? "font-mono" : ""} capitalize`}
1188 >
1189 {displayValue}
1190 </div>
1191 </div>
1192 );
1193 })}
1194 </div>
1195
1196 {travelerEntries.length <= 2 && (
1197 <div className="text-sm text-gray-400 dark:text-gray-500 italic mt-2">
1198 {__(
1199 "Limited traveler information provided",
1200 "yatra",
1201 )}
1202 </div>
1203 )}
1204 </div>
1205 );
1206 },
1207 )}
1208 </CardContent>
1209 </Card>
1210 )}
1211
1212 {/* Emergency Contact - Dynamic Fields */}
1213 {booking.emergency_contact &&
1214 Object.values(booking.emergency_contact).some(
1215 (v) => v && String(v).trim() !== "",
1216 ) && (
1217 <Card>
1218 <CardHeader className="pb-2">
1219 <CardTitle className="text-base flex items-center gap-2">
1220 <AlertCircle className="w-4 h-4" />
1221 {formConfig?.emergency_contact_form?.title ||
1222 __("Emergency Contact", "yatra")}
1223 </CardTitle>
1224 </CardHeader>
1225 <CardContent>
1226 <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1227 {Object.entries(booking.emergency_contact)
1228 .filter(
1229 ([_, value]) => value && String(value).trim() !== "",
1230 )
1231 .map(([fieldId, fieldValue]) => {
1232 const label = getFieldLabel(fieldId, emergencyFields);
1233 return (
1234 <div key={fieldId}>
1235 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1236 {label}
1237 </div>
1238 <div className="text-sm font-medium text-gray-900 dark:text-white capitalize">
1239 {String(fieldValue)}
1240 </div>
1241 </div>
1242 );
1243 })}
1244 </div>
1245 </CardContent>
1246 </Card>
1247 )}
1248
1249 {/* Notes */}
1250 {booking.notes && (
1251 <Card>
1252 <CardHeader className="pb-2">
1253 <CardTitle className="text-base flex items-center gap-2">
1254 <FileText className="w-4 h-4" />
1255 {__("Special Requests", "yatra")}
1256 </CardTitle>
1257 </CardHeader>
1258 <CardContent>
1259 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
1260 {booking.notes}
1261 </p>
1262 </CardContent>
1263 </Card>
1264 )}
1265 </div>
1266
1267 {/* Sidebar */}
1268 <div className="space-y-3">
1269 {/* Google Calendar Sync */}
1270 {(window as any).yatraAdmin?.isPro &&
1271 !!(window as any).yatraAdmin?.googleCalendar?.enabled &&
1272 (booking as any).google_calendar && (
1273 <Card>
1274 <CardHeader className="pb-2">
1275 <CardTitle className="text-base">
1276 {__("Google Calendar Sync", "yatra")}
1277 </CardTitle>
1278 </CardHeader>
1279 <CardContent className="space-y-3">
1280 {(() => {
1281 const gc = (booking as any)
1282 .google_calendar as GoogleCalendarSyncInfo;
1283 const status =
1284 gc.sync_status || (gc.synced ? "synced" : "not_synced");
1285 const statusClass =
1286 status === "synced"
1287 ? "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400"
1288 : status === "failed"
1289 ? "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400"
1290 : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400";
1291
1292 return (
1293 <>
1294 <div>
1295 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1296 {__("Status", "yatra")}
1297 </div>
1298 <span
1299 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusClass}`}
1300 >
1301 {status}
1302 </span>
1303 </div>
1304
1305 <div>
1306 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1307 {__("Last Synced", "yatra")}
1308 </div>
1309 <div className="text-sm text-gray-900 dark:text-white">
1310 {gc.last_synced_at
1311 ? formatDate(gc.last_synced_at)
1312 : __("—", "yatra")}
1313 </div>
1314 </div>
1315
1316 <div>
1317 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1318 {__("Calendar", "yatra")}
1319 </div>
1320 <div className="text-sm text-gray-900 dark:text-white break-all">
1321 {gc.calendar_id || __("—", "yatra")}
1322 </div>
1323 </div>
1324
1325 <div>
1326 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1327 {__("Event ID", "yatra")}
1328 </div>
1329 <div className="text-sm text-gray-900 dark:text-white font-mono break-all">
1330 {gc.event_id || __("—", "yatra")}
1331 </div>
1332 </div>
1333
1334 {gc.error_message && (
1335 <div>
1336 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1337 {__("Error", "yatra")}
1338 </div>
1339 <div className="text-sm text-red-600 dark:text-red-400 whitespace-pre-wrap">
1340 {gc.error_message}
1341 </div>
1342 </div>
1343 )}
1344 </>
1345 );
1346 })()}
1347 </CardContent>
1348 </Card>
1349 )}
1350
1351 {/* Payment Information */}
1352 <Card>
1353 <CardHeader className="pb-2">
1354 <CardTitle className="text-base">
1355 {__("Payment Information", "yatra")}
1356 </CardTitle>
1357 </CardHeader>
1358 <CardContent className="space-y-3">
1359 <div>
1360 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1361 {__("Payment Status", "yatra")}
1362 </div>
1363 <div className="mt-1">
1364 {getPaymentStatusBadge(booking.payment_status)}
1365 </div>
1366 </div>
1367 {booking.payment_method && (
1368 <div>
1369 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1370 <CreditCard className="w-3 h-3" />
1371 {__("Payment Method", "yatra")}
1372 </div>
1373 <div className="text-sm text-gray-900 dark:text-white">
1374 {booking.payment_method}
1375 </div>
1376 </div>
1377 )}
1378 <div>
1379 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1380 {__("Trip Price per Person", "yatra")}
1381 </div>
1382 <div className="text-sm text-gray-900 dark:text-white">
1383 {(() => {
1384 const subtotal =
1385 (booking as any).subtotal || booking.total_amount;
1386 const travelers = booking.travelers || 1;
1387 const pricePerPerson = subtotal / travelers;
1388 return formatPrice(pricePerPerson, booking.currency);
1389 })()}
1390 </div>
1391 </div>
1392 <div>
1393 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1394 {__("Total Amount", "yatra")}
1395 </div>
1396 <div className="text-lg font-semibold text-gray-900 dark:text-white">
1397 {formatPrice(booking.total_amount || 0, booking.currency)}
1398 </div>
1399 </div>
1400 </CardContent>
1401 </Card>
1402
1403 {/* Discount Applied Card */}
1404 {booking.discount_amount > 0 && (
1405 <Card className="border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20">
1406 <CardHeader className="pb-2">
1407 <CardTitle className="text-base flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
1408 <svg
1409 width="16"
1410 height="16"
1411 viewBox="0 0 24 24"
1412 fill="none"
1413 stroke="currentColor"
1414 strokeWidth="2"
1415 className="w-4 h-4"
1416 >
1417 <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
1418 <path d="M2 17l10 5 10-5"></path>
1419 <path d="M2 12l10 5 10-5"></path>
1420 </svg>
1421 {__("Discount Applied", "yatra")}
1422 </CardTitle>
1423 </CardHeader>
1424 <CardContent className="space-y-3">
1425 <div>
1426 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1427 {__("Discount Type", "yatra")}
1428 </div>
1429 <div className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
1430 {booking.discount_code?.toLowerCase().includes("group") ||
1431 booking.discount_code?.includes("GROUP")
1432 ? __("Group Discount", "yatra")
1433 : __("Coupon Discount", "yatra")}
1434 </div>
1435 </div>
1436 {booking.discount_code && (
1437 <div>
1438 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1439 {__("Code", "yatra")}
1440 </div>
1441 <div className="text-sm font-mono font-medium text-emerald-700 dark:text-emerald-300 bg-emerald-100 dark:bg-emerald-900/40 px-2 py-1 rounded inline-block">
1442 {booking.discount_code}
1443 </div>
1444 </div>
1445 )}
1446 <div>
1447 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1448 {__("Savings", "yatra")}
1449 </div>
1450 <div className="text-xl font-bold text-emerald-700 dark:text-emerald-300">
1451 -{formatPrice(booking.discount_amount, booking.currency)}
1452 </div>
1453 </div>
1454 </CardContent>
1455 </Card>
1456 )}
1457
1458 {/* Consent Status Card - Only show if Pro is active and there are consent forms */}
1459 {isPro && consentStatus && consentStatus.total_required > 0 && (
1460 <Card
1461 className={
1462 consentStatus.all_signed
1463 ? "border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20"
1464 : "border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20"
1465 }
1466 >
1467 <CardHeader className="pb-2">
1468 <CardTitle
1469 className={`text-base flex items-center gap-2 ${
1470 consentStatus.all_signed
1471 ? "text-green-700 dark:text-green-400"
1472 : "text-amber-700 dark:text-amber-400"
1473 }`}
1474 >
1475 <FileSignature className="w-4 h-4" />
1476 {__("Consent Status", "yatra")}
1477 </CardTitle>
1478 </CardHeader>
1479 <CardContent className="space-y-3">
1480 <div className="flex items-center justify-between">
1481 <span
1482 className={`text-sm ${consentStatus.all_signed ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-400"}`}
1483 >
1484 {consentStatus.all_signed ? (
1485 <span className="flex items-center gap-1">
1486 <CheckCircle className="w-4 h-4" />
1487 {__("All Consents Signed", "yatra")}
1488 </span>
1489 ) : (
1490 <span className="flex items-center gap-1">
1491 <Clock className="w-4 h-4" />
1492 {__("Pending Signatures", "yatra")}
1493 </span>
1494 )}
1495 </span>
1496 </div>
1497
1498 <div className="flex items-center justify-between text-sm">
1499 <span className="text-gray-600 dark:text-gray-400">
1500 {__("Signed", "yatra")}
1501 </span>
1502 <span className="font-medium text-gray-900 dark:text-white">
1503 {consentStatus.total_signed} /{" "}
1504 {consentStatus.total_required}
1505 </span>
1506 </div>
1507
1508 {/* Progress bar */}
1509 <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
1510 <div
1511 className={`h-2 rounded-full ${consentStatus.all_signed ? "bg-green-500" : "bg-amber-500"}`}
1512 style={{
1513 width: `${(consentStatus.total_signed / consentStatus.total_required) * 100}%`,
1514 }}
1515 />
1516 </div>
1517
1518 {/* Pending requests */}
1519 {consentStatus.pending_requests &&
1520 consentStatus.pending_requests.length > 0 && (
1521 <div className="pt-2 border-t border-amber-200 dark:border-amber-700">
1522 <div className="text-xs text-amber-600 dark:text-amber-400 uppercase tracking-wide mb-2">
1523 {__("Pending", "yatra")}
1524 </div>
1525 <div className="space-y-1">
1526 {consentStatus.pending_requests
1527 .slice(0, 3)
1528 .map((req: any, idx: number) => (
1529 <div
1530 key={idx}
1531 className="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1"
1532 >
1533 <Send className="w-3 h-3" />
1534 {req.recipient_name || req.recipient_email}
1535 </div>
1536 ))}
1537 {consentStatus.pending_requests.length > 3 && (
1538 <div className="text-xs text-gray-500">
1539 +{consentStatus.pending_requests.length - 3}{" "}
1540 {__("more", "yatra")}
1541 </div>
1542 )}
1543 </div>
1544 </div>
1545 )}
1546
1547 {/* Link to consent management */}
1548 <Button
1549 variant="outline"
1550 size="sm"
1551 className="w-full mt-2"
1552 onClick={() => {
1553 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=trip-consent`;
1554 }}
1555 >
1556 {__("Manage Consents", "yatra")}
1557 </Button>
1558 </CardContent>
1559 </Card>
1560 )}
1561
1562 {/* Booking Timeline */}
1563 <Card>
1564 <CardHeader className="pb-2">
1565 <CardTitle className="text-base">
1566 {__("Timeline", "yatra")}
1567 </CardTitle>
1568 </CardHeader>
1569 <CardContent className="space-y-3">
1570 <div>
1571 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1572 {__("Created", "yatra")}
1573 </div>
1574 <div className="text-sm text-gray-900 dark:text-white">
1575 {formatDate(booking.created_at)}
1576 </div>
1577 </div>
1578 {booking.updated_at &&
1579 booking.updated_at !== booking.created_at && (
1580 <div>
1581 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1582 {__("Last Updated", "yatra")}
1583 </div>
1584 <div className="text-sm text-gray-900 dark:text-white">
1585 {formatDate(booking.updated_at)}
1586 </div>
1587 </div>
1588 )}
1589 </CardContent>
1590 </Card>
1591 </div>
1592 </div>
1593 </ConditionalRender>
1594 </div>
1595 );
1596 };
1597
1598 export default ViewBooking;
1599