| @@ -300,8 +300,9 @@ | ||
| 300 | 300 | activities: namesFromIds(formData?.activities), |
| 301 | 301 | difficulty_level: formData?.difficulty_level ?? "", |
| 302 | 302 | duration_days: formData?.duration_days ?? "", |
| 303 | 303 | duration_nights: formData?.duration_nights ?? "", |
| 304 | + duration_hours: formData?.duration_hours ?? "", | |
| 304 | 305 | best_season: formData?.best_season ?? "", |
| 305 | 306 | price: formData?.price ?? "", |
| 306 | 307 | deposit_percentage: formData?.deposit_percentage ?? "", |
| 307 | 308 | booking_deadline_hours: formData?.booking_deadline_hours ?? "", |
| @@ -366,8 +367,9 @@ | ||
| 366 | 367 | // Duration & Schedule |
| 367 | 368 | trip_type: "single_day" | "multi_day" | "flexible"; |
| 368 | 369 | duration_days: string; |
| 369 | 370 | duration_nights: string; |
| 371 | + duration_hours: string; | |
| 370 | 372 | available_from: string; |
| 371 | 373 | available_to: string; |
| 372 | 374 | booking_window_days: string; |
| 373 | 375 | seasonal_availability: string; |
| @@ -715,8 +717,9 @@ | ||
| 715 | 717 | ], |
| 716 | 718 | trip_type: "multi_day", |
| 717 | 719 | duration_days: "7", |
| 718 | 720 | duration_nights: "6", |
| 721 | + duration_hours: "", | |
| 719 | 722 | available_from: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) |
| 720 | 723 | .toISOString() |
| 721 | 724 | .split("T")[0], // 30 days from now |
| 722 | 725 | available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| @@ -972,8 +975,9 @@ | ||
| 972 | 975 | ], |
| 973 | 976 | trip_type: "multi_day", |
| 974 | 977 | duration_days: "14", |
| 975 | 978 | duration_nights: "13", |
| 979 | + duration_hours: "", | |
| 976 | 980 | available_from: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) |
| 977 | 981 | .toISOString() |
| 978 | 982 | .split("T")[0], |
| 979 | 983 | available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| @@ -1234,8 +1238,9 @@ | ||
| 1234 | 1238 | ], |
| 1235 | 1239 | trip_type: "multi_day", |
| 1236 | 1240 | duration_days: "10", |
| 1237 | 1241 | duration_nights: "9", |
| 1242 | + duration_hours: "", | |
| 1238 | 1243 | available_from: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000) |
| 1239 | 1244 | .toISOString() |
| 1240 | 1245 | .split("T")[0], |
| 1241 | 1246 | available_to: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) |
| @@ -1473,8 +1478,9 @@ | ||
| 1473 | 1478 | landmarks: [], |
| 1474 | 1479 | trip_type: "multi_day", |
| 1475 | 1480 | duration_days: "", |
| 1476 | 1481 | duration_nights: "", |
| 1482 | + duration_hours: "", | |
| 1477 | 1483 | available_from: "", |
| 1478 | 1484 | available_to: "", |
| 1479 | 1485 | booking_window_days: "", |
| 1480 | 1486 | seasonal_availability: "", |
| @@ -2216,8 +2222,12 @@ | ||
| 2216 | 2222 | ? "single_day" |
| 2217 | 2223 | : "multi_day")) as "single_day" | "multi_day" | "flexible", |
| 2218 | 2224 | duration_days: tripData.duration_days?.toString() || "", |
| 2219 | 2225 | duration_nights: tripData.duration_nights?.toString() || "", |
| 2226 | + // 0 / null both mean "not hour-based" — show an empty field, not "0". | |
| 2227 | + duration_hours: tripData.duration_hours | |
| 2228 | + ? String(tripData.duration_hours) | |
| 2229 | + : "", | |
| 2220 | 2230 | available_from: tripData.available_from || "", |
| 2221 | 2231 | available_to: tripData.available_to || "", |
| 2222 | 2232 | booking_window_days: tripData.booking_window_days?.toString() || "", |
| 2223 | 2233 | seasonal_availability: tripData.seasonal_availability || "", |
| @@ -2479,8 +2489,9 @@ | ||
| 2479 | 2489 | "featured_image", |
| 2480 | 2490 | "trip_type", |
| 2481 | 2491 | "duration_days", |
| 2482 | 2492 | "duration_nights", |
| 2493 | + "duration_hours", | |
| 2483 | 2494 | ], |
| 2484 | 2495 | location: ["destinations", "starting_location", "ending_location"], |
| 2485 | 2496 | duration: ["available_from", "available_to", "booking_window_days"], |
| 2486 | 2497 | pricing: ["original_price", "discounted_price", "price_types"], |
| @@ -3315,8 +3326,20 @@ | ||
| 3315 | 3326 | duration_days: data.duration_days ? parseInt(data.duration_days) : null, |
| 3316 | 3327 | duration_nights: data.duration_nights |
| 3317 | 3328 | ? parseInt(data.duration_nights) |
| 3318 | 3329 | : null, |
| 3330 | + // Hour-based day tours. This key was missing from the payload, so the | |
| 3331 | + // Duration (Hours) field could never be saved: the form kept the value, | |
| 3332 | + // the request dropped it, and the field reloaded empty. Send 0 (never | |
| 3333 | + // null) when the field is blank — the validator treats null as "not | |
| 3334 | + // provided" and would leave a stale hours value on a trip switched | |
| 3335 | + // back to multi-day, which the storefront would then show as hours. | |
| 3336 | + duration_hours: | |
| 3337 | + data.duration_hours !== "" && | |
| 3338 | + data.duration_hours != null && | |
| 3339 | + !isNaN(parseInt(data.duration_hours)) | |
| 3340 | + ? parseInt(data.duration_hours) | |
| 3341 | + : 0, | |
| 3319 | 3342 | available_from: data.available_from || null, |
| 3320 | 3343 | available_to: data.available_to || null, |
| 3321 | 3344 | booking_window_days: data.booking_window_days |
| 3322 | 3345 | ? parseInt(data.booking_window_days) |
| @@ -4552,11 +4575,17 @@ | ||
| 4552 | 4575 | type="radio" |
| 4553 | 4576 | name="trip_type" |
| 4554 | 4577 | value="multi_day" |
| 4555 | 4578 | checked={formData.trip_type === "multi_day"} |
| 4556 | - onChange={(e) => | |
| 4557 | - handleFieldChange("trip_type", e.target.value) | |
| 4558 | - } | |
| 4579 | + onChange={(e) => { | |
| 4580 | + handleFieldChange("trip_type", e.target.value); | |
| 4581 | + // A multi-day trip is never hour-based — drop any | |
| 4582 | + // hours set while it was a single-day tour. | |
| 4583 | + setFormData((prev) => ({ | |
| 4584 | + ...prev, | |
| 4585 | + duration_hours: "", | |
| 4586 | + })); | |
| 4587 | + }} | |
| 4559 | 4588 | className="sr-only" |
| 4560 | 4589 | /> |
| 4561 | 4590 | <div className="flex flex-1"> |
| 4562 | 4591 | <div className="flex flex-col"> |
| @@ -4699,8 +4728,46 @@ | ||
| 4699 | 4728 | </p> |
| 4700 | 4729 | )} |
| 4701 | 4730 | </div> |
| 4702 | 4731 | </div> |
| 4732 | + | |
| 4733 | + {/* Duration (Hours) — hour-based single-day tours */} | |
| 4734 | + {formData.trip_type === "single_day" && ( | |
| 4735 | + <div className="mt-4"> | |
| 4736 | + <label | |
| 4737 | + htmlFor="duration_hours" | |
| 4738 | + className="block text-xs font-normal text-gray-500 dark:text-gray-400 mb-1.5" | |
| 4739 | + > | |
| 4740 | + {__("Duration (Hours)", "yatra")} | |
| 4741 | + </label> | |
| 4742 | + <Input | |
| 4743 | + id="duration_hours" | |
| 4744 | + type="number" | |
| 4745 | + min="1" | |
| 4746 | + max="24" | |
| 4747 | + value={formData.duration_hours} | |
| 4748 | + onChange={(e) => | |
| 4749 | + handleFieldChange("duration_hours", e.target.value) | |
| 4750 | + } | |
| 4751 | + placeholder={__("e.g., 8", "yatra")} | |
| 4752 | + className={ | |
| 4753 | + errors.duration_hours ? "border-red-500" : "" | |
| 4754 | + } | |
| 4755 | + /> | |
| 4756 | + <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400"> | |
| 4757 | + {__( | |
| 4758 | + "Optional — for an hour-based day tour (e.g. an 8-hour experience). When set, the tour's duration shows in hours and Google Calendar creates a same-day timed event instead of an all-day one. Leave empty for a normal day tour.", | |
| 4759 | + "yatra", | |
| 4760 | + )} | |
| 4761 | + </p> | |
| 4762 | + {errors.duration_hours && ( | |
| 4763 | + <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"> | |
| 4764 | + <AlertCircle className="w-4 h-4" /> | |
| 4765 | + {errors.duration_hours} | |
| 4766 | + </p> | |
| 4767 | + )} | |
| 4768 | + </div> | |
| 4769 | + )} | |
| 4703 | 4770 | </div> |
| 4704 | 4771 | </div> |
| 4705 | 4772 | </div> |
| 4706 | 4773 | </div> |