/** * Availability Calendar Component * Displays availability dates in a calendar grid with color coding */ import React, { useState, useMemo } from "react"; import { ChevronLeft, ChevronRight, Users, Bell, Ban, Edit, Eye, } from "lucide-react"; import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth, addMonths, subMonths, isToday, parseISO, } from "date-fns"; import { __ } from "../../lib/i18n"; import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { useNavigate } from "../../hooks/useNavigate"; interface AvailabilityDate { id: string; trip_id?: number; departure_date: string; departure_time?: string; arrival_date: string; arrival_time?: string; total_seats: number; booked_seats: number; available_seats: number; waitlist_count: number; status: | "available" | "sold_out" | "limited" | "closed" | "blocked" | "cancelled"; is_blocked?: boolean; block_reason?: string; alert_threshold?: number; original_price: string; discounted_price: string; } interface AvailabilityCalendarProps { dates: AvailabilityDate[]; tripType?: "single_day" | "multi_day"; currency?: string; onDateClick?: (date: AvailabilityDate) => void; } export const AvailabilityCalendar: React.FC = ({ dates, tripType = "multi_day", currency = "USD", onDateClick, }) => { const { navigate } = useNavigate(); const [currentMonth, setCurrentMonth] = useState(new Date()); // Group dates by departure date const datesByDate = useMemo(() => { const grouped: Record = {}; dates.forEach((date) => { const key = date.departure_date; if (!grouped[key]) { grouped[key] = []; } grouped[key].push(date); }); return grouped; }, [dates]); // Get availability info for a specific date const getDateAvailability = (date: Date): AvailabilityDate[] => { const dateKey = format(date, "yyyy-MM-dd"); return datesByDate[dateKey] || []; }; // Get color class based on availability status const getDateColorClass = ( dateAvailabilities: AvailabilityDate[], ): string => { if (dateAvailabilities.length === 0) { return "bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700"; } // Check if any date is blocked if (dateAvailabilities.some((d) => d.is_blocked)) { return "bg-red-100 dark:bg-red-900/20 border-red-300 dark:border-red-800"; } // Check if all are sold out if ( dateAvailabilities.every( (d) => d.available_seats === 0 || d.status === "sold_out", ) ) { return "bg-gray-200 dark:bg-gray-700 border-gray-300 dark:border-gray-600"; } // Check if any has low availability (below threshold) const hasLowAvailability = dateAvailabilities.some((d) => { const threshold = d.alert_threshold || 5; return d.available_seats > 0 && d.available_seats <= threshold; }); if (hasLowAvailability) { return "bg-yellow-100 dark:bg-yellow-900/20 border-yellow-300 dark:border-yellow-800"; } // Check if any has limited availability if (dateAvailabilities.some((d) => d.status === "limited")) { return "bg-orange-100 dark:bg-orange-900/20 border-orange-300 dark:border-orange-800"; } // Available return "bg-green-100 dark:bg-green-900/20 border-green-300 dark:border-green-800"; }; const monthStart = startOfMonth(currentMonth); const monthEnd = endOfMonth(currentMonth); const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 }); const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }); const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd }); const previousMonth = () => { setCurrentMonth(subMonths(currentMonth, 1)); }; const nextMonth = () => { setCurrentMonth(addMonths(currentMonth, 1)); }; const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const formatTime = (timeString: string) => { if (!timeString) return ""; const [hours, minutes] = timeString.split(":"); const hour = parseInt(hours); const ampm = hour >= 12 ? "PM" : "AM"; return `${hour % 12 || 12}:${minutes} ${ampm}`; }; const formatTimeForDisplay = (timeString: string): string => { if (!timeString) return ""; const time = new Date(`1970-01-01T${timeString}`); const timeStringFormatted = time.toLocaleTimeString("en-US", { hour12: false, hour: "numeric", minute: "2-digit", }); return timeStringFormatted; }; const getCurrencySymbol = (currency: string) => { const symbols: Record = { USD: "$", EUR: "€", GBP: "£", INR: "₹", }; return symbols[currency] || currency; }; return (
{/* Calendar Header */}

{format(currentMonth, "MMMM yyyy")}

{/* Calendar Grid */}
{/* Week Day Headers */}
{weekDays.map((day) => (
{day}
))}
{/* Calendar Days */}
{days.map((day, dayIdx) => { const isCurrentMonth = isSameMonth(day, currentMonth); const isTodayDate = isToday(day); const dateAvailabilities = getDateAvailability(day); const colorClass = getDateColorClass(dateAvailabilities); const hasMultipleSlots = dateAvailabilities.length > 1; return (
{/* Date Number */}
{isTodayDate ? format(day, "d") : format(day, "d")} {hasMultipleSlots && ( {dateAvailabilities.length} )}
{/* Availability Info */} {isCurrentMonth && dateAvailabilities.length > 0 && (
{dateAvailabilities.slice(0, 2).map((availability) => { const isBlocked = availability.is_blocked; const isSoldOut = availability.available_seats === 0; const isLow = availability.available_seats > 0 && availability.available_seats <= (availability.alert_threshold || 5); return (

{format( parseISO(availability.departure_date), "EEEE, MMMM d, yyyy", )}

{tripType === "single_day" && availability.departure_time && (
{__("Time", "yatra")}:{" "} {formatTimeForDisplay( availability.departure_time, )}{" "} -{" "} {availability.arrival_time ? formatTimeForDisplay( availability.arrival_time, ) : ""}
)}
{__("Capacity", "yatra")}
{availability.total_seats}
{__("Booked", "yatra")}
{availability.booked_seats}
{__("Available", "yatra")}
{availability.available_seats}
{__("Price", "yatra")}
{getCurrencySymbol(currency)} {parseFloat( availability.discounted_price || availability.original_price, ).toLocaleString()}
{availability.waitlist_count > 0 && (
{availability.waitlist_count}{" "} {__("people on waitlist", "yatra")}
)} {availability.is_blocked && availability.block_reason && (
{__("Blocked", "yatra")}:{" "} {availability.block_reason}
)}
); })} {dateAvailabilities.length > 2 && (
+{dateAvailabilities.length - 2} {__("more", "yatra")}
)}
)}
); })}
{/* Legend */}
{__("Available", "yatra")}
{__("Low Availability", "yatra")}
{__("Limited", "yatra")}
{__("Sold Out", "yatra")}
{__("Blocked", "yatra")}
{__("Not Available", "yatra")}
); };