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

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