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/BookingForm.tsx +499 -188 3.0.6 → 3.0.17 View file →
@@ -21,10 +21,12 @@
21 21 import { __ } from "../lib/i18n";
22 22 import {
23 23 formatDate as formatDateUtil,
24 24 formatDateForInput,
25 + todayYmd,
25 26 } from "../lib/dateFormat";
26 -import { getCountryOptions } from "../lib/countries";
27 +import { getCountrySelectOptions } from "../lib/countries";
28 +import { SearchableSelect } from "../components/ui/searchable-select";
27 29 import { apiService } from "../lib/api-client";
28 30 import { usePermissions } from "../hooks/usePermissions";
29 31 import { getCurrencySymbol } from "../data/currencies";
30 32 import { Button } from "../components/ui/button";
@@ -57,8 +59,10 @@
57 59 order: number;
58 60 width?: string;
59 61 options?: { value: string; label: string }[];
60 62 section?: string;
63 + /** Traveler section: "lead" shows only for the lead traveler; "all"/absent = every traveler. */
64 + applies_to?: "all" | "lead";
61 65 }
62 66
63 67 interface FormSectionConfig {
64 68 title: string;
@@ -80,8 +84,9 @@
80 84 customer_country: string;
81 85 trip_id: string;
82 86 booking_date: string;
83 87 travel_date: string;
88 + departure_time: string;
84 89 travelers: string;
85 90 subtotal: string;
86 91 tax_amount: string;
87 92 total_amount: string;
@@ -96,10 +101,20 @@
96 101 // FormatHelper::getCountries() → localized to window.yatraAdmin.countries).
97 102 // One source of truth; operators that want a curated subset apply
98 103 // the `yatra_countries_list` PHP filter once and every dropdown
99 104 // (admin + public booking + Pro modules) picks it up.
100 -const countryList = getCountryOptions();
105 +const countrySelectOptions = getCountrySelectOptions();
101 106
107 +// Core contact fields rendered explicitly (name/email/phone/country); everything
108 +// else in the contact form is treated as an "extra/custom" field.
109 +const CORE_CONTACT_IDS = [
110 + "first_name",
111 + "last_name",
112 + "email",
113 + "phone",
114 + "country",
115 +];
116 +
102 117 // Normalize ISO/date-like string to formatted date using shared date library
103 118 const normalizeDateInput = (value?: string | null) => {
104 119 if (!value) return "";
105 120 // DatePicker strictly requires YYYY-MM-DD; use formatDateForInput to ensure correct format
@@ -119,10 +134,11 @@
119 134 customer_email: "",
120 135 customer_phone: "",
121 136 customer_country: "",
122 137 trip_id: "",
123 - booking_date: normalizeDateInput(new Date().toISOString()),
138 + booking_date: todayYmd(),
124 139 travel_date: "",
140 + departure_time: "",
125 141 travelers: "1",
126 142 subtotal: "",
127 143 tax_amount: "",
128 144 total_amount: "",
@@ -136,8 +152,14 @@
136 152 const [expandedTravelers, setExpandedTravelers] = useState<number[]>([0]);
137 153 const [emergencyContactData, setEmergencyContactData] = useState<
138 154 Record<string, string>
139 155 >({});
156 + // Extra/custom contact fields (everything beyond the core name/email/phone/
157 + // country handled by formData), keyed by field id — kept in sync with the
158 + // booking's contact_data JSON so admins can edit custom contact fields.
159 + const [contactExtraData, setContactExtraData] = useState<
160 + Record<string, string>
161 + >({});
140 162 const [errors, setErrors] = useState<Record<string, string>>({});
141 163 const [isSubmitting, setIsSubmitting] = useState(false);
142 164
143 165 // Trip search state
@@ -158,13 +180,15 @@
158 180 document.addEventListener("mousedown", handleClickOutside);
159 181 return () => document.removeEventListener("mousedown", handleClickOutside);
160 182 }, []);
161 183
162 - // Fetch booking form configuration
184 + // Fetch booking form configuration, resolved for the selected trip so a
185 + // per-trip form version (Pro conditions) shows the right fields here too.
186 + const selectedTripId = formData.trip_id ? Number(formData.trip_id) : null;
163 187 const { data: formConfig } = useQuery<BookingFormConfig>({
164 - queryKey: ["booking-form-config"],
188 + queryKey: ["booking-form-config", selectedTripId],
165 189 queryFn: async () => {
166 - const response = await apiService.getSettings();
190 + const response = await apiService.getBookingFormConfig(selectedTripId);
167 191 return (
168 192 response?.data?.booking_form_config ||
169 193 response?.booking_form_config ||
170 194 null
@@ -187,8 +211,18 @@
187 211 .filter((field) => field.enabled)
188 212 .sort((a, b) => a.order - b.order);
189 213 }, [formConfig]);
190 214
215 + // Contact fields beyond the core ones already rendered above (name/email/
216 + // phone/country) — i.e. nationality, address, and any CUSTOM contact fields.
217 + const contactExtraFields = useMemo(() => {
218 + if (!formConfig?.contact_form?.fields) return [];
219 + if (formConfig.contact_form.enabled === false) return [];
220 + return formConfig.contact_form.fields
221 + .filter((field) => field.enabled && !CORE_CONTACT_IDS.includes(field.id))
222 + .sort((a, b) => a.order - b.order);
223 + }, [formConfig]);
224 +
191 225 // Create empty traveler based on enabled fields
192 226 const createEmptyTraveler = (): TravelerData => {
193 227 const empty: TravelerData = {};
194 228 travelerFields.forEach((field) => {
@@ -240,8 +274,37 @@
240 274 enabled: can("yatra_view_bookings") || can("yatra_view_trips"),
241 275 retry: 1,
242 276 });
243 277
278 + // Departures available on the chosen date. A trip can run several a day, and
279 + // capacity is tracked per departure — so the operator has to say which one this
280 + // booking is for, otherwise it occupies no slot at all.
281 + const { data: availableDatesData } = useQuery({
282 + queryKey: ["booking-form-available-dates", formData.trip_id],
283 + queryFn: async () => {
284 + if (!formData.trip_id) return null;
285 + return await apiService.getTripAvailableDates(formData.trip_id);
286 + },
287 + enabled: !!formData.trip_id,
288 + retry: 1,
289 + });
290 +
291 + const departureTimesForDate = React.useMemo(() => {
292 + const rows =
293 + (availableDatesData as any)?.data ?? (availableDatesData as any) ?? [];
294 + if (!Array.isArray(rows) || !formData.travel_date) return [];
295 + const row = rows.find(
296 + (r: any) => (r?.date ?? r?.departure_date) === formData.travel_date,
297 + );
298 + if (!row) return [];
299 + // `departure_times` lists every departure the date runs; `time` is the
300 + // single-slot fallback for dates that carry one.
301 + const times: string[] = Array.isArray((row as any).departure_times)
302 + ? (row as any).departure_times
303 + : [(row as any).time ?? (row as any).departure_time ?? ""];
304 + return Array.from(new Set(times.filter((t) => !!t)));
305 + }, [availableDatesData, formData.travel_date]);
306 +
244 307 // Fetch booking data if editing
245 308 const { data: bookingData, isLoading: isLoadingBooking } = useQuery({
246 309 queryKey: ["booking", bookingId],
247 310 queryFn: async () => {
@@ -362,8 +425,9 @@
362 425 booking_status: booking.booking_status || "pending",
363 426 payment_method: booking.payment_method || "",
364 427 notes: booking.notes || "",
365 428 emergency_contact: emergencyContact,
429 + contact_data: (booking as any).contact_data || null,
366 430 };
367 431
368 432 return mappedData;
369 433 }
@@ -386,11 +450,11 @@
386 450 (bookingData as any).contact_country ||
387 451 (bookingData as any).customer_country ||
388 452 "",
389 453 trip_id: String(bookingData.trip_id || ""),
390 - booking_date:
391 - bookingData.booking_date || new Date().toISOString().split("T")[0],
454 + booking_date: bookingData.booking_date || todayYmd(),
392 455 travel_date: bookingData.travel_date || "",
456 + departure_time: (bookingData as any).departure_time || "",
393 457 travelers: String(
394 458 (bookingData as any).travelers_count ||
395 459 bookingData.travelers_data?.length ||
396 460 "1",
@@ -426,8 +490,21 @@
426 490 if (bookingData.emergency_contact) {
427 491 setEmergencyContactData(bookingData.emergency_contact);
428 492 }
429 493
494 + // Set extra/custom contact fields from the booking's contact_data JSON
495 + // (everything beyond the core name/email/phone/country handled above).
496 + const cd = (bookingData as any).contact_data;
497 + if (cd && typeof cd === "object") {
498 + const extras: Record<string, string> = {};
499 + Object.entries(cd).forEach(([k, v]) => {
500 + if (!CORE_CONTACT_IDS.includes(k)) {
501 + extras[k] = v == null ? "" : String(v);
502 + }
503 + });
504 + setContactExtraData(extras);
505 + }
506 +
430 507 setIsDataLoaded(true);
431 508 }
432 509 }, [bookingData, isEditMode]);
433 510
@@ -481,8 +558,98 @@
481 558 const handleEmergencyContactChange = (field: string, value: string) => {
482 559 setEmergencyContactData((prev) => ({ ...prev, [field]: value }));
483 560 };
484 561
562 + const handleContactExtraChange = (field: string, value: string) => {
563 + setContactExtraData((prev) => ({ ...prev, [field]: value }));
564 + };
565 +
566 + // Shared renderer for a dynamic form field's input (matches the checkout
567 + // field types). Used for the extra/custom contact fields.
568 + const renderDynamicInput = (
569 + field: FormFieldConfig,
570 + value: string,
571 + onChange: (v: string) => void,
572 + idPrefix: string,
573 + ) => {
574 + const id = `${idPrefix}-${field.id}`;
575 + if (field.type === "select") {
576 + return (
577 + <Select
578 + id={id}
579 + value={value}
580 + onChange={(e) => onChange(e.target.value)}
581 + >
582 + <option value="">
583 + {field.placeholder || `Select ${field.label}`}
584 + </option>
585 + {field.options?.map((opt) => (
586 + <option key={opt.value} value={opt.value}>
587 + {opt.label}
588 + </option>
589 + ))}
590 + </Select>
591 + );
592 + }
593 + if (field.type === "country") {
594 + // Searchable list with national flags, matching the public booking form
595 + // and the phone country-code control.
596 + return (
597 + <SearchableSelect
598 + value={value}
599 + onChange={onChange}
600 + options={countrySelectOptions}
601 + placeholder={field.placeholder || __("Select Country", "yatra")}
602 + searchPlaceholder={__("Search country", "yatra")}
603 + />
604 + );
605 + }
606 + if (field.type === "textarea") {
607 + return (
608 + <textarea
609 + id={id}
610 + value={value}
611 + onChange={(e) => onChange(e.target.value)}
612 + placeholder={field.placeholder}
613 + rows={2}
614 + className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:placeholder:text-gray-400 resize-none"
615 + />
616 + );
617 + }
618 + if (field.type === "checkbox") {
619 + return (
620 + <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
621 + <input
622 + type="checkbox"
623 + className="rounded border-gray-300"
624 + checked={value === "1" || value === "true"}
625 + onChange={(e) => onChange(e.target.checked ? "1" : "")}
626 + />
627 + <span>{field.placeholder || field.label}</span>
628 + </label>
629 + );
630 + }
631 + return (
632 + <Input
633 + id={id}
634 + type={
635 + field.type === "email"
636 + ? "email"
637 + : field.type === "tel"
638 + ? "tel"
639 + : field.type === "date"
640 + ? "date"
641 + : field.type === "number"
642 + ? "number"
643 + : "text"
644 + }
645 + value={value}
646 + onChange={(e) => onChange(e.target.value)}
647 + placeholder={field.placeholder}
648 + />
649 + );
650 + };
651 +
485 652 // Traveler management functions
486 653 const handleTravelerChange = (
487 654 index: number,
488 655 field: keyof TravelerData,
@@ -494,8 +661,47 @@
494 661 return updated;
495 662 });
496 663 };
497 664
665 + // Typing in "Number of Travelers" has to resize the traveller list, not just
666 + // the number itself: the booking is saved with `travelers_count:
667 + // travelersData.length`, so a number typed here used to be dropped on save and
668 + // the change looked like it had never been made. Mirrors addTraveler /
669 + // removeTraveler so both routes stay in step.
670 + const handleTravelersCountChange = (value: string) => {
671 + setFormData((prev) => ({ ...prev, travelers: value }));
672 + if (errors.travelers) {
673 + setErrors((prev) => ({ ...prev, travelers: "" }));
674 + }
675 +
676 + // Allow the field to be cleared while typing without collapsing the list.
677 + if (value.trim() === "") {
678 + return;
679 + }
680 +
681 + const parsed = parseInt(value, 10);
682 + if (isNaN(parsed) || parsed < 1) {
683 + return;
684 + }
685 +
686 + setTravelersData((prev) => {
687 + if (parsed === prev.length) {
688 + return prev;
689 + }
690 + if (parsed < prev.length) {
691 + // Trim from the end so details already entered for earlier travellers stay.
692 + return prev.slice(0, parsed);
693 + }
694 + const grown = [...prev];
695 + while (grown.length < parsed) {
696 + grown.push(createEmptyTraveler());
697 + }
698 + return grown;
699 + });
700 +
701 + setExpandedTravelers((prev) => prev.filter((i) => i < parsed));
702 + };
703 +
498 704 const addTraveler = () => {
499 705 const newTraveler = createEmptyTraveler();
500 706 setTravelersData((prev) => [...prev, newTraveler]);
501 707 setFormData((prev) => ({
@@ -543,8 +749,11 @@
543 749 if (!formData.booking_date) {
544 750 newErrors.booking_date = __("Booking date is required", "yatra");
545 751 }
546 752
753 + if (departureTimesForDate.length > 1 && !formData.departure_time) {
754 + newErrors.departure_time = __("Departure time is required", "yatra");
755 + }
547 756 if (!formData.travel_date) {
548 757 newErrors.travel_date = __("Travel date is required", "yatra");
549 758 }
550 759
@@ -578,10 +787,16 @@
578 787 contact_first_name: firstName,
579 788 contact_last_name: lastName,
580 789 contact_email: data.customer_email.trim(),
581 790 contact_phone: data.customer_phone.trim(),
791 + // contact_country column (previously not submitted, so edits were lost).
792 + contact_country: data.customer_country || "",
582 793 trip_id: parseInt(data.trip_id),
583 794 travel_date: data.travel_date,
795 + // Which departure the booking occupies. A trip running several
796 + // departures a day counts capacity per (date, time), so a booking saved
797 + // without this matches no slot and never reduces frontend availability.
798 + departure_time: data.departure_time || "",
584 799 travelers_count: travelersData.length,
585 800 total_amount: parseFloat(data.total_amount),
586 801 payment_status: data.payment_status,
587 802 status: data.booking_status,
@@ -590,8 +805,18 @@
590 805 // Include travelers data - use 'travelers' key as expected by BookingService.updateBooking
591 806 travelers: travelersData,
592 807 // Include emergency contact data
593 808 emergency_contact: emergencyContactData,
809 + // Full contact_data JSON: core fields + every extra/custom contact field,
810 + // so admin edits round-trip and feed the {{contact_*}} email variables.
811 + contact_data: {
812 + first_name: firstName,
813 + last_name: lastName,
814 + email: data.customer_email.trim(),
815 + phone: data.customer_phone.trim(),
816 + country: data.customer_country || "",
817 + ...contactExtraData,
818 + },
594 819 };
595 820
596 821 return isEditMode
597 822 ? await apiService.updateBooking(bookingId!, payload)
@@ -602,12 +827,33 @@
602 827 // Redirect to bookings list
603 828 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
604 829 },
605 830 onError: (error: any) => {
831 + // The REST API returns field-level reasons under
832 + // response.data.data.validation_errors (WP_Error shape). The generic
833 + // "Booking validation failed" message alone hid which field was at fault
834 + // (e.g. a locale-formatted amount), so surface the specific reasons.
835 + const validationErrors =
836 + error?.response?.data?.data?.validation_errors ||
837 + error?.response?.data?.validation_errors;
838 +
839 + const fieldErrors: Record<string, string> = {};
840 + let detail = "";
841 + if (validationErrors && typeof validationErrors === "object") {
842 + for (const [field, messages] of Object.entries(validationErrors)) {
843 + const msg = Array.isArray(messages)
844 + ? String(messages[0])
845 + : String(messages);
846 + fieldErrors[field] = msg;
847 + detail = detail ? `${detail} ${msg}` : msg;
848 + }
849 + }
850 +
606 851 const errorMessage =
852 + detail ||
607 853 error?.message ||
608 854 __("An error occurred while saving the booking", "yatra");
609 - setErrors({ submit: errorMessage });
855 + setErrors({ ...fieldErrors, submit: errorMessage });
610 856 setIsSubmitting(false);
611 857 },
612 858 });
613 859
@@ -861,24 +1107,41 @@
861 1107 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
862 1108 >
863 1109 {__("Country", "yatra")}
864 1110 </label>
865 - <select
866 - id="customer_country"
1111 + <SearchableSelect
867 1112 value={formData.customer_country}
868 - onChange={(e) =>
869 - handleFieldChange("customer_country", e.target.value)
870 - }
871 - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1113 + onChange={(v) => handleFieldChange("customer_country", v)}
1114 + options={countrySelectOptions}
1115 + placeholder={__("Select country", "yatra")}
1116 + searchPlaceholder={__("Search country", "yatra")}
1117 + />
1118 + </div>
1119 +
1120 + {/* Extra / custom contact fields (nationality, address, and
1121 + any fields the operator added via the Dynamic Form module). */}
1122 + {contactExtraFields.map((field) => (
1123 + <div
1124 + key={field.id}
1125 + className={field.width === "full" ? "md:col-span-2" : ""}
872 1126 >
873 - <option value="">{__("Select country", "yatra")}</option>
874 - {countryList.map((country) => (
875 - <option key={country.code} value={country.code}>
876 - {country.name}
877 - </option>
878 - ))}
879 - </select>
880 - </div>
1127 + <label
1128 + htmlFor={`contact-${field.id}`}
1129 + className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1130 + >
1131 + {field.label}
1132 + {field.required && (
1133 + <span className="text-red-500 ml-1">*</span>
1134 + )}
1135 + </label>
1136 + {renderDynamicInput(
1137 + field,
1138 + contactExtraData[field.id] || "",
1139 + (v) => handleContactExtraChange(field.id, v),
1140 + "contact",
1141 + )}
1142 + </div>
1143 + ))}
881 1144 </CardContent>
882 1145 </Card>
883 1146
884 1147 {/* Booking Details */}
@@ -1080,11 +1343,13 @@
1080 1343 <span className="text-red-500">*</span>
1081 1344 </label>
1082 1345 <DatePicker
1083 1346 value={formData.travel_date}
1084 - onChange={(value: string) =>
1085 - handleFieldChange("travel_date", value)
1086 - }
1347 + onChange={(value: string) => {
1348 + handleFieldChange("travel_date", value);
1349 + // A time from the previous date no longer applies.
1350 + handleFieldChange("departure_time", "");
1351 + }}
1087 1352 placeholder={__("Select travel date", "yatra")}
1088 1353 error={!!errors.travel_date}
1089 1354 />
1090 1355 {errors.travel_date && (
@@ -1092,8 +1357,58 @@
1092 1357 {errors.travel_date}
1093 1358 </p>
1094 1359 )}
1095 1360 </div>
1361 +
1362 + {/* Departure time — only when the chosen date actually runs
1363 + more than one departure. Capacity is tracked per departure,
1364 + so without this the booking occupies no slot. */}
1365 + {departureTimesForDate.length > 1 && (
1366 + <div>
1367 + <label
1368 + htmlFor="departure_time"
1369 + className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1370 + >
1371 + {__("Departure Time", "yatra")}{" "}
1372 + <span className="text-red-500">*</span>
1373 + </label>
1374 + <select
1375 + id="departure_time"
1376 + name="departure_time"
1377 + value={formData.departure_time}
1378 + onChange={(e) =>
1379 + handleFieldChange("departure_time", e.target.value)
1380 + }
1381 + className={`w-full rounded-md border px-3 py-2 text-sm bg-white dark:bg-gray-800 dark:text-white ${
1382 + errors.departure_time
1383 + ? "border-red-500"
1384 + : "border-gray-300 dark:border-gray-600"
1385 + }`}
1386 + required
1387 + >
1388 + <option value="">
1389 + {__("Select departure time", "yatra")}
1390 + </option>
1391 + {departureTimesForDate.map((t: string) => (
1392 + <option key={t} value={t}>
1393 + {t}
1394 + </option>
1395 + ))}
1396 + </select>
1397 + {errors.departure_time ? (
1398 + <p className="mt-1 text-sm text-red-500">
1399 + {errors.departure_time}
1400 + </p>
1401 + ) : (
1402 + <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
1403 + {__(
1404 + "This date has several departures. Pick the one this booking is for so its seats are reserved.",
1405 + "yatra",
1406 + )}
1407 + </p>
1408 + )}
1409 + </div>
1410 + )}
1096 1411 </div>
1097 1412
1098 1413 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1099 1414 {/* Number of Travelers */}
@@ -1110,9 +1425,9 @@
1110 1425 type="number"
1111 1426 min="1"
1112 1427 value={formData.travelers}
1113 1428 onChange={(e) =>
1114 - handleFieldChange("travelers", e.target.value)
1429 + handleTravelersCountChange(e.target.value)
1115 1430 }
1116 1431 className={errors.travelers ? "border-red-500" : ""}
1117 1432 required
1118 1433 />
@@ -1310,30 +1625,23 @@
1310 1625 </option>
1311 1626 ))}
1312 1627 </Select>
1313 1628 ) : field.type === "country" ? (
1314 - <Select
1315 - id={`emergency-${field.id}`}
1629 + <SearchableSelect
1316 1630 value={emergencyContactData[field.id] || ""}
1317 - onChange={(e) =>
1318 - handleEmergencyContactChange(
1319 - field.id,
1320 - e.target.value,
1321 - )
1631 + onChange={(v) =>
1632 + handleEmergencyContactChange(field.id, v)
1322 1633 }
1323 - >
1324 - <option value="">
1325 - {field.placeholder || "Select Country"}
1326 - </option>
1327 - {countryList.map((country) => (
1328 - <option
1329 - key={country.code}
1330 - value={country.code}
1331 - >
1332 - {country.name}
1333 - </option>
1334 - ))}
1335 - </Select>
1634 + options={countrySelectOptions}
1635 + placeholder={
1636 + field.placeholder ||
1637 + __("Select Country", "yatra")
1638 + }
1639 + searchPlaceholder={__(
1640 + "Search country",
1641 + "yatra",
1642 + )}
1643 + />
1336 1644 ) : field.type === "textarea" ? (
1337 1645 <textarea
1338 1646 id={`emergency-${field.id}`}
1339 1647 value={emergencyContactData[field.id] || ""}
@@ -1520,160 +1828,163 @@
1520 1828 {/* Traveler Fields - Collapsible Content */}
1521 1829 {expandedTravelers.includes(travelerIndex) && (
1522 1830 <div className="p-3 pt-0 border-t border-gray-100 dark:border-gray-700/50">
1523 1831 <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
1524 - {travelerFields.map((field) => (
1525 - <div
1526 - key={field.id}
1527 - className={
1528 - field.width === "full"
1529 - ? "md:col-span-2"
1530 - : ""
1531 - }
1532 - >
1533 - <label
1534 - htmlFor={`traveler-${travelerIndex}-${field.id}`}
1535 - className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
1832 + {travelerFields
1833 + .filter(
1834 + (field) =>
1835 + (field.applies_to ?? "all") !== "lead" ||
1836 + travelerIndex === 0,
1837 + )
1838 + .map((field) => (
1839 + <div
1840 + key={field.id}
1841 + className={
1842 + field.width === "full"
1843 + ? "md:col-span-2"
1844 + : ""
1845 + }
1536 1846 >
1537 - {field.label}
1538 - {field.required && (
1539 - <span className="text-red-500 ml-1">
1540 - *
1541 - </span>
1542 - )}
1543 - </label>
1847 + <label
1848 + htmlFor={`traveler-${travelerIndex}-${field.id}`}
1849 + className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
1850 + >
1851 + {field.label}
1852 + {field.required && (
1853 + <span className="text-red-500 ml-1">
1854 + *
1855 + </span>
1856 + )}
1857 + </label>
1544 1858
1545 - {/* Render field based on type */}
1546 - {field.type === "select" ? (
1547 - <Select
1548 - id={`traveler-${travelerIndex}-${field.id}`}
1549 - value={traveler[field.id] || ""}
1550 - onChange={(e) =>
1551 - handleTravelerChange(
1552 - travelerIndex,
1553 - field.id,
1554 - e.target.value,
1555 - )
1556 - }
1557 - >
1558 - <option value="">
1559 - {field.placeholder ||
1560 - `Select ${field.label}`}
1561 - </option>
1562 - {field.options?.map((opt) => (
1563 - <option
1564 - key={opt.value}
1565 - value={opt.value}
1566 - >
1567 - {opt.label}
1859 + {/* Render field based on type */}
1860 + {field.type === "select" ? (
1861 + <Select
1862 + id={`traveler-${travelerIndex}-${field.id}`}
1863 + value={traveler[field.id] || ""}
1864 + onChange={(e) =>
1865 + handleTravelerChange(
1866 + travelerIndex,
1867 + field.id,
1868 + e.target.value,
1869 + )
1870 + }
1871 + >
1872 + <option value="">
1873 + {field.placeholder ||
1874 + `Select ${field.label}`}
1568 1875 </option>
1569 - ))}
1570 - </Select>
1571 - ) : field.type === "country" ? (
1572 - <Select
1573 - id={`traveler-${travelerIndex}-${field.id}`}
1574 - value={traveler[field.id] || ""}
1575 - onChange={(e) =>
1576 - handleTravelerChange(
1577 - travelerIndex,
1578 - field.id,
1579 - e.target.value,
1580 - )
1581 - }
1582 - >
1583 - <option value="">
1584 - {field.placeholder ||
1585 - "Select Country"}
1586 - </option>
1587 - {countryList.map((country) => (
1588 - <option
1589 - key={country.code}
1590 - value={country.code}
1591 - >
1592 - {country.name}
1593 - </option>
1594 - ))}
1595 - </Select>
1596 - ) : field.type === "textarea" ? (
1597 - <textarea
1598 - id={`traveler-${travelerIndex}-${field.id}`}
1599 - value={traveler[field.id] || ""}
1600 - onChange={(e) =>
1601 - handleTravelerChange(
1602 - travelerIndex,
1603 - field.id,
1604 - e.target.value,
1605 - )
1606 - }
1607 - placeholder={field.placeholder}
1608 - rows={2}
1609 - className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:placeholder:text-gray-400 resize-none"
1610 - />
1611 - ) : field.type === "checkbox" ? (
1612 - <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
1613 - <input
1614 - type="checkbox"
1615 - className="rounded border-gray-300"
1616 - checked={
1617 - traveler[field.id] === "1" ||
1618 - traveler[field.id] === "true"
1876 + {field.options?.map((opt) => (
1877 + <option
1878 + key={opt.value}
1879 + value={opt.value}
1880 + >
1881 + {opt.label}
1882 + </option>
1883 + ))}
1884 + </Select>
1885 + ) : field.type === "country" ? (
1886 + <SearchableSelect
1887 + value={traveler[field.id] || ""}
1888 + onChange={(v) =>
1889 + handleTravelerChange(
1890 + travelerIndex,
1891 + field.id,
1892 + v,
1893 + )
1619 1894 }
1895 + options={countrySelectOptions}
1896 + placeholder={
1897 + field.placeholder ||
1898 + __("Select Country", "yatra")
1899 + }
1900 + searchPlaceholder={__(
1901 + "Search country",
1902 + "yatra",
1903 + )}
1904 + />
1905 + ) : field.type === "textarea" ? (
1906 + <textarea
1907 + id={`traveler-${travelerIndex}-${field.id}`}
1908 + value={traveler[field.id] || ""}
1620 1909 onChange={(e) =>
1621 1910 handleTravelerChange(
1622 1911 travelerIndex,
1623 1912 field.id,
1624 - e.target.checked ? "1" : "",
1913 + e.target.value,
1625 1914 )
1626 1915 }
1916 + placeholder={field.placeholder}
1917 + rows={2}
1918 + className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:placeholder:text-gray-400 resize-none"
1627 1919 />
1628 - <span>
1629 - {field.placeholder || field.label}
1630 - </span>
1631 - </label>
1632 - ) : field.type === "date" ||
1633 - field.id.toLowerCase().includes("date") ||
1634 - field.id
1635 - .toLowerCase()
1636 - .includes("expiry") ? (
1637 - <DatePicker
1638 - value={traveler[field.id] || ""}
1639 - onChange={(value: string) =>
1640 - handleTravelerChange(
1641 - travelerIndex,
1642 - field.id,
1643 - value,
1644 - )
1645 - }
1646 - placeholder={
1647 - field.placeholder ||
1648 - __("Select date", "yatra")
1649 - }
1650 - />
1651 - ) : (
1652 - <Input
1653 - id={`traveler-${travelerIndex}-${field.id}`}
1654 - type={
1655 - field.type === "email"
1656 - ? "email"
1657 - : field.type === "tel"
1658 - ? "tel"
1659 - : field.type === "number"
1660 - ? "number"
1661 - : "text"
1662 - }
1663 - value={traveler[field.id] || ""}
1664 - onChange={(e) =>
1665 - handleTravelerChange(
1666 - travelerIndex,
1667 - field.id,
1668 - e.target.value,
1669 - )
1670 - }
1671 - placeholder={field.placeholder}
1672 - />
1673 - )}
1674 - </div>
1675 - ))}
1920 + ) : field.type === "checkbox" ? (
1921 + <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
1922 + <input
1923 + type="checkbox"
1924 + className="rounded border-gray-300"
1925 + checked={
1926 + traveler[field.id] === "1" ||
1927 + traveler[field.id] === "true"
1928 + }
1929 + onChange={(e) =>
1930 + handleTravelerChange(
1931 + travelerIndex,
1932 + field.id,
1933 + e.target.checked ? "1" : "",
1934 + )
1935 + }
1936 + />
1937 + <span>
1938 + {field.placeholder || field.label}
1939 + </span>
1940 + </label>
1941 + ) : field.type === "date" ||
1942 + field.id
1943 + .toLowerCase()
1944 + .includes("date") ||
1945 + field.id
1946 + .toLowerCase()
1947 + .includes("expiry") ? (
1948 + <DatePicker
1949 + value={traveler[field.id] || ""}
1950 + onChange={(value: string) =>
1951 + handleTravelerChange(
1952 + travelerIndex,
1953 + field.id,
1954 + value,
1955 + )
1956 + }
1957 + placeholder={
1958 + field.placeholder ||
1959 + __("Select date", "yatra")
1960 + }
1961 + />
1962 + ) : (
1963 + <Input
1964 + id={`traveler-${travelerIndex}-${field.id}`}
1965 + type={
1966 + field.type === "email"
1967 + ? "email"
1968 + : field.type === "tel"
1969 + ? "tel"
1970 + : field.type === "number"
1971 + ? "number"
1972 + : "text"
1973 + }
1974 + value={traveler[field.id] || ""}
1975 + onChange={(e) =>
1976 + handleTravelerChange(
1977 + travelerIndex,
1978 + field.id,
1979 + e.target.value,
1980 + )
1981 + }
1982 + placeholder={field.placeholder}
1983 + />
1984 + )}
1985 + </div>
1986 + ))}
1676 1987 </div>
1677 1988 </div>
1678 1989 )}
1679 1990 </div>