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 +279 -263 3.0.2.9 → 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[],
@@ -209,8 +203,50 @@
209 203 },
210 204 enabled: !!bookingId && can("yatra_view_bookings"),
211 205 });
212 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 +
213 249 // Fetch consent status for this booking (only if Pro is active)
214 250 const isPro = !!(window as any).yatraAdmin?.isPro;
215 251 const { data: consentStatus } = useQuery({
216 252 queryKey: ["booking-consent-status", bookingId],
@@ -227,229 +263,13 @@
227 263 const formatDate = (dateString: string) => {
228 264 return formatDateUtil(dateString);
229 265 };
230 266
231 - const formatPrice = (price: number, currencyCode: string = "USD") => {
232 - // Always format the price, even if 0 - don't show "Contact for pricing" for bookings
233 - const numPrice = Number(price) || 0;
234 - const symbol = getCurrencySymbol(currencyCode);
235 - const currencyData = getCurrency(currencyCode);
236 - const decimals = currencyData?.decimalDigits ?? 2;
267 + const formatPrice = (price: number, currencyCode: string = "USD") =>
268 + formatYatraMoney(Number(price) || 0, currencyCode, {
269 + zeroAsUnknown: false,
270 + });
237 271
238 - const formatted = new Intl.NumberFormat(undefined, {
239 - minimumFractionDigits: decimals,
240 - maximumFractionDigits: decimals,
241 - }).format(numPrice);
242 -
243 - return `${symbol}${formatted}`;
244 - };
245 -
246 - // Country code to name mapping
247 - const COUNTRY_NAMES: Record<string, string> = {
248 - AF: "Afghanistan",
249 - AL: "Albania",
250 - DZ: "Algeria",
251 - AD: "Andorra",
252 - AO: "Angola",
253 - AG: "Antigua and Barbuda",
254 - AR: "Argentina",
255 - AM: "Armenia",
256 - AU: "Australia",
257 - AT: "Austria",
258 - AZ: "Azerbaijan",
259 - BS: "Bahamas",
260 - BH: "Bahrain",
261 - BD: "Bangladesh",
262 - BB: "Barbados",
263 - BY: "Belarus",
264 - BE: "Belgium",
265 - BZ: "Belize",
266 - BJ: "Benin",
267 - BT: "Bhutan",
268 - BO: "Bolivia",
269 - BA: "Bosnia and Herzegovina",
270 - BW: "Botswana",
271 - BR: "Brazil",
272 - BN: "Brunei",
273 - BG: "Bulgaria",
274 - BF: "Burkina Faso",
275 - BI: "Burundi",
276 - KH: "Cambodia",
277 - CM: "Cameroon",
278 - CA: "Canada",
279 - CV: "Cape Verde",
280 - CF: "Central African Republic",
281 - TD: "Chad",
282 - CL: "Chile",
283 - CN: "China",
284 - CO: "Colombia",
285 - KM: "Comoros",
286 - CG: "Congo",
287 - CD: "DR Congo",
288 - CR: "Costa Rica",
289 - CI: "Ivory Coast",
290 - HR: "Croatia",
291 - CU: "Cuba",
292 - CY: "Cyprus",
293 - CZ: "Czech Republic",
294 - DK: "Denmark",
295 - DJ: "Djibouti",
296 - DM: "Dominica",
297 - DO: "Dominican Republic",
298 - EC: "Ecuador",
299 - EG: "Egypt",
300 - SV: "El Salvador",
301 - GQ: "Equatorial Guinea",
302 - ER: "Eritrea",
303 - EE: "Estonia",
304 - SZ: "Eswatini",
305 - ET: "Ethiopia",
306 - FJ: "Fiji",
307 - FI: "Finland",
308 - FR: "France",
309 - GA: "Gabon",
310 - GM: "Gambia",
311 - GE: "Georgia",
312 - DE: "Germany",
313 - GH: "Ghana",
314 - GR: "Greece",
315 - GD: "Grenada",
316 - GT: "Guatemala",
317 - GN: "Guinea",
318 - GW: "Guinea-Bissau",
319 - GY: "Guyana",
320 - HT: "Haiti",
321 - HN: "Honduras",
322 - HU: "Hungary",
323 - IS: "Iceland",
324 - IN: "India",
325 - ID: "Indonesia",
326 - IR: "Iran",
327 - IQ: "Iraq",
328 - IE: "Ireland",
329 - IL: "Israel",
330 - IT: "Italy",
331 - JM: "Jamaica",
332 - JP: "Japan",
333 - JO: "Jordan",
334 - KZ: "Kazakhstan",
335 - KE: "Kenya",
336 - KI: "Kiribati",
337 - KP: "North Korea",
338 - KR: "South Korea",
339 - KW: "Kuwait",
340 - KG: "Kyrgyzstan",
341 - LA: "Laos",
342 - LV: "Latvia",
343 - LB: "Lebanon",
344 - LS: "Lesotho",
345 - LR: "Liberia",
346 - LY: "Libya",
347 - LI: "Liechtenstein",
348 - LT: "Lithuania",
349 - LU: "Luxembourg",
350 - MG: "Madagascar",
351 - MW: "Malawi",
352 - MY: "Malaysia",
353 - MV: "Maldives",
354 - ML: "Mali",
355 - MT: "Malta",
356 - MH: "Marshall Islands",
357 - MR: "Mauritania",
358 - MU: "Mauritius",
359 - MX: "Mexico",
360 - FM: "Micronesia",
361 - MD: "Moldova",
362 - MC: "Monaco",
363 - MN: "Mongolia",
364 - ME: "Montenegro",
365 - MA: "Morocco",
366 - MZ: "Mozambique",
367 - MM: "Myanmar",
368 - NA: "Namibia",
369 - NR: "Nauru",
370 - NP: "Nepal",
371 - NL: "Netherlands",
372 - NZ: "New Zealand",
373 - NI: "Nicaragua",
374 - NE: "Niger",
375 - NG: "Nigeria",
376 - MK: "North Macedonia",
377 - NO: "Norway",
378 - OM: "Oman",
379 - PK: "Pakistan",
380 - PW: "Palau",
381 - PS: "Palestine",
382 - PA: "Panama",
383 - PG: "Papua New Guinea",
384 - PY: "Paraguay",
385 - PE: "Peru",
386 - PH: "Philippines",
387 - PL: "Poland",
388 - PT: "Portugal",
389 - QA: "Qatar",
390 - RO: "Romania",
391 - RU: "Russia",
392 - RW: "Rwanda",
393 - KN: "Saint Kitts and Nevis",
394 - LC: "Saint Lucia",
395 - VC: "Saint Vincent and the Grenadines",
396 - WS: "Samoa",
397 - SM: "San Marino",
398 - ST: "Sao Tome and Principe",
399 - SA: "Saudi Arabia",
400 - SN: "Senegal",
401 - RS: "Serbia",
402 - SC: "Seychelles",
403 - SL: "Sierra Leone",
404 - SG: "Singapore",
405 - SK: "Slovakia",
406 - SI: "Slovenia",
407 - SB: "Solomon Islands",
408 - SO: "Somalia",
409 - ZA: "South Africa",
410 - SS: "South Sudan",
411 - ES: "Spain",
412 - LK: "Sri Lanka",
413 - SD: "Sudan",
414 - SR: "Suriname",
415 - SE: "Sweden",
416 - CH: "Switzerland",
417 - SY: "Syria",
418 - TW: "Taiwan",
419 - TJ: "Tajikistan",
420 - TZ: "Tanzania",
421 - TH: "Thailand",
422 - TL: "Timor-Leste",
423 - TG: "Togo",
424 - TO: "Tonga",
425 - TT: "Trinidad and Tobago",
426 - TN: "Tunisia",
427 - TR: "Turkey",
428 - TM: "Turkmenistan",
429 - TV: "Tuvalu",
430 - UG: "Uganda",
431 - UA: "Ukraine",
432 - AE: "United Arab Emirates",
433 - GB: "United Kingdom",
434 - US: "United States",
435 - UY: "Uruguay",
436 - UZ: "Uzbekistan",
437 - VU: "Vanuatu",
438 - VA: "Vatican City",
439 - VE: "Venezuela",
440 - VN: "Vietnam",
441 - YE: "Yemen",
442 - ZM: "Zambia",
443 - ZW: "Zimbabwe",
444 - };
445 -
446 - const getCountryName = (code: string): string => {
447 - if (!code) return "";
448 - const upperCode = code.toUpperCase();
449 - return COUNTRY_NAMES[upperCode] || code;
450 - };
451 -
452 272 const getBookingStatusBadge = (status: string) => {
453 273 const statusMap: Record<string, { className: string; label: string }> = {
454 274 confirmed: {
455 275 className:
@@ -534,8 +354,29 @@
534 354 const handleEdit = () => {
535 355 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=edit&id=${bookingId}`;
536 356 };
537 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 +
538 379 if (isLoading) {
539 380 return (
540 381 <div className="space-y-3">
541 382 {/* Header Skeleton */}
@@ -685,9 +526,26 @@
685 526 title={__("Booking Details", "yatra")}
686 527 description={__("View complete booking information", "yatra")}
687 528 actions={
688 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>
689 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">
690 548 <Button onClick={handleEdit} className="flex items-center gap-2">
691 549 {__("Edit Booking", "yatra")}
692 550 </Button>
693 551 </ConditionalRender>
@@ -702,8 +560,18 @@
702 560 </div>
703 561 }
704 562 />
705 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 +
706 574 <ConditionalRender capability="yatra_view_bookings">
707 575 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
708 576 {/* Main Content */}
709 577 <div className="lg:col-span-2 space-y-3">
@@ -1091,8 +959,81 @@
1091 959 </div>
1092 960 </CardContent>
1093 961 </Card>
1094 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 +
1095 1036 {/* Customer Information */}
1096 1037 <Card>
1097 1038 <CardHeader className="pb-2">
1098 1039 <CardTitle className="text-base">
@@ -1111,13 +1052,69 @@
1111 1052 </div>
1112 1053 {booking.customer_phone && (
1113 1054 <div className="flex items-center gap-1.5">
1114 1055 <Phone className="w-4 h-4" />
1115 - {booking.customer_phone}
1056 + <PhoneDisplay value={booking.customer_phone} />
1116 1057 </div>
1117 1058 )}
1118 1059 </div>
1119 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 + })()}
1120 1117 </CardContent>
1121 1118 </Card>
1122 1119
1123 1120 {/* Travelers Information - Dynamic Fields */}
@@ -1226,22 +1223,20 @@
1226 1223 const isLongField =
1227 1224 fieldConfig?.type === "textarea" ||
1228 1225 String(fieldValue).length > 50;
1229 1226
1230 - // 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.
1231 1230 let displayValue = String(fieldValue);
1232 1231 if (
1233 1232 fieldConfig?.type === "date" ||
1234 1233 fieldId.includes("date") ||
1234 + fieldId.includes("birth") ||
1235 + fieldId.includes("dob") ||
1235 1236 fieldId.includes("expiry")
1236 1237 ) {
1237 - try {
1238 - displayValue = new Date(
1239 - fieldValue as string,
1240 - ).toLocaleDateString();
1241 - } catch {
1242 - displayValue = String(fieldValue);
1243 - }
1238 + displayValue = formatDate(fieldValue as string);
1244 1239 }
1245 1240
1246 1241 // Format country/nationality fields - convert code to full name
1247 1242 if (
@@ -1253,8 +1248,20 @@
1253 1248 ) {
1254 1249 displayValue = getCountryName(fieldValue);
1255 1250 }
1256 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 +
1257 1264 return (
1258 1265 <div
1259 1266 key={fieldId}
1260 1267 className={
@@ -1266,9 +1273,9 @@
1266 1273 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1267 1274 {label}
1268 1275 </div>
1269 1276 <div
1270 - 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"}`}
1271 1278 >
1272 1279 {displayValue}
1273 1280 </div>
1274 1281 </div>
@@ -1312,15 +1319,24 @@
1312 1319 ([_, value]) => value && String(value).trim() !== "",
1313 1320 )
1314 1321 .map(([fieldId, fieldValue]) => {
1315 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 + );
1316 1330 return (
1317 1331 <div key={fieldId}>
1318 1332 <div className="text-xs text-gray-500 dark:text-gray-400 mb-0.5">
1319 1333 {label}
1320 1334 </div>
1321 - <div className="text-sm font-medium text-gray-900 dark:text-white capitalize">
1322 - {String(fieldValue)}
1335 + <div
1336 + className={`text-sm font-medium text-gray-900 dark:text-white ${emergencyDisplay !== String(fieldValue) ? "" : "capitalize"}`}
1337 + >
1338 + {emergencyDisplay}
1323 1339 </div>
1324 1340 </div>
1325 1341 );
1326 1342 })}