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

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

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