/** * Dynamic Pricing Rule Form Page * * Create and edit dynamic pricing rules with sidebar layout * * @package Yatra * @since 3.0.0 */ import React, { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "../components/ui/card"; import { Button } from "../components/ui/button"; import { Label } from "../components/ui/label"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { Skeleton } from "../components/ui/skeleton"; import { DatePicker } from "../components/ui/date-picker"; import { PageHeader } from "../components/common/PageHeader"; import { RuleTypeSelectionModal } from "../components/modals/RuleTypeSelectionModal"; import { useToast } from "../components/ui/toast"; import { apiClient } from "../lib/api-client"; import { __ } from "../lib/i18n"; import { ArrowLeft, Save, Loader2, Calendar, Clock, TrendingUp, Package, Sun, Target, Check, } from "lucide-react"; interface RuleFormData { name: string; rule_type: string; adjustment_type: string; adjustment_value: number; min_days_before: number; max_days_before: number; min_inventory: number; max_inventory: number; start_date: string; end_date: string; applicable_trips: string; trip_ids: number[]; status: string; priority: number; // Demand-based conditions demand_threshold_high?: number; demand_threshold_low?: number; // Time-based conditions apply_on_weekends?: boolean; apply_on_weekdays?: boolean; weekend_days?: string[]; } const RULE_TYPES = [ { id: "early_bird", name: __("Early Bird Discount"), description: __("Reward customers who book well in advance"), icon: Calendar, color: "blue", example: __("10% off for bookings 30+ days early"), }, { id: "last_minute", name: __("Last Minute Deals"), description: __("Fill remaining spots close to departure"), icon: Clock, color: "orange", example: __("15% off for bookings within 7 days"), }, { id: "demand", name: __("Demand-Based Pricing"), description: __("Adjust prices based on booking velocity"), icon: TrendingUp, color: "green", example: __("Increase price for hot trips, decrease for slow"), }, { id: "inventory", name: __("Inventory-Based"), description: __("Price changes based on remaining capacity"), icon: Package, color: "purple", example: __("Higher price when few spots left"), }, { id: "seasonal", name: __("Seasonal Pricing"), description: __("Adjust for peak and off-peak seasons"), icon: Sun, color: "yellow", example: __("25% premium during summer months"), }, { id: "time_based", name: __("Time-Based"), description: __("Different prices for specific days and date ranges"), icon: Target, color: "indigo", example: __("15% premium on weekends within specific dates"), }, ]; const DynamicPricingRuleForm: React.FC = () => { const { showToast } = useToast(); const queryClient = useQueryClient(); const urlParams = new URLSearchParams(window.location.search); const ruleId = urlParams.get("id"); const ruleTypeFromUrl = urlParams.get("rule_type") || ""; const isEdit = !!ruleId; const [formData, setFormData] = useState({ name: "", rule_type: ruleTypeFromUrl, adjustment_type: "percentage", adjustment_value: 0, min_days_before: 0, max_days_before: 0, min_inventory: 0, max_inventory: 0, start_date: "", end_date: "", applicable_trips: "all", trip_ids: [], status: "active", priority: 1, demand_threshold_high: 80, demand_threshold_low: 30, apply_on_weekends: false, apply_on_weekdays: false, weekend_days: [], }); const [showTripDropdown, setShowTripDropdown] = useState(false); const [tripSearchQuery, setTripSearchQuery] = useState(""); const [debouncedTripSearch, setDebouncedTripSearch] = useState(""); const [showRuleTypeModal, setShowRuleTypeModal] = useState(false); const handleSelectRuleType = (ruleType: string) => { handleChange("rule_type", ruleType); setShowRuleTypeModal(false); }; // Debounce trip search useEffect(() => { const timer = setTimeout(() => { setDebouncedTripSearch(tripSearchQuery); }, 300); return () => clearTimeout(timer); }, [tripSearchQuery]); // Fetch trips for selection const { data: tripsData } = useQuery({ queryKey: ["trips-list", debouncedTripSearch], queryFn: async () => { const params = new URLSearchParams(); if (debouncedTripSearch) { params.append("search", debouncedTripSearch); } params.append("per_page", "50"); const response = await apiClient.get(`/trips?${params.toString()}`); return response.data || []; }, }); const tripOptions = (tripsData || []).map((trip: any) => ({ value: trip.id, label: trip.title || `Trip #${trip.id}`, })); // Fetch existing rule if editing const { data: ruleData, isLoading, isError, error, } = useQuery({ queryKey: ["dynamic-pricing-rule", ruleId], queryFn: async () => { if (!ruleId) return null; const response = await apiClient.get(`/dynamic-pricing/rules/${ruleId}`); return response; }, enabled: isEdit, retry: 1, }); // Log loading state // Populate form with existing data useEffect(() => { if (ruleData?.data) { const ruleDetails = ruleData.data; setFormData({ name: ruleDetails.name || "", rule_type: ruleDetails.rule_type || "", adjustment_type: ruleDetails.adjustment_type || "percentage", adjustment_value: Number(ruleDetails.adjustment_value) || 0, min_days_before: Number(ruleDetails.min_days_before) || 0, max_days_before: Number(ruleDetails.max_days_before) || 0, min_inventory: Number(ruleDetails.min_inventory) || 0, max_inventory: Number(ruleDetails.max_inventory) || 0, start_date: ruleDetails.start_date || "", end_date: ruleDetails.end_date || "", applicable_trips: ruleDetails.applicable_trips || "all", trip_ids: Array.isArray(ruleDetails.trip_ids) ? ruleDetails.trip_ids : ruleDetails.trip_ids ? JSON.parse(ruleDetails.trip_ids) : [], status: ruleDetails.status || "active", priority: Number(ruleDetails.priority) || 1, demand_threshold_high: ruleDetails.demand_threshold_high ? Number(ruleDetails.demand_threshold_high) : 80, demand_threshold_low: ruleDetails.demand_threshold_low ? Number(ruleDetails.demand_threshold_low) : 30, apply_on_weekends: Boolean(ruleDetails.apply_on_weekends), apply_on_weekdays: Boolean(ruleDetails.apply_on_weekdays), weekend_days: Array.isArray(ruleDetails.weekend_days) ? ruleDetails.weekend_days : ruleDetails.weekend_days ? JSON.parse(ruleDetails.weekend_days) : [], }); } }, [ruleData]); // Create/Update mutation const saveMutation = useMutation({ mutationFn: async (data: RuleFormData) => { if (isEdit) { return await apiClient.put(`/dynamic-pricing/rules/${ruleId}`, data); } else { return await apiClient.post("/dynamic-pricing/rules", data); } }, onSuccess: (response) => { showToast( isEdit ? __("Pricing rule updated successfully") : __("Pricing rule created successfully"), "success", ); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"] }); if (!isEdit && response?.data?.id) { // For new rules, redirect to edit page const baseUrl = window.location.href.split("&action=")[0]; const editUrl = `${baseUrl}&action=edit-pricing-rule&id=${response.data.id}`; window.location.href = editUrl; } // For updates, stay on the same page (no redirect) }, onError: (error: any) => { showToast(error?.message || __("Failed to save pricing rule"), "error"); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); saveMutation.mutate(formData); }; const handleCancel = () => { window.location.href = window.location.href.split("&action=")[0]; }; const handleChange = (field: keyof RuleFormData, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); }; // Show error state if API call fails if (isError) { return (

{__("Failed to load pricing rule")}

{error?.message || __("An error occurred while fetching the rule data")}

); } if (isLoading) { return (
{/* Main Content Skeleton - Left Side (2/3) */}
{/* Rule Type Badge Skeleton */}
{/* Rule Details Skeleton */}
{/* Price Adjustment Skeleton */}
{/* Conditions Skeleton */}
{/* Applicable Trips Skeleton */}
{/* Sidebar Skeleton - Right Side (1/3) */}
{/* Status & Priority Skeleton */}
{/* Actions Skeleton */}
); } const selectedRuleType = RULE_TYPES.find( (rt) => rt.id === formData.rule_type, ); return (
{/* Two Column Layout: Main Content + Sidebar */}
{/* Main Content - Left Side (2/3) */}
{/* Step 1: Select Rule Type (Card-Based Selection) */} {!isEdit && !formData.rule_type && ( {__("Step 1: Select Rule Type")} {__( "Choose the type of dynamic pricing rule you want to create", )}
{RULE_TYPES.map((ruleType) => { const Icon = ruleType.icon; const colorClasses = { blue: "border-blue-200 dark:border-blue-800 hover:border-blue-400 dark:hover:border-blue-600 bg-blue-50 dark:bg-blue-900/20", orange: "border-orange-200 dark:border-orange-800 hover:border-orange-400 dark:hover:border-orange-600 bg-orange-50 dark:bg-orange-900/20", green: "border-green-200 dark:border-green-800 hover:border-green-400 dark:hover:border-green-600 bg-green-50 dark:bg-green-900/20", purple: "border-purple-200 dark:border-purple-800 hover:border-purple-400 dark:hover:border-purple-600 bg-purple-50 dark:bg-purple-900/20", yellow: "border-yellow-200 dark:border-yellow-800 hover:border-yellow-400 dark:hover:border-yellow-600 bg-yellow-50 dark:bg-yellow-900/20", indigo: "border-indigo-200 dark:border-indigo-800 hover:border-indigo-400 dark:hover:border-indigo-600 bg-indigo-50 dark:bg-indigo-900/20", }; return ( ); })}
)} {/* Show selected rule type badge if editing or type selected */} {(isEdit || formData.rule_type) && selectedRuleType && (
{React.createElement(selectedRuleType.icon, { className: "w-5 h-5 text-gray-600 dark:text-gray-400", })}

{__("Rule Type")}

{selectedRuleType.name}

{selectedRuleType.description}

)} {/* Step 2: Basic Information (Only show after rule type selected) */} {formData.rule_type && ( <> {__("Rule Details")} {__("Name and description for this pricing rule")}
handleChange("name", e.target.value)} placeholder={__("e.g., Summer Early Bird Discount")} required />
{__("Price Adjustment")} {__("How much to adjust the price")}
handleChange( "adjustment_value", parseFloat(e.target.value), ) } placeholder={ formData.adjustment_type === "percentage" ? "10" : "100" } required />

{formData.adjustment_type === "percentage" ? __( "Enter percentage (e.g., 10 for 10% discount, -10 for 10% increase)", ) : __( "Enter fixed amount (e.g., 100 for $100 discount, -100 for $100 increase)", )}

{__("Conditions")} {__("When this rule should apply")} {/* Early Bird & Last Minute - Days before departure */} {(formData.rule_type === "early_bird" || formData.rule_type === "last_minute") && ( <>
handleChange( "min_days_before", parseInt(e.target.value), ) } min="0" />
handleChange( "max_days_before", parseInt(e.target.value), ) } min="0" />
handleChange("start_date", value) } placeholder={__("Select start date")} />

{__("Leave empty for no date restriction")}

handleChange("end_date", value) } placeholder={__("Select end date")} />

{__("Leave empty for no date restriction")}

)} {formData.rule_type === "inventory" && ( <>
handleChange( "min_inventory", parseInt(e.target.value), ) } min="0" />
handleChange( "max_inventory", parseInt(e.target.value), ) } min="0" />
)} {/* Seasonal - Date range */} {formData.rule_type === "seasonal" && ( <>
handleChange("start_date", value) } placeholder={__("Select start date")} />

{__("When this seasonal pricing starts")}

handleChange("end_date", value) } placeholder={__("Select end date")} />

{__("When this seasonal pricing ends")}

)} {/* Demand-Based - Booking velocity thresholds */} {formData.rule_type === "demand" && ( <>
handleChange( "demand_threshold_high", parseInt(e.target.value), ) } min="0" max="100" />

{__( "Apply pricing when booking velocity exceeds this percentage (e.g., 80 = high demand)", )}

handleChange( "demand_threshold_low", parseInt(e.target.value), ) } min="0" max="100" />

{__( "Apply pricing when booking velocity falls below this percentage (e.g., 30 = low demand)", )}

{__( "💡 Demand scores are calculated automatically via cron job based on booking velocity.", )}

)} {/* Time-Based - Day of week selection */} {formData.rule_type === "time_based" && ( <>
{[ "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", ].map((day) => (
{ const current = formData.weekend_days || []; if (e.target.checked) { handleChange("weekend_days", [ ...current, day, ]); } else { handleChange( "weekend_days", current.filter((d) => d !== day), ); } }} className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
))}

{__( "Select the days of the week when this pricing rule applies", )}

handleChange("start_date", value) } placeholder={__("Select start date")} />

{__("Leave empty for no date restriction")}

handleChange("end_date", value) } placeholder={__("Select end date")} />

{__("Leave empty for no date restriction")}

{__( "💡 Use positive values for premium pricing (weekends) or negative for discounts (weekdays).", )}

)}
)}
{/* Sidebar - Right Side (1/3) */} {formData.rule_type && (
{/* Status */} {__("Status")}

{__("Only active rules will be applied to trips.")}

{/* Priority */} {__("Priority")} handleChange("priority", parseInt(e.target.value)) } min="1" max="100" />

{__("Higher priority rules are applied first (1-100)")}

{/* Applicable Trips */} {__("Applicable To")} {formData.applicable_trips === "specific" && (
setShowTripDropdown(!showTripDropdown)} > {formData.trip_ids.length > 0 ? (
{formData.trip_ids.slice(0, 2).map((tripId) => { const trip = tripOptions.find( (t: any) => t.value === tripId, ); return ( {trip?.label || `Trip #${tripId}`}{" "} #{tripId} ); })} {formData.trip_ids.length > 2 && ( +{formData.trip_ids.length - 2} {__("more")} )}
) : ( {__("Click to select trips...")} )}
{showTripDropdown && (
setTripSearchQuery(e.target.value) } className="w-full text-sm" onClick={(e) => e.stopPropagation()} />
{tripOptions.map((trip: any) => { const isSelected = formData.trip_ids.includes( trip.value, ); return (
{ e.stopPropagation(); if (isSelected) { handleChange( "trip_ids", formData.trip_ids.filter( (id) => id !== trip.value, ), ); } else { handleChange("trip_ids", [ ...formData.trip_ids, trip.value, ]); } }} >
{isSelected && ( )}
{trip.label} #{trip.value}
); })}
)}
)}
{/* Create Rule Button - Below Applicable To */}
)}
{/* Remove old actions section */} {false && formData.rule_type && (
)}
{/* Rule Type Selection Modal */} setShowRuleTypeModal(false)} onSelectType={handleSelectRuleType} />
); }; export default DynamicPricingRuleForm;