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/AvailabilityForm.tsx +77 -23 3.0.8 → 3.0.16 View file →
@@ -14,9 +14,10 @@
14 14 Loader2,
15 15 MapPin,
16 16 CheckCircle2,
17 17 } from "lucide-react";
18 -import { __ } from "../lib/i18n";
18 +import { __, sprintf } from "../lib/i18n";
19 +import { toDateValue } from "../lib/dateFormat";
19 20 import { Button } from "../components/ui/button";
20 21 import { Input } from "../components/ui/input";
21 22 import { Select } from "../components/ui/select";
22 23 import { PageHeader } from "../components/common/PageHeader";
@@ -146,8 +147,27 @@
146 147 },
147 148 enabled: !!tripId,
148 149 });
149 150
151 + const isSingleDayTrip = tripData?.trip_type === "single_day";
152 +
153 + // A day tour can run several departures a day ("multiple time slots", set on
154 + // the trip). Only then does a date need a departure time — it identifies
155 + // which slot this row overrides, exactly as the server resolves it
156 + // (AvailabilityResolutionService: has_default_time_slots && single_day).
157 + // A day tour with one departure stores no time, which every consumer
158 + // already handles (CapacityService falls back to the rule/date seats).
159 + const hasTimeSlots = isSingleDayTrip && !!tripData?.has_default_time_slots;
160 +
161 + // The trip's max travellers seeds a new date's capacity so it doesn't have
162 + // to be retyped for every date. It stays editable per date, and it is still
163 + // stored explicitly: an empty/zero seat count means "sold out" to the
164 + // availability layer, so it must never be left blank.
165 + const tripMaxTravelers = (() => {
166 + const raw = Number(tripData?.max_travelers ?? 0);
167 + return Number.isFinite(raw) && raw > 0 ? raw : 0;
168 + })();
169 +
150 170 // Fetch traveler categories
151 171 const { data: categoriesData, isLoading: isLoadingCategories } = useQuery({
152 172 queryKey: ["traveler-categories"],
153 173 queryFn: async () => {
@@ -198,8 +218,13 @@
198 218 : tripData.pricing_type || "regular";
199 219
200 220 setFormData((prev) => ({
201 221 ...prev,
222 + // Only seeds an untouched field — never overwrites a typed value if
223 + // the trip query refetches.
224 + total_seats:
225 + prev.total_seats ||
226 + (tripMaxTravelers > 0 ? String(tripMaxTravelers) : prev.total_seats),
202 227 from_location: tripData.starting_location || "",
203 228 to_location: tripData.ending_location || "",
204 229 from_latitude: coordFromApi(tripData.starting_latitude),
205 230 from_longitude: coordFromApi(tripData.starting_longitude),
@@ -208,9 +233,9 @@
208 233 // Default pricing type based on trip's pricing type
209 234 pricing_type: effectivePricingType as "regular" | "traveler_based",
210 235 }));
211 236 }
212 - }, [tripData, isEditMode]);
237 + }, [tripData, isEditMode, tripMaxTravelers]);
213 238
214 239 useEffect(() => {
215 240 if (availabilityData) {
216 241 const totalSeats =
@@ -357,9 +382,9 @@
357 382 newErrors.arrival_date = __("Arrival date is required", "yatra");
358 383 }
359 384
360 385 // Validate dates and times based on trip type
361 - const isSingleDay = tripData?.trip_type === "single_day";
386 + const isSingleDay = isSingleDayTrip;
362 387
363 388 if (isSingleDay) {
364 389 // For single day trips, both dates should be the same
365 390 if (
@@ -371,21 +396,18 @@
371 396 "For single day trips, departure and arrival must be on the same date",
372 397 "yatra",
373 398 );
374 399 }
375 - // Validate times
376 - if (!formData.departure_time) {
400 + // Times are only needed when the day tour runs several departures a
401 + // day: the departure time is what identifies the slot. A single-
402 + // departure day tour saves without times (arrival time is display-only
403 + // and never required).
404 + if (hasTimeSlots && !formData.departure_time) {
377 405 newErrors.departure_time = __(
378 - "Departure time is required for single day trips",
406 + "Departure time is required when the trip has multiple time slots",
379 407 "yatra",
380 408 );
381 409 }
382 - if (!formData.arrival_time) {
383 - newErrors.arrival_time = __(
384 - "Arrival time is required for single day trips",
385 - "yatra",
386 - );
387 - }
388 410 // Validate that arrival time is after departure time
389 411 if (
390 412 formData.departure_time &&
391 413 formData.arrival_time &&
@@ -705,9 +727,15 @@
705 727 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
706 728 <div>
707 729 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
708 730 {__("Departure Time", "yatra")}{" "}
709 - <span className="text-red-500">*</span>
731 + {hasTimeSlots ? (
732 + <span className="text-red-500">*</span>
733 + ) : (
734 + <span className="text-gray-400 font-normal">
735 + {__("(optional)", "yatra")}
736 + </span>
737 + )}
710 738 </label>
711 739 <TimePicker
712 740 value={formData.departure_time}
713 741 onChange={(value: string) =>
@@ -724,9 +752,11 @@
724 752 </div>
725 753 <div>
726 754 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
727 755 {__("Arrival Time", "yatra")}{" "}
728 - <span className="text-red-500">*</span>
756 + <span className="text-gray-400 font-normal">
757 + {__("(optional)", "yatra")}
758 + </span>
729 759 </label>
730 760 <TimePicker
731 761 value={formData.arrival_time}
732 762 onChange={(value: string) =>
@@ -777,9 +807,9 @@
777 807 handleFieldChange("arrival_date", value)
778 808 }
779 809 minDate={
780 810 formData.departure_date
781 - ? new Date(formData.departure_date)
811 + ? toDateValue(formData.departure_date)
782 812 : undefined
783 813 }
784 814 placeholder={__("Select arrival date", "yatra")}
785 815 error={!!errors.arrival_date}
@@ -793,14 +823,27 @@
793 823 formData.departure_date &&
794 824 formData.arrival_date && (
795 825 <div className="mt-1.5">
796 826 {(() => {
827 + // Intentionally `new Date()` (UTC), NOT toDateValue():
828 + // this is a day-COUNT between two dates. UTC midnights
829 + // are DST-immune so the diff is a whole number of
830 + // days; parsing as local could make it off by the DST
831 + // hour and round the duration wrong. (Display of these
832 + // dates elsewhere uses the local-safe shared formatter.)
797 833 const departure = new Date(formData.departure_date);
798 834 const arrival = new Date(formData.arrival_date);
799 - const selectedDays = Math.ceil(
800 - (arrival.getTime() - departure.getTime()) /
801 - (1000 * 60 * 60 * 24),
802 - );
835 + // INCLUSIVE calendar-day span (both endpoints count):
836 + // Jun 25 → Jul 04 is 10 days, not 9. duration_days is
837 + // also inclusive (a 9-day trip ends on departure + 8;
838 + // see BookingRepository::calculateEndDate), so the two
839 + // are compared like-for-like. The bare date difference
840 + // was one day short.
841 + const selectedDays =
842 + Math.round(
843 + (arrival.getTime() - departure.getTime()) /
844 + (1000 * 60 * 60 * 24),
845 + ) + 1;
803 846 const expectedDays = tripData.duration_days;
804 847 const diff = selectedDays - expectedDays;
805 848
806 849 if (diff === 0) {
@@ -1511,12 +1554,23 @@
1511 1554 {errors.total_seats}
1512 1555 </p>
1513 1556 )}
1514 1557 <HelpText
1515 - text={__(
1516 - "Maximum number of seats available for this date",
1517 - "yatra",
1518 - )}
1558 + text={
1559 + tripMaxTravelers > 0
1560 + ? sprintf(
1561 + /* translators: %d: the trip's max travellers. */
1562 + __(
1563 + "Seats for this date. Pre-filled from the trip's max travellers (%d) — change it for this date if it differs.",
1564 + "yatra",
1565 + ),
1566 + tripMaxTravelers,
1567 + )
1568 + : __(
1569 + "Maximum number of seats available for this date",
1570 + "yatra",
1571 + )
1572 + }
1519 1573 className="mt-1"
1520 1574 />
1521 1575 </div>
1522 1576 {isEditMode && (