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

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