| @@ -18,8 +18,9 @@ | ||
| 18 | 18 | MapPin, |
| 19 | 19 | CheckCircle2, |
| 20 | 20 | } from "lucide-react"; |
| 21 | 21 | import { __ } from "../lib/i18n"; |
| 22 | +import { toDateValue, todayYmd } from "../lib/dateFormat"; | |
| 22 | 23 | import { Button } from "../components/ui/button"; |
| 23 | 24 | import { Input } from "../components/ui/input"; |
| 24 | 25 | import { Select } from "../components/ui/select"; |
| 25 | 26 | import { SearchableSelect } from "../components/ui/searchable-select"; |
| @@ -86,8 +87,26 @@ | ||
| 86 | 87 | sale_price?: number; |
| 87 | 88 | traveler_pricing?: TravelerPricing[]; |
| 88 | 89 | } |
| 89 | 90 | |
| 91 | +/** | |
| 92 | + * One entry in a rule's exclusion list: either a single day ("2027-03-08", the | |
| 93 | + * original format) or an inclusive period. Both are stored in the same | |
| 94 | + * `excluded_dates` column, so rules saved before periods existed load as-is. | |
| 95 | + */ | |
| 96 | +type ExcludedEntry = string | { start: string; end: string }; | |
| 97 | + | |
| 98 | +const isPeriod = (e: ExcludedEntry): e is { start: string; end: string } => | |
| 99 | + typeof e === "object" && e !== null && "start" in e && "end" in e; | |
| 100 | + | |
| 101 | +/** Sort key: a period sorts by its first day, alongside single dates. */ | |
| 102 | +const excludedSortKey = (e: ExcludedEntry): string => | |
| 103 | + isPeriod(e) ? e.start : e; | |
| 104 | + | |
| 105 | +/** Stable identity for list keys and removal, for both shapes. */ | |
| 106 | +const excludedId = (e: ExcludedEntry): string => | |
| 107 | + isPeriod(e) ? `${e.start}..${e.end}` : e; | |
| 108 | + | |
| 90 | 109 | interface RecurringRule { |
| 91 | 110 | id?: number; |
| 92 | 111 | trip_id: number; |
| 93 | 112 | name: string; |
| @@ -97,9 +116,9 @@ | ||
| 97 | 116 | day_of_week?: number; |
| 98 | 117 | interval_days?: number; |
| 99 | 118 | start_date: string; |
| 100 | 119 | end_date?: string; |
| 101 | - excluded_dates: string[]; | |
| 120 | + excluded_dates: ExcludedEntry[]; | |
| 102 | 121 | months: number[]; // Array of month numbers (1-12) to filter by |
| 103 | 122 | time_slots: TimeSlot[]; // For single-day trips with multiple slots |
| 104 | 123 | pricing_type: "regular" | "traveler_based"; // Allow override of trip's pricing type |
| 105 | 124 | original_price?: number; |
| @@ -156,9 +175,9 @@ | ||
| 156 | 175 | days_of_week: [0], // Sunday by default |
| 157 | 176 | week_of_month: "first", |
| 158 | 177 | day_of_week: 0, |
| 159 | 178 | interval_days: 7, |
| 160 | - start_date: new Date().toISOString().split("T")[0], | |
| 179 | + start_date: todayYmd(), | |
| 161 | 180 | end_date: "", |
| 162 | 181 | excluded_dates: [], |
| 163 | 182 | months: [], // Empty = all months, otherwise specific months (1-12) |
| 164 | 183 | time_slots: [], // For single-day trips with multiple slots |
| @@ -180,8 +199,11 @@ | ||
| 180 | 199 | status: "active", |
| 181 | 200 | }); |
| 182 | 201 | |
| 183 | 202 | const [newExcludedDate, setNewExcludedDate] = useState(""); |
| 203 | + const [newExcludedStart, setNewExcludedStart] = useState(""); | |
| 204 | + const [newExcludedEnd, setNewExcludedEnd] = useState(""); | |
| 205 | + const [excludedError, setExcludedError] = useState(""); | |
| 184 | 206 | const [previewData, setPreviewData] = useState<{ |
| 185 | 207 | total: number; |
| 186 | 208 | dates: any[]; |
| 187 | 209 | } | null>(null); |
| @@ -194,19 +216,33 @@ | ||
| 194 | 216 | const response = await apiClient.get("/trips", { |
| 195 | 217 | params: { per_page: 100, status: "publish" }, |
| 196 | 218 | }); |
| 197 | 219 | return { |
| 198 | - trips: (response?.data || []).map((trip: any) => ({ | |
| 199 | - id: Number(trip.id) || 0, | |
| 200 | - title: trip.title, | |
| 201 | - trip_type: | |
| 202 | - trip.trip_type || | |
| 203 | - (trip.duration_days <= 1 ? "single_day" : "multi_day"), | |
| 204 | - duration_days: trip.duration_days || 1, | |
| 205 | - starting_location: trip.starting_location, | |
| 206 | - ending_location: trip.ending_location, | |
| 207 | - pricing_type: trip.pricing_type || "regular", | |
| 208 | - })) as Trip[], | |
| 220 | + trips: (response?.data || []).map((trip: any) => { | |
| 221 | + // Some endpoints return `pricing_type` as "regular" even when the trip is | |
| 222 | + // effectively traveler-based (price types configured). Infer the effective | |
| 223 | + // pricing type from `price_types` when present so the Rules UI reflects | |
| 224 | + // real trip configuration. | |
| 225 | + const rawPriceTypes = trip.price_types; | |
| 226 | + const hasTravelerPricing = Array.isArray(rawPriceTypes) | |
| 227 | + ? rawPriceTypes.length > 0 | |
| 228 | + : false; | |
| 229 | + const effectivePricingType = hasTravelerPricing | |
| 230 | + ? "traveler_based" | |
| 231 | + : trip.pricing_type || "regular"; | |
| 232 | + | |
| 233 | + return { | |
| 234 | + id: Number(trip.id) || 0, | |
| 235 | + title: trip.title, | |
| 236 | + trip_type: | |
| 237 | + trip.trip_type || | |
| 238 | + (trip.duration_days <= 1 ? "single_day" : "multi_day"), | |
| 239 | + duration_days: trip.duration_days || 1, | |
| 240 | + starting_location: trip.starting_location, | |
| 241 | + ending_location: trip.ending_location, | |
| 242 | + pricing_type: effectivePricingType, | |
| 243 | + }; | |
| 244 | + }) as Trip[], | |
| 209 | 245 | }; |
| 210 | 246 | }, |
| 211 | 247 | }); |
| 212 | 248 | |
| @@ -305,8 +341,23 @@ | ||
| 305 | 341 | | "weekly" |
| 306 | 342 | | "monthly" |
| 307 | 343 | | "interval"; |
| 308 | 344 | |
| 345 | + // Prefer the *trip's effective* pricing type over any stale rule.pricing_type. | |
| 346 | + // Trips can be traveler-based simply by having price_types configured, even if | |
| 347 | + // trip.pricing_type is still "regular". | |
| 348 | + const tripRow = tripsData.trips.find( | |
| 349 | + (t) => t.id === Number(existingRule.trip_id), | |
| 350 | + ); | |
| 351 | + const effectivePricingType = | |
| 352 | + tripRow?.pricing_type || | |
| 353 | + ((tripForLocations as any)?.price_types && | |
| 354 | + Array.isArray((tripForLocations as any).price_types) && | |
| 355 | + (tripForLocations as any).price_types.length > 0 | |
| 356 | + ? "traveler_based" | |
| 357 | + : (tripForLocations as any)?.pricing_type) || | |
| 358 | + "regular"; | |
| 359 | + | |
| 309 | 360 | setFormData({ |
| 310 | 361 | trip_id: existingRule.trip_id || 0, |
| 311 | 362 | name: existingRule.name || "", |
| 312 | 363 | rule_type: ruleType, |
| @@ -313,10 +364,9 @@ | ||
| 313 | 364 | days_of_week: daysOfWeek.length > 0 ? daysOfWeek : [0], |
| 314 | 365 | week_of_month: existingRule.week_of_month || "first", |
| 315 | 366 | day_of_week: existingRule.day_of_week ?? 0, |
| 316 | 367 | interval_days: existingRule.interval_days || 7, |
| 317 | - start_date: | |
| 318 | - existingRule.start_date || new Date().toISOString().split("T")[0], | |
| 368 | + start_date: existingRule.start_date || todayYmd(), | |
| 319 | 369 | end_date: existingRule.end_date || "", |
| 320 | 370 | excluded_dates: Array.isArray(existingRule.excluded_dates) |
| 321 | 371 | ? existingRule.excluded_dates |
| 322 | 372 | : [], |
| @@ -323,9 +373,9 @@ | ||
| 323 | 373 | months: Array.isArray(existingRule.months) ? existingRule.months : [], |
| 324 | 374 | time_slots: Array.isArray(existingRule.time_slots) |
| 325 | 375 | ? existingRule.time_slots |
| 326 | 376 | : [], |
| 327 | - pricing_type: existingRule.pricing_type || "regular", | |
| 377 | + pricing_type: effectivePricingType as "regular" | "traveler_based", | |
| 328 | 378 | original_price: existingRule.original_price, |
| 329 | 379 | sale_price: existingRule.sale_price, |
| 330 | 380 | traveler_pricing: Array.isArray(existingRule.traveler_pricing) |
| 331 | 381 | ? existingRule.traveler_pricing |
| @@ -343,21 +393,31 @@ | ||
| 343 | 393 | alert_threshold: existingRule.alert_threshold || 5, |
| 344 | 394 | status: existingRule.status || "active", |
| 345 | 395 | }); |
| 346 | 396 | } |
| 347 | - }, [existingRule, tripsData]); | |
| 397 | + }, [existingRule, tripsData, tripForLocations]); | |
| 348 | 398 | |
| 349 | 399 | // Set pricing type based on selected trip when not editing |
| 350 | 400 | useEffect(() => { |
| 351 | 401 | if (!isEditing && !existingRule) { |
| 402 | + const inferred = (() => { | |
| 403 | + const priceTypes = (tripForLocations as any)?.price_types; | |
| 404 | + const hasTravelerPricing = | |
| 405 | + Array.isArray(priceTypes) && priceTypes.length > 0; | |
| 406 | + return ( | |
| 407 | + hasTravelerPricing | |
| 408 | + ? "traveler_based" | |
| 409 | + : selectedTrip?.pricing_type || | |
| 410 | + (tripForLocations as Trip)?.pricing_type || | |
| 411 | + "regular" | |
| 412 | + ) as "regular" | "traveler_based"; | |
| 413 | + })(); | |
| 414 | + | |
| 352 | 415 | setFormData((prev) => ({ |
| 353 | 416 | ...prev, |
| 354 | 417 | ...(selectedTrip || tripForLocations |
| 355 | 418 | ? { |
| 356 | - pricing_type: (selectedTrip?.pricing_type || | |
| 357 | - (tripForLocations as Trip)?.pricing_type || | |
| 358 | - "regular" || | |
| 359 | - "regular") as "regular" | "traveler_based", | |
| 419 | + pricing_type: inferred, | |
| 360 | 420 | } |
| 361 | 421 | : {}), |
| 362 | 422 | ...(tripForLocations |
| 363 | 423 | ? { |
| @@ -389,11 +449,20 @@ | ||
| 389 | 449 | |
| 390 | 450 | // Create mutation |
| 391 | 451 | const createMutation = useMutation({ |
| 392 | 452 | mutationFn: async (data: RecurringRule) => { |
| 453 | + // `days_of_week` is a JSON column on the backend; send the array as-is | |
| 454 | + // and let the API JSON-encode it. Sending a CSV string (e.g. "0,1,2,3") | |
| 455 | + // is rejected by MySQL with "Invalid JSON text". | |
| 393 | 456 | return await apiClient.post("/recurring-availability", { |
| 394 | 457 | ...data, |
| 395 | - days_of_week: data.days_of_week.join(","), | |
| 458 | + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], | |
| 459 | + // Normalize month-week selector to backend contract. | |
| 460 | + week_of_month: | |
| 461 | + data.rule_type === "monthly" | |
| 462 | + ? ((data.week_of_month || "first") as string).toLowerCase() | |
| 463 | + : data.week_of_month, | |
| 464 | + // On create, omit empty arrays to keep payload small. | |
| 396 | 465 | time_slots: data.time_slots.length > 0 ? data.time_slots : undefined, |
| 397 | 466 | traveler_pricing: |
| 398 | 467 | data.traveler_pricing && data.traveler_pricing.length > 0 |
| 399 | 468 | ? data.traveler_pricing |
| @@ -421,14 +490,19 @@ | ||
| 421 | 490 | const updateMutation = useMutation({ |
| 422 | 491 | mutationFn: async (data: RecurringRule) => { |
| 423 | 492 | return await apiClient.put(`/recurring-availability/${ruleId}`, { |
| 424 | 493 | ...data, |
| 425 | - days_of_week: data.days_of_week.join(","), | |
| 426 | - time_slots: data.time_slots.length > 0 ? data.time_slots : undefined, | |
| 427 | - traveler_pricing: | |
| 428 | - data.traveler_pricing && data.traveler_pricing.length > 0 | |
| 429 | - ? data.traveler_pricing | |
| 430 | - : undefined, | |
| 494 | + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], | |
| 495 | + week_of_month: | |
| 496 | + data.rule_type === "monthly" | |
| 497 | + ? ((data.week_of_month || "first") as string).toLowerCase() | |
| 498 | + : data.week_of_month, | |
| 499 | + // IMPORTANT: on update, send empty arrays explicitly to clear persisted | |
| 500 | + // JSON columns; omitting the key leaves the old value in DB. | |
| 501 | + time_slots: Array.isArray(data.time_slots) ? data.time_slots : [], | |
| 502 | + traveler_pricing: Array.isArray(data.traveler_pricing) | |
| 503 | + ? data.traveler_pricing | |
| 504 | + : [], | |
| 431 | 505 | }); |
| 432 | 506 | }, |
| 433 | 507 | onSuccess: () => { |
| 434 | 508 | queryClient.invalidateQueries({ queryKey: ["recurring-availability"] }); |
| @@ -451,9 +525,13 @@ | ||
| 451 | 525 | const previewMutation = useMutation({ |
| 452 | 526 | mutationFn: async (data: RecurringRule) => { |
| 453 | 527 | return await apiClient.post("/recurring-availability/preview", { |
| 454 | 528 | ...data, |
| 455 | - days_of_week: data.days_of_week.join(","), | |
| 529 | + days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], | |
| 530 | + week_of_month: | |
| 531 | + data.rule_type === "monthly" | |
| 532 | + ? ((data.week_of_month || "first") as string).toLowerCase() | |
| 533 | + : data.week_of_month, | |
| 456 | 534 | time_slots: data.time_slots.length > 0 ? data.time_slots : undefined, |
| 457 | 535 | traveler_pricing: |
| 458 | 536 | data.traveler_pricing && data.traveler_pricing.length > 0 |
| 459 | 537 | ? data.traveler_pricing |
| @@ -507,24 +585,79 @@ | ||
| 507 | 585 | : [...prev.days_of_week, day].sort((a, b) => a - b), |
| 508 | 586 | })); |
| 509 | 587 | }; |
| 510 | 588 | |
| 511 | - // Add excluded date | |
| 589 | + const sortExcluded = (list: ExcludedEntry[]): ExcludedEntry[] => | |
| 590 | + [...list].sort((a, b) => excludedSortKey(a).localeCompare(excludedSortKey(b))); | |
| 591 | + | |
| 592 | + // Add a single excluded day | |
| 512 | 593 | const addExcludedDate = () => { |
| 513 | - if (newExcludedDate && !formData.excluded_dates.includes(newExcludedDate)) { | |
| 594 | + if (!newExcludedDate) { | |
| 595 | + return; | |
| 596 | + } | |
| 597 | + const exists = formData.excluded_dates.some( | |
| 598 | + (e) => excludedId(e) === newExcludedDate, | |
| 599 | + ); | |
| 600 | + if (exists) { | |
| 601 | + setExcludedError(__("That date is already excluded.", "yatra")); | |
| 602 | + return; | |
| 603 | + } | |
| 604 | + setExcludedError(""); | |
| 605 | + setFormData((prev) => ({ | |
| 606 | + ...prev, | |
| 607 | + excluded_dates: sortExcluded([...prev.excluded_dates, newExcludedDate]), | |
| 608 | + })); | |
| 609 | + setNewExcludedDate(""); | |
| 610 | + }; | |
| 611 | + | |
| 612 | + // Add an excluded period — one entry covering every day from start to end, | |
| 613 | + // so a shutdown doesn't have to be added a day at a time. | |
| 614 | + const addExcludedPeriod = () => { | |
| 615 | + if (!newExcludedStart || !newExcludedEnd) { | |
| 616 | + return; | |
| 617 | + } | |
| 618 | + // Accept the two dates in either order rather than rejecting the input. | |
| 619 | + const start = | |
| 620 | + newExcludedStart <= newExcludedEnd ? newExcludedStart : newExcludedEnd; | |
| 621 | + const end = | |
| 622 | + newExcludedStart <= newExcludedEnd ? newExcludedEnd : newExcludedStart; | |
| 623 | + | |
| 624 | + if (start === end) { | |
| 625 | + // A one-day period is just a single date; keep the list in its simplest form. | |
| 626 | + if (formData.excluded_dates.some((e) => excludedId(e) === start)) { | |
| 627 | + setExcludedError(__("That date is already excluded.", "yatra")); | |
| 628 | + return; | |
| 629 | + } | |
| 630 | + setExcludedError(""); | |
| 514 | 631 | setFormData((prev) => ({ |
| 515 | 632 | ...prev, |
| 516 | - excluded_dates: [...prev.excluded_dates, newExcludedDate].sort(), | |
| 633 | + excluded_dates: sortExcluded([...prev.excluded_dates, start]), | |
| 517 | 634 | })); |
| 518 | - setNewExcludedDate(""); | |
| 635 | + setNewExcludedStart(""); | |
| 636 | + setNewExcludedEnd(""); | |
| 637 | + return; | |
| 519 | 638 | } |
| 639 | + | |
| 640 | + const id = `${start}..${end}`; | |
| 641 | + if (formData.excluded_dates.some((e) => excludedId(e) === id)) { | |
| 642 | + setExcludedError(__("That period is already excluded.", "yatra")); | |
| 643 | + return; | |
| 644 | + } | |
| 645 | + setExcludedError(""); | |
| 646 | + setFormData((prev) => ({ | |
| 647 | + ...prev, | |
| 648 | + excluded_dates: sortExcluded([...prev.excluded_dates, { start, end }]), | |
| 649 | + })); | |
| 650 | + setNewExcludedStart(""); | |
| 651 | + setNewExcludedEnd(""); | |
| 520 | 652 | }; |
| 521 | 653 | |
| 522 | - // Remove excluded date | |
| 523 | - const removeExcludedDate = (date: string) => { | |
| 654 | + // Remove an excluded day or period | |
| 655 | + const removeExcluded = (id: string) => { | |
| 656 | + setExcludedError(""); | |
| 524 | 657 | setFormData((prev) => ({ |
| 525 | 658 | ...prev, |
| 526 | - excluded_dates: prev.excluded_dates.filter((d) => d !== date), | |
| 659 | + excluded_dates: prev.excluded_dates.filter((e) => excludedId(e) !== id), | |
| 527 | 660 | })); |
| 528 | 661 | }; |
| 529 | 662 | |
| 530 | 663 | const isLoading = createMutation.isPending || updateMutation.isPending; |
| @@ -779,9 +912,9 @@ | ||
| 779 | 912 | setFormData((prev) => ({ ...prev, end_date: value })) |
| 780 | 913 | } |
| 781 | 914 | minDate={ |
| 782 | 915 | formData.start_date |
| 783 | - ? new Date(formData.start_date) | |
| 916 | + ? toDateValue(formData.start_date) | |
| 784 | 917 | : undefined |
| 785 | 918 | } |
| 786 | 919 | placeholder={__("Select end date (optional)", "yatra")} |
| 787 | 920 | /> |
| @@ -850,9 +983,9 @@ | ||
| 850 | 983 | </p> |
| 851 | 984 | )} |
| 852 | 985 | </div> |
| 853 | 986 | |
| 854 | - {/* Excluded Dates */} | |
| 987 | + {/* Excluded Dates & Periods */} | |
| 855 | 988 | <div className="pt-4 border-t border-gray-200 dark:border-gray-700"> |
| 856 | 989 | <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> |
| 857 | 990 | {__("Excluded Dates", "yatra")}{" "} |
| 858 | 991 | <span className="text-gray-400"> |
| @@ -875,34 +1008,86 @@ | ||
| 875 | 1008 | > |
| 876 | 1009 | <Plus className="w-4 h-4" /> |
| 877 | 1010 | </Button> |
| 878 | 1011 | </div> |
| 1012 | + | |
| 1013 | + {/* A whole period in one go — a shutdown shouldn't have to be | |
| 1014 | + added a day at a time. Named "Periods" so it can't be | |
| 1015 | + confused with the rule's own Date Range above. */} | |
| 1016 | + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 mt-4"> | |
| 1017 | + {__("Excluded Periods", "yatra")}{" "} | |
| 1018 | + <span className="text-gray-400"> | |
| 1019 | + ({__("holiday closures, vacations", "yatra")}) | |
| 1020 | + </span> | |
| 1021 | + </label> | |
| 1022 | + <div className="flex flex-col sm:flex-row gap-2 mb-1"> | |
| 1023 | + <div className="flex-1"> | |
| 1024 | + <DatePicker | |
| 1025 | + value={newExcludedStart} | |
| 1026 | + onChange={(value: string) => setNewExcludedStart(value)} | |
| 1027 | + placeholder={__("First day", "yatra")} | |
| 1028 | + /> | |
| 1029 | + </div> | |
| 1030 | + <div className="flex-1"> | |
| 1031 | + <DatePicker | |
| 1032 | + value={newExcludedEnd} | |
| 1033 | + onChange={(value: string) => setNewExcludedEnd(value)} | |
| 1034 | + placeholder={__("Last day", "yatra")} | |
| 1035 | + /> | |
| 1036 | + </div> | |
| 1037 | + <Button | |
| 1038 | + type="button" | |
| 1039 | + variant="outline" | |
| 1040 | + onClick={addExcludedPeriod} | |
| 1041 | + disabled={!newExcludedStart || !newExcludedEnd} | |
| 1042 | + > | |
| 1043 | + <Plus className="w-4 h-4" /> | |
| 1044 | + </Button> | |
| 1045 | + </div> | |
| 1046 | + <p className="text-xs text-gray-500 dark:text-gray-400 mb-2"> | |
| 1047 | + {__( | |
| 1048 | + "Both days are included. Every date in between is skipped by this rule.", | |
| 1049 | + "yatra", | |
| 1050 | + )} | |
| 1051 | + </p> | |
| 1052 | + | |
| 1053 | + {excludedError && ( | |
| 1054 | + <p className="text-xs text-red-600 dark:text-red-400 mb-2"> | |
| 1055 | + {excludedError} | |
| 1056 | + </p> | |
| 1057 | + )} | |
| 1058 | + | |
| 879 | 1059 | {formData.excluded_dates.length > 0 && ( |
| 880 | 1060 | <div className="flex flex-wrap gap-2"> |
| 881 | - {formData.excluded_dates.map((date) => ( | |
| 882 | - <Badge | |
| 883 | - key={date} | |
| 884 | - variant="outline" | |
| 885 | - className="flex items-center gap-1" | |
| 886 | - > | |
| 887 | - {new Date(date + "T00:00:00").toLocaleDateString( | |
| 888 | - "en-US", | |
| 889 | - { | |
| 890 | - weekday: "short", | |
| 891 | - month: "short", | |
| 892 | - day: "numeric", | |
| 893 | - year: "numeric", | |
| 894 | - }, | |
| 895 | - )} | |
| 896 | - <button | |
| 897 | - type="button" | |
| 898 | - onClick={() => removeExcludedDate(date)} | |
| 899 | - className="ml-1 hover:text-red-500" | |
| 1061 | + {formData.excluded_dates.map((entry) => { | |
| 1062 | + const id = excludedId(entry); | |
| 1063 | + const fmt = (d: string) => | |
| 1064 | + new Date(d + "T00:00:00").toLocaleDateString("en-US", { | |
| 1065 | + weekday: "short", | |
| 1066 | + month: "short", | |
| 1067 | + day: "numeric", | |
| 1068 | + year: "numeric", | |
| 1069 | + }); | |
| 1070 | + return ( | |
| 1071 | + <Badge | |
| 1072 | + key={id} | |
| 1073 | + variant="outline" | |
| 1074 | + className="flex items-center gap-1" | |
| 900 | 1075 | > |
| 901 | - <X className="w-3 h-3" /> | |
| 902 | - </button> | |
| 903 | - </Badge> | |
| 904 | - ))} | |
| 1076 | + {isPeriod(entry) | |
| 1077 | + ? `${fmt(entry.start)} → ${fmt(entry.end)}` | |
| 1078 | + : fmt(entry)} | |
| 1079 | + <button | |
| 1080 | + type="button" | |
| 1081 | + onClick={() => removeExcluded(id)} | |
| 1082 | + className="ml-1 hover:text-red-500" | |
| 1083 | + aria-label={__("Remove exclusion", "yatra")} | |
| 1084 | + > | |
| 1085 | + <X className="w-3 h-3" /> | |
| 1086 | + </button> | |
| 1087 | + </Badge> | |
| 1088 | + ); | |
| 1089 | + })} | |
| 905 | 1090 | </div> |
| 906 | 1091 | )} |
| 907 | 1092 | </div> |
| 908 | 1093 | </CardContent> |
| @@ -1299,17 +1484,22 @@ | ||
| 1299 | 1484 | )} |
| 1300 | 1485 | </div> |
| 1301 | 1486 | <button |
| 1302 | 1487 | type="button" |
| 1303 | - onClick={() => | |
| 1488 | + onClick={() => { | |
| 1489 | + const categoryIdToRemove = | |
| 1490 | + pricing.category_id; | |
| 1304 | 1491 | setFormData((prev) => ({ |
| 1305 | 1492 | ...prev, |
| 1306 | - traveler_pricing: | |
| 1307 | - prev.traveler_pricing?.filter( | |
| 1308 | - (_, i) => i !== index, | |
| 1309 | - ) || [], | |
| 1310 | - })) | |
| 1311 | - } | |
| 1493 | + traveler_pricing: ( | |
| 1494 | + prev.traveler_pricing || [] | |
| 1495 | + ).filter( | |
| 1496 | + (tp) => | |
| 1497 | + tp.category_id !== | |
| 1498 | + categoryIdToRemove, | |
| 1499 | + ), | |
| 1500 | + })); | |
| 1501 | + }} | |
| 1312 | 1502 | className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" |
| 1313 | 1503 | title={__( |
| 1314 | 1504 | "Remove Pricing", |
| 1315 | 1505 | "yatra", |
| @@ -1946,33 +2136,37 @@ | ||
| 1946 | 2136 | </div> |
| 1947 | 2137 | <button |
| 1948 | 2138 | type="button" |
| 1949 | 2139 | onClick={() => { |
| 1950 | - const newSlots = [ | |
| 1951 | - ...formData.time_slots, | |
| 1952 | - ]; | |
| 1953 | - if ( | |
| 1954 | - newSlots[index] && | |
| 1955 | - newSlots[index] | |
| 1956 | - .traveler_pricing | |
| 1957 | - ) { | |
| 1958 | - newSlots[ | |
| 1959 | - index | |
| 1960 | - ].traveler_pricing = | |
| 1961 | - newSlots[ | |
| 1962 | - index | |
| 1963 | - ].traveler_pricing!.filter( | |
| 1964 | - (_, i) => | |
| 1965 | - i !== tpIndex, | |
| 1966 | - ); | |
| 1967 | - setFormData( | |
| 1968 | - (prev) => ({ | |
| 1969 | - ...prev, | |
| 1970 | - time_slots: | |
| 1971 | - newSlots, | |
| 1972 | - }), | |
| 1973 | - ); | |
| 1974 | - } | |
| 2140 | + const categoryIdToRemove = | |
| 2141 | + tp.category_id; | |
| 2142 | + setFormData((prev) => { | |
| 2143 | + const newSlots = [ | |
| 2144 | + ...prev.time_slots, | |
| 2145 | + ]; | |
| 2146 | + if ( | |
| 2147 | + !newSlots[index] | |
| 2148 | + ) { | |
| 2149 | + return prev; | |
| 2150 | + } | |
| 2151 | + newSlots[index] = { | |
| 2152 | + ...newSlots[index], | |
| 2153 | + traveler_pricing: ( | |
| 2154 | + newSlots[index] | |
| 2155 | + .traveler_pricing || | |
| 2156 | + [] | |
| 2157 | + ).filter( | |
| 2158 | + (p) => | |
| 2159 | + p.category_id !== | |
| 2160 | + categoryIdToRemove, | |
| 2161 | + ), | |
| 2162 | + }; | |
| 2163 | + return { | |
| 2164 | + ...prev, | |
| 2165 | + time_slots: | |
| 2166 | + newSlots, | |
| 2167 | + }; | |
| 2168 | + }); | |
| 1975 | 2169 | }} |
| 1976 | 2170 | className="p-0.5 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" |
| 1977 | 2171 | > |
| 1978 | 2172 | <X className="w-3 h-3" /> |
| @@ -2260,11 +2454,18 @@ | ||
| 2260 | 2454 | tripForLocations.starting_longitude, |
| 2261 | 2455 | ), |
| 2262 | 2456 | ), |
| 2263 | 2457 | ] |
| 2264 | - : [-8.3405, 115.092] | |
| 2458 | + : [20, 0] | |
| 2265 | 2459 | } |
| 2266 | - defaultZoom={13} | |
| 2460 | + defaultZoom={ | |
| 2461 | + formData.from_latitude && formData.from_longitude | |
| 2462 | + ? 13 | |
| 2463 | + : tripForLocations?.starting_latitude && | |
| 2464 | + tripForLocations?.starting_longitude | |
| 2465 | + ? 13 | |
| 2466 | + : 2 | |
| 2467 | + } | |
| 2267 | 2468 | mapHeight="300px" |
| 2268 | 2469 | showMapButton={false} |
| 2269 | 2470 | searchLimit={8} |
| 2270 | 2471 | __={__} |
| @@ -2345,11 +2546,44 @@ | ||
| 2345 | 2546 | tripForLocations.ending_longitude, |
| 2346 | 2547 | ), |
| 2347 | 2548 | ), |
| 2348 | 2549 | ] |
| 2349 | - : [-8.5069, 115.2625] | |
| 2550 | + : formData.from_latitude && | |
| 2551 | + formData.from_longitude | |
| 2552 | + ? [ | |
| 2553 | + parseFloat(formData.from_latitude), | |
| 2554 | + parseFloat(formData.from_longitude), | |
| 2555 | + ] | |
| 2556 | + : tripForLocations?.starting_latitude && | |
| 2557 | + tripForLocations?.starting_longitude | |
| 2558 | + ? [ | |
| 2559 | + parseFloat( | |
| 2560 | + String( | |
| 2561 | + tripForLocations.starting_latitude, | |
| 2562 | + ), | |
| 2563 | + ), | |
| 2564 | + parseFloat( | |
| 2565 | + String( | |
| 2566 | + tripForLocations.starting_longitude, | |
| 2567 | + ), | |
| 2568 | + ), | |
| 2569 | + ] | |
| 2570 | + : [20, 0] | |
| 2350 | 2571 | } |
| 2351 | - defaultZoom={13} | |
| 2572 | + defaultZoom={ | |
| 2573 | + formData.to_latitude && formData.to_longitude | |
| 2574 | + ? 13 | |
| 2575 | + : tripForLocations?.ending_latitude && | |
| 2576 | + tripForLocations?.ending_longitude | |
| 2577 | + ? 13 | |
| 2578 | + : formData.from_latitude && | |
| 2579 | + formData.from_longitude | |
| 2580 | + ? 13 | |
| 2581 | + : tripForLocations?.starting_latitude && | |
| 2582 | + tripForLocations?.starting_longitude | |
| 2583 | + ? 13 | |
| 2584 | + : 2 | |
| 2585 | + } | |
| 2352 | 2586 | mapHeight="300px" |
| 2353 | 2587 | showMapButton={false} |
| 2354 | 2588 | searchLimit={8} |
| 2355 | 2589 | __={__} |
| @@ -2454,17 +2688,16 @@ | ||
| 2454 | 2688 | key={index} |
| 2455 | 2689 | className="text-xs px-2 py-1 bg-gray-50 dark:bg-gray-800 rounded flex justify-between" |
| 2456 | 2690 | > |
| 2457 | 2691 | <span> |
| 2458 | - {new Date(date.departure_date).toLocaleDateString( | |
| 2459 | - "en-US", | |
| 2460 | - { | |
| 2461 | - weekday: "short", | |
| 2462 | - month: "short", | |
| 2463 | - day: "numeric", | |
| 2464 | - year: "numeric", | |
| 2465 | - }, | |
| 2466 | - )} | |
| 2692 | + {toDateValue( | |
| 2693 | + date.departure_date, | |
| 2694 | + ).toLocaleDateString("en-US", { | |
| 2695 | + weekday: "short", | |
| 2696 | + month: "short", | |
| 2697 | + day: "numeric", | |
| 2698 | + year: "numeric", | |
| 2699 | + })} | |
| 2467 | 2700 | </span> |
| 2468 | 2701 | {date.departure_time && ( |
| 2469 | 2702 | <span className="text-gray-500"> |
| 2470 | 2703 | {date.departure_time} |