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

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

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