/** * Recurring Availability Rule Form * Create and edit recurring availability patterns */ import React, { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Save, Clock, DollarSign, RefreshCw, Plus, X, AlertCircle, Eye, MapPin, CheckCircle2, } from "lucide-react"; import { __ } from "../lib/i18n"; 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 { DatePicker } from "../components/ui/date-picker"; import { TimePicker } from "../components/ui/time-picker"; 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 { apiClient } from "../lib/api-client"; import { useToast } from "../components/ui/toast"; import { RecurringRuleFormSkeleton } from "../components/availability/RecurringRuleFormSkeleton"; import { LocationPicker } from "../components/trip-form/LocationPicker"; function coordFromApi(v: unknown): string { if (v == null || v === "") return ""; return String(v); } interface Trip { id: number; title: string; trip_type?: "single_day" | "multi_day"; duration_days?: number; starting_location?: string; ending_location?: string; starting_latitude?: string | number; starting_longitude?: string | number; ending_latitude?: string | number; ending_longitude?: string | number; pricing_type?: "regular" | "traveler_based"; } interface TravelerCategory { id: number; name?: string; label?: string; description?: string; min_age?: number; max_age?: number; age_min?: number; age_max?: number; status?: string; } interface TravelerPricing { category_id: number; original_price: number; sale_price?: number; } interface TimeSlot { departure_time: string; arrival_time: string; seats: number; price: number; sale_price?: number; traveler_pricing?: TravelerPricing[]; } interface RecurringRule { id?: number; trip_id: number; name: string; rule_type: "weekly" | "monthly" | "interval"; days_of_week: number[]; week_of_month?: "first" | "second" | "third" | "fourth" | "last"; day_of_week?: number; interval_days?: number; start_date: string; end_date?: string; excluded_dates: string[]; months: number[]; // Array of month numbers (1-12) to filter by time_slots: TimeSlot[]; // For single-day trips with multiple slots pricing_type: "regular" | "traveler_based"; // Allow override of trip's pricing type original_price?: number; sale_price?: number; traveler_pricing?: TravelerPricing[]; // For traveler-based pricing seats_total: number; departure_time?: string; arrival_time?: string; from_location?: string; to_location?: string; from_latitude?: string; from_longitude?: string; to_latitude?: string; to_longitude?: string; cutoff_hours: number; alert_threshold: number; status: "active" | "inactive"; } const dayOptions = [ { value: 0, label: "Sunday" }, { value: 1, label: "Monday" }, { value: 2, label: "Tuesday" }, { value: 3, label: "Wednesday" }, { value: 4, label: "Thursday" }, { value: 5, label: "Friday" }, { value: 6, label: "Saturday" }, ]; const weekOptions = [ { value: "first", label: "First" }, { value: "second", label: "Second" }, { value: "third", label: "Third" }, { value: "fourth", label: "Fourth" }, { value: "last", label: "Last" }, ]; const RecurringRuleForm: React.FC = () => { const { navigate } = useNavigate(); const queryClient = useQueryClient(); const { showToast } = useToast(); // Get parameters from URL const params = new URLSearchParams(window.location.search); const ruleId = params.get("id"); const tripIdFromUrl = params.get("trip_id"); const isEditing = !!ruleId; // Form state const [formData, setFormData] = useState({ trip_id: tripIdFromUrl ? parseInt(tripIdFromUrl) : 0, name: "", rule_type: "weekly", days_of_week: [0], // Sunday by default week_of_month: "first", day_of_week: 0, interval_days: 7, start_date: new Date().toISOString().split("T")[0], end_date: "", excluded_dates: [], months: [], // Empty = all months, otherwise specific months (1-12) time_slots: [], // For single-day trips with multiple slots pricing_type: "regular", // Will be updated based on trip's pricing type original_price: undefined, sale_price: undefined, traveler_pricing: [], // For traveler-based pricing seats_total: 20, departure_time: "", arrival_time: "", from_location: "", to_location: "", from_latitude: "", from_longitude: "", to_latitude: "", to_longitude: "", cutoff_hours: 24, alert_threshold: 5, status: "active", }); const [newExcludedDate, setNewExcludedDate] = useState(""); const [previewData, setPreviewData] = useState<{ total: number; dates: any[]; } | null>(null); const [showCategorySelector, setShowCategorySelector] = useState(false); // Fetch trips for dropdown const { data: tripsData } = useQuery({ queryKey: ["trips", "all"], queryFn: async () => { const response = await apiClient.get("/trips", { params: { per_page: 100, status: "publish" }, }); return { trips: (response?.data || []).map((trip: any) => { // Some endpoints return `pricing_type` as "regular" even when the trip is // effectively traveler-based (price types configured). Infer the effective // pricing type from `price_types` when present so the Rules UI reflects // real trip configuration. const rawPriceTypes = trip.price_types; const hasTravelerPricing = Array.isArray(rawPriceTypes) ? rawPriceTypes.length > 0 : false; const effectivePricingType = hasTravelerPricing ? "traveler_based" : trip.pricing_type || "regular"; return { id: Number(trip.id) || 0, title: trip.title, trip_type: trip.trip_type || (trip.duration_days <= 1 ? "single_day" : "multi_day"), duration_days: trip.duration_days || 1, starting_location: trip.starting_location, ending_location: trip.ending_location, pricing_type: effectivePricingType, }; }) as Trip[], }; }, }); // Fetch traveler categories const { data: travelerCategories = [] } = useQuery({ queryKey: ["traveler-categories"], queryFn: async () => { const response = await apiClient.get("/traveler-categories", { params: { per_page: 100 }, }); return (response?.data || []) as TravelerCategory[]; }, }); // Get selected trip details const selectedTrip = tripsData?.trips.find((t) => t.id === formData.trip_id); const { data: tripForLocations } = useQuery({ queryKey: ["trip", formData.trip_id, "recurring-rule-locations"], queryFn: async () => { const response = await apiClient.get(`/trips/${formData.trip_id}`); return response?.data || response || null; }, enabled: formData.trip_id > 0, staleTime: 5 * 60 * 1000, }); const { data: fallbackTripData } = useQuery({ queryKey: ["trip", formData.trip_id, "recurring-rule-form"], queryFn: async () => { if (!formData.trip_id || selectedTrip) { return null; } const response = await apiClient.get(`/trips/${formData.trip_id}`); return response?.data || response || null; }, enabled: !!formData.trip_id && !selectedTrip, staleTime: 5 * 60 * 1000, }); const effectiveTrip = selectedTrip || (fallbackTripData ? { id: Number(fallbackTripData.id) || formData.trip_id, title: fallbackTripData.title, } : null); const tripNameLabel = effectiveTrip?.title || __("Unnamed Trip", "yatra"); let headerDescription = __( "Set up automatic availability patterns for your trips", "yatra", ); if (effectiveTrip) { headerDescription = `${isEditing ? __("Edit", "yatra") : __("Add", "yatra")} ${__("availability rule for", "yatra")} ${tripNameLabel} (Trip ID: ${effectiveTrip.id})`; } else if (formData.trip_id) { headerDescription = `${isEditing ? __("Edit", "yatra") : __("Add", "yatra")} ${__("availability rule for Trip ID:", "yatra")} ${formData.trip_id}`; } const isSingleDayTrip = selectedTrip?.trip_type === "single_day" || (selectedTrip?.duration_days || 1) <= 1; // Use form's pricing_type which defaults to trip's pricing type but can be overridden const isTravelerBasedPricing = formData.pricing_type === "traveler_based"; // Fetch existing rule if editing const { data: existingRule, isLoading: isLoadingRule } = useQuery({ queryKey: ["recurring-availability", ruleId], queryFn: async () => { if (!ruleId) return null; const response = await apiClient.get(`/recurring-availability/${ruleId}`); return response?.data || response; // Unwrap data property from API response }, enabled: !!ruleId, }); // Update form when existing rule is loaded useEffect(() => { if (existingRule && tripsData) { // Parse days_of_week - handle both array and string formats let daysOfWeek = [0]; // Default to Sunday if ( existingRule.days_of_week_array && Array.isArray(existingRule.days_of_week_array) ) { daysOfWeek = existingRule.days_of_week_array; } else if (existingRule.days_of_week) { if (typeof existingRule.days_of_week === "string") { daysOfWeek = existingRule.days_of_week .split(",") .map(Number) .filter((n: number) => !isNaN(n)); } else if (Array.isArray(existingRule.days_of_week)) { daysOfWeek = existingRule.days_of_week.map(Number); } } // Ensure rule_type is properly typed const ruleType = (existingRule.rule_type || "weekly") as | "weekly" | "monthly" | "interval"; // Prefer the *trip's effective* pricing type over any stale rule.pricing_type. // Trips can be traveler-based simply by having price_types configured, even if // trip.pricing_type is still "regular". const tripRow = tripsData.trips.find( (t) => t.id === Number(existingRule.trip_id), ); const effectivePricingType = tripRow?.pricing_type || ((tripForLocations as any)?.price_types && Array.isArray((tripForLocations as any).price_types) && (tripForLocations as any).price_types.length > 0 ? "traveler_based" : (tripForLocations as any)?.pricing_type) || "regular"; setFormData({ trip_id: existingRule.trip_id || 0, name: existingRule.name || "", rule_type: ruleType, days_of_week: daysOfWeek.length > 0 ? daysOfWeek : [0], week_of_month: existingRule.week_of_month || "first", day_of_week: existingRule.day_of_week ?? 0, interval_days: existingRule.interval_days || 7, start_date: existingRule.start_date || new Date().toISOString().split("T")[0], end_date: existingRule.end_date || "", excluded_dates: Array.isArray(existingRule.excluded_dates) ? existingRule.excluded_dates : [], months: Array.isArray(existingRule.months) ? existingRule.months : [], time_slots: Array.isArray(existingRule.time_slots) ? existingRule.time_slots : [], pricing_type: effectivePricingType as "regular" | "traveler_based", original_price: existingRule.original_price, sale_price: existingRule.sale_price, traveler_pricing: Array.isArray(existingRule.traveler_pricing) ? existingRule.traveler_pricing : [], seats_total: existingRule.seats_total || 20, departure_time: existingRule.departure_time || "", arrival_time: existingRule.arrival_time || "", from_location: existingRule.from_location || "", to_location: existingRule.to_location || "", from_latitude: coordFromApi(existingRule.from_latitude), from_longitude: coordFromApi(existingRule.from_longitude), to_latitude: coordFromApi(existingRule.to_latitude), to_longitude: coordFromApi(existingRule.to_longitude), cutoff_hours: existingRule.cutoff_hours || 24, alert_threshold: existingRule.alert_threshold || 5, status: existingRule.status || "active", }); } }, [existingRule, tripsData, tripForLocations]); // Set pricing type based on selected trip when not editing useEffect(() => { if (!isEditing && !existingRule) { const inferred = (() => { const priceTypes = (tripForLocations as any)?.price_types; const hasTravelerPricing = Array.isArray(priceTypes) && priceTypes.length > 0; return ( hasTravelerPricing ? "traveler_based" : selectedTrip?.pricing_type || (tripForLocations as Trip)?.pricing_type || "regular" ) as "regular" | "traveler_based"; })(); setFormData((prev) => ({ ...prev, ...(selectedTrip || tripForLocations ? { pricing_type: inferred, } : {}), ...(tripForLocations ? { from_location: prev.from_location || (tripForLocations as Trip).starting_location || "", to_location: prev.to_location || (tripForLocations as Trip).ending_location || "", from_latitude: prev.from_latitude || coordFromApi((tripForLocations as Trip).starting_latitude), from_longitude: prev.from_longitude || coordFromApi((tripForLocations as Trip).starting_longitude), to_latitude: prev.to_latitude || coordFromApi((tripForLocations as Trip).ending_latitude), to_longitude: prev.to_longitude || coordFromApi((tripForLocations as Trip).ending_longitude), } : {}), })); } }, [isEditing, selectedTrip, tripForLocations, existingRule]); // Create mutation const createMutation = useMutation({ mutationFn: async (data: RecurringRule) => { // `days_of_week` is a JSON column on the backend; send the array as-is // and let the API JSON-encode it. Sending a CSV string (e.g. "0,1,2,3") // is rejected by MySQL with "Invalid JSON text". return await apiClient.post("/recurring-availability", { ...data, days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], // Normalize month-week selector to backend contract. week_of_month: data.rule_type === "monthly" ? ((data.week_of_month || "first") as string).toLowerCase() : data.week_of_month, // On create, omit empty arrays to keep payload small. time_slots: data.time_slots.length > 0 ? data.time_slots : undefined, traveler_pricing: data.traveler_pricing && data.traveler_pricing.length > 0 ? data.traveler_pricing : undefined, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["recurring-availability"] }); showToast(__("Recurring rule created successfully", "yatra"), "success"); navigate({ subpage: "trips", tab: "availability", trip_id: formData.trip_id.toString(), }); }, onError: (error: any) => { showToast( error?.message || __("Failed to create rule", "yatra"), "error", ); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: async (data: RecurringRule) => { return await apiClient.put(`/recurring-availability/${ruleId}`, { ...data, days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], week_of_month: data.rule_type === "monthly" ? ((data.week_of_month || "first") as string).toLowerCase() : data.week_of_month, // IMPORTANT: on update, send empty arrays explicitly to clear persisted // JSON columns; omitting the key leaves the old value in DB. time_slots: Array.isArray(data.time_slots) ? data.time_slots : [], traveler_pricing: Array.isArray(data.traveler_pricing) ? data.traveler_pricing : [], }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["recurring-availability"] }); showToast(__("Recurring rule updated successfully", "yatra"), "success"); navigate({ subpage: "trips", tab: "availability", trip_id: formData.trip_id.toString(), }); }, onError: (error: any) => { showToast( error?.message || __("Failed to update rule", "yatra"), "error", ); }, }); // Preview mutation const previewMutation = useMutation({ mutationFn: async (data: RecurringRule) => { return await apiClient.post("/recurring-availability/preview", { ...data, days_of_week: Array.isArray(data.days_of_week) ? data.days_of_week : [], week_of_month: data.rule_type === "monthly" ? ((data.week_of_month || "first") as string).toLowerCase() : data.week_of_month, time_slots: data.time_slots.length > 0 ? data.time_slots : undefined, traveler_pricing: data.traveler_pricing && data.traveler_pricing.length > 0 ? data.traveler_pricing : undefined, preview_limit: 20, }); }, onSuccess: (response) => { // Unwrap the data property from API response const previewResult = response?.data || response; setPreviewData(previewResult); }, onError: (error: any) => { showToast( error?.message || __("Failed to generate preview", "yatra"), "error", ); }, }); // Handle form submit const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!formData.trip_id) { showToast(__("Please select a trip", "yatra"), "error"); return; } if (formData.rule_type === "weekly" && formData.days_of_week.length === 0) { showToast( __("Please select at least one day of the week", "yatra"), "error", ); return; } if (isEditing) { updateMutation.mutate(formData); } else { createMutation.mutate(formData); } }; // Handle day toggle for weekly rules const toggleDay = (day: number) => { setFormData((prev) => ({ ...prev, days_of_week: prev.days_of_week.includes(day) ? prev.days_of_week.filter((d) => d !== day) : [...prev.days_of_week, day].sort((a, b) => a - b), })); }; // Add excluded date const addExcludedDate = () => { if (newExcludedDate && !formData.excluded_dates.includes(newExcludedDate)) { setFormData((prev) => ({ ...prev, excluded_dates: [...prev.excluded_dates, newExcludedDate].sort(), })); setNewExcludedDate(""); } }; // Remove excluded date const removeExcludedDate = (date: string) => { setFormData((prev) => ({ ...prev, excluded_dates: prev.excluded_dates.filter((d) => d !== date), })); }; const isLoading = createMutation.isPending || updateMutation.isPending; if (isEditing && isLoadingRule) { return ; } return (
navigate({ subpage: "trips", tab: "availability", trip_id: formData.trip_id?.toString() || tripIdFromUrl || "", }) } > {__("Back to Availability", "yatra")} } />
{/* Main Form */}
{/* Basic Info */} {__("Basic Information", "yatra")}
setFormData((prev) => ({ ...prev, trip_id: parseInt(value) || 0, // Reset pricing when trip changes and set pricing_type based on new trip pricing_type: (tripsData?.trips.find( (t) => t.id === parseInt(value), )?.pricing_type || "regular") as | "regular" | "traveler_based", traveler_pricing: [], original_price: undefined, sale_price: undefined, })) } options={[ { value: "", label: __("-- Select Trip --", "yatra") }, ...(tripsData?.trips.map((trip) => ({ value: trip.id.toString(), label: `${trip.title} (${trip.pricing_type === "traveler_based" ? "Traveler-Based" : "Regular"})`, })) || []), ]} placeholder={__("Select a trip", "yatra")} disabled={isEditing} />
setFormData((prev) => ({ ...prev, name: e.target.value, })) } placeholder={__("e.g., Weekend Departures", "yatra")} />
{/* Pattern Configuration */} {__("Recurrence Pattern", "yatra")} {formData.rule_type === "weekly" && (
{dayOptions.map((day) => ( ))}
)} {formData.rule_type === "monthly" && (
)} {formData.rule_type === "interval" && (
setFormData((prev) => ({ ...prev, interval_days: parseInt(e.target.value) || 7, })) } />
)} {/* Date Range */}
setFormData((prev) => ({ ...prev, start_date: value })) } placeholder={__("Select start date", "yatra")} />
setFormData((prev) => ({ ...prev, end_date: value })) } minDate={ formData.start_date ? new Date(formData.start_date) : undefined } placeholder={__("Select end date (optional)", "yatra")} />
{/* Month Filter */}
{[ { value: 1, label: __("January", "yatra") }, { value: 2, label: __("February", "yatra") }, { value: 3, label: __("March", "yatra") }, { value: 4, label: __("April", "yatra") }, { value: 5, label: __("May", "yatra") }, { value: 6, label: __("June", "yatra") }, { value: 7, label: __("July", "yatra") }, { value: 8, label: __("August", "yatra") }, { value: 9, label: __("September", "yatra") }, { value: 10, label: __("October", "yatra") }, { value: 11, label: __("November", "yatra") }, { value: 12, label: __("December", "yatra") }, ].map((month) => ( ))}
{formData.months.length > 0 && (

{__("Selected:", "yatra")} {formData.months.length}{" "} {formData.months.length === 1 ? __("month", "yatra") : __("months", "yatra")}

)}
{/* Excluded Dates */}
setNewExcludedDate(value)} placeholder={__("Select date to exclude", "yatra")} />
{formData.excluded_dates.length > 0 && (
{formData.excluded_dates.map((date) => ( {new Date(date + "T00:00:00").toLocaleDateString( "en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", }, )} ))}
)}
{/* Pricing & Capacity */} {__("Pricing & Availability", "yatra")} {__( "Set pricing for traveler categories and seat availability for this rule", "yatra", )} {/* Pricing Override Info */}

{__("Pricing Override (Optional)", "yatra")}

{__( "Leave pricing fields empty to use the trip's default pricing. Fill them in only if you want to override the default pricing for dates generated by this rule.", "yatra", )}

{/* Pricing Type Info - Inherited from Trip */}
{isTravelerBasedPricing ? ( ) : ( )}

{isTravelerBasedPricing ? __("Traveler-Based Pricing", "yatra") : __("Regular Pricing", "yatra")}

{isTravelerBasedPricing ? __( "This trip uses traveler category pricing. Set prices for each category below.", "yatra", ) : __( "This trip uses regular pricing. Set a single price for all travelers below.", "yatra", )}

{/* Regular Pricing Fields */} {!isTravelerBasedPricing && (
setFormData((prev) => ({ ...prev, original_price: parseFloat(e.target.value) || undefined, })) } placeholder="0.00" />
setFormData((prev) => ({ ...prev, sale_price: parseFloat(e.target.value) || undefined, })) } placeholder="0.00" />

{__("Leave empty if no discount", "yatra")}

)} {/* Traveler-Based Pricing */} {isTravelerBasedPricing && (

{__( "Add pricing for traveler categories. Categories are managed in Traveler Categories page.", "yatra", )}

{/* Active Categories Filter */} {(() => { const activeCategories = travelerCategories.filter( (cat: TravelerCategory) => cat.status === "active" || cat.status === "publish", ); if (activeCategories.length === 0) { return (

{__( "No active traveler categories found.", "yatra", )}

); } return (
{/* Add Pricing Button with Dropdown */}
{/* Category Selection Dropdown */} {showCategorySelector && ( <>
setShowCategorySelector(false)} />
{__( "Select a category to add pricing", "yatra", )}
{activeCategories.filter( (cat) => !formData.traveler_pricing?.some( (tp) => tp.category_id === cat.id, ), ).length === 0 ? (
{__( "All categories have pricing added", "yatra", )}
) : (
{activeCategories .filter( (cat) => !formData.traveler_pricing?.some( (tp) => tp.category_id === cat.id, ), ) .map((category: TravelerCategory) => { const minAge = category.age_min ?? category.min_age; const maxAge = category.age_max ?? category.max_age; const ageRange = minAge !== undefined || maxAge !== undefined ? minAge !== undefined && maxAge !== undefined ? `${minAge}-${maxAge} ${__("years", "yatra")}` : minAge !== undefined ? `${minAge}+ ${__("years", "yatra")}` : maxAge !== undefined ? `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}` : "" : null; const categoryName = category.label || category.name || `Category ${category.id}`; return ( ); })}
)}
)}
{/* Added Pricing List */} {formData.traveler_pricing && formData.traveler_pricing.length > 0 && (
{formData.traveler_pricing.map( (pricing, index) => { const category = activeCategories.find( (cat) => cat.id === pricing.category_id, ); if (!category) return null; const minAge = category.age_min ?? category.min_age; const maxAge = category.age_max ?? category.max_age; const categoryName = category.label || category.name || `Category ${pricing.category_id}`; return (

{categoryName} {(minAge !== undefined || maxAge !== undefined) && ( ( {minAge !== undefined && maxAge !== undefined ? `${minAge}-${maxAge} ${__("years", "yatra")}` : minAge !== undefined ? `${minAge}+ ${__("years", "yatra")}` : maxAge !== undefined ? `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}` : ""} ) )}

{category.description && (

{category.description}

)}
{ const newPricing = [ ...(formData.traveler_pricing || []), ]; newPricing[ index ].original_price = parseFloat(e.target.value) || 0; setFormData((prev) => ({ ...prev, traveler_pricing: newPricing, })); }} placeholder="0.00" />
{ const newPricing = [ ...(formData.traveler_pricing || []), ]; newPricing[index].sale_price = parseFloat(e.target.value) || undefined; setFormData((prev) => ({ ...prev, traveler_pricing: newPricing, })); }} className="text-sm" placeholder="0.00" />
); }, )}
)}
); })()}
)} {/* Inventory Management - Common for both pricing types */} {selectedTrip && (

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

setFormData((prev) => ({ ...prev, seats_total: parseInt(e.target.value) || 1, })) } />

{__( "Maximum number of seats available for this rule", "yatra", )}

setFormData((prev) => ({ ...prev, alert_threshold: parseInt(e.target.value) || 0, })) } />

{__( "Alert when available seats drop below this number", "yatra", )}

)} {/* No trip selected message */} {!selectedTrip && (

{__("Select a trip above to configure pricing", "yatra")}

)} {/* Time & Location */} {__("Time & Location", "yatra")} {selectedTrip && ( {isSingleDayTrip ? __("Single-Day Trip", "yatra") : __("Multi-Day Trip", "yatra")} {!isSingleDayTrip && selectedTrip.duration_days && ( ({selectedTrip.duration_days} {__("days", "yatra")}) )} )} {/* Single-Day Trip: Multiple Time Slots */} {isSingleDayTrip && formData.trip_id > 0 && (
{formData.time_slots.length === 0 ? (

{__("No time slots added yet", "yatra")}

{__( "Add multiple time slots for tours throughout the day (e.g., morning, afternoon, evening)", "yatra", )}

) : (
{formData.time_slots.map((slot, index) => (
{__("Slot", "yatra")} {index + 1}
{/* Time fields on first line */}
{ const newSlots = [...formData.time_slots]; newSlots[index].departure_time = value; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} placeholder="09:00" />
{ const newSlots = [...formData.time_slots]; newSlots[index].arrival_time = value; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} placeholder="17:00" />
{/* Seats, Price and Sale Price on second line */} {!isTravelerBasedPricing && (
{ const newSlots = [ ...formData.time_slots, ]; newSlots[index].seats = parseInt(e.target.value) || 1; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} className="text-sm" />
{ const newSlots = [ ...formData.time_slots, ]; newSlots[index].price = parseFloat(e.target.value) || 0; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} className="text-sm" placeholder="0.00" />
{ const newSlots = [ ...formData.time_slots, ]; newSlots[index].sale_price = parseFloat(e.target.value) || undefined; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} className="text-sm" placeholder="0.00" />
)} {/* For traveler-based pricing, only show seats on second line */} {isTravelerBasedPricing && (
{ const newSlots = [...formData.time_slots]; newSlots[index].seats = parseInt(e.target.value) || 1; setFormData((prev) => ({ ...prev, time_slots: newSlots, })); }} className="text-sm" />
)}
{/* Traveler Pricing for Time Slot - only show when traveler-based */} {isTravelerBasedPricing && (
{__("Traveler Category Pricing", "yatra")}

{__( "Set pricing for each traveler category for this time slot", "yatra", )}

{(() => { const activeCategories = travelerCategories.filter( (cat: TravelerCategory) => cat.status === "active" || cat.status === "publish", ); const slotTravelerPricing = slot.traveler_pricing || []; const availableCategories = activeCategories.filter( (cat) => !slotTravelerPricing.some( (tp) => tp.category_id === cat.id, ), ); return (
{/* Add Pricing Button with Dropdown */}
{/* Category Selection Dropdown */}
{__( "Select a category to add pricing", "yatra", )}
{availableCategories.length === 0 ? (
{__( "All categories have pricing added", "yatra", )}
) : (
{availableCategories.map( ( category: TravelerCategory, ) => { const minAge = category.age_min ?? category.min_age; const maxAge = category.age_max ?? category.max_age; const ageRange = minAge !== undefined || maxAge !== undefined ? minAge !== undefined && maxAge !== undefined ? `${minAge}-${maxAge} ${__("years", "yatra")}` : minAge !== undefined ? `${minAge}+ ${__("years", "yatra")}` : `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}` : null; const categoryName = category.label || category.name || `Category ${category.id}`; return ( ); }, )}
)}
{/* Added Pricing List */} {slotTravelerPricing.length > 0 && (
{slotTravelerPricing.map( (tp, tpIndex) => { const category = activeCategories.find( (cat) => cat.id === tp.category_id, ); if (!category) return null; const minAge = category.age_min ?? category.min_age; const maxAge = category.age_max ?? category.max_age; const categoryName = category.label || category.name || `Category ${tp.category_id}`; return (
{categoryName} {(minAge !== undefined || maxAge !== undefined) && ( ( {minAge !== undefined && maxAge !== undefined ? `${minAge}-${maxAge}` : minAge !== undefined ? `${minAge}+` : `<${maxAge}`}{" "} {__( "years", "yatra", )} ) )}
{ const newSlots = [ ...formData.time_slots, ]; if ( newSlots[index] && newSlots[index] .traveler_pricing ) { newSlots[ index ].traveler_pricing![ tpIndex ].original_price = parseFloat( e.target.value, ) || 0; setFormData( (prev) => ({ ...prev, time_slots: newSlots, }), ); } }} className="text-xs" placeholder="0.00" />
{ const newSlots = [ ...formData.time_slots, ]; if ( newSlots[index] && newSlots[index] .traveler_pricing ) { newSlots[ index ].traveler_pricing![ tpIndex ].sale_price = parseFloat( e.target.value, ) || undefined; setFormData( (prev) => ({ ...prev, time_slots: newSlots, }), ); } }} className="text-xs" placeholder="0.00" />
); }, )}
)} {slotTravelerPricing.length === 0 && (

{__( 'Click "Add Pricing" to set prices for traveler categories', "yatra", )}

)}
); })()}
)}
))}
)} {/* If no time slots, show default fields as fallback */} {formData.time_slots.length === 0 && (

{__( "Or use default time (applies to all generated dates):", "yatra", )}

setFormData((prev) => ({ ...prev, departure_time: value, })) } placeholder="09:00" />
setFormData((prev) => ({ ...prev, arrival_time: value, })) } placeholder="17:00" />
)}
)} {/* Multi-Day Trip: Single Departure Time */} {!isSingleDayTrip && formData.trip_id > 0 && (
setFormData((prev) => ({ ...prev, departure_time: value, })) } placeholder="08:00" />
setFormData((prev) => ({ ...prev, arrival_time: value, })) } placeholder="18:00" />
)} {/* No trip selected */} {!formData.trip_id && (

{__( "Select a trip above to configure time settings", "yatra", )}

)} {/* Location & Cutoff - always shown when trip selected */} {formData.trip_id > 0 && ( <>

{__("Starting Point", "yatra")}

{__("Where the journey begins", "yatra")}

{formData.from_latitude && formData.from_longitude && (
)}
setFormData((prev) => ({ ...prev, from_location: loc.name, from_latitude: loc.latitude, from_longitude: loc.longitude, })) } label="" placeholder={__( "Search for starting location...", "yatra", )} helpText="" required={false} defaultMapCenter={ formData.from_latitude && formData.from_longitude ? [ parseFloat(formData.from_latitude), parseFloat(formData.from_longitude), ] : tripForLocations?.starting_latitude && tripForLocations?.starting_longitude ? [ parseFloat( String( tripForLocations.starting_latitude, ), ), parseFloat( String( tripForLocations.starting_longitude, ), ), ] : [20, 0] } defaultZoom={ formData.from_latitude && formData.from_longitude ? 13 : tripForLocations?.starting_latitude && tripForLocations?.starting_longitude ? 13 : 2 } mapHeight="300px" showMapButton={false} searchLimit={8} __={__} className="" mapClassName="rounded-lg" showManualCoordinateFields />

{__( "Default: trip starting location. Set per rule to override.", "yatra", )}

{__("Ending Point", "yatra")}

{__("Where the journey concludes", "yatra")}

{formData.to_latitude && formData.to_longitude && (
)}
setFormData((prev) => ({ ...prev, to_location: loc.name, to_latitude: loc.latitude, to_longitude: loc.longitude, })) } label="" placeholder={__( "Search for ending location...", "yatra", )} helpText="" required={false} defaultMapCenter={ formData.to_latitude && formData.to_longitude ? [ parseFloat(formData.to_latitude), parseFloat(formData.to_longitude), ] : tripForLocations?.ending_latitude && tripForLocations?.ending_longitude ? [ parseFloat( String( tripForLocations.ending_latitude, ), ), parseFloat( String( tripForLocations.ending_longitude, ), ), ] : formData.from_latitude && formData.from_longitude ? [ parseFloat(formData.from_latitude), parseFloat(formData.from_longitude), ] : tripForLocations?.starting_latitude && tripForLocations?.starting_longitude ? [ parseFloat( String( tripForLocations.starting_latitude, ), ), parseFloat( String( tripForLocations.starting_longitude, ), ), ] : [20, 0] } defaultZoom={ formData.to_latitude && formData.to_longitude ? 13 : tripForLocations?.ending_latitude && tripForLocations?.ending_longitude ? 13 : formData.from_latitude && formData.from_longitude ? 13 : tripForLocations?.starting_latitude && tripForLocations?.starting_longitude ? 13 : 2 } mapHeight="300px" showMapButton={false} searchLimit={8} __={__} className="" mapClassName="rounded-lg" showManualCoordinateFields />

{__( "Default: trip ending location. Set per rule to override.", "yatra", )}

setFormData((prev) => ({ ...prev, cutoff_hours: parseInt(e.target.value) || 0, })) } />
)}
{/* Sidebar */}
{/* Status */} {__("Status", "yatra")} {/* Preview */} {__("Preview", "yatra")} {__("See which dates will be generated", "yatra")} {previewData && (
{__("Total dates:", "yatra")} {previewData.total}
{previewData.dates && Array.isArray(previewData.dates) && previewData.dates.map((date: any, index: number) => (
{new Date(date.departure_date).toLocaleDateString( "en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", }, )} {date.departure_time && ( {date.departure_time} )}
))} {previewData.total > 20 && (
+{previewData.total - 20} {__("more dates", "yatra")}
)}
)}
{/* Actions */}
{/* Help */}

{__("How it works", "yatra")}

{__( "Dates are generated automatically based on your pattern. Manually added specific dates will take priority over generated dates.", "yatra", )}

); }; export default RecurringRuleForm;