PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / ViewBooking.tsx

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

1,573 lines 66.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 PhoneDisplay from "../components/common/PhoneDisplay";
27 import { usePermissions } from "../hooks/usePermissions";
28 import { getCountryName } from "../lib/countries";
29 import { Button } from "../components/ui/button";
30 import { PageHeader } from "../components/common/PageHeader";
31 import {
32 Card,
33 CardContent,
34 CardHeader,
35 CardTitle,
36 } from "../components/ui/card";
37 import { ConditionalRender } from "../components/ui/conditional-render";
38 import { Skeleton } from "../components/ui/skeleton";
39 import { formatYatraMoney } from "../lib/currency-display";
40
41 interface GoogleCalendarSyncInfo {
42 synced: boolean;
43 calendar_id: string | null;
44 event_id: string | null;
45 event_type: string | null;
46 sync_status: string | null;
47 error_message: string | null;
48 last_synced_at: string | null;
49 }
50
51 interface FormFieldConfig {
52 id: string;
53 type: string;
54 label: string;
55 enabled: boolean;
56 order: number;
57 section?: string;
58 }
59
60 interface FormSectionConfig {
61 title: string;
62 enabled: boolean;
63 fields: FormFieldConfig[];
64 }
65
66 interface BookingFormConfig {
67 contact_form: FormSectionConfig;
68 emergency_contact_form: FormSectionConfig;
69 traveler_form: FormSectionConfig;
70 }
71
72 const ViewBooking: React.FC = () => {
73 const { can } = usePermissions();
74
75 // Get booking id from URL
76 const bookingId = useMemo(() => {
77 const params = new URLSearchParams(window.location.search);
78 return params.get("id") ? parseInt(params.get("id") || "0") : null;
79 }, []);
80
81 // Fetch booking form configuration for dynamic field labels
82 const { data: formConfig } = useQuery<BookingFormConfig>({
83 queryKey: ["booking-form-config"],
84 queryFn: async () => {
85 const response = await apiService.getSettings();
86 return (
87 response?.data?.booking_form_config ||
88 response?.booking_form_config ||
89 null
90 );
91 },
92 });
93
94 // Get enabled traveler fields from config
95 const travelerFields = useMemo(() => {
96 if (!formConfig?.traveler_form?.fields) return [];
97 return formConfig.traveler_form.fields
98 .filter((field) => field.enabled)
99 .sort((a, b) => a.order - b.order);
100 }, [formConfig]);
101
102 // Get enabled emergency contact fields
103 const emergencyFields = useMemo(() => {
104 if (!formConfig?.emergency_contact_form?.fields) return [];
105 return formConfig.emergency_contact_form.fields
106 .filter((field) => field.enabled)
107 .sort((a, b) => a.order - b.order);
108 }, [formConfig]);
109
110 // Get enabled contact (lead traveler) fields — used to label and format the
111 // country / nationality / address / custom contact fields in the summary.
112 const contactFields = useMemo(() => {
113 if (!formConfig?.contact_form?.fields) return [];
114 return formConfig.contact_form.fields
115 .filter((field) => field.enabled)
116 .sort((a, b) => a.order - b.order);
117 }, [formConfig]);
118
119 // Helper to get field label by ID
120 const getFieldLabel = (
121 fieldId: string,
122 fields: FormFieldConfig[],
123 ): string => {
124 const field = fields.find((f) => f.id === fieldId);
125 return (
126 field?.label ||
127 fieldId.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
128 );
129 };
130
131 // Fetch booking data from API
132 const {
133 data: booking,
134 isLoading,
135 error,
136 } = useQuery({
137 queryKey: ["booking", bookingId],
138 queryFn: async () => {
139 if (!bookingId) return null;
140
141 const result = await apiService.getBooking(bookingId);
142
143 if (!result) {
144 throw new Error("Failed to fetch booking");
145 }
146
147 // Handle both wrapped { success, data } and direct data response formats
148 const data = (result as any)?.data ?? result;
149
150 if (data && data.id) {
151 const contact = (data as any).contact || {};
152 return {
153 id: data.id,
154 booking_number: data.reference,
155 customer_name:
156 data.customer_name ||
157 (data.contact?.first_name && data.contact?.last_name
158 ? `${data.contact.first_name} ${data.contact.last_name}`.trim()
159 : `${data.contact_first_name || ""} ${data.contact_last_name || ""}`.trim()) ||
160 "N/A",
161 customer_email:
162 data.customer_email || data.contact_email || contact.email || "",
163 customer_phone:
164 data.customer_phone || data.contact_phone || contact.phone || "",
165 customer_country: data.contact_country || contact.country || "",
166 trip_id: data.trip_id,
167 trip_title: data.trip_title || `Trip #${data.trip_id}`,
168 trip_image: data.trip_image,
169 trip_price:
170 (parseFloat(data.subtotal) || data.total_amount || 0) /
171 (data.travelers_count || 1),
172 booking_date: data.created_at,
173 travel_date: data.travel_date,
174 travelers: data.travelers_count,
175 travelers_data: data.travelers || [],
176 total_amount: data.total_amount,
177 amount_paid: data.amount_paid || 0,
178 amount_due: data.amount_due || 0,
179 discount_amount: data.discount_amount || 0,
180 discount_code: data.discount_code || null,
181 currency: data.currency || "USD",
182 // Tax fields
183 subtotal: data.subtotal,
184 tax_amount: data.tax_amount,
185 tax_rate: data.tax_rate,
186 tax_inclusive: data.tax_inclusive,
187 tax_details: data.tax_details,
188 tax_breakdown: data.tax_breakdown,
189 taxable_amount: data.taxable_amount,
190 // Itinerary costs
191 itinerary_costs: data.itinerary_costs,
192 itinerary_costs_total: data.itinerary_costs_total,
193 // Additional services
194 additional_services: data.additional_services,
195 services_total: data.services_total,
196 // End tax fields
197 payment_status: data.payment_status,
198 booking_status: data.status,
199 payment_method: data.payment_gateway,
200 payment_date: data.payment_date,
201 notes: data.special_requests,
202 internal_notes: data.internal_notes,
203 emergency_contact: data.emergency_contact,
204 contact_data: data.contact_data,
205 payments: data.payments || [],
206 created_at: data.created_at,
207 updated_at: data.updated_at,
208 confirmed_at: data.confirmed_at,
209 completed_at: data.completed_at,
210 cancelled_at: data.cancelled_at,
211 cancellation_reason: data.cancellation_reason,
212 trip_details: data.trip_details,
213 google_calendar: data.google_calendar as
214 | GoogleCalendarSyncInfo
215 | undefined,
216 };
217 }
218
219 return null;
220 },
221 enabled: !!bookingId && can("yatra_view_bookings"),
222 });
223
224 // Fetch consent status for this booking (only if Pro is active)
225 const isPro = !!(window as any).yatraAdmin?.isPro;
226 const { data: consentStatus } = useQuery({
227 queryKey: ["booking-consent-status", bookingId],
228 queryFn: async () => {
229 if (!bookingId) return null;
230 const response = await apiClient.get(
231 `/bookings/${bookingId}/consent-status`,
232 );
233 return response?.data || null;
234 },
235 enabled: !!bookingId && isPro,
236 });
237
238 const formatDate = (dateString: string) => {
239 return formatDateUtil(dateString);
240 };
241
242 const formatPrice = (price: number, currencyCode: string = "USD") =>
243 formatYatraMoney(Number(price) || 0, currencyCode, {
244 zeroAsUnknown: false,
245 });
246
247 const getBookingStatusBadge = (status: string) => {
248 const statusMap: Record<string, { className: string; label: string }> = {
249 confirmed: {
250 className:
251 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
252 label: __("Confirmed", "yatra"),
253 },
254 pending: {
255 className:
256 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
257 label: __("Pending", "yatra"),
258 },
259 cancelled: {
260 className:
261 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
262 label: __("Cancelled", "yatra"),
263 },
264 completed: {
265 className:
266 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
267 label: __("Completed", "yatra"),
268 },
269 };
270
271 const statusInfo = statusMap[status] || {
272 className:
273 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
274 label: status,
275 };
276
277 return (
278 <span
279 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
280 >
281 {statusInfo.label}
282 </span>
283 );
284 };
285
286 const getPaymentStatusBadge = (status: string) => {
287 const statusMap: Record<string, { className: string; label: string }> = {
288 paid: {
289 className:
290 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
291 label: __("Paid", "yatra"),
292 },
293 pending: {
294 className:
295 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
296 label: __("Pending", "yatra"),
297 },
298 partial: {
299 className:
300 "bg-orange-100 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400",
301 label: __("Partial", "yatra"),
302 },
303 refunded: {
304 className:
305 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
306 label: __("Refunded", "yatra"),
307 },
308 };
309
310 const statusInfo = statusMap[status] || {
311 className:
312 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
313 label: status,
314 };
315
316 return (
317 <span
318 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
319 >
320 {statusInfo.label}
321 </span>
322 );
323 };
324
325 const handleBack = () => {
326 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
327 };
328
329 const handleEdit = () => {
330 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=edit&id=${bookingId}`;
331 };
332
333 if (isLoading) {
334 return (
335 <div className="space-y-3">
336 {/* Header Skeleton */}
337 <div className="flex items-center justify-between">
338 <div className="space-y-2">
339 <Skeleton className="h-8 w-48" />
340 <Skeleton className="h-4 w-64" />
341 </div>
342 <div className="flex gap-2">
343 <Skeleton className="h-10 w-28" />
344 <Skeleton className="h-10 w-20" />
345 </div>
346 </div>
347
348 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
349 {/* Main Content Skeleton */}
350 <div className="lg:col-span-2 space-y-3">
351 {/* Booking Overview Card */}
352 <Card>
353 <CardHeader className="pb-2">
354 <div className="flex items-center justify-between">
355 <Skeleton className="h-5 w-36" />
356 <div className="flex items-center gap-2">
357 <Skeleton className="h-6 w-20 rounded-md" />
358 <Skeleton className="h-6 w-16 rounded-md" />
359 </div>
360 </div>
361 </CardHeader>
362 <CardContent className="space-y-4">
363 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
364 {[...Array(6)].map((_, i) => (
365 <div key={i} className="space-y-1">
366 <Skeleton className="h-3 w-24" />
367 <Skeleton className="h-5 w-32" />
368 </div>
369 ))}
370 </div>
371 </CardContent>
372 </Card>
373
374 {/* Customer Information Card */}
375 <Card>
376 <CardHeader className="pb-2">
377 <Skeleton className="h-5 w-40" />
378 </CardHeader>
379 <CardContent className="space-y-3">
380 <Skeleton className="h-5 w-36" />
381 <div className="flex items-center gap-4">
382 <Skeleton className="h-4 w-48" />
383 <Skeleton className="h-4 w-32" />
384 </div>
385 </CardContent>
386 </Card>
387
388 {/* Notes Card */}
389 <Card>
390 <CardHeader className="pb-2">
391 <Skeleton className="h-5 w-16" />
392 </CardHeader>
393 <CardContent>
394 <Skeleton className="h-20 w-full" />
395 </CardContent>
396 </Card>
397 </div>
398
399 {/* Sidebar Skeleton */}
400 <div className="space-y-3">
401 {/* Payment Information Card */}
402 <Card>
403 <CardHeader className="pb-2">
404 <Skeleton className="h-5 w-36" />
405 </CardHeader>
406 <CardContent className="space-y-3">
407 <div className="space-y-1">
408 <Skeleton className="h-3 w-24" />
409 <Skeleton className="h-6 w-16 rounded-md" />
410 </div>
411 <div className="space-y-1">
412 <Skeleton className="h-3 w-28" />
413 <Skeleton className="h-4 w-24" />
414 </div>
415 <div className="space-y-1">
416 <Skeleton className="h-3 w-32" />
417 <Skeleton className="h-4 w-20" />
418 </div>
419 <div className="space-y-1">
420 <Skeleton className="h-3 w-24" />
421 <Skeleton className="h-6 w-28" />
422 </div>
423 </CardContent>
424 </Card>
425
426 {/* Timeline Card */}
427 <Card>
428 <CardHeader className="pb-2">
429 <Skeleton className="h-5 w-20" />
430 </CardHeader>
431 <CardContent className="space-y-3">
432 <div className="space-y-1">
433 <Skeleton className="h-3 w-16" />
434 <Skeleton className="h-4 w-40" />
435 </div>
436 <div className="space-y-1">
437 <Skeleton className="h-3 w-24" />
438 <Skeleton className="h-4 w-40" />
439 </div>
440 </CardContent>
441 </Card>
442 </div>
443 </div>
444 </div>
445 );
446 }
447
448 if (error || !booking) {
449 return (
450 <div className="space-y-3">
451 <PageHeader
452 title={__("Booking Not Found", "yatra")}
453 description={__(
454 "The booking you are looking for does not exist",
455 "yatra",
456 )}
457 actions={
458 <Button
459 variant="outline"
460 onClick={handleBack}
461 className="flex items-center gap-2"
462 >
463 <ArrowLeft className="w-4 h-4" />
464 {__("Back to Bookings", "yatra")}
465 </Button>
466 }
467 />
468 <Card>
469 <CardContent className="p-8 text-center text-red-500">
470 {__("Error loading booking or booking not found", "yatra")}
471 </CardContent>
472 </Card>
473 </div>
474 );
475 }
476
477 return (
478 <div className="space-y-3">
479 <PageHeader
480 title={__("Booking Details", "yatra")}
481 description={__("View complete booking information", "yatra")}
482 actions={
483 <div className="flex gap-2">
484 <ConditionalRender capability="yatra_edit_bookings">
485 <Button onClick={handleEdit} className="flex items-center gap-2">
486 {__("Edit Booking", "yatra")}
487 </Button>
488 </ConditionalRender>
489 <Button
490 variant="outline"
491 onClick={handleBack}
492 className="flex items-center gap-2"
493 >
494 <ArrowLeft className="w-4 h-4" />
495 {__("Back", "yatra")}
496 </Button>
497 </div>
498 }
499 />
500
501 <ConditionalRender capability="yatra_view_bookings">
502 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
503 {/* Main Content */}
504 <div className="lg:col-span-2 space-y-3">
505 {/* Booking Overview */}
506 <Card>
507 <CardHeader className="pb-2">
508 <div className="flex items-center justify-between">
509 <CardTitle className="text-base">
510 {__("Booking Overview", "yatra")}
511 </CardTitle>
512 <div className="flex items-center gap-2">
513 {getBookingStatusBadge(booking.booking_status)}
514 {getPaymentStatusBadge(booking.payment_status)}
515 </div>
516 </div>
517 </CardHeader>
518 <CardContent className="space-y-4">
519 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
520 <div>
521 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
522 {__("Booking Number", "yatra")}
523 </div>
524 <div className="text-lg font-semibold text-gray-900 dark:text-white">
525 {booking.booking_number}
526 </div>
527 </div>
528 <div>
529 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
530 {__("Trip", "yatra")}
531 </div>
532 <div className="text-lg font-semibold text-gray-900 dark:text-white">
533 {booking.trip_title}
534 </div>
535 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
536 {__("Trip ID", "yatra")}: #{booking.trip_id}
537 </div>
538 </div>
539 <div>
540 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
541 <Calendar className="w-3 h-3" />
542 {__("Booking Date", "yatra")}
543 </div>
544 <div className="text-sm text-gray-900 dark:text-white">
545 {formatDate(booking.booking_date)}
546 </div>
547 </div>
548 <div>
549 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
550 <Calendar className="w-3 h-3" />
551 {__("Travel Date", "yatra")}
552 </div>
553 <div className="text-sm text-gray-900 dark:text-white">
554 {formatDate(booking.travel_date)}
555 </div>
556 </div>
557 <div>
558 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
559 <Users className="w-3 h-3" />
560 {__("Number of Travelers", "yatra")}
561 </div>
562 <div className="text-sm text-gray-900 dark:text-white">
563 {booking.travelers}{" "}
564 {booking.travelers === 1
565 ? __("Traveler", "yatra")
566 : __("Travelers", "yatra")}
567 </div>
568 </div>
569 </div>
570
571 {/* Payment Summary */}
572 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
573 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
574 {__("Payment Summary", "yatra")}
575 </h4>
576 {(() => {
577 // ── Raw figures ──────────────────────────────────────────
578 const dbSubtotal =
579 parseFloat((booking as any).subtotal) ||
580 booking.total_amount ||
581 0;
582 const discountAmount = booking.discount_amount || 0;
583 const itineraryCostsTotal =
584 parseFloat((booking as any).itinerary_costs_total) || 0;
585 const taxAmount =
586 parseFloat((booking as any).tax_amount) || 0;
587 // Handle both PHP bool (true/false) and int/string (1/0/"1"/"0")
588 const taxInclusiveRaw = (booking as any).tax_inclusive;
589 const taxInclusive =
590 taxInclusiveRaw === true ||
591 parseInt(String(taxInclusiveRaw), 10) === 1;
592 const totalAmount = booking.total_amount || 0;
593 const amountPaid = booking.amount_paid || 0;
594 const amountDue = booking.amount_due || 0;
595
596 // ── Tax breakdown ────────────────────────────────────────
597 let taxLines: {
598 name: string;
599 rate: number;
600 amount: number;
601 }[] = [];
602 const taxBreakdownRaw = (booking as any).tax_breakdown;
603 if (
604 Array.isArray(taxBreakdownRaw) &&
605 taxBreakdownRaw.length > 0
606 ) {
607 taxLines = taxBreakdownRaw;
608 } else {
609 const taxDetailsRaw = (booking as any).tax_details;
610 if (taxDetailsRaw) {
611 try {
612 const parsed =
613 typeof taxDetailsRaw === "string"
614 ? JSON.parse(taxDetailsRaw)
615 : taxDetailsRaw;
616 if (Array.isArray(parsed) && parsed.length > 0)
617 taxLines = parsed;
618 } catch (_) {}
619 }
620 if (taxLines.length === 0 && taxAmount > 0) {
621 taxLines = [
622 {
623 name: __("Tax", "yatra"),
624 rate: parseFloat((booking as any).tax_rate) || 0,
625 amount: taxAmount,
626 },
627 ];
628 }
629 }
630
631 // ── Itinerary costs ──────────────────────────────────────
632 let itineraryCosts: any[] = [];
633 const itineraryCostsRaw = (booking as any).itinerary_costs;
634 if (typeof itineraryCostsRaw === "string") {
635 try {
636 itineraryCosts = JSON.parse(itineraryCostsRaw);
637 } catch (_) {}
638 } else if (Array.isArray(itineraryCostsRaw)) {
639 itineraryCosts = itineraryCostsRaw;
640 }
641
642 // ── Services ─────────────────────────────────────────────
643 const rawServices = (booking as any).additional_services;
644 const selectedServices: any[] = Array.isArray(rawServices)
645 ? rawServices.filter((s: any) => s?.selected !== false)
646 : [];
647 const getServiceAmt = (s: any) =>
648 parseFloat(
649 s?.total_price ??
650 s?.calculated_price ??
651 s?.total_cost ??
652 s?.amount ??
653 s?.unit_price ??
654 s?.price ??
655 0,
656 );
657 const servicesTotal = selectedServices.reduce(
658 (sum, s) => sum + getServiceAmt(s),
659 0,
660 );
661 const baseTripCost = dbSubtotal - servicesTotal;
662
663 // ── Derived amounts ──────────────────────────────────────
664 const taxableAmount =
665 Math.max(0, dbSubtotal - discountAmount) +
666 itineraryCostsTotal;
667
668 const hasServices = selectedServices.length > 0;
669 const hasDiscount = discountAmount > 0;
670 const hasItinerary =
671 itineraryCosts.length > 0 && itineraryCostsTotal > 0;
672 const hasTax = taxLines.length > 0;
673 const showTaxableRow =
674 hasTax && (hasDiscount || hasItinerary);
675
676 const Row = ({
677 label,
678 value,
679 sub = false,
680 green = false,
681 bold = false,
682 }: any) => (
683 <div
684 className={`flex justify-between text-sm${sub ? " ml-4" : ""}`}
685 >
686 <span
687 className={
688 bold
689 ? "font-semibold text-gray-900 dark:text-white"
690 : "text-gray-600 dark:text-gray-400"
691 }
692 >
693 {label}
694 </span>
695 <span
696 className={
697 bold
698 ? "font-semibold text-gray-900 dark:text-white"
699 : green
700 ? "text-green-600 dark:text-green-400"
701 : "text-gray-900 dark:text-white"
702 }
703 >
704 {value}
705 </span>
706 </div>
707 );
708
709 return (
710 <div className="space-y-1.5">
711 {/* Trip Base Price — always the first line */}
712 <div className="flex justify-between text-sm">
713 <span className="text-gray-600 dark:text-gray-400 flex items-center gap-1.5">
714 {__("Trip Base Price", "yatra")}
715 {taxInclusive && (
716 <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">
717 {__("Tax Incl.", "yatra")}
718 </span>
719 )}
720 </span>
721 <span className="text-gray-900 dark:text-white">
722 {formatPrice(baseTripCost, booking.currency)}
723 </span>
724 </div>
725
726 {/* Additional Services section — only when services exist */}
727 {hasServices && (
728 <>
729 <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
730 <span>
731 + {__("Additional Services", "yatra")}
732 </span>
733 <span></span>
734 </div>
735 {selectedServices.map((s: any, i: number) => (
736 <Row
737 key={i}
738 sub
739 label={
740 s?.service_name ||
741 s?.name ||
742 s?.title ||
743 s?.label ||
744 __("Service", "yatra")
745 }
746 value={formatPrice(
747 getServiceAmt(s),
748 booking.currency,
749 )}
750 />
751 ))}
752 {/* Gross Total = Trip Base Price + Services */}
753 <div className="flex justify-between text-sm font-semibold border-t border-gray-200 dark:border-gray-600 pt-1.5 mt-0.5">
754 <span className="text-gray-900 dark:text-white">
755 {__("Gross Total", "yatra")}
756 </span>
757 <span className="text-gray-900 dark:text-white">
758 {formatPrice(dbSubtotal, booking.currency)}
759 </span>
760 </div>
761 </>
762 )}
763
764 {/* Discount */}
765 {hasDiscount && (
766 <Row
767 label={__("Discount", "yatra")}
768 value={`-${formatPrice(discountAmount, booking.currency)}`}
769 green
770 />
771 )}
772
773 {/* Itinerary Costs section */}
774 {hasItinerary && (
775 <>
776 <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
777 <span>+ {__("Itinerary Costs", "yatra")}</span>
778 <span></span>
779 </div>
780 {itineraryCosts.map((cost: any, i: number) => (
781 <Row
782 key={i}
783 sub
784 label={`${cost.name} ${cost.price_per === "person" ? __("(per person)", "yatra") : cost.price_per === "group" ? __("(per booking)", "yatra") : __("(flat rate)", "yatra")}`}
785 value={formatPrice(
786 cost.total_cost ?? cost.price,
787 booking.currency,
788 )}
789 />
790 ))}
791 </>
792 )}
793
794 {/* Taxable Amount — only for exclusive tax when discount/itinerary exist above it */}
795 {!taxInclusive && showTaxableRow && (
796 <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">
797 <span className="text-gray-700 dark:text-gray-300">
798 {__("Taxable Amount", "yatra")}
799 </span>
800 <span className="text-gray-900 dark:text-white">
801 {formatPrice(taxableAmount, booking.currency)}
802 </span>
803 </div>
804 )}
805
806 {/* Exclusive tax — shown as an addition before Net Amount */}
807 {hasTax && !taxInclusive && (
808 <>
809 <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
810 <span>+ {__("Tax", "yatra")}</span>
811 <span></span>
812 </div>
813 {taxLines.map((tax: any, i: number) => (
814 <div
815 key={i}
816 className="flex justify-between text-sm ml-4"
817 >
818 <span className="text-gray-600 dark:text-gray-400">
819 {tax.name}
820 {tax.rate > 0 ? ` (${tax.rate}%)` : ""}
821 </span>
822 <span className="text-gray-900 dark:text-white">
823 +
824 {formatPrice(
825 parseFloat(tax.amount) || 0,
826 booking.currency,
827 )}
828 </span>
829 </div>
830 ))}
831 </>
832 )}
833
834 {/* Net Amount */}
835 <div className="flex justify-between text-base font-bold border-t-2 border-gray-800 dark:border-gray-300 pt-2 mt-1">
836 <span className="text-gray-900 dark:text-white">
837 {__("Net Amount", "yatra")}
838 </span>
839 <span className="text-gray-900 dark:text-white">
840 {formatPrice(totalAmount, booking.currency)}
841 </span>
842 </div>
843
844 {/* Inclusive tax — informational footnote after Net Amount */}
845 {hasTax && taxInclusive && (
846 <div className="mt-1 space-y-0.5">
847 {taxLines.map((tax: any, i: number) => (
848 <div
849 key={i}
850 className="text-xs text-gray-400 dark:text-gray-500 italic"
851 >
852 {__("Incl.", "yatra")} {tax.name}
853 {tax.rate > 0 ? ` (${tax.rate}%)` : ""}:{" "}
854 {formatPrice(
855 parseFloat(tax.amount) || 0,
856 booking.currency,
857 )}
858 </div>
859 ))}
860 </div>
861 )}
862
863 {/* Amount Paid */}
864 {amountPaid > 0 && (
865 <Row
866 label={__("Amount Paid", "yatra")}
867 value={formatPrice(amountPaid, booking.currency)}
868 green
869 />
870 )}
871
872 {/* Due Now */}
873 {amountDue > 0 && (
874 <div className="flex justify-between text-sm font-semibold">
875 <span className="text-orange-600 dark:text-orange-400">
876 {__("Due Now", "yatra")}
877 </span>
878 <span className="text-orange-600 dark:text-orange-400">
879 {formatPrice(amountDue, booking.currency)}
880 </span>
881 </div>
882 )}
883 </div>
884 );
885 })()}
886 </div>
887 </CardContent>
888 </Card>
889
890 {/* Customer Information */}
891 <Card>
892 <CardHeader className="pb-2">
893 <CardTitle className="text-base">
894 {__("Customer Information", "yatra")}
895 </CardTitle>
896 </CardHeader>
897 <CardContent className="space-y-3">
898 <div>
899 <div className="text-sm font-medium text-gray-900 dark:text-white mb-1">
900 {booking.customer_name}
901 </div>
902 <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
903 <div className="flex items-center gap-1.5">
904 <Mail className="w-4 h-4" />
905 {booking.customer_email}
906 </div>
907 {booking.customer_phone && (
908 <div className="flex items-center gap-1.5">
909 <Phone className="w-4 h-4" />
910 <PhoneDisplay value={booking.customer_phone} />
911 </div>
912 )}
913 </div>
914 </div>
915
916 {/* Country + dynamic/custom contact fields (nationality,
917 address, and any custom contact field). Mirrors the
918 Emergency/Traveler dynamic-field display. */}
919 {(() => {
920 const cd =
921 booking.contact_data &&
922 typeof booking.contact_data === "object"
923 ? (booking.contact_data as Record<string, unknown>)
924 : {};
925 const CORE = ["first_name", "last_name", "email", "phone"];
926 const extras = Object.entries(cd).filter(
927 ([k, v]) =>
928 !CORE.includes(k) &&
929 k !== "country" &&
930 v != null &&
931 String(v).trim() !== "",
932 );
933 const countryCode =
934 booking.customer_country ||
935 (cd.country ? String(cd.country) : "");
936 if (!countryCode && extras.length === 0) return null;
937 return (
938 <div className="grid grid-cols-1 md:grid-cols-3 gap-3 pt-1 border-t border-gray-100 dark:border-gray-700">
939 {countryCode && (
940 <div>
941 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
942 {getFieldLabel("country", contactFields)}
943 </div>
944 <div className="text-sm font-medium text-gray-900 dark:text-white">
945 {getCountryName(countryCode)}
946 </div>
947 </div>
948 )}
949 {extras.map(([fieldId, value]) => {
950 const field = contactFields.find(
951 (f) => f.id === fieldId,
952 );
953 const display =
954 field?.type === "country"
955 ? getCountryName(String(value))
956 : String(value);
957 return (
958 <div key={fieldId}>
959 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
960 {getFieldLabel(fieldId, contactFields)}
961 </div>
962 <div className="text-sm font-medium text-gray-900 dark:text-white">
963 {display}
964 </div>
965 </div>
966 );
967 })}
968 </div>
969 );
970 })()}
971 </CardContent>
972 </Card>
973
974 {/* Travelers Information - Dynamic Fields */}
975 {booking.travelers_data && booking.travelers_data.length > 0 && (
976 <Card>
977 <CardHeader className="pb-2">
978 <CardTitle className="text-base flex items-center gap-2">
979 <Users className="w-4 h-4" />
980 {formConfig?.traveler_form?.title ||
981 __("Travelers Information", "yatra")}
982 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
983 ({booking.travelers_data.length}{" "}
984 {booking.travelers_data.length === 1
985 ? __("traveler", "yatra")
986 : __("travelers", "yatra")}
987 )
988 </span>
989 </CardTitle>
990 </CardHeader>
991 <CardContent className="space-y-4">
992 {booking.travelers_data.map(
993 (traveler: any, index: number) => {
994 // Extract fields from traveler - handle both flat structure and nested fields property
995 const travelerFieldsData = traveler.fields || traveler;
996 const systemFields = [
997 "id",
998 "booking_id",
999 "traveller_index",
1000 "is_lead",
1001 "created_at",
1002 "updated_at",
1003 "fields",
1004 ];
1005
1006 // Get all non-empty fields from the traveler data, excluding system fields
1007 const travelerEntries = Object.entries(
1008 travelerFieldsData,
1009 ).filter(([key, value]) => {
1010 // Exclude system fields
1011 if (systemFields.includes(key)) return false;
1012 // Exclude empty values
1013 if (
1014 !value ||
1015 (typeof value === "string" && value.trim() === "")
1016 )
1017 return false;
1018 // Exclude objects (they should be in fields)
1019 if (typeof value === "object" && !Array.isArray(value))
1020 return false;
1021 return true;
1022 });
1023
1024 // Get name from fields or direct properties
1025 const firstName =
1026 travelerFieldsData.first_name ||
1027 traveler.first_name ||
1028 "";
1029 const lastName =
1030 travelerFieldsData.last_name ||
1031 traveler.last_name ||
1032 "";
1033
1034 return (
1035 <div
1036 key={index}
1037 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"}`}
1038 >
1039 <div className="flex items-center justify-between mb-3">
1040 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1041 {index === 0
1042 ? __("Lead Traveler", "yatra")
1043 : `${__("Traveler", "yatra")} ${index + 1}`}
1044 {/* Show name if available */}
1045 {(firstName || lastName) && (
1046 <span className="font-normal text-gray-500 dark:text-gray-400 ml-2">
1047 -{" "}
1048 {[firstName, lastName]
1049 .filter(Boolean)
1050 .join(" ")}
1051 </span>
1052 )}
1053 </h4>
1054 {index === 0 && (
1055 <span className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 px-2 py-0.5 rounded">
1056 {__("Primary Contact", "yatra")}
1057 </span>
1058 )}
1059 </div>
1060
1061 {/* Dynamic Fields Display */}
1062 <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
1063 {travelerEntries.map(([fieldId, fieldValue]) => {
1064 // Skip first_name and last_name as they're shown in header
1065 if (
1066 fieldId === "first_name" ||
1067 fieldId === "last_name"
1068 )
1069 return null;
1070
1071 const fieldConfig = travelerFields.find(
1072 (f) => f.id === fieldId,
1073 );
1074 const label =
1075 fieldConfig?.label ||
1076 getFieldLabel(fieldId, travelerFields);
1077 const isLongField =
1078 fieldConfig?.type === "textarea" ||
1079 String(fieldValue).length > 50;
1080
1081 // Format date fields (e.g. Date of Birth) with the
1082 // global WP date-format setting via the shared
1083 // formatter — not the browser locale.
1084 let displayValue = String(fieldValue);
1085 if (
1086 fieldConfig?.type === "date" ||
1087 fieldId.includes("date") ||
1088 fieldId.includes("birth") ||
1089 fieldId.includes("dob") ||
1090 fieldId.includes("expiry")
1091 ) {
1092 displayValue = formatDate(fieldValue as string);
1093 }
1094
1095 // Format country/nationality fields - convert code to full name
1096 if (
1097 (fieldId === "nationality" ||
1098 fieldId === "country") &&
1099 fieldValue &&
1100 typeof fieldValue === "string" &&
1101 fieldValue.length === 2
1102 ) {
1103 displayValue = getCountryName(fieldValue);
1104 }
1105
1106 return (
1107 <div
1108 key={fieldId}
1109 className={
1110 isLongField
1111 ? "col-span-2 md:col-span-3"
1112 : ""
1113 }
1114 >
1115 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1116 {label}
1117 </div>
1118 <div
1119 className={`text-sm text-gray-900 dark:text-white ${fieldId === "passport" ? "font-mono" : ""} capitalize`}
1120 >
1121 {displayValue}
1122 </div>
1123 </div>
1124 );
1125 })}
1126 </div>
1127
1128 {travelerEntries.length <= 2 && (
1129 <div className="text-sm text-gray-400 dark:text-gray-500 italic mt-2">
1130 {__(
1131 "Limited traveler information provided",
1132 "yatra",
1133 )}
1134 </div>
1135 )}
1136 </div>
1137 );
1138 },
1139 )}
1140 </CardContent>
1141 </Card>
1142 )}
1143
1144 {/* Emergency Contact - Dynamic Fields */}
1145 {booking.emergency_contact &&
1146 Object.values(booking.emergency_contact).some(
1147 (v) => v && String(v).trim() !== "",
1148 ) && (
1149 <Card>
1150 <CardHeader className="pb-2">
1151 <CardTitle className="text-base flex items-center gap-2">
1152 <AlertCircle className="w-4 h-4" />
1153 {formConfig?.emergency_contact_form?.title ||
1154 __("Emergency Contact", "yatra")}
1155 </CardTitle>
1156 </CardHeader>
1157 <CardContent>
1158 <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1159 {Object.entries(booking.emergency_contact)
1160 .filter(
1161 ([_, value]) => value && String(value).trim() !== "",
1162 )
1163 .map(([fieldId, fieldValue]) => {
1164 const label = getFieldLabel(fieldId, emergencyFields);
1165 return (
1166 <div key={fieldId}>
1167 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1168 {label}
1169 </div>
1170 <div className="text-sm font-medium text-gray-900 dark:text-white capitalize">
1171 {String(fieldValue)}
1172 </div>
1173 </div>
1174 );
1175 })}
1176 </div>
1177 </CardContent>
1178 </Card>
1179 )}
1180
1181 {/* Notes */}
1182 {booking.notes && (
1183 <Card>
1184 <CardHeader className="pb-2">
1185 <CardTitle className="text-base flex items-center gap-2">
1186 <FileText className="w-4 h-4" />
1187 {__("Special Requests", "yatra")}
1188 </CardTitle>
1189 </CardHeader>
1190 <CardContent>
1191 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
1192 {booking.notes}
1193 </p>
1194 </CardContent>
1195 </Card>
1196 )}
1197 </div>
1198
1199 {/* Sidebar */}
1200 <div className="space-y-3">
1201 {/* Google Calendar Sync */}
1202 {(window as any).yatraAdmin?.isPro &&
1203 !!(window as any).yatraAdmin?.googleCalendar?.enabled &&
1204 (booking as any).google_calendar && (
1205 <Card>
1206 <CardHeader className="pb-2">
1207 <CardTitle className="text-base">
1208 {__("Google Calendar Sync", "yatra")}
1209 </CardTitle>
1210 </CardHeader>
1211 <CardContent className="space-y-3">
1212 {(() => {
1213 const gc = (booking as any)
1214 .google_calendar as GoogleCalendarSyncInfo;
1215 const status =
1216 gc.sync_status || (gc.synced ? "synced" : "not_synced");
1217 const statusClass =
1218 status === "synced"
1219 ? "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400"
1220 : status === "failed"
1221 ? "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400"
1222 : "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400";
1223
1224 return (
1225 <>
1226 <div>
1227 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1228 {__("Status", "yatra")}
1229 </div>
1230 <span
1231 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusClass}`}
1232 >
1233 {status}
1234 </span>
1235 </div>
1236
1237 <div>
1238 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1239 {__("Last Synced", "yatra")}
1240 </div>
1241 <div className="text-sm text-gray-900 dark:text-white">
1242 {gc.last_synced_at
1243 ? formatDate(gc.last_synced_at)
1244 : __("—", "yatra")}
1245 </div>
1246 </div>
1247
1248 <div>
1249 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1250 {__("Calendar", "yatra")}
1251 </div>
1252 <div className="text-sm text-gray-900 dark:text-white break-all">
1253 {gc.calendar_id || __("—", "yatra")}
1254 </div>
1255 </div>
1256
1257 <div>
1258 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1259 {__("Event ID", "yatra")}
1260 </div>
1261 <div className="text-sm text-gray-900 dark:text-white font-mono break-all">
1262 {gc.event_id || __("—", "yatra")}
1263 </div>
1264 </div>
1265
1266 {gc.error_message && (
1267 <div>
1268 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1269 {__("Error", "yatra")}
1270 </div>
1271 <div className="text-sm text-red-600 dark:text-red-400 whitespace-pre-wrap">
1272 {gc.error_message}
1273 </div>
1274 </div>
1275 )}
1276 </>
1277 );
1278 })()}
1279 </CardContent>
1280 </Card>
1281 )}
1282
1283 {/* Payment Information */}
1284 <Card>
1285 <CardHeader className="pb-2">
1286 <CardTitle className="text-base">
1287 {__("Payment Information", "yatra")}
1288 </CardTitle>
1289 </CardHeader>
1290 <CardContent className="space-y-3">
1291 <div>
1292 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1293 {__("Payment Status", "yatra")}
1294 </div>
1295 <div className="mt-1">
1296 {getPaymentStatusBadge(booking.payment_status)}
1297 </div>
1298 </div>
1299 {booking.payment_method && (
1300 <div>
1301 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1302 <CreditCard className="w-3 h-3" />
1303 {__("Payment Method", "yatra")}
1304 </div>
1305 <div className="text-sm text-gray-900 dark:text-white">
1306 {booking.payment_method}
1307 </div>
1308 </div>
1309 )}
1310 <div>
1311 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1312 {__("Trip Price per Person", "yatra")}
1313 {((v) => v === true || parseInt(String(v), 10) === 1)(
1314 (booking as any).tax_inclusive,
1315 ) && (
1316 <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">
1317 {__("Tax Incl.", "yatra")}
1318 </span>
1319 )}
1320 </div>
1321 <div className="text-sm text-gray-900 dark:text-white">
1322 {(() => {
1323 // subtotal (DB) = base trip cost + services when Pro is active.
1324 // Subtract services total to get the pure trip base cost.
1325 const subtotal =
1326 parseFloat((booking as any).subtotal) ||
1327 booking.total_amount ||
1328 0;
1329 const travelers = booking.travelers || 1;
1330
1331 const rawServices = (booking as any).additional_services;
1332 const servicesTotal = Array.isArray(rawServices)
1333 ? rawServices
1334 .filter((s: any) => s?.selected !== false)
1335 .reduce(
1336 (sum: number, s: any) =>
1337 sum +
1338 parseFloat(
1339 s?.total_price ??
1340 s?.calculated_price ??
1341 s?.total_cost ??
1342 s?.amount ??
1343 s?.unit_price ??
1344 s?.price ??
1345 0,
1346 ),
1347 0,
1348 )
1349 : 0;
1350
1351 const baseTripCost = subtotal - servicesTotal;
1352 return formatPrice(
1353 baseTripCost / travelers,
1354 booking.currency,
1355 );
1356 })()}
1357 </div>
1358 </div>
1359 <div>
1360 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1361 {__("Total Amount", "yatra")}
1362 {((v) => v === true || parseInt(String(v), 10) === 1)(
1363 (booking as any).tax_inclusive,
1364 ) && (
1365 <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">
1366 {__("Tax Incl.", "yatra")}
1367 </span>
1368 )}
1369 </div>
1370 <div className="text-lg font-semibold text-gray-900 dark:text-white">
1371 {formatPrice(booking.total_amount || 0, booking.currency)}
1372 </div>
1373 </div>
1374 </CardContent>
1375 </Card>
1376
1377 {/* Discount Applied Card */}
1378 {booking.discount_amount > 0 && (
1379 <Card className="border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20">
1380 <CardHeader className="pb-2">
1381 <CardTitle className="text-base flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
1382 <svg
1383 width="16"
1384 height="16"
1385 viewBox="0 0 24 24"
1386 fill="none"
1387 stroke="currentColor"
1388 strokeWidth="2"
1389 className="w-4 h-4"
1390 >
1391 <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
1392 <path d="M2 17l10 5 10-5"></path>
1393 <path d="M2 12l10 5 10-5"></path>
1394 </svg>
1395 {__("Discount Applied", "yatra")}
1396 </CardTitle>
1397 </CardHeader>
1398 <CardContent className="space-y-3">
1399 <div>
1400 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1401 {__("Discount Type", "yatra")}
1402 </div>
1403 <div className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
1404 {booking.discount_code?.toLowerCase().includes("group") ||
1405 booking.discount_code?.includes("GROUP")
1406 ? __("Group Discount", "yatra")
1407 : __("Coupon Discount", "yatra")}
1408 </div>
1409 </div>
1410 {booking.discount_code && (
1411 <div>
1412 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1413 {__("Code", "yatra")}
1414 </div>
1415 <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">
1416 {booking.discount_code}
1417 </div>
1418 </div>
1419 )}
1420 <div>
1421 <div className="text-xs text-emerald-600 dark:text-emerald-400 uppercase tracking-wide mb-1">
1422 {__("Savings", "yatra")}
1423 </div>
1424 <div className="text-xl font-bold text-emerald-700 dark:text-emerald-300">
1425 -{formatPrice(booking.discount_amount, booking.currency)}
1426 </div>
1427 </div>
1428 </CardContent>
1429 </Card>
1430 )}
1431
1432 {/* Consent Status Card - Only show if Pro is active and there are consent forms */}
1433 {isPro && consentStatus && consentStatus.total_required > 0 && (
1434 <Card
1435 className={
1436 consentStatus.all_signed
1437 ? "border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20"
1438 : "border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20"
1439 }
1440 >
1441 <CardHeader className="pb-2">
1442 <CardTitle
1443 className={`text-base flex items-center gap-2 ${
1444 consentStatus.all_signed
1445 ? "text-green-700 dark:text-green-400"
1446 : "text-amber-700 dark:text-amber-400"
1447 }`}
1448 >
1449 <FileSignature className="w-4 h-4" />
1450 {__("Consent Status", "yatra")}
1451 </CardTitle>
1452 </CardHeader>
1453 <CardContent className="space-y-3">
1454 <div className="flex items-center justify-between">
1455 <span
1456 className={`text-sm ${consentStatus.all_signed ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-400"}`}
1457 >
1458 {consentStatus.all_signed ? (
1459 <span className="flex items-center gap-1">
1460 <CheckCircle className="w-4 h-4" />
1461 {__("All Consents Signed", "yatra")}
1462 </span>
1463 ) : (
1464 <span className="flex items-center gap-1">
1465 <Clock className="w-4 h-4" />
1466 {__("Pending Signatures", "yatra")}
1467 </span>
1468 )}
1469 </span>
1470 </div>
1471
1472 <div className="flex items-center justify-between text-sm">
1473 <span className="text-gray-600 dark:text-gray-400">
1474 {__("Signed", "yatra")}
1475 </span>
1476 <span className="font-medium text-gray-900 dark:text-white">
1477 {consentStatus.total_signed} /{" "}
1478 {consentStatus.total_required}
1479 </span>
1480 </div>
1481
1482 {/* Progress bar */}
1483 <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
1484 <div
1485 className={`h-2 rounded-full ${consentStatus.all_signed ? "bg-green-500" : "bg-amber-500"}`}
1486 style={{
1487 width: `${(consentStatus.total_signed / consentStatus.total_required) * 100}%`,
1488 }}
1489 />
1490 </div>
1491
1492 {/* Pending requests */}
1493 {consentStatus.pending_requests &&
1494 consentStatus.pending_requests.length > 0 && (
1495 <div className="pt-2 border-t border-amber-200 dark:border-amber-700">
1496 <div className="text-xs text-amber-600 dark:text-amber-400 uppercase tracking-wide mb-2">
1497 {__("Pending", "yatra")}
1498 </div>
1499 <div className="space-y-1">
1500 {consentStatus.pending_requests
1501 .slice(0, 3)
1502 .map((req: any, idx: number) => (
1503 <div
1504 key={idx}
1505 className="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1"
1506 >
1507 <Send className="w-3 h-3" />
1508 {req.recipient_name || req.recipient_email}
1509 </div>
1510 ))}
1511 {consentStatus.pending_requests.length > 3 && (
1512 <div className="text-xs text-gray-500">
1513 +{consentStatus.pending_requests.length - 3}{" "}
1514 {__("more", "yatra")}
1515 </div>
1516 )}
1517 </div>
1518 </div>
1519 )}
1520
1521 {/* Link to consent management */}
1522 <Button
1523 variant="outline"
1524 size="sm"
1525 className="w-full mt-2"
1526 onClick={() => {
1527 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=trip-consent`;
1528 }}
1529 >
1530 {__("Manage Consents", "yatra")}
1531 </Button>
1532 </CardContent>
1533 </Card>
1534 )}
1535
1536 {/* Booking Timeline */}
1537 <Card>
1538 <CardHeader className="pb-2">
1539 <CardTitle className="text-base">
1540 {__("Timeline", "yatra")}
1541 </CardTitle>
1542 </CardHeader>
1543 <CardContent className="space-y-3">
1544 <div>
1545 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1546 {__("Created", "yatra")}
1547 </div>
1548 <div className="text-sm text-gray-900 dark:text-white">
1549 {formatDate(booking.created_at)}
1550 </div>
1551 </div>
1552 {booking.updated_at &&
1553 booking.updated_at !== booking.created_at && (
1554 <div>
1555 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1556 {__("Last Updated", "yatra")}
1557 </div>
1558 <div className="text-sm text-gray-900 dark:text-white">
1559 {formatDate(booking.updated_at)}
1560 </div>
1561 </div>
1562 )}
1563 </CardContent>
1564 </Card>
1565 </div>
1566 </div>
1567 </ConditionalRender>
1568 </div>
1569 );
1570 };
1571
1572 export default ViewBooking;
1573