| @@ -348,8 +348,23 @@ | ||
| 348 | 348 | }, |
| 349 | 349 | enabled: !!selectedTripId, |
| 350 | 350 | }); |
| 351 | 351 | |
| 352 | + // Dates generated by the trip's recurring rules — shown READ-ONLY on the | |
| 353 | + // calendar so a rules-only trip isn't a blank screen. Kept separate from the | |
| 354 | + // main (specific-date) query so the paginated List view and its edit/delete/ | |
| 355 | + // bulk actions are untouched: those only ever operate on real rows. | |
| 356 | + const { data: generatedData } = useQuery({ | |
| 357 | + queryKey: ["availability-generated", selectedTripId], | |
| 358 | + queryFn: async () => { | |
| 359 | + if (!selectedTripId) return { dates: [] }; | |
| 360 | + return await apiClient.get("/availability/generated", { | |
| 361 | + params: { trip_id: selectedTripId }, | |
| 362 | + }); | |
| 363 | + }, | |
| 364 | + enabled: !!selectedTripId && viewMode === "calendar", | |
| 365 | + }); | |
| 366 | + | |
| 352 | 367 | // Delete availability date |
| 353 | 368 | const deleteMutation = useMutation({ |
| 354 | 369 | mutationFn: async (id: string) => { |
| 355 | 370 | return await apiClient.delete(`/availability/${id}`); |
| @@ -540,8 +555,42 @@ | ||
| 540 | 555 | |
| 541 | 556 | return filtered; |
| 542 | 557 | }, [availabilityData, searchTerm, statusFilter, monthFilter, selectedTrip]); |
| 543 | 558 | |
| 559 | + // Calendar-only dataset: the specific dates above PLUS read-only rule-generated | |
| 560 | + // dates for any (date, time) that has no specific row. A specific row always | |
| 561 | + // wins — the operator can edit it — so an existing manual override is never | |
| 562 | + // masked by its rule. Only the calendar uses this; the List view keeps using | |
| 563 | + // filteredDates untouched. | |
| 564 | + const calendarDates = useMemo(() => { | |
| 565 | + const rows = [...filteredDates]; | |
| 566 | + // Dedup against ALL specific rows, not just the filtered ones: a specific | |
| 567 | + // override must mask its rule-generated twin even when the active status/ | |
| 568 | + // month filter has hidden that override — otherwise the calendar would show | |
| 569 | + // the rule's "available" date over a real blocked/edited row. | |
| 570 | + const seen = new Set( | |
| 571 | + (availabilityData?.dates || []).map( | |
| 572 | + (d: any) => `${d.departure_date}|${d.departure_time || ""}`, | |
| 573 | + ), | |
| 574 | + ); | |
| 575 | + const generated = (generatedData as any)?.dates || []; | |
| 576 | + for (const g of generated) { | |
| 577 | + const key = `${g.departure_date}|${g.departure_time || ""}`; | |
| 578 | + if (seen.has(key)) continue; | |
| 579 | + seen.add(key); | |
| 580 | + rows.push({ | |
| 581 | + ...g, | |
| 582 | + id: String(g.id), | |
| 583 | + seats_remaining: | |
| 584 | + (g.available_seats || 0) > 10 | |
| 585 | + ? "10+" | |
| 586 | + : String(g.available_seats || 0), | |
| 587 | + is_virtual: true, | |
| 588 | + } as unknown as AvailabilityDate); | |
| 589 | + } | |
| 590 | + return rows; | |
| 591 | + }, [filteredDates, generatedData, availabilityData]); | |
| 592 | + | |
| 544 | 593 | // Get available months for filter (only for multi-day trips) |
| 545 | 594 | const availableMonths = useMemo(() => { |
| 546 | 595 | if (!availabilityData?.dates || selectedTrip?.trip_type === "single_day") |
| 547 | 596 | return []; |
| @@ -1782,12 +1831,17 @@ | ||
| 1782 | 1831 | </div> |
| 1783 | 1832 | ) : ( |
| 1784 | 1833 | <div className="mt-4"> |
| 1785 | 1834 | <AvailabilityCalendar |
| 1786 | - dates={filteredDates} | |
| 1835 | + dates={calendarDates} | |
| 1787 | 1836 | tripType={selectedTrip?.trip_type} |
| 1788 | 1837 | currency={selectedTrip?.currency || "USD"} |
| 1789 | 1838 | onDateClick={(date) => { |
| 1839 | + // Rule-generated dates are read-only — they're edited | |
| 1840 | + // on the Recurring Rules tab, not as individual rows. | |
| 1841 | + if ((date as any).is_virtual) { | |
| 1842 | + return; | |
| 1843 | + } | |
| 1790 | 1844 | navigate({ |
| 1791 | 1845 | subpage: "trips", |
| 1792 | 1846 | tab: "availability", |
| 1793 | 1847 | action: "edit", |