PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
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
← All changes | resources/js/pages/ViewBooking.tsx +617 -476 3.0.2.6 → 3.0.17 View file →
@@ -2,11 +2,12 @@
2 2 * View Booking Page
3 3 * Display booking details in a clean, minimal SaaS-style design
4 4 */
5 5
6 -import React, { useMemo } from "react";
6 +import React, { useMemo, useState } from "react";
7 7 import { useQuery } from "@tanstack/react-query";
8 8 import {
9 + Download,
9 10 ArrowLeft,
10 11 Mail,
11 12 Phone,
12 13 Calendar,
@@ -22,9 +23,11 @@
22 23 } from "lucide-react";
23 24 import { apiClient, apiService } from "../lib/api-client";
24 25 import { __ } from "../lib/i18n";
25 26 import { formatDate as formatDateUtil } from "../lib/dateFormat";
27 +import PhoneDisplay from "../components/common/PhoneDisplay";
26 28 import { usePermissions } from "../hooks/usePermissions";
29 +import { getCountryName } from "../lib/countries";
27 30 import { Button } from "../components/ui/button";
28 31 import { PageHeader } from "../components/common/PageHeader";
29 32 import {
30 33 Card,
@@ -32,10 +35,12 @@
32 35 CardHeader,
33 36 CardTitle,
34 37 } from "../components/ui/card";
35 38 import { ConditionalRender } from "../components/ui/conditional-render";
39 +import { ResendEmailMenu } from "../components/bookings/ResendEmailMenu";
40 +import { downloadAdminBookingInvoice } from "../lib/invoice-download";
36 41 import { Skeleton } from "../components/ui/skeleton";
37 -import { getCurrencySymbol, getCurrency } from "../data/currencies";
42 +import { formatYatraMoney } from "../lib/currency-display";
38 43
39 44 interface GoogleCalendarSyncInfo {
40 45 synced: boolean;
41 46 calendar_id: string | null;
@@ -52,10 +57,28 @@
52 57 label: string;
53 58 enabled: boolean;
54 59 order: number;
55 60 section?: string;
61 + options?: { value: string; label: string }[];
56 62 }
57 63
64 +/**
65 + * Dropdown answers are stored as the option VALUE (e.g. "spouse"); show the
66 + * option's label from the form the trip used ("Spouse/Partner") when we have
67 + * it, otherwise the raw value as before.
68 + */
69 +const optionLabel = (
70 + field: FormFieldConfig | undefined,
71 + value: unknown,
72 +): string => {
73 + const raw = String(value ?? "");
74 + if (field?.type === "select" && Array.isArray(field.options)) {
75 + const match = field.options.find((o) => String(o.value) === raw);
76 + if (match?.label) return match.label;
77 + }
78 + return raw;
79 +};
80 +
58 81 interface FormSectionConfig {
59 82 title: string;
60 83 enabled: boolean;
61 84 fields: FormFieldConfig[];
@@ -75,37 +98,8 @@
75 98 const params = new URLSearchParams(window.location.search);
76 99 return params.get("id") ? parseInt(params.get("id") || "0") : null;
77 100 }, []);
78 101
79 - // Fetch booking form configuration for dynamic field labels
80 - const { data: formConfig } = useQuery<BookingFormConfig>({
81 - queryKey: ["booking-form-config"],
82 - queryFn: async () => {
83 - const response = await apiService.getSettings();
84 - return (
85 - response?.data?.booking_form_config ||
86 - response?.booking_form_config ||
87 - null
88 - );
89 - },
90 - });
91 -
92 - // Get enabled traveler fields from config
93 - const travelerFields = useMemo(() => {
94 - if (!formConfig?.traveler_form?.fields) return [];
95 - return formConfig.traveler_form.fields
96 - .filter((field) => field.enabled)
97 - .sort((a, b) => a.order - b.order);
98 - }, [formConfig]);
99 -
100 - // Get enabled emergency contact fields
101 - const emergencyFields = useMemo(() => {
102 - if (!formConfig?.emergency_contact_form?.fields) return [];
103 - return formConfig.emergency_contact_form.fields
104 - .filter((field) => field.enabled)
105 - .sort((a, b) => a.order - b.order);
106 - }, [formConfig]);
107 -
108 102 // Helper to get field label by ID
109 103 const getFieldLabel = (
110 104 fieldId: string,
111 105 fields: FormFieldConfig[],
@@ -154,9 +148,11 @@
154 148 customer_country: data.contact_country || contact.country || "",
155 149 trip_id: data.trip_id,
156 150 trip_title: data.trip_title || `Trip #${data.trip_id}`,
157 151 trip_image: data.trip_image,
158 - trip_price: data.total_amount / (data.travelers_count || 1),
152 + trip_price:
153 + (parseFloat(data.subtotal) || data.total_amount || 0) /
154 + (data.travelers_count || 1),
159 155 booking_date: data.created_at,
160 156 travel_date: data.travel_date,
161 157 travelers: data.travelers_count,
162 158 travelers_data: data.travelers || [],
@@ -176,8 +172,11 @@
176 172 taxable_amount: data.taxable_amount,
177 173 // Itinerary costs
178 174 itinerary_costs: data.itinerary_costs,
179 175 itinerary_costs_total: data.itinerary_costs_total,
176 + // Additional services
177 + additional_services: data.additional_services,
178 + services_total: data.services_total,
180 179 // End tax fields
181 180 payment_status: data.payment_status,
182 181 booking_status: data.status,
183 182 payment_method: data.payment_gateway,
@@ -204,8 +203,50 @@
204 203 },
205 204 enabled: !!bookingId && can("yatra_view_bookings"),
206 205 });
207 206
207 + // Fetch booking form configuration for dynamic field labels, resolved for
208 + // this booking's trip so per-trip form versions (Pro conditions) label the
209 + // fields the customer actually saw. Waits for the booking to know its trip.
210 + const bookingTripId = booking?.trip_id ? Number(booking.trip_id) : null;
211 + const { data: formConfig } = useQuery<BookingFormConfig>({
212 + queryKey: ["booking-form-config", bookingTripId],
213 + queryFn: async () => {
214 + const response = await apiService.getBookingFormConfig(bookingTripId);
215 + return (
216 + response?.data?.booking_form_config ||
217 + response?.booking_form_config ||
218 + null
219 + );
220 + },
221 + enabled: !!booking,
222 + });
223 +
224 + // Get enabled traveler fields from config
225 + const travelerFields = useMemo(() => {
226 + if (!formConfig?.traveler_form?.fields) return [];
227 + return formConfig.traveler_form.fields
228 + .filter((field) => field.enabled)
229 + .sort((a, b) => a.order - b.order);
230 + }, [formConfig]);
231 +
232 + // Get enabled emergency contact fields
233 + const emergencyFields = useMemo(() => {
234 + if (!formConfig?.emergency_contact_form?.fields) return [];
235 + return formConfig.emergency_contact_form.fields
236 + .filter((field) => field.enabled)
237 + .sort((a, b) => a.order - b.order);
238 + }, [formConfig]);
239 +
240 + // Get enabled contact (lead traveler) fields — used to label and format the
241 + // country / nationality / address / custom contact fields in the summary.
242 + const contactFields = useMemo(() => {
243 + if (!formConfig?.contact_form?.fields) return [];
244 + return formConfig.contact_form.fields
245 + .filter((field) => field.enabled)
246 + .sort((a, b) => a.order - b.order);
247 + }, [formConfig]);
248 +
208 249 // Fetch consent status for this booking (only if Pro is active)
209 250 const isPro = !!(window as any).yatraAdmin?.isPro;
210 251 const { data: consentStatus } = useQuery({
211 252 queryKey: ["booking-consent-status", bookingId],
@@ -222,229 +263,13 @@
222 263 const formatDate = (dateString: string) => {
223 264 return formatDateUtil(dateString);
224 265 };
225 266
226 - const formatPrice = (price: number, currencyCode: string = "USD") => {
227 - // Always format the price, even if 0 - don't show "Contact for pricing" for bookings
228 - const numPrice = Number(price) || 0;
229 - const symbol = getCurrencySymbol(currencyCode);
230 - const currencyData = getCurrency(currencyCode);
231 - const decimals = currencyData?.decimalDigits ?? 2;
267 + const formatPrice = (price: number, currencyCode: string = "USD") =>
268 + formatYatraMoney(Number(price) || 0, currencyCode, {
269 + zeroAsUnknown: false,
270 + });
232 271
233 - const formatted = new Intl.NumberFormat(undefined, {
234 - minimumFractionDigits: decimals,
235 - maximumFractionDigits: decimals,
236 - }).format(numPrice);
237 -
238 - return `${symbol}${formatted}`;
239 - };
240 -
241 - // Country code to name mapping
242 - const COUNTRY_NAMES: Record<string, string> = {
243 - AF: "Afghanistan",
244 - AL: "Albania",
245 - DZ: "Algeria",
246 - AD: "Andorra",
247 - AO: "Angola",
248 - AG: "Antigua and Barbuda",
249 - AR: "Argentina",
250 - AM: "Armenia",
251 - AU: "Australia",
252 - AT: "Austria",
253 - AZ: "Azerbaijan",
254 - BS: "Bahamas",
255 - BH: "Bahrain",
256 - BD: "Bangladesh",
257 - BB: "Barbados",
258 - BY: "Belarus",
259 - BE: "Belgium",
260 - BZ: "Belize",
261 - BJ: "Benin",
262 - BT: "Bhutan",
263 - BO: "Bolivia",
264 - BA: "Bosnia and Herzegovina",
265 - BW: "Botswana",
266 - BR: "Brazil",
267 - BN: "Brunei",
268 - BG: "Bulgaria",
269 - BF: "Burkina Faso",
270 - BI: "Burundi",
271 - KH: "Cambodia",
272 - CM: "Cameroon",
273 - CA: "Canada",
274 - CV: "Cape Verde",
275 - CF: "Central African Republic",
276 - TD: "Chad",
277 - CL: "Chile",
278 - CN: "China",
279 - CO: "Colombia",
280 - KM: "Comoros",
281 - CG: "Congo",
282 - CD: "DR Congo",
283 - CR: "Costa Rica",
284 - CI: "Ivory Coast",
285 - HR: "Croatia",
286 - CU: "Cuba",
287 - CY: "Cyprus",
288 - CZ: "Czech Republic",
289 - DK: "Denmark",
290 - DJ: "Djibouti",
291 - DM: "Dominica",
292 - DO: "Dominican Republic",
293 - EC: "Ecuador",
294 - EG: "Egypt",
295 - SV: "El Salvador",
296 - GQ: "Equatorial Guinea",
297 - ER: "Eritrea",
298 - EE: "Estonia",
299 - SZ: "Eswatini",
300 - ET: "Ethiopia",
301 - FJ: "Fiji",
302 - FI: "Finland",
303 - FR: "France",
304 - GA: "Gabon",
305 - GM: "Gambia",
306 - GE: "Georgia",
307 - DE: "Germany",
308 - GH: "Ghana",
309 - GR: "Greece",
310 - GD: "Grenada",
311 - GT: "Guatemala",
312 - GN: "Guinea",
313 - GW: "Guinea-Bissau",
314 - GY: "Guyana",
315 - HT: "Haiti",
316 - HN: "Honduras",
317 - HU: "Hungary",
318 - IS: "Iceland",
319 - IN: "India",
320 - ID: "Indonesia",
321 - IR: "Iran",
322 - IQ: "Iraq",
323 - IE: "Ireland",
324 - IL: "Israel",
325 - IT: "Italy",
326 - JM: "Jamaica",
327 - JP: "Japan",
328 - JO: "Jordan",
329 - KZ: "Kazakhstan",
330 - KE: "Kenya",
331 - KI: "Kiribati",
332 - KP: "North Korea",
333 - KR: "South Korea",
334 - KW: "Kuwait",
335 - KG: "Kyrgyzstan",
336 - LA: "Laos",
337 - LV: "Latvia",
338 - LB: "Lebanon",
339 - LS: "Lesotho",
340 - LR: "Liberia",
341 - LY: "Libya",
342 - LI: "Liechtenstein",
343 - LT: "Lithuania",
344 - LU: "Luxembourg",
345 - MG: "Madagascar",
346 - MW: "Malawi",
347 - MY: "Malaysia",
348 - MV: "Maldives",
349 - ML: "Mali",
350 - MT: "Malta",
351 - MH: "Marshall Islands",
352 - MR: "Mauritania",
353 - MU: "Mauritius",
354 - MX: "Mexico",
355 - FM: "Micronesia",
356 - MD: "Moldova",
357 - MC: "Monaco",
358 - MN: "Mongolia",
359 - ME: "Montenegro",
360 - MA: "Morocco",
361 - MZ: "Mozambique",
362 - MM: "Myanmar",
363 - NA: "Namibia",
364 - NR: "Nauru",
365 - NP: "Nepal",
366 - NL: "Netherlands",
367 - NZ: "New Zealand",
368 - NI: "Nicaragua",
369 - NE: "Niger",
370 - NG: "Nigeria",
371 - MK: "North Macedonia",
372 - NO: "Norway",
373 - OM: "Oman",
374 - PK: "Pakistan",
375 - PW: "Palau",
376 - PS: "Palestine",
377 - PA: "Panama",
378 - PG: "Papua New Guinea",
379 - PY: "Paraguay",
380 - PE: "Peru",
381 - PH: "Philippines",
382 - PL: "Poland",
383 - PT: "Portugal",
384 - QA: "Qatar",
385 - RO: "Romania",
386 - RU: "Russia",
387 - RW: "Rwanda",
388 - KN: "Saint Kitts and Nevis",
389 - LC: "Saint Lucia",
390 - VC: "Saint Vincent and the Grenadines",
391 - WS: "Samoa",
392 - SM: "San Marino",
393 - ST: "Sao Tome and Principe",
394 - SA: "Saudi Arabia",
395 - SN: "Senegal",
396 - RS: "Serbia",
397 - SC: "Seychelles",
398 - SL: "Sierra Leone",
399 - SG: "Singapore",
400 - SK: "Slovakia",
401 - SI: "Slovenia",
402 - SB: "Solomon Islands",
403 - SO: "Somalia",
404 - ZA: "South Africa",
405 - SS: "South Sudan",
406 - ES: "Spain",
407 - LK: "Sri Lanka",
408 - SD: "Sudan",
409 - SR: "Suriname",
410 - SE: "Sweden",
411 - CH: "Switzerland",
412 - SY: "Syria",
413 - TW: "Taiwan",
414 - TJ: "Tajikistan",
415 - TZ: "Tanzania",
416 - TH: "Thailand",
417 - TL: "Timor-Leste",
418 - TG: "Togo",
419 - TO: "Tonga",
420 - TT: "Trinidad and Tobago",
421 - TN: "Tunisia",
422 - TR: "Turkey",
423 - TM: "Turkmenistan",
424 - TV: "Tuvalu",
425 - UG: "Uganda",
426 - UA: "Ukraine",
427 - AE: "United Arab Emirates",
428 - GB: "United Kingdom",
429 - US: "United States",
430 - UY: "Uruguay",
431 - UZ: "Uzbekistan",
432 - VU: "Vanuatu",
433 - VA: "Vatican City",
434 - VE: "Venezuela",
435 - VN: "Vietnam",
436 - YE: "Yemen",
437 - ZM: "Zambia",
438 - ZW: "Zimbabwe",
439 - };
440 -
441 - const getCountryName = (code: string): string => {
442 - if (!code) return "";
443 - const upperCode = code.toUpperCase();
444 - return COUNTRY_NAMES[upperCode] || code;
445 - };
446 -
447 272 const getBookingStatusBadge = (status: string) => {
448 273 const statusMap: Record<string, { className: string; label: string }> = {
449 274 confirmed: {
450 275 className:
@@ -529,8 +354,29 @@
529 354 const handleEdit = () => {
530 355 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=edit&id=${bookingId}`;
531 356 };
532 357
358 + // The booking invoice works for unpaid bookings too — it renders as a
359 + // pro-forma invoice with the gateway's payment instructions. This page has
360 + // no toast provider, so a failure is rendered as an inline banner under the
361 + // header; a tooltip alone would leave a failed download looking like nothing
362 + // happened.
363 + const [invoiceError, setInvoiceError] = useState<string>("");
364 + const handleDownloadInvoice = async () => {
365 + const id = Number(booking?.id ?? 0);
366 + if (!id) {
367 + return;
368 + }
369 + setInvoiceError("");
370 + try {
371 + await downloadAdminBookingInvoice(id);
372 + } catch (error: any) {
373 + setInvoiceError(
374 + error?.message || __("Failed to download invoice", "yatra"),
375 + );
376 + }
377 + };
378 +
533 379 if (isLoading) {
534 380 return (
535 381 <div className="space-y-3">
536 382 {/* Header Skeleton */}
@@ -680,9 +526,26 @@
680 526 title={__("Booking Details", "yatra")}
681 527 description={__("View complete booking information", "yatra")}
682 528 actions={
683 529 <div className="flex gap-2">
530 + <ConditionalRender capability="yatra_view_bookings">
531 + <Button
532 + variant="outline"
533 + onClick={handleDownloadInvoice}
534 + className="flex items-center gap-2"
535 + >
536 + <Download className="w-4 h-4" />
537 + {__("Download invoice", "yatra")}
538 + </Button>
539 + </ConditionalRender>
684 540 <ConditionalRender capability="yatra_edit_bookings">
541 + <ResendEmailMenu
542 + bookingId={Number(booking.id)}
543 + status={booking.booking_status}
544 + amountPaid={Number(booking.amount_paid) || 0}
545 + />
546 + </ConditionalRender>
547 + <ConditionalRender capability="yatra_edit_bookings">
685 548 <Button onClick={handleEdit} className="flex items-center gap-2">
686 549 {__("Edit Booking", "yatra")}
687 550 </Button>
688 551 </ConditionalRender>
@@ -697,8 +560,18 @@
697 560 </div>
698 561 }
699 562 />
700 563
564 + {invoiceError && (
565 + <div
566 + role="alert"
567 + className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
568 + >
569 + <AlertCircle className="w-4 h-4 shrink-0" />
570 + {invoiceError}
571 + </div>
572 + )}
573 +
701 574 <ConditionalRender capability="yatra_view_bookings">
702 575 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
703 576 {/* Main Content */}
704 577 <div className="lg:col-span-2 space-y-3">
@@ -767,249 +640,400 @@
767 640 </div>
768 641 </div>
769 642 </div>
770 643
771 - {/* Payment Summary with Gross/Net and Tax Breakdown */}
644 + {/* Payment Summary */}
772 645 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
773 646 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
774 647 {__("Payment Summary", "yatra")}
775 648 </h4>
776 - <div className="space-y-2">
777 - {/* Gross Total */}
778 - <div className="flex justify-between text-sm">
779 - <span className="font-bold text-gray-900 dark:text-white">
780 - {__("Gross Total", "yatra")}
781 - </span>
782 - <span className="text-gray-900 dark:text-white">
783 - {formatPrice(
784 - (booking as any).subtotal || booking.total_amount,
785 - booking.currency,
786 - )}
787 - </span>
788 - </div>
649 + {(() => {
650 + // ── Raw figures ──────────────────────────────────────────
651 + const dbSubtotal =
652 + parseFloat((booking as any).subtotal) ||
653 + booking.total_amount ||
654 + 0;
655 + const discountAmount = booking.discount_amount || 0;
656 + const itineraryCostsTotal =
657 + parseFloat((booking as any).itinerary_costs_total) || 0;
658 + const taxAmount =
659 + parseFloat((booking as any).tax_amount) || 0;
660 + // Handle both PHP bool (true/false) and int/string (1/0/"1"/"0")
661 + const taxInclusiveRaw = (booking as any).tax_inclusive;
662 + const taxInclusive =
663 + taxInclusiveRaw === true ||
664 + parseInt(String(taxInclusiveRaw), 10) === 1;
665 + const totalAmount = booking.total_amount || 0;
666 + const amountPaid = booking.amount_paid || 0;
667 + const amountDue = booking.amount_due || 0;
789 668
790 - {/* Itinerary Costs */}
791 - {(() => {
792 - const itineraryCosts = (booking as any).itinerary_costs;
793 - const itineraryCostsTotal = parseFloat(
794 - (booking as any).itinerary_costs_total || 0,
669 + // ── Tax breakdown ────────────────────────────────────────
670 + let taxLines: {
671 + name: string;
672 + rate: number;
673 + amount: number;
674 + }[] = [];
675 + const taxBreakdownRaw = (booking as any).tax_breakdown;
676 + if (
677 + Array.isArray(taxBreakdownRaw) &&
678 + taxBreakdownRaw.length > 0
679 + ) {
680 + taxLines = taxBreakdownRaw;
681 + } else {
682 + const taxDetailsRaw = (booking as any).tax_details;
683 + if (taxDetailsRaw) {
684 + try {
685 + const parsed =
686 + typeof taxDetailsRaw === "string"
687 + ? JSON.parse(taxDetailsRaw)
688 + : taxDetailsRaw;
689 + if (Array.isArray(parsed) && parsed.length > 0)
690 + taxLines = parsed;
691 + } catch (_) {}
692 + }
693 + if (taxLines.length === 0 && taxAmount > 0) {
694 + taxLines = [
695 + {
696 + name: __("Tax", "yatra"),
697 + rate: parseFloat((booking as any).tax_rate) || 0,
698 + amount: taxAmount,
699 + },
700 + ];
701 + }
702 + }
703 +
704 + // ── Itinerary costs ──────────────────────────────────────
705 + let itineraryCosts: any[] = [];
706 + const itineraryCostsRaw = (booking as any).itinerary_costs;
707 + if (typeof itineraryCostsRaw === "string") {
708 + try {
709 + itineraryCosts = JSON.parse(itineraryCostsRaw);
710 + } catch (_) {}
711 + } else if (Array.isArray(itineraryCostsRaw)) {
712 + itineraryCosts = itineraryCostsRaw;
713 + }
714 +
715 + // ── Services ─────────────────────────────────────────────
716 + const rawServices = (booking as any).additional_services;
717 + const selectedServices: any[] = Array.isArray(rawServices)
718 + ? rawServices.filter((s: any) => s?.selected !== false)
719 + : [];
720 + const getServiceAmt = (s: any) =>
721 + parseFloat(
722 + s?.total_price ??
723 + s?.calculated_price ??
724 + s?.total_cost ??
725 + s?.amount ??
726 + s?.unit_price ??
727 + s?.price ??
728 + 0,
795 729 );
730 + const servicesTotal = selectedServices.reduce(
731 + (sum, s) => sum + getServiceAmt(s),
732 + 0,
733 + );
734 + const baseTripCost = dbSubtotal - servicesTotal;
796 735
797 - if (itineraryCostsTotal > 0) {
798 - let costs = [];
736 + // ── Derived amounts ──────────────────────────────────────
737 + const taxableAmount =
738 + Math.max(0, dbSubtotal - discountAmount) +
739 + itineraryCostsTotal;
799 740
800 - // Try to parse itinerary_costs if it's a string
801 - if (typeof itineraryCosts === "string") {
802 - try {
803 - costs = JSON.parse(itineraryCosts);
804 - } catch (e) {
805 - console.error(
806 - "Failed to parse itinerary_costs:",
807 - e,
808 - );
741 + const hasServices = selectedServices.length > 0;
742 + const hasDiscount = discountAmount > 0;
743 + const hasItinerary =
744 + itineraryCosts.length > 0 && itineraryCostsTotal > 0;
745 + const hasTax = taxLines.length > 0;
746 + const showTaxableRow =
747 + hasTax && (hasDiscount || hasItinerary);
748 +
749 + const Row = ({
750 + label,
751 + value,
752 + sub = false,
753 + green = false,
754 + bold = false,
755 + }: any) => (
756 + <div
757 + className={`flex justify-between text-sm${sub ? " ml-4" : ""}`}
758 + >
759 + <span
760 + className={
761 + bold
762 + ? "font-semibold text-gray-900 dark:text-white"
763 + : "text-gray-600 dark:text-gray-400"
809 764 }
810 - } else if (Array.isArray(itineraryCosts)) {
811 - costs = itineraryCosts;
812 - }
765 + >
766 + {label}
767 + </span>
768 + <span
769 + className={
770 + bold
771 + ? "font-semibold text-gray-900 dark:text-white"
772 + : green
773 + ? "text-green-600 dark:text-green-400"
774 + : "text-gray-900 dark:text-white"
775 + }
776 + >
777 + {value}
778 + </span>
779 + </div>
780 + );
813 781
814 - if (costs.length > 0) {
815 - return (
816 - <>
817 - <div className="flex justify-between text-sm font-medium">
818 - <span className="text-gray-600 dark:text-gray-400">
819 - {__("Itinerary Costs", "yatra")}
820 - </span>
821 - </div>
822 - {costs.map((cost: any, index: number) => (
823 - <div
824 - key={index}
825 - className="flex justify-between text-sm ml-4"
826 - >
827 - <span className="text-gray-600 dark:text-gray-400">
828 - {cost.name} (
829 - {cost.price_per === "person"
830 - ? "per person"
831 - : "per group"}
832 - )
833 - </span>
834 - <span className="text-gray-900 dark:text-white">
835 - {formatPrice(
836 - cost.total_cost,
837 - booking.currency,
838 - )}
839 - </span>
840 - </div>
841 - ))}
842 - <div className="flex justify-between text-sm font-medium">
843 - <span className="text-gray-600 dark:text-gray-400">
844 - {__("Itinerary Costs Total", "yatra")}
845 - </span>
846 - <span className="text-gray-900 dark:text-white">
847 - {formatPrice(
848 - itineraryCostsTotal,
849 - booking.currency,
850 - )}
851 - </span>
852 - </div>
853 - </>
854 - );
855 - } else if (itineraryCostsTotal > 0) {
856 - // Show total even if individual items aren't available
857 - return (
858 - <div className="flex justify-between text-sm">
859 - <span className="text-gray-600 dark:text-gray-400">
860 - {__("Itinerary Costs", "yatra")}
782 + return (
783 + <div className="space-y-1.5">
784 + {/* Trip Base Price — always the first line */}
785 + <div className="flex justify-between text-sm">
786 + <span className="text-gray-600 dark:text-gray-400 flex items-center gap-1.5">
787 + {__("Trip Base Price", "yatra")}
788 + {taxInclusive && (
789 + <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">
790 + {__("Tax Incl.", "yatra")}
861 791 </span>
862 - <span className="text-gray-900 dark:text-white">
863 - {formatPrice(
864 - itineraryCostsTotal,
792 + )}
793 + </span>
794 + <span className="text-gray-900 dark:text-white">
795 + {formatPrice(baseTripCost, booking.currency)}
796 + </span>
797 + </div>
798 +
799 + {/* Additional Services section — only when services exist */}
800 + {hasServices && (
801 + <>
802 + <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
803 + <span>
804 + + {__("Additional Services", "yatra")}
805 + </span>
806 + <span></span>
807 + </div>
808 + {selectedServices.map((s: any, i: number) => (
809 + <Row
810 + key={i}
811 + sub
812 + label={
813 + s?.service_name ||
814 + s?.name ||
815 + s?.title ||
816 + s?.label ||
817 + __("Service", "yatra")
818 + }
819 + value={formatPrice(
820 + getServiceAmt(s),
865 821 booking.currency,
866 822 )}
823 + />
824 + ))}
825 + {/* Gross Total = Trip Base Price + Services */}
826 + <div className="flex justify-between text-sm font-semibold border-t border-gray-200 dark:border-gray-600 pt-1.5 mt-0.5">
827 + <span className="text-gray-900 dark:text-white">
828 + {__("Gross Total", "yatra")}
867 829 </span>
830 + <span className="text-gray-900 dark:text-white">
831 + {formatPrice(dbSubtotal, booking.currency)}
832 + </span>
868 833 </div>
869 - );
870 - }
871 - }
834 + </>
835 + )}
872 836
873 - return null;
874 - })()}
837 + {/* Discount */}
838 + {hasDiscount && (
839 + <Row
840 + label={__("Discount", "yatra")}
841 + value={`-${formatPrice(discountAmount, booking.currency)}`}
842 + green
843 + />
844 + )}
875 845
876 - {/* Taxable Amount (SubTotal) - Displayed after all costs are added */}
877 - {((booking as any).tax_amount > 0 ||
878 - (booking as any).tax_breakdown?.length > 0) && (
879 - <div className="flex justify-between text-sm font-medium border-t border-gray-200 dark:border-gray-700 pt-2">
880 - <span className="font-bold text-gray-900 dark:text-white">
881 - {__("Subtotal (Taxable Amount)", "yatra")}
882 - </span>
883 - <span className="text-gray-900 dark:text-white">
884 - {formatPrice(
885 - (booking as any).taxable_amount > 0
886 - ? (booking as any).taxable_amount
887 - : (booking as any).subtotal ||
888 - booking.total_amount,
889 - booking.currency,
890 - )}
891 - </span>
892 - </div>
893 - )}
846 + {/* Itinerary Costs section */}
847 + {hasItinerary && (
848 + <>
849 + <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
850 + <span>+ {__("Itinerary Costs", "yatra")}</span>
851 + <span></span>
852 + </div>
853 + {itineraryCosts.map((cost: any, i: number) => (
854 + <Row
855 + key={i}
856 + sub
857 + label={`${cost.name} ${cost.price_per === "person" ? __("(per person)", "yatra") : cost.price_per === "group" ? __("(per booking)", "yatra") : __("(flat rate)", "yatra")}`}
858 + value={formatPrice(
859 + cost.total_cost ?? cost.price,
860 + booking.currency,
861 + )}
862 + />
863 + ))}
864 + </>
865 + )}
894 866
895 - {/* Discount if any */}
896 - {booking.discount_amount > 0 && (
897 - <div className="flex justify-between text-sm">
898 - <span className="text-gray-600 dark:text-gray-400">
899 - {__("Discount", "yatra")}
900 - </span>
901 - <span className="text-green-600 dark:text-green-400">
902 - -
903 - {formatPrice(
904 - booking.discount_amount,
905 - booking.currency,
906 - )}
907 - </span>
908 - </div>
909 - )}
867 + {/* Taxable Amount — only for exclusive tax when discount/itinerary exist above it */}
868 + {!taxInclusive && showTaxableRow && (
869 + <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">
870 + <span className="text-gray-700 dark:text-gray-300">
871 + {__("Taxable Amount", "yatra")}
872 + </span>
873 + <span className="text-gray-900 dark:text-white">
874 + {formatPrice(taxableAmount, booking.currency)}
875 + </span>
876 + </div>
877 + )}
910 878
911 - {/* Tax Breakdown */}
912 - {(parseFloat((booking as any).tax_amount) > 0 ||
913 - (booking as any).tax_breakdown?.length > 0) && (
914 - <>
915 - {(() => {
916 - // Use tax_breakdown if available (already parsed by backend)
917 - const taxBreakdown = (booking as any).tax_breakdown;
918 -
919 - if (
920 - Array.isArray(taxBreakdown) &&
921 - taxBreakdown.length > 0
922 - ) {
923 - return taxBreakdown.map(
924 - (tax: any, index: number) => (
925 - <div
926 - key={index}
927 - className="flex justify-between text-sm"
928 - >
929 - <span className="text-gray-600 dark:text-gray-400">
930 - {tax.name} ({tax.rate}%)
931 - </span>
932 - <span className="text-gray-900 dark:text-white">
933 - {formatPrice(tax.amount, booking.currency)}
934 - </span>
935 - </div>
936 - ),
937 - );
938 - }
939 -
940 - // Fallback: try parsing tax_details JSON string
941 - const taxDetails = (booking as any).tax_details;
942 - if (taxDetails) {
943 - try {
944 - const taxes =
945 - typeof taxDetails === "string"
946 - ? JSON.parse(taxDetails)
947 - : taxDetails;
948 -
949 - if (Array.isArray(taxes) && taxes.length > 0) {
950 - return taxes.map((tax: any, index: number) => (
951 - <div
952 - key={index}
953 - className="flex justify-between text-sm"
954 - >
955 - <span className="text-gray-600 dark:text-gray-400">
956 - {tax.name} ({tax.rate}%)
957 - </span>
958 - <span className="text-gray-900 dark:text-white">
959 - {formatPrice(
960 - tax.amount,
961 - booking.currency,
962 - )}
963 - </span>
964 - </div>
965 - ));
966 - }
967 - } catch (e) {
968 - console.error("Failed to parse tax_details:", e);
969 - }
970 - }
971 -
972 - // Final fallback: show single tax line
973 - if ((booking as any).tax_amount > 0) {
974 - return (
975 - <div className="flex justify-between text-sm">
879 + {/* Exclusive tax — shown as an addition before Net Amount */}
880 + {hasTax && !taxInclusive && (
881 + <>
882 + <div className="flex justify-between text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mt-1">
883 + <span>+ {__("Tax", "yatra")}</span>
884 + <span></span>
885 + </div>
886 + {taxLines.map((tax: any, i: number) => (
887 + <div
888 + key={i}
889 + className="flex justify-between text-sm ml-4"
890 + >
976 891 <span className="text-gray-600 dark:text-gray-400">
977 - {__("Tax", "yatra")} (
978 - {(booking as any).tax_rate}%)
892 + {tax.name}
893 + {tax.rate > 0 ? ` (${tax.rate}%)` : ""}
979 894 </span>
980 895 <span className="text-gray-900 dark:text-white">
896 + +
981 897 {formatPrice(
982 - (booking as any).tax_amount,
898 + parseFloat(tax.amount) || 0,
983 899 booking.currency,
984 900 )}
985 901 </span>
986 902 </div>
987 - );
988 - }
903 + ))}
904 + </>
905 + )}
989 906
990 - return null;
991 - })()}
992 - </>
993 - )}
907 + {/* Net Amount */}
908 + <div className="flex justify-between text-base font-bold border-t-2 border-gray-800 dark:border-gray-300 pt-2 mt-1">
909 + <span className="text-gray-900 dark:text-white">
910 + {__("Net Amount", "yatra")}
911 + </span>
912 + <span className="text-gray-900 dark:text-white">
913 + {formatPrice(totalAmount, booking.currency)}
914 + </span>
915 + </div>
994 916
995 - {/* Net Amount (Total with tax) */}
996 - <div className="flex justify-between text-base font-semibold pt-2 border-t border-gray-200 dark:border-gray-700">
997 - <span className="text-gray-900 dark:text-white">
998 - {__("Net Amount", "yatra")}
999 - </span>
1000 - <span className="text-gray-900 dark:text-white">
1001 - {formatPrice(
1002 - booking.total_amount || 0,
1003 - booking.currency,
917 + {/* Inclusive tax — informational footnote after Net Amount */}
918 + {hasTax && taxInclusive && (
919 + <div className="mt-1 space-y-0.5">
920 + {taxLines.map((tax: any, i: number) => (
921 + <div
922 + key={i}
923 + className="text-xs text-gray-400 dark:text-gray-500 italic"
924 + >
925 + {__("Incl.", "yatra")} {tax.name}
926 + {tax.rate > 0 ? ` (${tax.rate}%)` : ""}:{" "}
927 + {formatPrice(
928 + parseFloat(tax.amount) || 0,
929 + booking.currency,
930 + )}
931 + </div>
932 + ))}
933 + </div>
1004 934 )}
1005 - </span>
1006 - </div>
1007 - </div>
935 +
936 + {/* Amount Paid */}
937 + {amountPaid > 0 && (
938 + <Row
939 + label={__("Amount Paid", "yatra")}
940 + value={formatPrice(amountPaid, booking.currency)}
941 + green
942 + />
943 + )}
944 +
945 + {/* Due Now */}
946 + {amountDue > 0 && (
947 + <div className="flex justify-between text-sm font-semibold">
948 + <span className="text-orange-600 dark:text-orange-400">
949 + {__("Due Now", "yatra")}
950 + </span>
951 + <span className="text-orange-600 dark:text-orange-400">
952 + {formatPrice(amountDue, booking.currency)}
953 + </span>
954 + </div>
955 + )}
956 + </div>
957 + );
958 + })()}
1008 959 </div>
1009 960 </CardContent>
1010 961 </Card>
1011 962
963 + {/* Payments recorded against this booking (gateway captures and
964 + manual entries). Only completed rows count towards Amount
965 + Paid — a pending row is money expected, not received. */}
966 + {Array.isArray(booking.payments) && booking.payments.length > 0 && (
967 + <Card data-testid="booking-payments-card">
968 + <CardHeader className="pb-2">
969 + <CardTitle className="text-base">
970 + {__("Payments", "yatra")}
971 + </CardTitle>
972 + </CardHeader>
973 + <CardContent>
974 + <div className="divide-y divide-gray-100 dark:divide-gray-800">
975 + {booking.payments.map((payment: any) => {
976 + const status = String(
977 + payment.status || payment.payment_status || "pending",
978 + );
979 + const statusClass =
980 + status === "completed"
981 + ? "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400"
982 + : status === "pending"
983 + ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400"
984 + : status === "refunded"
985 + ? "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400"
986 + : "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400";
987 + const statusLabel: Record<string, string> = {
988 + completed: __("Completed", "yatra"),
989 + pending: __("Pending", "yatra"),
990 + failed: __("Failed", "yatra"),
991 + refunded: __("Refunded", "yatra"),
992 + cancelled: __("Cancelled", "yatra"),
993 + };
994 + return (
995 + <div
996 + key={payment.id}
997 + className="py-2 flex items-start justify-between gap-3 text-sm"
998 + >
999 + <div className="min-w-0">
1000 + <div className="font-medium text-gray-900 dark:text-white">
1001 + {formatPrice(
1002 + Number(payment.amount || 0),
1003 + payment.currency || booking.currency,
1004 + )}
1005 + <span className="ml-2 font-normal text-gray-500 dark:text-gray-400">
1006 + {payment.payment_method_label ||
1007 + payment.gateway ||
1008 + ""}
1009 + </span>
1010 + </div>
1011 + <div className="text-xs text-gray-500 dark:text-gray-400 truncate">
1012 + {payment.payment_date
1013 + ? formatDate(payment.payment_date)
1014 + : ""}
1015 + {payment.transaction_id
1016 + ? ` · ${payment.transaction_id}`
1017 + : ""}
1018 + {payment.payment_number
1019 + ? ` · ${payment.payment_number}`
1020 + : ""}
1021 + </div>
1022 + </div>
1023 + <span
1024 + className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium whitespace-nowrap ${statusClass}`}
1025 + >
1026 + {statusLabel[status] || status}
1027 + </span>
1028 + </div>
1029 + );
1030 + })}
1031 + </div>
1032 + </CardContent>
1033 + </Card>
1034 + )}
1035 +
1012 1036 {/* Customer Information */}
1013 1037 <Card>
1014 1038 <CardHeader className="pb-2">
1015 1039 <CardTitle className="text-base">
@@ -1028,13 +1052,69 @@
1028 1052 </div>
1029 1053 {booking.customer_phone && (
1030 1054 <div className="flex items-center gap-1.5">
1031 1055 <Phone className="w-4 h-4" />
1032 - {booking.customer_phone}
1056 + <PhoneDisplay value={booking.customer_phone} />
1033 1057 </div>
1034 1058 )}
1035 1059 </div>
1036 1060 </div>
1061 +
1062 + {/* Country + dynamic/custom contact fields (nationality,
1063 + address, and any custom contact field). Mirrors the
1064 + Emergency/Traveler dynamic-field display. */}
1065 + {(() => {
1066 + const cd =
1067 + booking.contact_data &&
1068 + typeof booking.contact_data === "object"
1069 + ? (booking.contact_data as Record<string, unknown>)
1070 + : {};
1071 + const CORE = ["first_name", "last_name", "email", "phone"];
1072 + const extras = Object.entries(cd).filter(
1073 + ([k, v]) =>
1074 + !CORE.includes(k) &&
1075 + k !== "country" &&
1076 + v != null &&
1077 + String(v).trim() !== "",
1078 + );
1079 + const countryCode =
1080 + booking.customer_country ||
1081 + (cd.country ? String(cd.country) : "");
1082 + if (!countryCode && extras.length === 0) return null;
1083 + return (
1084 + <div className="grid grid-cols-1 md:grid-cols-3 gap-3 pt-1 border-t border-gray-100 dark:border-gray-700">
1085 + {countryCode && (
1086 + <div>
1087 + <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1088 + {getFieldLabel("country", contactFields)}
1089 + </div>
1090 + <div className="text-sm font-medium text-gray-900 dark:text-white">
1091 + {getCountryName(countryCode)}
1092 + </div>
1093 + </div>
1094 + )}
1095 + {extras.map(([fieldId, value]) => {
1096 + const field = contactFields.find(
1097 + (f) => f.id === fieldId,
1098 + );
1099 + const display =
1100 + field?.type === "country"
1101 + ? getCountryName(String(value))
1102 + : optionLabel(field, value);
1103 + return (
1104 + <div key={fieldId}>
1105 + <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1106 + {getFieldLabel(fieldId, contactFields)}
1107 + </div>
1108 + <div className="text-sm font-medium text-gray-900 dark:text-white">
1109 + {display}
1110 + </div>
1111 + </div>
1112 + );
1113 + })}
1114 + </div>
1115 + );
1116 + })()}
1037 1117 </CardContent>
1038 1118 </Card>
1039 1119
1040 1120 {/* Travelers Information - Dynamic Fields */}
@@ -1143,22 +1223,20 @@
1143 1223 const isLongField =
1144 1224 fieldConfig?.type === "textarea" ||
1145 1225 String(fieldValue).length > 50;
1146 1226
1147 - // Format date fields
1227 + // Format date fields (e.g. Date of Birth) with the
1228 + // global WP date-format setting via the shared
1229 + // formatter — not the browser locale.
1148 1230 let displayValue = String(fieldValue);
1149 1231 if (
1150 1232 fieldConfig?.type === "date" ||
1151 1233 fieldId.includes("date") ||
1234 + fieldId.includes("birth") ||
1235 + fieldId.includes("dob") ||
1152 1236 fieldId.includes("expiry")
1153 1237 ) {
1154 - try {
1155 - displayValue = new Date(
1156 - fieldValue as string,
1157 - ).toLocaleDateString();
1158 - } catch {
1159 - displayValue = String(fieldValue);
1160 - }
1238 + displayValue = formatDate(fieldValue as string);
1161 1239 }
1162 1240
1163 1241 // Format country/nationality fields - convert code to full name
1164 1242 if (
@@ -1170,8 +1248,20 @@
1170 1248 ) {
1171 1249 displayValue = getCountryName(fieldValue);
1172 1250 }
1173 1251
1252 + // Dropdown answers: option label instead of value.
1253 + // The label is the operator's own text, so it is
1254 + // shown as written (no title-casing).
1255 + let asWritten = false;
1256 + if (fieldConfig?.type === "select") {
1257 + displayValue = optionLabel(
1258 + fieldConfig,
1259 + fieldValue,
1260 + );
1261 + asWritten = displayValue !== String(fieldValue);
1262 + }
1263 +
1174 1264 return (
1175 1265 <div
1176 1266 key={fieldId}
1177 1267 className={
@@ -1183,9 +1273,9 @@
1183 1273 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1184 1274 {label}
1185 1275 </div>
1186 1276 <div
1187 - className={`text-sm text-gray-900 dark:text-white ${fieldId === "passport" ? "font-mono" : ""} capitalize`}
1277 + className={`text-sm text-gray-900 dark:text-white ${fieldId === "passport" ? "font-mono" : ""} ${asWritten ? "" : "capitalize"}`}
1188 1278 >
1189 1279 {displayValue}
1190 1280 </div>
1191 1281 </div>
@@ -1229,15 +1319,24 @@
1229 1319 ([_, value]) => value && String(value).trim() !== "",
1230 1320 )
1231 1321 .map(([fieldId, fieldValue]) => {
1232 1322 const label = getFieldLabel(fieldId, emergencyFields);
1323 + const emergencyField = emergencyFields.find(
1324 + (f) => f.id === fieldId,
1325 + );
1326 + const emergencyDisplay = optionLabel(
1327 + emergencyField,
1328 + fieldValue,
1329 + );
1233 1330 return (
1234 1331 <div key={fieldId}>
1235 1332 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1236 1333 {label}
1237 1334 </div>
1238 - <div className="text-sm font-medium text-gray-900 dark:text-white capitalize">
1239 - {String(fieldValue)}
1335 + <div
1336 + className={`text-sm font-medium text-gray-900 dark:text-white ${emergencyDisplay !== String(fieldValue) ? "" : "capitalize"}`}
1337 + >
1338 + {emergencyDisplay}
1240 1339 </div>
1241 1340 </div>
1242 1341 );
1243 1342 })}
@@ -1375,24 +1474,66 @@
1375 1474 </div>
1376 1475 </div>
1377 1476 )}
1378 1477 <div>
1379 - <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1478 + <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1380 1479 {__("Trip Price per Person", "yatra")}
1480 + {((v) => v === true || parseInt(String(v), 10) === 1)(
1481 + (booking as any).tax_inclusive,
1482 + ) && (
1483 + <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">
1484 + {__("Tax Incl.", "yatra")}
1485 + </span>
1486 + )}
1381 1487 </div>
1382 1488 <div className="text-sm text-gray-900 dark:text-white">
1383 1489 {(() => {
1490 + // subtotal (DB) = base trip cost + services when Pro is active.
1491 + // Subtract services total to get the pure trip base cost.
1384 1492 const subtotal =
1385 - (booking as any).subtotal || booking.total_amount;
1493 + parseFloat((booking as any).subtotal) ||
1494 + booking.total_amount ||
1495 + 0;
1386 1496 const travelers = booking.travelers || 1;
1387 - const pricePerPerson = subtotal / travelers;
1388 - return formatPrice(pricePerPerson, booking.currency);
1497 +
1498 + const rawServices = (booking as any).additional_services;
1499 + const servicesTotal = Array.isArray(rawServices)
1500 + ? rawServices
1501 + .filter((s: any) => s?.selected !== false)
1502 + .reduce(
1503 + (sum: number, s: any) =>
1504 + sum +
1505 + parseFloat(
1506 + s?.total_price ??
1507 + s?.calculated_price ??
1508 + s?.total_cost ??
1509 + s?.amount ??
1510 + s?.unit_price ??
1511 + s?.price ??
1512 + 0,
1513 + ),
1514 + 0,
1515 + )
1516 + : 0;
1517 +
1518 + const baseTripCost = subtotal - servicesTotal;
1519 + return formatPrice(
1520 + baseTripCost / travelers,
1521 + booking.currency,
1522 + );
1389 1523 })()}
1390 1524 </div>
1391 1525 </div>
1392 1526 <div>
1393 - <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
1527 + <div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 flex items-center gap-1">
1394 1528 {__("Total Amount", "yatra")}
1529 + {((v) => v === true || parseInt(String(v), 10) === 1)(
1530 + (booking as any).tax_inclusive,
1531 + ) && (
1532 + <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">
1533 + {__("Tax Incl.", "yatra")}
1534 + </span>
1535 + )}
1395 1536 </div>
1396 1537 <div className="text-lg font-semibold text-gray-900 dark:text-white">
1397 1538 {formatPrice(booking.total_amount || 0, booking.currency)}
1398 1539 </div>