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