| @@ -14,9 +14,9 @@ | ||
| 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 | 19 | import { toDateValue } from "../lib/dateFormat"; |
| 20 | 20 | import { Button } from "../components/ui/button"; |
| 21 | 21 | import { Input } from "../components/ui/input"; |
| 22 | 22 | import { Select } from "../components/ui/select"; |
| @@ -147,8 +147,27 @@ | ||
| 147 | 147 | }, |
| 148 | 148 | enabled: !!tripId, |
| 149 | 149 | }); |
| 150 | 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 | + | |
| 151 | 170 | // Fetch traveler categories |
| 152 | 171 | const { data: categoriesData, isLoading: isLoadingCategories } = useQuery({ |
| 153 | 172 | queryKey: ["traveler-categories"], |
| 154 | 173 | queryFn: async () => { |
| @@ -199,8 +218,13 @@ | ||
| 199 | 218 | : tripData.pricing_type || "regular"; |
| 200 | 219 | |
| 201 | 220 | setFormData((prev) => ({ |
| 202 | 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), | |
| 203 | 227 | from_location: tripData.starting_location || "", |
| 204 | 228 | to_location: tripData.ending_location || "", |
| 205 | 229 | from_latitude: coordFromApi(tripData.starting_latitude), |
| 206 | 230 | from_longitude: coordFromApi(tripData.starting_longitude), |
| @@ -209,9 +233,9 @@ | ||
| 209 | 233 | // Default pricing type based on trip's pricing type |
| 210 | 234 | pricing_type: effectivePricingType as "regular" | "traveler_based", |
| 211 | 235 | })); |
| 212 | 236 | } |
| 213 | - }, [tripData, isEditMode]); | |
| 237 | + }, [tripData, isEditMode, tripMaxTravelers]); | |
| 214 | 238 | |
| 215 | 239 | useEffect(() => { |
| 216 | 240 | if (availabilityData) { |
| 217 | 241 | const totalSeats = |
| @@ -358,9 +382,9 @@ | ||
| 358 | 382 | newErrors.arrival_date = __("Arrival date is required", "yatra"); |
| 359 | 383 | } |
| 360 | 384 | |
| 361 | 385 | // Validate dates and times based on trip type |
| 362 | - const isSingleDay = tripData?.trip_type === "single_day"; | |
| 386 | + const isSingleDay = isSingleDayTrip; | |
| 363 | 387 | |
| 364 | 388 | if (isSingleDay) { |
| 365 | 389 | // For single day trips, both dates should be the same |
| 366 | 390 | if ( |
| @@ -372,21 +396,18 @@ | ||
| 372 | 396 | "For single day trips, departure and arrival must be on the same date", |
| 373 | 397 | "yatra", |
| 374 | 398 | ); |
| 375 | 399 | } |
| 376 | - // Validate times | |
| 377 | - 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) { | |
| 378 | 405 | newErrors.departure_time = __( |
| 379 | - "Departure time is required for single day trips", | |
| 406 | + "Departure time is required when the trip has multiple time slots", | |
| 380 | 407 | "yatra", |
| 381 | 408 | ); |
| 382 | 409 | } |
| 383 | - if (!formData.arrival_time) { | |
| 384 | - newErrors.arrival_time = __( | |
| 385 | - "Arrival time is required for single day trips", | |
| 386 | - "yatra", | |
| 387 | - ); | |
| 388 | - } | |
| 389 | 410 | // Validate that arrival time is after departure time |
| 390 | 411 | if ( |
| 391 | 412 | formData.departure_time && |
| 392 | 413 | formData.arrival_time && |
| @@ -706,9 +727,15 @@ | ||
| 706 | 727 | <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 707 | 728 | <div> |
| 708 | 729 | <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"> |
| 709 | 730 | {__("Departure Time", "yatra")}{" "} |
| 710 | - <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 | + )} | |
| 711 | 738 | </label> |
| 712 | 739 | <TimePicker |
| 713 | 740 | value={formData.departure_time} |
| 714 | 741 | onChange={(value: string) => |
| @@ -725,9 +752,11 @@ | ||
| 725 | 752 | </div> |
| 726 | 753 | <div> |
| 727 | 754 | <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"> |
| 728 | 755 | {__("Arrival Time", "yatra")}{" "} |
| 729 | - <span className="text-red-500">*</span> | |
| 756 | + <span className="text-gray-400 font-normal"> | |
| 757 | + {__("(optional)", "yatra")} | |
| 758 | + </span> | |
| 730 | 759 | </label> |
| 731 | 760 | <TimePicker |
| 732 | 761 | value={formData.arrival_time} |
| 733 | 762 | onChange={(value: string) => |
| @@ -1525,12 +1554,23 @@ | ||
| 1525 | 1554 | {errors.total_seats} |
| 1526 | 1555 | </p> |
| 1527 | 1556 | )} |
| 1528 | 1557 | <HelpText |
| 1529 | - text={__( | |
| 1530 | - "Maximum number of seats available for this date", | |
| 1531 | - "yatra", | |
| 1532 | - )} | |
| 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 | + } | |
| 1533 | 1573 | className="mt-1" |
| 1534 | 1574 | /> |
| 1535 | 1575 | </div> |
| 1536 | 1576 | {isEditMode && ( |