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 +143 -8 3.0.11 → 3.0.17 View file →
@@ -84,8 +84,9 @@
84 84 customer_country: string;
85 85 trip_id: string;
86 86 booking_date: string;
87 87 travel_date: string;
88 + departure_time: string;
88 89 travelers: string;
89 90 subtotal: string;
90 91 tax_amount: string;
91 92 total_amount: string;
@@ -135,8 +136,9 @@
135 136 customer_country: "",
136 137 trip_id: "",
137 138 booking_date: todayYmd(),
138 139 travel_date: "",
140 + departure_time: "",
139 141 travelers: "1",
140 142 subtotal: "",
141 143 tax_amount: "",
142 144 total_amount: "",
@@ -178,13 +180,15 @@
178 180 document.addEventListener("mousedown", handleClickOutside);
179 181 return () => document.removeEventListener("mousedown", handleClickOutside);
180 182 }, []);
181 183
182 - // 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;
183 187 const { data: formConfig } = useQuery<BookingFormConfig>({
184 - queryKey: ["booking-form-config"],
188 + queryKey: ["booking-form-config", selectedTripId],
185 189 queryFn: async () => {
186 - const response = await apiService.getSettings();
190 + const response = await apiService.getBookingFormConfig(selectedTripId);
187 191 return (
188 192 response?.data?.booking_form_config ||
189 193 response?.booking_form_config ||
190 194 null
@@ -270,8 +274,37 @@
270 274 enabled: can("yatra_view_bookings") || can("yatra_view_trips"),
271 275 retry: 1,
272 276 });
273 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 +
274 307 // Fetch booking data if editing
275 308 const { data: bookingData, isLoading: isLoadingBooking } = useQuery({
276 309 queryKey: ["booking", bookingId],
277 310 queryFn: async () => {
@@ -419,8 +452,9 @@
419 452 "",
420 453 trip_id: String(bookingData.trip_id || ""),
421 454 booking_date: bookingData.booking_date || todayYmd(),
422 455 travel_date: bookingData.travel_date || "",
456 + departure_time: (bookingData as any).departure_time || "",
423 457 travelers: String(
424 458 (bookingData as any).travelers_count ||
425 459 bookingData.travelers_data?.length ||
426 460 "1",
@@ -627,8 +661,47 @@
627 661 return updated;
628 662 });
629 663 };
630 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 +
631 704 const addTraveler = () => {
632 705 const newTraveler = createEmptyTraveler();
633 706 setTravelersData((prev) => [...prev, newTraveler]);
634 707 setFormData((prev) => ({
@@ -676,8 +749,11 @@
676 749 if (!formData.booking_date) {
677 750 newErrors.booking_date = __("Booking date is required", "yatra");
678 751 }
679 752
753 + if (departureTimesForDate.length > 1 && !formData.departure_time) {
754 + newErrors.departure_time = __("Departure time is required", "yatra");
755 + }
680 756 if (!formData.travel_date) {
681 757 newErrors.travel_date = __("Travel date is required", "yatra");
682 758 }
683 759
@@ -715,8 +791,12 @@
715 791 // contact_country column (previously not submitted, so edits were lost).
716 792 contact_country: data.customer_country || "",
717 793 trip_id: parseInt(data.trip_id),
718 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 || "",
719 799 travelers_count: travelersData.length,
720 800 total_amount: parseFloat(data.total_amount),
721 801 payment_status: data.payment_status,
722 802 status: data.booking_status,
@@ -1263,11 +1343,13 @@
1263 1343 <span className="text-red-500">*</span>
1264 1344 </label>
1265 1345 <DatePicker
1266 1346 value={formData.travel_date}
1267 - onChange={(value: string) =>
1268 - handleFieldChange("travel_date", value)
1269 - }
1347 + onChange={(value: string) => {
1348 + handleFieldChange("travel_date", value);
1349 + // A time from the previous date no longer applies.
1350 + handleFieldChange("departure_time", "");
1351 + }}
1270 1352 placeholder={__("Select travel date", "yatra")}
1271 1353 error={!!errors.travel_date}
1272 1354 />
1273 1355 {errors.travel_date && (
@@ -1275,8 +1357,58 @@
1275 1357 {errors.travel_date}
1276 1358 </p>
1277 1359 )}
1278 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 + )}
1279 1411 </div>
1280 1412
1281 1413 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1282 1414 {/* Number of Travelers */}
@@ -1293,9 +1425,9 @@
1293 1425 type="number"
1294 1426 min="1"
1295 1427 value={formData.travelers}
1296 1428 onChange={(e) =>
1297 - handleFieldChange("travelers", e.target.value)
1429 + handleTravelersCountChange(e.target.value)
1298 1430 }
1299 1431 className={errors.travelers ? "border-red-500" : ""}
1300 1432 required
1301 1433 />
@@ -1503,9 +1635,12 @@
1503 1635 placeholder={
1504 1636 field.placeholder ||
1505 1637 __("Select Country", "yatra")
1506 1638 }
1507 - searchPlaceholder={__("Search country", "yatra")}
1639 + searchPlaceholder={__(
1640 + "Search country",
1641 + "yatra",
1642 + )}
1508 1643 />
1509 1644 ) : field.type === "textarea" ? (
1510 1645 <textarea
1511 1646 id={`emergency-${field.id}`}