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

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

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