PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
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.14.2, at resources/js/pages/ViewBooking.tsx

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