PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
3.0.17 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 All 85 releases
yatra / resources / js / pages / account / BookingDetails.tsx

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

845 lines 34.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2 import {
3 Calendar as CalendarIcon,
4 Users,
5 DollarSign as DollarSignIcon,
6 CreditCard,
7 Mail,
8 Phone as PhoneIcon,
9 FileText as FileTextIcon,
10 AlertCircle,
11 ArrowRight,
12 ExternalLink,
13 Download,
14 } from "lucide-react";
15 import { __ } from "../../lib/i18n";
16 import {
17 formatDate,
18 formatTravelDateRange,
19 getBadge,
20 formatPriceForBooking,
21 getYatraAccountPageGlobals,
22 } from "./utils";
23 import TaxDisplay from "../../components/booking/TaxDisplay";
24
25 const getCountryName = (code: string): string => {
26 if (!code || typeof code !== "string") {
27 return "";
28 }
29 const upperCode = code.trim().toUpperCase();
30 if (upperCode.length !== 2) {
31 return code;
32 }
33 try {
34 const locale = getYatraAccountPageGlobals().locale;
35 const tag = locale ? locale.replace(/_/g, "-") : undefined;
36 const display = new Intl.DisplayNames(tag ? [tag] : undefined, {
37 type: "region",
38 });
39 const name = display.of(upperCode);
40 return name && name !== upperCode ? name : code;
41 } catch {
42 return code;
43 }
44 };
45
46 interface BookingDetailsData {
47 id: number;
48 booking_number: string;
49 customer_name: string;
50 customer_email?: string;
51 customer_phone?: string;
52 customer_country?: string;
53 trip_id: number;
54 trip_title: string;
55 trip_url?: string;
56 trip_image?: string;
57 trip_price: number;
58 booking_date: string;
59 travel_date: string;
60 end_date?: string | null;
61 start_date?: string | null;
62 travelers: number;
63 travelers_data?: any[];
64 total_amount: number;
65 amount_paid: number;
66 amount_due: number;
67 currency: string;
68 discount_amount?: number;
69 payment_status: string;
70 booking_status: string;
71 payment_method?: string;
72 payment_date?: string;
73 notes?: string;
74 emergency_contact?: any;
75 contact_data?: any;
76 payments?: any[];
77 itinerary_costs?: Array<{
78 id: number;
79 name: string;
80 description?: string;
81 price: number;
82 price_per: string;
83 total_cost: number;
84 cost_per_person: boolean;
85 item_type?: string;
86 day_number?: number;
87 }>;
88 itinerary_costs_total?: number;
89 downloads?: Array<{
90 id: string;
91 title: string;
92 url: string;
93 visibility?: string;
94 access_label?: string;
95 locked?: boolean;
96 locked_reason?: string;
97 }>;
98 created_at: string;
99 updated_at?: string;
100 }
101
102 interface BookingDetailsProps {
103 booking: BookingDetailsData | null;
104 isLoading: boolean;
105 onBack: () => void;
106 }
107
108 const BookingDetails: React.FC<BookingDetailsProps> = ({
109 booking,
110 isLoading,
111 onBack,
112 }) => {
113 if (isLoading) {
114 return (
115 <div className="space-y-6">
116 <div className="flex items-center justify-between">
117 <div>
118 <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
119 {__("Booking Details", "yatra")}
120 </h2>
121 </div>
122 <div
123 role="button"
124 tabIndex={0}
125 onClick={onBack}
126 className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
127 >
128 <ArrowRight className="w-4 h-4 rotate-180" />
129 {__("Back to Bookings", "yatra")}
130 </div>
131 </div>
132 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
133 <div className="animate-pulse space-y-4">
134 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
135 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
136 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
137 </div>
138 </div>
139 </div>
140 );
141 }
142
143 if (!booking) {
144 return (
145 <div className="space-y-6">
146 <div className="flex items-center justify-between">
147 <div>
148 <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
149 {__("Booking Details", "yatra")}
150 </h2>
151 </div>
152 <div
153 role="button"
154 tabIndex={0}
155 onClick={onBack}
156 className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
157 >
158 <ArrowRight className="w-4 h-4 rotate-180" />
159 {__("Back to Bookings", "yatra")}
160 </div>
161 </div>
162 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6 text-center text-red-500">
163 {__("Booking not found or error loading booking", "yatra")}
164 </div>
165 </div>
166 );
167 }
168
169 const normalizeRecord = (value: any): Record<string, any> | null => {
170 if (!value) return null;
171 if (typeof value === "string") {
172 try {
173 const parsed = JSON.parse(value);
174 if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
175 return parsed as Record<string, any>;
176 } catch {
177 return null;
178 }
179 return null;
180 }
181 if (typeof value === "object" && !Array.isArray(value)) {
182 return value as Record<string, any>;
183 }
184 return null;
185 };
186
187 const emergencyContact = normalizeRecord((booking as any).emergency_contact);
188
189 return (
190 <div className="space-y-6">
191 <div className="flex items-center justify-between">
192 <div>
193 <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
194 {__("Booking Details", "yatra")}
195 </h2>
196 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
197 {__("Complete booking information", "yatra")}
198 </p>
199 </div>
200 <div
201 role="button"
202 tabIndex={0}
203 onClick={onBack}
204 className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
205 >
206 <ArrowRight className="w-4 h-4 rotate-180" />
207 {__("Back to Bookings", "yatra")}
208 </div>
209 </div>
210
211 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
212 {/* Main Content */}
213 <div className="lg:col-span-2 space-y-6">
214 {/* Booking Overview */}
215 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
216 <div className="flex items-center justify-between mb-4">
217 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
218 {__("Booking Overview", "yatra")}
219 </h3>
220 <div className="flex items-center gap-2">
221 <span className={getBadge(booking.booking_status)}>
222 {__(
223 booking.booking_status || "Pending",
224 booking.booking_status || "Pending",
225 )}
226 </span>
227 <span className={getBadge(booking.payment_status)}>
228 {__(
229 booking.payment_status || "Pending",
230 booking.payment_status || "Pending",
231 )}
232 </span>
233 </div>
234 </div>
235 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
236 <div>
237 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
238 {__("Booking Number", "yatra")}
239 </div>
240 <div className="text-lg font-semibold text-gray-900 dark:text-white">
241 {booking.booking_number || `#${booking.id}`}
242 </div>
243 </div>
244 <div>
245 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
246 {__("Trip", "yatra")}
247 </div>
248 <div className="text-lg font-semibold text-gray-900 dark:text-white">
249 {booking.trip_title}
250 </div>
251 {booking.trip_url ? (
252 <a
253 href={booking.trip_url}
254 className="mt-2 inline-flex items-center gap-2 text-sm text-yatra-primary dark:text-yatra-on-dark hover:underline"
255 >
256 <ExternalLink className="w-4 h-4" />
257 {__("View Trip", "yatra")}
258 </a>
259 ) : null}
260 </div>
261 <div>
262 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
263 <CalendarIcon className="w-3 h-3" />
264 {__("Booking Date", "yatra")}
265 </div>
266 <div className="text-sm text-gray-900 dark:text-white">
267 {formatDate(booking.booking_date || booking.created_at)}
268 </div>
269 </div>
270 <div>
271 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
272 <CalendarIcon className="w-3 h-3" />
273 {__("Travel Date", "yatra")}
274 </div>
275 <div className="text-sm text-gray-900 dark:text-white">
276 {formatTravelDateRange(booking.travel_date, booking.end_date)}
277 </div>
278 </div>
279 <div>
280 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
281 <Users className="w-3 h-3" />
282 {__("Number of Travelers", "yatra")}
283 </div>
284 <div className="text-sm text-gray-900 dark:text-white">
285 {booking.travelers}{" "}
286 {booking.travelers === 1
287 ? __("Traveler", "yatra")
288 : __("Travelers", "yatra")}
289 </div>
290 </div>
291 </div>
292
293 {/* Payment Summary with Gross/Net and Tax Breakdown */}
294 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
295 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
296 {__("Payment Summary", "yatra")}
297 </h4>
298 <div className="space-y-2">
299 {/* Gross Total */}
300 <div className="flex justify-between text-sm">
301 <span className="text-gray-600 dark:text-gray-400">
302 {__("Gross Total", "yatra")}
303 </span>
304 <span className="text-gray-900 dark:text-white">
305 {formatPriceForBooking(
306 (booking as any).subtotal || booking.total_amount,
307 booking.currency,
308 )}
309 </span>
310 </div>
311
312 {/* Taxable Amount (SubTotal) */}
313 {typeof (booking as any).taxable_amount === "number" &&
314 (booking as any).taxable_amount > 0 &&
315 ((booking as any).tax_amount > 0 ||
316 (booking as any).tax_breakdown?.length > 0) && (
317 <div className="flex justify-between text-sm font-medium">
318 <span className="text-gray-600 dark:text-gray-400">
319 {__("Subtotal (Taxable Amount)", "yatra")}
320 </span>
321 <span className="text-gray-900 dark:text-white">
322 {formatPriceForBooking(
323 (booking as any).taxable_amount,
324 booking.currency,
325 )}
326 </span>
327 </div>
328 )}
329
330 {/* Discount if any */}
331 {booking.discount_amount && booking.discount_amount > 0 && (
332 <div className="flex justify-between text-sm">
333 <span className="text-gray-600 dark:text-gray-400">
334 {__("Discount", "yatra")}
335 </span>
336 <span className="text-green-600 dark:text-green-400">
337 -
338 {formatPriceForBooking(
339 booking.discount_amount,
340 booking.currency,
341 )}
342 </span>
343 </div>
344 )}
345
346 {/* Itinerary Costs */}
347 {booking.itinerary_costs &&
348 booking.itinerary_costs.length > 0 && (
349 <>
350 <div className="flex justify-between text-sm">
351 <span className="text-gray-600 dark:text-gray-400 font-medium">
352 {__("Itinerary Costs", "yatra")}
353 </span>
354 </div>
355 {booking.itinerary_costs.map((cost, index) => (
356 <div
357 key={index}
358 className="flex justify-between text-sm ml-4"
359 >
360 <span className="text-gray-600 dark:text-gray-400">
361 {cost.name}
362 {cost.price_per === "person" && (
363 <span className="text-xs text-gray-500 dark:text-gray-500 ml-1">
364 ({__("per person", "yatra")})
365 </span>
366 )}
367 {cost.price_per === "group" && (
368 <span className="text-xs text-gray-500 dark:text-gray-500 ml-1">
369 ({__("per booking", "yatra")})
370 </span>
371 )}
372 </span>
373 <span className="text-gray-900 dark:text-white">
374 {formatPriceForBooking(
375 cost.total_cost,
376 booking.currency,
377 )}
378 </span>
379 </div>
380 ))}
381 </>
382 )}
383
384 {/* Tax Breakdown */}
385 {(booking as any).tax_amount > 0 && (
386 <>
387 {(() => {
388 // Use tax_breakdown if available (already parsed by backend)
389 const taxBreakdown = (booking as any).tax_breakdown;
390
391 if (
392 Array.isArray(taxBreakdown) &&
393 taxBreakdown.length > 0
394 ) {
395 return taxBreakdown.map((tax: any, index: number) => (
396 <div
397 key={index}
398 className="flex justify-between text-sm"
399 >
400 <span className="text-gray-600 dark:text-gray-400">
401 {tax.name} ({tax.rate}%)
402 </span>
403 <span className="text-gray-900 dark:text-white">
404 {formatPriceForBooking(
405 tax.amount,
406 booking.currency,
407 )}
408 </span>
409 </div>
410 ));
411 }
412
413 // Fallback: try parsing tax_details JSON string
414 const taxDetails = (booking as any).tax_details;
415 if (taxDetails) {
416 try {
417 const taxes =
418 typeof taxDetails === "string"
419 ? JSON.parse(taxDetails)
420 : taxDetails;
421
422 if (Array.isArray(taxes) && taxes.length > 0) {
423 return taxes.map((tax: any, index: number) => (
424 <div
425 key={index}
426 className="flex justify-between text-sm"
427 >
428 <span className="text-gray-600 dark:text-gray-400">
429 {tax.name} ({tax.rate}%)
430 </span>
431 <span className="text-gray-900 dark:text-white">
432 {formatPriceForBooking(
433 tax.amount,
434 booking.currency,
435 )}
436 </span>
437 </div>
438 ));
439 }
440 } catch (e) {
441 console.error("Failed to parse tax_details:", e);
442 }
443 }
444
445 // Final fallback: show single tax line
446 if ((booking as any).tax_amount > 0) {
447 return (
448 <div className="flex justify-between text-sm">
449 <span className="text-gray-600 dark:text-gray-400">
450 {__("Tax", "yatra")} ({(booking as any).tax_rate}
451 %)
452 </span>
453 <span className="text-gray-900 dark:text-white">
454 {formatPriceForBooking(
455 (booking as any).tax_amount,
456 booking.currency,
457 )}
458 </span>
459 </div>
460 );
461 }
462
463 return null;
464 })()}
465 </>
466 )}
467
468 {/* Net Amount (Total with tax) */}
469 <div className="flex justify-between text-base font-semibold pt-2 border-t border-gray-200 dark:border-gray-700">
470 <span className="text-gray-900 dark:text-white">
471 {__("Net Amount", "yatra")}
472 </span>
473 <span className="text-gray-900 dark:text-white">
474 {formatPriceForBooking(
475 booking.total_amount,
476 booking.currency,
477 )}
478 </span>
479 </div>
480 </div>
481 </div>
482 </div>
483
484 {/* Customer Information */}
485 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
486 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
487 {__("Customer Information", "yatra")}
488 </h3>
489 <div className="space-y-3">
490 <div>
491 <div className="text-sm font-medium text-gray-900 dark:text-white mb-1">
492 {booking.customer_name}
493 </div>
494 <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
495 {booking.customer_email && (
496 <div className="flex items-center gap-1.5">
497 <Mail className="w-4 h-4" />
498 {booking.customer_email}
499 </div>
500 )}
501 {booking.customer_phone && (
502 <div className="flex items-center gap-1.5">
503 <PhoneIcon className="w-4 h-4" />
504 {booking.customer_phone}
505 </div>
506 )}
507 </div>
508 </div>
509 </div>
510 </div>
511
512 {/* Travelers Information */}
513 {booking.travelers_data && booking.travelers_data.length > 0 && (
514 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
515 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
516 <Users className="w-5 h-5" />
517 {__("Travelers Information", "yatra")}
518 <span className="text-sm font-normal text-gray-500 dark:text-gray-400">
519 ({booking.travelers_data.length}{" "}
520 {booking.travelers_data.length === 1
521 ? __("traveler", "yatra")
522 : __("travelers", "yatra")}
523 )
524 </span>
525 </h3>
526 <div className="space-y-4">
527 {booking.travelers_data.map((traveler: any, index: number) => {
528 // Extract fields from traveler - handle both flat structure and nested fields property
529 const travelerFieldsData = traveler.fields || traveler;
530 const systemFields = [
531 "id",
532 "booking_id",
533 "traveller_index",
534 "is_lead",
535 "created_at",
536 "updated_at",
537 "fields",
538 ];
539
540 // Get all non-empty fields from the traveler data, excluding system fields
541 const travelerEntries = Object.entries(
542 travelerFieldsData,
543 ).filter(([key, value]) => {
544 // Exclude system fields
545 if (systemFields.includes(key)) return false;
546 // Exclude empty values
547 if (
548 !value ||
549 (typeof value === "string" && value.trim() === "")
550 )
551 return false;
552 // Exclude objects (they should be in fields)
553 if (typeof value === "object" && !Array.isArray(value))
554 return false;
555 return true;
556 });
557
558 // Get name from fields or direct properties
559 const firstName =
560 travelerFieldsData.first_name || traveler.first_name || "";
561 const lastName =
562 travelerFieldsData.last_name || traveler.last_name || "";
563
564 return (
565 <div
566 key={index}
567 className={`p-4 rounded-lg ${index === 0 ? "bg-yatra-soft dark:bg-yatra-surface-dark-muted border border-yatra-border-subtle dark:border-yatra-border-dark" : "bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700"}`}
568 >
569 <div className="flex items-center justify-between mb-3">
570 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
571 {index === 0
572 ? __("Lead Traveler", "yatra")
573 : `${__("Traveler", "yatra")} ${index + 1}`}
574 {(firstName || lastName) && (
575 <span className="font-normal text-gray-500 dark:text-gray-400 ml-2">
576 -{" "}
577 {[firstName, lastName].filter(Boolean).join(" ")}
578 </span>
579 )}
580 </h4>
581 {index === 0 && (
582 <span className="text-xs bg-yatra-chip-bg text-yatra-primary-dark dark:bg-yatra-surface-dark dark:text-yatra-primary-light px-2 py-0.5 rounded">
583 {__("Primary Contact", "yatra")}
584 </span>
585 )}
586 </div>
587 <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
588 {travelerEntries.map(([fieldId, fieldValue]) => {
589 if (
590 fieldId === "first_name" ||
591 fieldId === "last_name"
592 )
593 return null;
594 const label = fieldId
595 .replace(/_/g, " ")
596 .replace(/\b\w/g, (l) => l.toUpperCase());
597 let displayValue = String(fieldValue);
598 if (
599 fieldId.includes("date") ||
600 fieldId.includes("expiry")
601 ) {
602 try {
603 displayValue = formatDate(fieldValue as string);
604 } catch {
605 displayValue = String(fieldValue);
606 }
607 }
608
609 // Format country/nationality fields - convert code to full name
610 if (
611 (fieldId === "nationality" ||
612 fieldId === "country") &&
613 fieldValue &&
614 typeof fieldValue === "string" &&
615 fieldValue.length === 2
616 ) {
617 displayValue = getCountryName(fieldValue);
618 }
619 return (
620 <div key={fieldId}>
621 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
622 {label}
623 </div>
624 <div
625 className={`text-sm text-gray-900 dark:text-white ${fieldId === "passport" ? "font-mono" : ""}`}
626 >
627 {displayValue}
628 </div>
629 </div>
630 );
631 })}
632 </div>
633 </div>
634 );
635 })}
636 </div>
637 </div>
638 )}
639
640 {/* Emergency Contact */}
641 {emergencyContact &&
642 Object.values(emergencyContact).some(
643 (v: any) => v && String(v).trim() !== "",
644 ) && (
645 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
646 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
647 <AlertCircle className="w-5 h-5" />
648 {__("Emergency Contact", "yatra")}
649 </h3>
650 <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
651 {Object.entries(emergencyContact)
652 .filter(
653 ([_, value]) => value && String(value).trim() !== "",
654 )
655 .map(([fieldId, fieldValue]) => {
656 const label = fieldId
657 .replace(/_/g, " ")
658 .replace(/\b\w/g, (l) => l.toUpperCase());
659 return (
660 <div key={fieldId}>
661 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
662 {label}
663 </div>
664 <div className="text-sm font-medium text-gray-900 dark:text-white">
665 {String(fieldValue)}
666 </div>
667 </div>
668 );
669 })}
670 </div>
671 </div>
672 )}
673
674 {/* Special Requests */}
675 {booking.notes && (
676 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
677 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
678 <FileTextIcon className="w-5 h-5" />
679 {__("Special Requests", "yatra")}
680 </h3>
681 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
682 {booking.notes}
683 </p>
684 </div>
685 )}
686
687 {/* Downloads */}
688 {Array.isArray((booking as any).downloads) &&
689 (booking as any).downloads.length > 0 && (
690 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
691 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
692 <Download className="w-5 h-5" />
693 {__("Downloads", "yatra")}
694 </h3>
695 <div className="space-y-3">
696 {(booking as any).downloads.map((d: any) => (
697 <div
698 key={d.id}
699 className="flex items-center justify-between gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700"
700 >
701 <div className="min-w-0">
702 <div className="text-sm font-medium text-gray-900 dark:text-white truncate">
703 {d.title}
704 </div>
705 {d.access_label ? (
706 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
707 {d.access_label}
708 </div>
709 ) : null}
710 {d.locked && d.locked_reason ? (
711 <div className="text-xs text-amber-700 dark:text-amber-300 mt-1">
712 {d.locked_reason}
713 </div>
714 ) : null}
715 </div>
716 {d.locked || !d.url ? (
717 <div className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-sm font-medium cursor-not-allowed">
718 <Download className="w-4 h-4" />
719 {__("Not Available", "yatra")}
720 </div>
721 ) : (
722 <a
723 href={d.url}
724 target="_blank"
725 rel="noreferrer"
726 className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium"
727 >
728 <Download className="w-4 h-4" />
729 {__("Download", "yatra")}
730 </a>
731 )}
732 </div>
733 ))}
734 </div>
735 </div>
736 )}
737 </div>
738
739 {/* Sidebar */}
740 <div className="space-y-6">
741 {/* Payment Information */}
742 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
743 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
744 {__("Payment Information", "yatra")}
745 </h3>
746 <div className="space-y-3">
747 <div>
748 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
749 {__("Payment Status", "yatra")}
750 </div>
751 <div className="mt-1">
752 <span className={getBadge(booking.payment_status)}>
753 {__(
754 booking.payment_status || "Pending",
755 booking.payment_status || "Pending",
756 )}
757 </span>
758 </div>
759 </div>
760 {booking.payment_method && (
761 <div>
762 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
763 <CreditCard className="w-3 h-3" />
764 {__("Payment Method", "yatra")}
765 </div>
766 <div className="text-sm text-gray-900 dark:text-white">
767 {booking.payment_method}
768 </div>
769 </div>
770 )}
771 <div>
772 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
773 {__("Total Amount", "yatra")}
774 </div>
775 <div className="text-lg font-semibold text-gray-900 dark:text-white">
776 {formatPriceForBooking(
777 booking.total_amount,
778 booking.currency,
779 )}
780 </div>
781 </div>
782 {booking.amount_paid > 0 && (
783 <div>
784 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
785 {__("Amount Paid", "yatra")}
786 </div>
787 <div className="text-sm text-gray-900 dark:text-white">
788 {formatPriceForBooking(
789 booking.amount_paid,
790 booking.currency,
791 )}
792 </div>
793 </div>
794 )}
795 {booking.amount_due > 0 && (
796 <div>
797 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
798 {__("Amount Due", "yatra")}
799 </div>
800 <div className="text-sm font-semibold text-amber-600 dark:text-amber-400">
801 {formatPriceForBooking(
802 booking.amount_due,
803 booking.currency,
804 )}
805 </div>
806 </div>
807 )}
808 </div>
809 </div>
810
811 {/* Timeline */}
812 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
813 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
814 {__("Timeline", "yatra")}
815 </h3>
816 <div className="space-y-3">
817 <div>
818 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
819 {__("Created", "yatra")}
820 </div>
821 <div className="text-sm text-gray-900 dark:text-white">
822 {formatDate(booking.created_at || booking.booking_date)}
823 </div>
824 </div>
825 {booking.updated_at &&
826 booking.updated_at !== booking.created_at && (
827 <div>
828 <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
829 {__("Last Updated", "yatra")}
830 </div>
831 <div className="text-sm text-gray-900 dark:text-white">
832 {formatDate(booking.updated_at)}
833 </div>
834 </div>
835 )}
836 </div>
837 </div>
838 </div>
839 </div>
840 </div>
841 );
842 };
843
844 export default BookingDetails;
845