PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | resources/js/pages/BookingForm.tsx +503 -218 3.0.3 → 3.0.16 View file →
@@ -21,9 +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";
27 +import { getCountrySelectOptions } from "../lib/countries";
28 +import { SearchableSelect } from "../components/ui/searchable-select";
26 29 import { apiService } from "../lib/api-client";
27 30 import { usePermissions } from "../hooks/usePermissions";
28 31 import { getCurrencySymbol } from "../data/currencies";
29 32 import { Button } from "../components/ui/button";
@@ -56,8 +59,10 @@
56 59 order: number;
57 60 width?: string;
58 61 options?: { value: string; label: string }[];
59 62 section?: string;
63 + /** Traveler section: "lead" shows only for the lead traveler; "all"/absent = every traveler. */
64 + applies_to?: "all" | "lead";
60 65 }
61 66
62 67 interface FormSectionConfig {
63 68 title: string;
@@ -79,8 +84,9 @@
79 84 customer_country: string;
80 85 trip_id: string;
81 86 booking_date: string;
82 87 travel_date: string;
88 + departure_time: string;
83 89 travelers: string;
84 90 subtotal: string;
85 91 tax_amount: string;
86 92 total_amount: string;
@@ -90,40 +96,23 @@
90 96 payment_method: string;
91 97 notes: string;
92 98 }
93 99
94 -// Country list for country fields
95 -const countryList = [
96 - { code: "US", name: "United States" },
97 - { code: "GB", name: "United Kingdom" },
98 - { code: "CA", name: "Canada" },
99 - { code: "AU", name: "Australia" },
100 - { code: "DE", name: "Germany" },
101 - { code: "FR", name: "France" },
102 - { code: "IN", name: "India" },
103 - { code: "NP", name: "Nepal" },
104 - { code: "CN", name: "China" },
105 - { code: "JP", name: "Japan" },
106 - { code: "BR", name: "Brazil" },
107 - { code: "MX", name: "Mexico" },
108 - { code: "ES", name: "Spain" },
109 - { code: "IT", name: "Italy" },
110 - { code: "NL", name: "Netherlands" },
111 - { code: "CH", name: "Switzerland" },
112 - { code: "SE", name: "Sweden" },
113 - { code: "NO", name: "Norway" },
114 - { code: "DK", name: "Denmark" },
115 - { code: "FI", name: "Finland" },
116 - { code: "NZ", name: "New Zealand" },
117 - { code: "SG", name: "Singapore" },
118 - { code: "AE", name: "United Arab Emirates" },
119 - { code: "TH", name: "Thailand" },
120 - { code: "MY", name: "Malaysia" },
121 - { code: "ID", name: "Indonesia" },
122 - { code: "PH", name: "Philippines" },
123 - { code: "VN", name: "Vietnam" },
124 - { code: "KR", name: "South Korea" },
125 - { code: "ZA", name: "South Africa" },
100 +// Country list — pulled from the canonical source (PHP
101 +// FormatHelper::getCountries() → localized to window.yatraAdmin.countries).
102 +// One source of truth; operators that want a curated subset apply
103 +// the `yatra_countries_list` PHP filter once and every dropdown
104 +// (admin + public booking + Pro modules) picks it up.
105 +const countrySelectOptions = getCountrySelectOptions();
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",
126 115 ];
127 116
128 117 // Normalize ISO/date-like string to formatted date using shared date library
129 118 const normalizeDateInput = (value?: string | null) => {
@@ -145,10 +134,11 @@
145 134 customer_email: "",
146 135 customer_phone: "",
147 136 customer_country: "",
148 137 trip_id: "",
149 - booking_date: normalizeDateInput(new Date().toISOString()),
138 + booking_date: todayYmd(),
150 139 travel_date: "",
140 + departure_time: "",
151 141 travelers: "1",
152 142 subtotal: "",
153 143 tax_amount: "",
154 144 total_amount: "",
@@ -162,8 +152,14 @@
162 152 const [expandedTravelers, setExpandedTravelers] = useState<number[]>([0]);
163 153 const [emergencyContactData, setEmergencyContactData] = useState<
164 154 Record<string, string>
165 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 + >({});
166 162 const [errors, setErrors] = useState<Record<string, string>>({});
167 163 const [isSubmitting, setIsSubmitting] = useState(false);
168 164
169 165 // Trip search state
@@ -184,13 +180,15 @@
184 180 document.addEventListener("mousedown", handleClickOutside);
185 181 return () => document.removeEventListener("mousedown", handleClickOutside);
186 182 }, []);
187 183
188 - // 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;
189 187 const { data: formConfig } = useQuery<BookingFormConfig>({
190 - queryKey: ["booking-form-config"],
188 + queryKey: ["booking-form-config", selectedTripId],
191 189 queryFn: async () => {
192 - const response = await apiService.getSettings();
190 + const response = await apiService.getBookingFormConfig(selectedTripId);
193 191 return (
194 192 response?.data?.booking_form_config ||
195 193 response?.booking_form_config ||
196 194 null
@@ -213,8 +211,18 @@
213 211 .filter((field) => field.enabled)
214 212 .sort((a, b) => a.order - b.order);
215 213 }, [formConfig]);
216 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 +
217 225 // Create empty traveler based on enabled fields
218 226 const createEmptyTraveler = (): TravelerData => {
219 227 const empty: TravelerData = {};
220 228 travelerFields.forEach((field) => {
@@ -266,8 +274,37 @@
266 274 enabled: can("yatra_view_bookings") || can("yatra_view_trips"),
267 275 retry: 1,
268 276 });
269 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 +
270 307 // Fetch booking data if editing
271 308 const { data: bookingData, isLoading: isLoadingBooking } = useQuery({
272 309 queryKey: ["booking", bookingId],
273 310 queryFn: async () => {
@@ -388,8 +425,9 @@
388 425 booking_status: booking.booking_status || "pending",
389 426 payment_method: booking.payment_method || "",
390 427 notes: booking.notes || "",
391 428 emergency_contact: emergencyContact,
429 + contact_data: (booking as any).contact_data || null,
392 430 };
393 431
394 432 return mappedData;
395 433 }
@@ -412,11 +450,11 @@
412 450 (bookingData as any).contact_country ||
413 451 (bookingData as any).customer_country ||
414 452 "",
415 453 trip_id: String(bookingData.trip_id || ""),
416 - booking_date:
417 - bookingData.booking_date || new Date().toISOString().split("T")[0],
454 + booking_date: bookingData.booking_date || todayYmd(),
418 455 travel_date: bookingData.travel_date || "",
456 + departure_time: (bookingData as any).departure_time || "",
419 457 travelers: String(
420 458 (bookingData as any).travelers_count ||
421 459 bookingData.travelers_data?.length ||
422 460 "1",
@@ -452,8 +490,21 @@
452 490 if (bookingData.emergency_contact) {
453 491 setEmergencyContactData(bookingData.emergency_contact);
454 492 }
455 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 +
456 507 setIsDataLoaded(true);
457 508 }
458 509 }, [bookingData, isEditMode]);
459 510
@@ -507,8 +558,98 @@
507 558 const handleEmergencyContactChange = (field: string, value: string) => {
508 559 setEmergencyContactData((prev) => ({ ...prev, [field]: value }));
509 560 };
510 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 +
511 652 // Traveler management functions
512 653 const handleTravelerChange = (
513 654 index: number,
514 655 field: keyof TravelerData,
@@ -520,8 +661,47 @@
520 661 return updated;
521 662 });
522 663 };
523 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 +
524 704 const addTraveler = () => {
525 705 const newTraveler = createEmptyTraveler();
526 706 setTravelersData((prev) => [...prev, newTraveler]);
527 707 setFormData((prev) => ({
@@ -569,8 +749,11 @@
569 749 if (!formData.booking_date) {
570 750 newErrors.booking_date = __("Booking date is required", "yatra");
571 751 }
572 752
753 + if (departureTimesForDate.length > 1 && !formData.departure_time) {
754 + newErrors.departure_time = __("Departure time is required", "yatra");
755 + }
573 756 if (!formData.travel_date) {
574 757 newErrors.travel_date = __("Travel date is required", "yatra");
575 758 }
576 759
@@ -604,10 +787,16 @@
604 787 contact_first_name: firstName,
605 788 contact_last_name: lastName,
606 789 contact_email: data.customer_email.trim(),
607 790 contact_phone: data.customer_phone.trim(),
791 + // contact_country column (previously not submitted, so edits were lost).
792 + contact_country: data.customer_country || "",
608 793 trip_id: parseInt(data.trip_id),
609 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 || "",
610 799 travelers_count: travelersData.length,
611 800 total_amount: parseFloat(data.total_amount),
612 801 payment_status: data.payment_status,
613 802 status: data.booking_status,
@@ -616,8 +805,18 @@
616 805 // Include travelers data - use 'travelers' key as expected by BookingService.updateBooking
617 806 travelers: travelersData,
618 807 // Include emergency contact data
619 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 + },
620 819 };
621 820
622 821 return isEditMode
623 822 ? await apiService.updateBooking(bookingId!, payload)
@@ -628,12 +827,33 @@
628 827 // Redirect to bookings list
629 828 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings`;
630 829 },
631 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 +
632 851 const errorMessage =
852 + detail ||
633 853 error?.message ||
634 854 __("An error occurred while saving the booking", "yatra");
635 - setErrors({ submit: errorMessage });
855 + setErrors({ ...fieldErrors, submit: errorMessage });
636 856 setIsSubmitting(false);
637 857 },
638 858 });
639 859
@@ -887,24 +1107,41 @@
887 1107 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
888 1108 >
889 1109 {__("Country", "yatra")}
890 1110 </label>
891 - <select
892 - id="customer_country"
1111 + <SearchableSelect
893 1112 value={formData.customer_country}
894 - onChange={(e) =>
895 - handleFieldChange("customer_country", e.target.value)
896 - }
897 - 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" : ""}
898 1126 >
899 - <option value="">{__("Select country", "yatra")}</option>
900 - {countryList.map((country) => (
901 - <option key={country.code} value={country.code}>
902 - {country.name}
903 - </option>
904 - ))}
905 - </select>
906 - </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 + ))}
907 1144 </CardContent>
908 1145 </Card>
909 1146
910 1147 {/* Booking Details */}
@@ -1106,11 +1343,13 @@
1106 1343 <span className="text-red-500">*</span>
1107 1344 </label>
1108 1345 <DatePicker
1109 1346 value={formData.travel_date}
1110 - onChange={(value: string) =>
1111 - handleFieldChange("travel_date", value)
1112 - }
1347 + onChange={(value: string) => {
1348 + handleFieldChange("travel_date", value);
1349 + // A time from the previous date no longer applies.
1350 + handleFieldChange("departure_time", "");
1351 + }}
1113 1352 placeholder={__("Select travel date", "yatra")}
1114 1353 error={!!errors.travel_date}
1115 1354 />
1116 1355 {errors.travel_date && (
@@ -1118,8 +1357,58 @@
1118 1357 {errors.travel_date}
1119 1358 </p>
1120 1359 )}
1121 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 + )}
1122 1411 </div>
1123 1412
1124 1413 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1125 1414 {/* Number of Travelers */}
@@ -1136,9 +1425,9 @@
1136 1425 type="number"
1137 1426 min="1"
1138 1427 value={formData.travelers}
1139 1428 onChange={(e) =>
1140 - handleFieldChange("travelers", e.target.value)
1429 + handleTravelersCountChange(e.target.value)
1141 1430 }
1142 1431 className={errors.travelers ? "border-red-500" : ""}
1143 1432 required
1144 1433 />
@@ -1336,30 +1625,23 @@
1336 1625 </option>
1337 1626 ))}
1338 1627 </Select>
1339 1628 ) : field.type === "country" ? (
1340 - <Select
1341 - id={`emergency-${field.id}`}
1629 + <SearchableSelect
1342 1630 value={emergencyContactData[field.id] || ""}
1343 - onChange={(e) =>
1344 - handleEmergencyContactChange(
1345 - field.id,
1346 - e.target.value,
1347 - )
1631 + onChange={(v) =>
1632 + handleEmergencyContactChange(field.id, v)
1348 1633 }
1349 - >
1350 - <option value="">
1351 - {field.placeholder || "Select Country"}
1352 - </option>
1353 - {countryList.map((country) => (
1354 - <option
1355 - key={country.code}
1356 - value={country.code}
1357 - >
1358 - {country.name}
1359 - </option>
1360 - ))}
1361 - </Select>
1634 + options={countrySelectOptions}
1635 + placeholder={
1636 + field.placeholder ||
1637 + __("Select Country", "yatra")
1638 + }
1639 + searchPlaceholder={__(
1640 + "Search country",
1641 + "yatra",
1642 + )}
1643 + />
1362 1644 ) : field.type === "textarea" ? (
1363 1645 <textarea
1364 1646 id={`emergency-${field.id}`}
1365 1647 value={emergencyContactData[field.id] || ""}
@@ -1546,160 +1828,163 @@
1546 1828 {/* Traveler Fields - Collapsible Content */}
1547 1829 {expandedTravelers.includes(travelerIndex) && (
1548 1830 <div className="p-3 pt-0 border-t border-gray-100 dark:border-gray-700/50">
1549 1831 <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
1550 - {travelerFields.map((field) => (
1551 - <div
1552 - key={field.id}
1553 - className={
1554 - field.width === "full"
1555 - ? "md:col-span-2"
1556 - : ""
1557 - }
1558 - >
1559 - <label
1560 - htmlFor={`traveler-${travelerIndex}-${field.id}`}
1561 - 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 + }
1562 1846 >
1563 - {field.label}
1564 - {field.required && (
1565 - <span className="text-red-500 ml-1">
1566 - *
1567 - </span>
1568 - )}
1569 - </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>
1570 1858
1571 - {/* Render field based on type */}
1572 - {field.type === "select" ? (
1573 - <Select
1574 - id={`traveler-${travelerIndex}-${field.id}`}
1575 - value={traveler[field.id] || ""}
1576 - onChange={(e) =>
1577 - handleTravelerChange(
1578 - travelerIndex,
1579 - field.id,
1580 - e.target.value,
1581 - )
1582 - }
1583 - >
1584 - <option value="">
1585 - {field.placeholder ||
1586 - `Select ${field.label}`}
1587 - </option>
1588 - {field.options?.map((opt) => (
1589 - <option
1590 - key={opt.value}
1591 - value={opt.value}
1592 - >
1593 - {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}`}
1594 1875 </option>
1595 - ))}
1596 - </Select>
1597 - ) : field.type === "country" ? (
1598 - <Select
1599 - id={`traveler-${travelerIndex}-${field.id}`}
1600 - value={traveler[field.id] || ""}
1601 - onChange={(e) =>
1602 - handleTravelerChange(
1603 - travelerIndex,
1604 - field.id,
1605 - e.target.value,
1606 - )
1607 - }
1608 - >
1609 - <option value="">
1610 - {field.placeholder ||
1611 - "Select Country"}
1612 - </option>
1613 - {countryList.map((country) => (
1614 - <option
1615 - key={country.code}
1616 - value={country.code}
1617 - >
1618 - {country.name}
1619 - </option>
1620 - ))}
1621 - </Select>
1622 - ) : field.type === "textarea" ? (
1623 - <textarea
1624 - id={`traveler-${travelerIndex}-${field.id}`}
1625 - value={traveler[field.id] || ""}
1626 - onChange={(e) =>
1627 - handleTravelerChange(
1628 - travelerIndex,
1629 - field.id,
1630 - e.target.value,
1631 - )
1632 - }
1633 - placeholder={field.placeholder}
1634 - rows={2}
1635 - 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"
1636 - />
1637 - ) : field.type === "checkbox" ? (
1638 - <label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
1639 - <input
1640 - type="checkbox"
1641 - className="rounded border-gray-300"
1642 - checked={
1643 - traveler[field.id] === "1" ||
1644 - 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 + )
1645 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] || ""}
1646 1909 onChange={(e) =>
1647 1910 handleTravelerChange(
1648 1911 travelerIndex,
1649 1912 field.id,
1650 - e.target.checked ? "1" : "",
1913 + e.target.value,
1651 1914 )
1652 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"
1653 1919 />
1654 - <span>
1655 - {field.placeholder || field.label}
1656 - </span>
1657 - </label>
1658 - ) : field.type === "date" ||
1659 - field.id.toLowerCase().includes("date") ||
1660 - field.id
1661 - .toLowerCase()
1662 - .includes("expiry") ? (
1663 - <DatePicker
1664 - value={traveler[field.id] || ""}
1665 - onChange={(value: string) =>
1666 - handleTravelerChange(
1667 - travelerIndex,
1668 - field.id,
1669 - value,
1670 - )
1671 - }
1672 - placeholder={
1673 - field.placeholder ||
1674 - __("Select date", "yatra")
1675 - }
1676 - />
1677 - ) : (
1678 - <Input
1679 - id={`traveler-${travelerIndex}-${field.id}`}
1680 - type={
1681 - field.type === "email"
1682 - ? "email"
1683 - : field.type === "tel"
1684 - ? "tel"
1685 - : field.type === "number"
1686 - ? "number"
1687 - : "text"
1688 - }
1689 - value={traveler[field.id] || ""}
1690 - onChange={(e) =>
1691 - handleTravelerChange(
1692 - travelerIndex,
1693 - field.id,
1694 - e.target.value,
1695 - )
1696 - }
1697 - placeholder={field.placeholder}
1698 - />
1699 - )}
1700 - </div>
1701 - ))}
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 + ))}
1702 1987 </div>
1703 1988 </div>
1704 1989 )}
1705 1990 </div>