/** * Availability Management Page * Manage trip availability dates separately from trip packages * Allows adding/editing dates for trips without modifying the trip itself */ import React, { useState, useMemo, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { CalendarDays, Plus, Search, X, Edit, Copy, Trash2, MapPin, Users, AlertCircle, List, Calendar, RefreshCw, Bell, Ban, Clock, Eye, Settings, } from "lucide-react"; import { __ } from "../lib/i18n"; import { toDateValue } from "../lib/dateFormat"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { SearchableSelect } from "../components/ui/searchable-select"; import { PageHeader } from "../components/common/PageHeader"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "../components/ui/card"; import { Badge } from "../components/ui/badge"; import { Alert } from "../components/ui/alert"; import { useNavigate } from "../hooks/useNavigate"; import { AvailabilityCalendar } from "../components/availability/AvailabilityCalendar"; import { RecurringRules } from "../components/availability/RecurringRules"; import { apiClient } from "../lib/api-client"; import { getErrorContext } from "../lib/errors"; import { useToast } from "../components/ui/toast"; import { BulkActionToolbar, Table as SharedTable } from "../components/shared"; import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; import { DatePicker } from "../components/ui/date-picker"; import { TimePicker } from "../components/ui/time-picker"; interface Trip { id: number; title: string; slug: string; currency: string; starting_location?: string; ending_location?: string; trip_type?: "single_day" | "multi_day"; } interface AvailabilityDate { id: string; trip_id: number; departure_date: string; departure_time?: string; arrival_date: string; arrival_time?: string; total_seats: number; // Total capacity booked_seats: number; // Currently booked available_seats: number; // Calculated: total_seats - booked_seats waitlist_count: number; // Number of people on waitlist seats_remaining: string; // Legacy field for display original_price: string; discounted_price: string; discount_percentage: string; // Per-category pricing for traveler-based trips (adult/child/senior, etc.). // When present, the single original/discounted columns are NULL. price_types?: Array<{ category_id: number; category_label?: string; original_price: number; discounted_price?: number | null; sale_price?: number | null; }>; status: | "available" | "sold_out" | "limited" | "closed" | "blocked" | "cancelled"; from_location?: string; to_location?: string; is_blocked?: boolean; // Blocked for maintenance/holidays block_reason?: string; // Reason for blocking alert_threshold?: number; // Alert when seats drop below this last_synced_at?: string; // Last real-time sync timestamp created_at?: string; updated_at?: string; } function mapApiRowToTrip(trip: Record): Trip { return { id: Number(trip.id), title: String(trip.title ?? ""), slug: String(trip.slug ?? ""), currency: String(trip.currency || "USD"), starting_location: trip.starting_location as string | undefined, ending_location: trip.ending_location as string | undefined, trip_type: (trip.trip_type as Trip["trip_type"]) || "multi_day", }; } /** * Normalize /trips payloads and stale TanStack Query cache (client nav can reuse envelopes without `trips`). */ function tripsFromAvailabilityQueryCached(raw: unknown): Trip[] { if (raw == null) { return []; } if (Array.isArray(raw)) { return raw as Trip[]; } if (typeof raw === "object") { const o = raw as Record; if (Array.isArray(o.trips)) { return o.trips as Trip[]; } let rows: unknown[] = []; if (Array.isArray(o.data)) { rows = o.data as unknown[]; } else if ( o.data && typeof o.data === "object" && Array.isArray((o.data as Record).data) ) { rows = (o.data as Record).data as unknown[]; } return rows.map((t) => mapApiRowToTrip(t as Record)); } return []; } const Availability: React.FC = () => { const { navigate } = useNavigate(); const queryClient = useQueryClient(); const { showToast } = useToast(); // Get trip_id from URL on mount and when URL changes const [urlKey, setUrlKey] = useState(0); useEffect(() => { const handleLocationChange = () => { setUrlKey((prev) => prev + 1); }; // Listen for popstate (back/forward button) window.addEventListener("popstate", handleLocationChange); // Also check periodically (fallback for direct navigation) const interval = setInterval(() => { const currentSearch = window.location.search; if (currentSearch !== (window as any).__lastAvailabilitySearch) { (window as any).__lastAvailabilitySearch = currentSearch; handleLocationChange(); } }, 100); return () => { window.removeEventListener("popstate", handleLocationChange); clearInterval(interval); }; }, []); // Read trip_id from URL const tripIdFromUrl = useMemo(() => { const params = new URLSearchParams(window.location.search); const tripId = params.get("trip_id"); return tripId ? parseInt(tripId) : null; // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); // Trip selection - initialize from localStorage or URL if available const [selectedTripId, setSelectedTripId] = useState(() => { const storedTripId = localStorage.getItem("yatra_selected_trip_id"); if (storedTripId) { const parsed = parseInt(storedTripId, 10); if (!Number.isNaN(parsed)) { return parsed; } } return tripIdFromUrl; }); // When URL trip_id changes, prefer it over stored value useEffect(() => { if (tripIdFromUrl && tripIdFromUrl !== selectedTripId) { setSelectedTripId(tripIdFromUrl); localStorage.setItem("yatra_selected_trip_id", tripIdFromUrl.toString()); } }, [tripIdFromUrl, selectedTripId]); // Persist current selection useEffect(() => { if (selectedTripId) { localStorage.setItem("yatra_selected_trip_id", selectedTripId.toString()); } else { localStorage.removeItem("yatra_selected_trip_id"); } }, [selectedTripId]); // View mode: 'list' or 'calendar' const [viewMode, setViewMode] = useState<"list" | "calendar">("list"); // Tab mode: 'specific' or 'recurring' const [tabMode, _setTabMode] = useState<"specific" | "recurring">(() => { const params = new URLSearchParams(window.location.search); const tabModeParam = params.get("tab_mode"); return tabModeParam === "recurring" ? "recurring" : "specific"; }); const setTabMode = (mode: "specific" | "recurring") => { _setTabMode(mode); const params = new URLSearchParams(window.location.search); params.set("tab_mode", mode); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.replaceState({}, "", newUrl); }; // Filters const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [monthFilter, setMonthFilter] = useState("all"); // Real-time sync const [isSyncing, setIsSyncing] = useState(false); const [lastSyncTime, setLastSyncTime] = useState(null); // Inventory alerts visibility const [showAlerts, setShowAlerts] = useState(true); // Pagination const [page, setPage] = useState(1); // Bulk selection & columns state const [selectedIds, setSelectedIds] = useState([]); const [bulkAction, setBulkAction] = useState(""); const [showColumnsDropdown, setShowColumnsDropdown] = useState(false); const [visibleColumns, setVisibleColumns] = useState({ departure: true, arrival: true, locations: true, capacity: true, booked: true, available: true, waitlist: true, price: true, status: true, // Always show status column }); const toggleColumn = (key: keyof typeof visibleColumns) => { setVisibleColumns((prev) => ({ ...prev, [key]: !prev[key], })); }; // Fetch all trips for dropdown — store as Trip[]; normalize stale cache via tripsList const { data: tripsData } = useQuery({ queryKey: ["trips", "availability-dropdown", "v2"], queryFn: async () => { const response = await apiClient.get("/trips", { params: { per_page: 100, status: "publish" }, }); return tripsFromAvailabilityQueryCached(response); }, }); const tripsList = useMemo( () => tripsFromAvailabilityQueryCached(tripsData), [tripsData], ); // Fetch availability dates for selected trip const { data: availabilityData, isLoading, error: availabilityError, } = useQuery({ queryKey: ["availability", selectedTripId, monthFilter, page, searchTerm], queryFn: async () => { if (!selectedTripId) return { dates: [], total: 0 }; const response = await apiClient.get("/availability", { params: { trip_id: selectedTripId, month: monthFilter !== "all" ? monthFilter : undefined, search: searchTerm || undefined, page, per_page: 50, }, }); return { dates: (response?.dates || []).map((date: any) => ({ id: date.id.toString(), trip_id: date.trip_id, departure_date: date.departure_date, departure_time: date.departure_time, arrival_date: date.arrival_date || date.departure_date, arrival_time: date.arrival_time, total_seats: date.total_seats || date.seats_total || 0, booked_seats: date.booked_seats || (date.total_seats || date.seats_total) - (date.available_seats || date.seats_available || 0), available_seats: date.available_seats || date.seats_available || 0, waitlist_count: date.waitlist_count || date.seats_waitlist || 0, seats_remaining: (date.available_seats || date.seats_available || 0) > 10 ? "10+" : (date.available_seats || date.seats_available || 0).toString(), original_price: date.original_price?.toString() || "0", discounted_price: date.discounted_price?.toString() || date.original_price?.toString() || "0", discount_percentage: date.discount_percentage?.toString() || "0", // Carry per-category pricing through so the Price column can show the // real (traveler-based) price; otherwise it falls back to "0". price_types: Array.isArray(date.price_types) ? date.price_types : [], status: date.status || "available", is_blocked: date.is_blocked || date.status === "blocked", block_reason: date.block_reason, alert_threshold: date.alert_threshold || 5, last_synced_at: date.last_synced_at || date.updated_at, from_location: date.from_location, to_location: date.to_location, created_at: date.created_at, updated_at: date.updated_at, })) as AvailabilityDate[], total: response?.total || 0, }; }, enabled: !!selectedTripId, }); // Delete availability date const deleteMutation = useMutation({ mutationFn: async (id: string) => { return await apiClient.delete(`/availability/${id}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["availability"] }); showToast( __("Availability date deleted successfully", "yatra"), "success", ); setDeleteConfirm({ isOpen: false, date: null }); }, onError: (error: any) => { showToast( error?.message || __("Failed to delete availability date", "yatra"), "error", ); }, }); // Format date for display // eslint-disable-next-line react-hooks/exhaustive-deps const formatDate = (dateString: string) => { const date = toDateValue(dateString); return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); }; // Format time for display // eslint-disable-next-line react-hooks/exhaustive-deps const formatTime = (timeString: string) => { if (!timeString) return ""; const [hours, minutes] = timeString.split(":"); const hour = parseInt(hours); const ampm = hour >= 12 ? "PM" : "AM"; const displayHour = hour % 12 || 12; return `${displayHour}:${minutes} ${ampm}`; }; // Get currency symbol // eslint-disable-next-line react-hooks/exhaustive-deps const getCurrencySymbol = (currency: string) => { const symbols: Record = { USD: "$", EUR: "€", GBP: "£", INR: "₹", }; return symbols[currency] || currency; }; // Get status badge variant // eslint-disable-next-line react-hooks/exhaustive-deps const getStatusBadge = (status: string, isBlocked?: boolean) => { // Debug: Log the status values to help with debugging if (isBlocked) { return ( {__("Blocked", "yatra")} ); } switch (status) { case "available": return ( {__("Available", "yatra")} ); case "limited": return ( {__("Limited", "yatra")} ); case "sold_out": return ( {__("Sold Out", "yatra")} ); case "closed": return ( {__("Closed", "yatra")} ); case "blocked": return ( {__("Blocked", "yatra")} ); case "cancelled": return ( {__("Cancelled", "yatra")} ); default: return ( {status || "Unknown"} ); } }; // Real-time sync function const handleSync = async () => { setIsSyncing(true); try { // Refresh availability data await queryClient.invalidateQueries({ queryKey: ["availability"] }); setLastSyncTime(new Date()); showToast( __("Availability data synced successfully", "yatra"), "success", ); } catch (error) { console.error("Sync failed:", error); showToast(__("Failed to sync availability data", "yatra"), "error"); } finally { setIsSyncing(false); } }; const selectedTrip = tripsList.find((t) => t.id === selectedTripId); const availabilityErrorContext = availabilityError ? getErrorContext(availabilityError) : null; // Filter availability dates const filteredDates = useMemo(() => { if (!availabilityData?.dates) return []; let filtered = [...availabilityData.dates]; // Filter by search term (date range or time) if (searchTerm) { const searchLower = searchTerm.toLowerCase(); filtered = filtered.filter( (date) => date.departure_date.includes(searchLower) || date.arrival_date.includes(searchLower) || date.departure_time?.toLowerCase().includes(searchLower) || date.arrival_time?.toLowerCase().includes(searchLower) || date.from_location?.toLowerCase().includes(searchLower) || date.to_location?.toLowerCase().includes(searchLower), ); } // Filter by status if (statusFilter !== "all") { filtered = filtered.filter((date) => date.status === statusFilter); } // Filter by month (only for multi-day trips) if (monthFilter !== "all" && selectedTrip?.trip_type !== "single_day") { const [year, month] = monthFilter.split("-"); filtered = filtered.filter((date) => { const dateObj = toDateValue(date.departure_date); return ( dateObj.getFullYear() === parseInt(year) && dateObj.getMonth() === parseInt(month) - 1 ); }); } return filtered; }, [availabilityData, searchTerm, statusFilter, monthFilter, selectedTrip]); // Get available months for filter (only for multi-day trips) const availableMonths = useMemo(() => { if (!availabilityData?.dates || selectedTrip?.trip_type === "single_day") return []; const months = new Set(); availabilityData.dates.forEach((date) => { const dateObj = toDateValue(date.departure_date); const monthKey = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, "0")}`; months.add(monthKey); }); return Array.from(months).sort(); }, [availabilityData, selectedTrip]); // Calculate inventory alerts (after filteredDates is defined) const inventoryAlerts = useMemo(() => { if (!filteredDates || filteredDates.length === 0) return []; return filteredDates.filter((date) => { if (date.is_blocked) return false; const threshold = date.alert_threshold || 5; return date.available_seats <= threshold && date.available_seats > 0; }); }, [filteredDates]); // Get waitlist entries const waitlistEntries = useMemo(() => { if (!filteredDates || filteredDates.length === 0) return []; return filteredDates.filter((date) => date.waitlist_count > 0); }, [filteredDates]); // Get blocked dates const blockedDates = useMemo(() => { if (!filteredDates || filteredDates.length === 0) return []; return filteredDates.filter((date) => date.is_blocked); }, [filteredDates]); // Status counts for bulk toolbar const statusCounts = useMemo(() => { const counts = { all: 0, available: 0, limited: 0, sold_out: 0, closed: 0, blocked: 0, }; if (!availabilityData?.dates) return counts; availabilityData.dates.forEach((date) => { counts.all++; if (date.status === "available") counts.available++; else if (date.status === "limited") counts.limited++; else if (date.status === "sold_out") counts.sold_out++; else if (date.status === "closed") counts.closed++; else if (date.status === "blocked") counts.blocked++; }); return counts; }, [availabilityData]); // Bulk delete mutation const bulkDeleteMutation = useMutation({ mutationFn: async (ids: string[]) => { await Promise.all( ids.map((id) => apiClient.delete(`/availability/${id}`)), ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["availability"] }); showToast( __("Selected availability dates deleted successfully", "yatra"), "success", ); setSelectedIds([]); setBulkAction(""); }, onError: (error: any) => { showToast( error?.message || __("Failed to delete selected availability dates", "yatra"), "error", ); }, }); const handleBulkApply = () => { if (!bulkAction) { showToast(__("Select a bulk action first.", "yatra"), "warning"); return; } if (selectedIds.length === 0) { showToast( __("Select at least one availability date.", "yatra"), "warning", ); return; } if (bulkAction === "delete") { bulkDeleteMutation.mutate(selectedIds); } else { showToast( __("Unsupported bulk action for availability.", "yatra"), "warning", ); } }; const bulkStatusOptions = [ { value: "delete", label: __("Delete Permanently", "yatra") }, ]; const totalFiltered = filteredDates.length; // Single delete confirmation state const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; date: AvailabilityDate | null; }>({ isOpen: false, date: null, }); const [duplicateConfirm, setDuplicateConfirm] = useState<{ isOpen: boolean; date: AvailabilityDate | null; }>({ isOpen: false, date: null, }); const [duplicateDepartureDate, setDuplicateDepartureDate] = useState(""); const [duplicateDepartureTime, setDuplicateDepartureTime] = useState(""); const duplicateMutation = useMutation({ mutationFn: async (payload: { id: string; departure_date: string; departure_time?: string | null; }) => { return await apiClient.post(`/availability/${payload.id}/duplicate`, { departure_date: payload.departure_date, departure_time: payload.departure_time ?? null, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["availability"] }); showToast( __("Availability date duplicated successfully", "yatra"), "success", ); setDuplicateConfirm({ isOpen: false, date: null }); setDuplicateDepartureDate(""); setDuplicateDepartureTime(""); }, onError: (error: any) => { showToast( error?.message || __("Failed to duplicate availability date", "yatra"), "error", ); }, }); // Table columns and actions for shared Table const tableColumns = useMemo(() => { const cols: { key: string; label: string; visible?: boolean; render?: (date: AvailabilityDate, index: number) => React.ReactNode; }[] = []; // Departure cols.push({ key: "departure", label: selectedTrip?.trip_type === "single_day" ? __("Departure Time", "yatra") : __("Departure", "yatra"), visible: visibleColumns.departure, render: (date) => selectedTrip?.trip_type === "single_day" ? (
{formatDate(date.departure_date)}
{date.departure_time ? formatTime(date.departure_time) : ""}
) : (
{formatDate(date.departure_date)}
), }); // Arrival cols.push({ key: "arrival", label: selectedTrip?.trip_type === "single_day" ? __("Arrival Time", "yatra") : __("Arrival", "yatra"), visible: visibleColumns.arrival, render: (date) => selectedTrip?.trip_type === "single_day" ? (
{formatDate(date.arrival_date)}
{date.arrival_time ? formatTime(date.arrival_time) : ""}
) : ( {formatDate(date.arrival_date)} ), }); // From/To cols.push({ key: "locations", label: __("From/To", "yatra"), visible: visibleColumns.locations, render: (date) => (
{date.from_location || selectedTrip?.starting_location} {date.to_location || selectedTrip?.ending_location}
), }); // Capacity cols.push({ key: "capacity", label: __("Capacity", "yatra"), visible: visibleColumns.capacity, render: (date) => (
{date.total_seats || 0} {__("total", "yatra")}
), }); // Booked cols.push({ key: "booked", label: __("Booked", "yatra"), visible: visibleColumns.booked, render: (date) => (
{date.booked_seats || 0} {__("booked", "yatra")}
), }); // Available cols.push({ key: "available", label: __("Available", "yatra"), visible: visibleColumns.available, render: (date) => (
{date.available_seats || 0} {__("available", "yatra")} {date.available_seats <= (date.alert_threshold || 5) && date.available_seats > 0 && ( )}
), }); // Waitlist cols.push({ key: "waitlist", label: __("Waitlist", "yatra"), visible: visibleColumns.waitlist, render: (date) => date.waitlist_count > 0 ? (
{date.waitlist_count} {__("people", "yatra")}
) : ( - ), }); // Price cols.push({ key: "price", label: __("Price", "yatra"), visible: visibleColumns.price, render: (date) => { // Traveler-based dates carry per-category prices in price_types and leave // original_price/discounted_price NULL. Show a price (or min–max range). const pts = Array.isArray(date.price_types) ? date.price_types : []; const travelerPrices = pts .map( (pt) => Number( pt.sale_price ?? pt.discounted_price ?? pt.original_price, ) || 0, ) .filter((n) => n > 0); if (travelerPrices.length > 0) { const sym = getCurrencySymbol(selectedTrip?.currency || "USD"); const min = Math.min(...travelerPrices); const max = Math.max(...travelerPrices); return ( {min === max ? `${sym}${min.toLocaleString()}` : `${sym}${min.toLocaleString()} – ${sym}${max.toLocaleString()}`} ); } return (
{date.discounted_price && parseFloat(date.discounted_price) < parseFloat(date.original_price) ? ( <> {getCurrencySymbol(selectedTrip?.currency || "USD")} {parseFloat(date.original_price).toLocaleString()}
{getCurrencySymbol(selectedTrip?.currency || "USD")} {parseFloat(date.discounted_price).toLocaleString()} {date.discount_percentage && parseFloat(date.discount_percentage) > 0 && ( {date.discount_percentage}% {__("OFF", "yatra")} )}
) : ( {getCurrencySymbol(selectedTrip?.currency || "USD")} {parseFloat(date.original_price).toLocaleString()} )}
); }, }); // Status cols.push({ key: "status", label: __("Status", "yatra"), visible: visibleColumns.status, render: (date) => (
{getStatusBadge(date.status, date.is_blocked)} {date.is_blocked && date.block_reason && ( {date.block_reason} )} {date.last_synced_at && (
{new Date(date.last_synced_at).toLocaleTimeString()}
)}
), }); return cols; }, [ selectedTrip, visibleColumns, getCurrencySymbol, formatDate, formatTime, getStatusBadge, ]); const tableActions = [ { key: "edit", label: __("Edit", "yatra"), icon: , onClick: (date: AvailabilityDate) => navigate({ subpage: "trips", tab: "availability", action: "edit", id: date.id, trip_id: date.trip_id, }), }, { key: "duplicate", label: __("Duplicate", "yatra"), icon: , onClick: (date: AvailabilityDate) => { setDuplicateConfirm({ isOpen: true, date }); setDuplicateDepartureDate(date.departure_date || ""); setDuplicateDepartureTime(date.departure_time || ""); }, }, { key: "delete", label: __("Delete", "yatra"), icon: , onClick: (date: AvailabilityDate) => setDeleteConfirm({ isOpen: true, date }), variant: "destructive" as const, }, ]; return (
setDeleteConfirm({ isOpen: false, date: null })} onConfirm={() => { if (deleteConfirm.date) { deleteMutation.mutate(deleteConfirm.date.id); } }} title={__("Delete Availability Date", "yatra")} message={ deleteConfirm.date ? __( "Are you sure you want to delete this availability date on {date}? This action cannot be undone.", "yatra", ).replace("{date}", formatDate(deleteConfirm.date.departure_date)) : __( "Are you sure you want to delete this availability date? This action cannot be undone.", "yatra", ) } confirmText={__("Delete", "yatra")} cancelText={__("Cancel", "yatra")} variant="danger" isLoading={deleteMutation.isPending} /> {duplicateConfirm.isOpen && duplicateConfirm.date && (
{ if (!duplicateMutation.isPending) { setDuplicateConfirm({ isOpen: false, date: null }); } }} />

{__("Duplicate Availability Date", "yatra")}

{__( "Select the new departure date. Arrival/return will be shifted automatically.", "yatra", )}

setDuplicateDepartureDate(value)} placeholder={__("Select date", "yatra")} />
{selectedTrip?.trip_type === "single_day" && (
setDuplicateDepartureTime(value) } placeholder={__("Select departure time", "yatra")} />
)}
)} {/* Trip Selector - Clean Design */}

{__("Select Trip", "yatra")}

{__( "Choose a trip to manage its availability dates and departure schedules", "yatra", )}

{ const tripId = value ? parseInt(value) : null; setSelectedTripId(tripId); setPage(1); }} options={[ { value: "", label: __("-- Select a Trip --", "yatra") }, ...tripsList.map((trip) => ({ value: trip.id.toString(), label: `${trip.title}${trip.trip_type === "single_day" ? " (Single Day)" : trip.trip_type === "multi_day" ? " (Multi-Day)" : ""}`, })), ]} placeholder={__("Search or select a trip...", "yatra")} searchPlaceholder={__("Search by trip name or ID...", "yatra")} className="w-full" required />
{selectedTrip && (
{selectedTrip.starting_location} {selectedTrip.ending_location}
{selectedTrip.trip_type && ( {selectedTrip.trip_type === "single_day" ? __("Single Day", "yatra") : __("Multi-Day", "yatra")} )}
)} {!selectedTripId && (

{__("Get Started", "yatra")}

{__( "Select a trip from the dropdown above to view and manage its availability dates, pricing, and booking status.", "yatra", )}

)}
{selectedTripId && ( <> {/* Tab Navigation */}
{/* Recurring Rules Tab Content */} {tabMode === "recurring" && ( navigate({ subpage: "trips", tab: "availability", action: "create-rule", trip_id: selectedTripId.toString(), }) } onEditRule={(id: number) => navigate({ subpage: "trips", tab: "availability", action: "edit-rule", id: id.toString(), }) } /> )} {/* Specific Dates Tab Content */} {tabMode === "specific" && ( <> {/* Action Buttons - Right Aligned */} {selectedTripId && (
{/* View Toggle */}
{/* Sync Button */} {/* Add Availability Button */}
)} {/* Inventory Alerts */} {showAlerts && inventoryAlerts.length > 0 && (

{__("Inventory Alerts", "yatra")}

{inventoryAlerts.length}

{__( "The following dates have low availability (below threshold).", "yatra", )}

{inventoryAlerts.slice(0, 5).map((alert) => ( ))} {inventoryAlerts.length > 5 && ( +{inventoryAlerts.length - 5}{" "} {__("more", "yatra")} )}
)} {/* Waitlist Summary */} {waitlistEntries.length > 0 && (
{__("Waitlist", "yatra")} {waitlistEntries.reduce( (sum, entry) => sum + entry.waitlist_count, 0, )}{" "} {__("people", "yatra")}
{waitlistEntries.slice(0, 3).map((entry) => (
{formatDate(entry.departure_date)} {entry.departure_time && ` ${formatTime(entry.departure_time)}`}
{entry.booked_seats}/{entry.total_seats}{" "} {__("booked", "yatra")}
{entry.waitlist_count} {__("on waitlist", "yatra")}
))}
)} {/* Blocked Dates Summary */} {blockedDates.length > 0 && (
{__("Blocked Dates", "yatra")} {blockedDates.length}
{blockedDates.slice(0, 3).map((entry) => (
{formatDate(entry.departure_date)} {entry.departure_time && ` ${formatTime(entry.departure_time)}`}
{entry.block_reason && (
{__("Reason", "yatra")}: {entry.block_reason}
)}
))}
)} {/* Filters */}
setSearchTerm(e.target.value)} placeholder={__( "Search dates, locations...", "yatra", )} className="pl-10" />
{selectedTrip?.trip_type !== "single_day" && (
)}
{/* Bulk actions toolbar for availability list (directly above table, no extra card background) */} setSelectedIds([])} statusFilter={statusFilter} setStatusFilter={setStatusFilter} statusOptions={[ { key: "all", label: __("All", "yatra"), count: statusCounts.all, }, { key: "available", label: __("Available", "yatra"), count: statusCounts.available, }, { key: "limited", label: __("Limited", "yatra"), count: statusCounts.limited, }, { key: "sold_out", label: __("Sold Out", "yatra"), count: statusCounts.sold_out, }, { key: "closed", label: __("Closed", "yatra"), count: statusCounts.closed, }, { key: "blocked", label: __("Blocked", "yatra"), count: statusCounts.blocked, }, ]} showColumnsDropdown={showColumnsDropdown} setShowColumnsDropdown={setShowColumnsDropdown} columnOptions={[ { key: "departure", label: __("Departure", "yatra"), visible: visibleColumns.departure, }, { key: "arrival", label: __("Arrival", "yatra"), visible: visibleColumns.arrival, }, { key: "locations", label: __("From/To", "yatra"), visible: visibleColumns.locations, }, { key: "capacity", label: __("Capacity", "yatra"), visible: visibleColumns.capacity, }, { key: "booked", label: __("Booked", "yatra"), visible: visibleColumns.booked, }, { key: "available", label: __("Available", "yatra"), visible: visibleColumns.available, }, { key: "waitlist", label: __("Waitlist", "yatra"), visible: visibleColumns.waitlist, }, { key: "price", label: __("Price", "yatra"), visible: visibleColumns.price, }, { key: "status", label: __("Status", "yatra"), visible: visibleColumns.status, }, ]} onToggleColumn={(key) => toggleColumn(key as keyof typeof visibleColumns) } bulkMutationPending={bulkDeleteMutation.isPending} totalItems={totalFiltered} bulkActionOptions={bulkStatusOptions} /> {/* Availability Dates List */} {__("Availability Dates", "yatra")} {selectedTrip && ( <> {__("Managing availability for", "yatra")}{" "} {selectedTrip.title} {filteredDates.length > 0 && ( ({filteredDates.length}{" "} {selectedTrip?.trip_type === "single_day" ? __("time slots", "yatra") : __("dates", "yatra")} ) )} )} {viewMode === "calendar" ? ( filteredDates.length === 0 ? (

{__( "No availability dates found for this trip.", "yatra", )}

{__( "Try adjusting your filters or add a new availability date.", "yatra", )}

) : (
{ navigate({ subpage: "trips", tab: "availability", action: "edit", id: date.id, trip_id: date.trip_id, }); }} />
) ) : ( queryClient.invalidateQueries({ queryKey: ["availability"], }) } emptyText={__( "No availability dates found for this trip.", "yatra", )} emptyDescription={__( "Try adjusting your filters or add a new availability date.", "yatra", )} selectedItemIds={selectedIds} onSelectItem={(id, checked) => { if (checked) { setSelectedIds((prev) => Array.from(new Set([...prev, id as string])), ); } else { setSelectedIds((prev) => prev.filter((existingId) => existingId !== id), ); } }} onSelectAll={(checked) => { if (checked) { setSelectedIds(filteredDates.map((d) => d.id)); } else { setSelectedIds([]); } }} isAllSelected={ filteredDates.length > 0 && selectedIds.length === filteredDates.length } getItemId={(date: AvailabilityDate) => date.id} getItemStatus={(date: AvailabilityDate) => date.status} statusFilter={statusFilter} skeletonRows={5} capability="yatra_view_trips" /> )}
)} )}
); }; export default Availability;