/** * Dynamic Pricing Page * * Premium module for intelligent price adjustments. * This page only loads when Yatra Pro is active and module is enabled. * Premium upgrade content is handled by premium-pages/DynamicPricing.tsx * * @package Yatra * @since 3.0.0 */ import React, { useState, useMemo } 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 { Badge } from "../components/ui/badge"; import { PageHeader } from "../components/common/PageHeader"; import { RuleTypeSelectionModal } from "../components/modals/RuleTypeSelectionModal"; import { Toggle } from "../components/ui/toggle"; import { useToast } from "../components/ui/toast"; import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; import { Table as SharedTable } from "../components/shared/Table"; import { SearchFilterToolbar, BulkActionToolbar } from "../components/shared"; import { apiClient } from "../lib/api-client"; import { __ } from "../lib/i18n"; import { toDateValue } from "../lib/dateFormat"; import { formatYatraMoney } from "../lib/currency-display"; import PremiumUpgradeCard from "./premium-pages/DynamicPricing"; import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, } from "recharts"; import { Clock, Target, Check, Package, BarChart, Settings, Plus, AlertCircle, Edit, Trash2, DollarSign, CheckCircle, Save, } from "lucide-react"; // Check if module is available (Pro active) const isModuleAvailable = (): boolean => { const yatraAdmin = (window as any)?.yatraAdmin; return Boolean(yatraAdmin?.isPro); }; // Main Component const DynamicPricingPage: React.FC = () => { // Pro gate is a stable, server-injected flag — but we compute it once // at the top so every hook below can read it and the early return can // live below all hook calls (rules-of-hooks). const moduleAvailable = isModuleAvailable(); const [activeTab, setActiveTab] = useState("rules"); const [showRuleTypeModal, setShowRuleTypeModal] = useState(false); const [settings, setSettings] = useState({ rule_priority_mode: "highest", maximum_markup_percent: 50, maximum_discount_percent: 30, calculation_period: 7, update_frequency: "hourly", show_original_price: true, show_savings_badge: true, show_urgency_messages: false, /** discounted = rules use sale/effective price; regular = rules use list price when known */ calculation_base: "discounted" as "discounted" | "regular", }); const { showToast } = useToast(); const queryClient = useQueryClient(); // Search and filter state const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [sortBy, setSortBy] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [selectedIds, setSelectedIds] = useState([]); const [bulkAction, setBulkAction] = useState(""); const [showColumnsDropdown, setShowColumnsDropdown] = useState(false); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; rule: any | null; title?: string; message?: string; confirmText?: string; variant?: "danger" | "warning" | "info"; isLoading?: boolean; onConfirm?: () => void | Promise; }>({ isOpen: false, rule: null, }); const [isSaving, setIsSaving] = useState(false); const closeConfirmDialog = () => setConfirmDialog({ isOpen: false, rule: null, title: "", message: "", confirmText: "", variant: "danger", isLoading: false, onConfirm: () => {}, }); const invalidateRules = () => { queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"] }); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-statistics"], }); }; const handleSettingChange = (key: string, value: any) => { setSettings((prev) => ({ ...prev, [key]: value })); }; const handleSaveSettings = async () => { setIsSaving(true); try { await apiClient.post("/dynamic-pricing/settings", settings); showToast(__("Settings saved successfully"), "success"); } catch (error) { console.error("Failed to save settings:", error); showToast(__("Failed to save settings. Please try again."), "error"); } finally { setIsSaving(false); } }; const handleSelectRuleType = (ruleType: string) => { const baseUrl = window.location.href.split("&action=")[0]; window.location.href = `${baseUrl}&action=create-pricing-rule&rule_type=${ruleType}`; }; // Fetch settings from backend const { data: settingsData } = useQuery({ queryKey: ["dynamic-pricing-settings"], queryFn: async () => { const response = await apiClient.get("/dynamic-pricing/settings"); const body = (response as any)?.data ?? response; const payload = (body as any)?.data ?? body; return payload && typeof payload === "object" ? payload : {}; }, enabled: moduleAvailable, }); // Update settings state when data is loaded React.useEffect(() => { if (settingsData && typeof settingsData === "object") { setSettings((prev) => ({ ...prev, ...settingsData, calculation_base: (settingsData as any).calculation_base === "regular" ? "regular" : "discounted", })); } }, [settingsData]); // Fetch pricing rules const { data: rulesData, isLoading } = useQuery({ queryKey: ["dynamic-pricing-rules"], queryFn: async () => { const response = await apiClient.get("/dynamic-pricing/rules"); return response; }, enabled: moduleAvailable, }); // Fetch statistics const { data: statsData, isLoading: isStatsLoading, error: statsError, } = useQuery({ queryKey: ["dynamic-pricing-statistics"], queryFn: async () => { const response = await apiClient.get("/dynamic-pricing/statistics"); return response; }, enabled: moduleAvailable, }); // The API client returns the decoded JSON body. // Some endpoints return { data: {...} } while others might return { data: { data: {...} } }. const rulesPayload = (rulesData as any)?.data?.data ?? (rulesData as any)?.data ?? []; const statsPayload = (statsData as any)?.data?.data ?? (statsData as any)?.data ?? {}; // eslint-disable-next-line react-hooks/exhaustive-deps const rules = rulesPayload || []; const stats = statsPayload || {}; const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD"; const formatCurrencyAmount = (amount: number) => formatYatraMoney(Number(amount) || 0, globalCurrency, { zeroAsUnknown: false, }); const trendData = Array.isArray( (stats as any)?.pricing_history_trend_last_30_days, ) ? (stats as any).pricing_history_trend_last_30_days.map((row: any) => { const day = String(row.day || ""); const dateLabel = day ? toDateValue(day).toLocaleDateString() : ""; return { day, dateLabel, events: Number(row.events) || 0, totalAdjustmentAmount: Number(row.total_adjustment_amount) || 0, avgAdjustmentPercentage: Number(row.avg_adjustment_percentage) || 0, }; }) : []; const ruleImpact = Array.isArray( (stats as any)?.pricing_history_rule_impact_last_30_days, ) ? (stats as any).pricing_history_rule_impact_last_30_days : []; const analyticsLoading = isLoading || isStatsLoading; const statsErrorMessage = statsError ? (statsError as any)?.message || String(statsError) : ""; // Filter and sort rules const filteredRules = useMemo(() => { let filtered = [...rules]; // Apply search filter if (searchTerm) { filtered = filtered.filter( (rule: any) => rule.name?.toLowerCase().includes(searchTerm.toLowerCase()) || rule.rule_type?.toLowerCase().includes(searchTerm.toLowerCase()), ); } // Apply status filter. The "All" view excludes Trash by WordPress // convention — trashed rules are only visible from the Trash filter. if (statusFilter === "all") { filtered = filtered.filter((rule: any) => rule.status !== "trash"); } else { filtered = filtered.filter((rule: any) => rule.status === statusFilter); } // Apply sorting filtered.sort((a: any, b: any) => { let aVal = a[sortBy]; let bVal = b[sortBy]; if (sortBy === "created_at" || sortBy === "updated_at") { aVal = new Date(aVal).getTime(); bVal = new Date(bVal).getTime(); } if (sortOrder === "asc") { return aVal > bVal ? 1 : -1; } else { return aVal < bVal ? 1 : -1; } }); return filtered; }, [rules, searchTerm, statusFilter, sortBy, sortOrder]); // Bulk action mutation. Uses Promise.allSettled so a single failed row does // not throw away the work that succeeded; the toast reports partial state. const bulkMutation = useMutation({ mutationFn: async ({ action, ids }: { action: string; ids: number[] }) => { const requests = ids.map((id) => { if (action === "delete") { return apiClient.delete(`/dynamic-pricing/rules/${id}`); } else if (action === "restore") { return apiClient.put(`/dynamic-pricing/rules/${id}`, { status: "active", }); } else if (action === "trash") { return apiClient.put(`/dynamic-pricing/rules/${id}`, { status: "trash", }); } else if (action === "active" || action === "inactive") { return apiClient.put(`/dynamic-pricing/rules/${id}`, { status: action, }); } return Promise.resolve(); }); const results = await Promise.allSettled(requests); const succeeded = results.filter((r) => r.status === "fulfilled").length; const failed = results.length - succeeded; return { succeeded, failed, total: results.length }; }, onSuccess: (result) => { invalidateRules(); setSelectedIds([]); setBulkAction(""); if (result.failed === 0) { showToast(__("Bulk action completed successfully"), "success"); } else if (result.succeeded === 0) { showToast(__("Failed to complete bulk action"), "error"); } else { showToast( `${result.succeeded}/${result.total} ${__("rules updated; some failed")}`, "warning", ); } }, onError: () => { showToast(__("Failed to complete bulk action"), "error"); }, }); const handleBulkApply = () => { if (!bulkAction) { showToast(__("Select a bulk action first"), "warning"); return; } if (selectedIds.length === 0) { showToast(__("Select at least one rule"), "warning"); return; } bulkMutation.mutate({ action: bulkAction, ids: selectedIds }); }; // Gate after every hook is registered so hook order stays consistent. if (!moduleAvailable) return ; return (
{/* Tabs with Create Button */}
{activeTab === "rules" && ( )}
{/* Rules Tab */} {activeTab === "rules" && (
{/* Stats Cards */} {isLoading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : (

{__("Total Rules")}

{stats.total_rules || 0}

{__("Active Rules")}

{stats.active_rules || 0}

{__("Inactive Rules")}

{stats.inactive_rules || 0}

{__("Rule Types")}

{stats.rule_types_used || 0}

)} {/* Search and Filter Toolbar */} { setSearchTerm(""); setStatusFilter("all"); setSortBy("name"); setSortOrder("asc"); }} hasFilters={ !!searchTerm || statusFilter !== "all" || sortBy !== "name" || sortOrder !== "asc" } placeholder={__("Search rules...")} /> {/* Bulk Action Toolbar */} setSelectedIds([])} statusFilter={statusFilter} setStatusFilter={setStatusFilter} statusOptions={[ { key: "all", label: __("All"), count: stats.total_rules ?? rules.filter((r: any) => r.status !== "trash").length, }, { key: "active", label: __("Active"), count: stats.active_rules || 0, }, { key: "inactive", label: __("Inactive"), count: stats.inactive_rules || 0, }, { key: "trash", label: __("Trash"), count: stats.trash_rules || 0, }, ]} showColumnsDropdown={showColumnsDropdown} setShowColumnsDropdown={setShowColumnsDropdown} columnOptions={[ { key: "name", label: __("Rule Name"), visible: true }, { key: "adjustment", label: __("Adjustment"), visible: true }, { key: "applicable_trips", label: __("Applicable To"), visible: true, }, { key: "priority", label: __("Priority"), visible: true }, { key: "status", label: __("Status"), visible: true }, { key: "created_at", label: __("Created"), visible: true }, ]} onToggleColumn={() => {}} bulkMutationPending={bulkMutation.isPending} totalItems={filteredRules.length} bulkActionOptions={ statusFilter === "trash" ? [ { value: "restore", label: __("Restore") }, { value: "delete", label: __("Delete Permanently") }, ] : [ { value: "active", label: __("Mark as Active") }, { value: "inactive", label: __("Mark as Inactive") }, { value: "trash", label: __("Move to Trash") }, ] } /> {/* Rules List */} {__("Pricing Rules")} {__("Manage your dynamic pricing rules")} (
{rule.name}
{rule.rule_type?.replace("_", " ")}
), }, { key: "adjustment", label: __("Adjustment"), visible: true, render: (rule: any) => ( {rule.adjustment_type === "percentage" ? `${rule.adjustment_value > 0 ? "+" : ""}${rule.adjustment_value}%` : formatYatraMoney( Number(rule.adjustment_value) || 0, globalCurrency, { zeroAsUnknown: false }, )} ), }, { key: "applicable_trips", label: __("Applicable To"), visible: true, render: (rule: any) => ( {rule.applicable_trips === "all" ? __("All Trips") : __("Specific Trips")} ), }, { key: "priority", label: __("Priority"), sortable: true, visible: true, render: (rule: any) => ( {rule.priority} ), }, { key: "status", label: __("Status"), sortable: true, visible: true, render: (rule: any) => ( {rule.status} ), }, { key: "created_at", label: __("Created"), sortable: true, visible: true, render: (rule: any) => ( {new Date(rule.created_at).toLocaleDateString()} ), }, ]} actions={[ { key: "edit", label: __("Edit"), icon: , onClick: (rule: any) => { const baseUrl = window.location.href.split("&action=")[0]; window.location.href = `${baseUrl}&action=edit-pricing-rule&id=${rule.id}`; }, }, { key: "active", label: __("Mark as Active"), icon: , onClick: (rule: any) => { setConfirmDialog({ isOpen: true, rule, title: __("Mark as Active"), message: __( "Are you sure you want to mark this pricing rule as active? It will start applying to trip pricing.", ), onConfirm: async () => { try { await apiClient.put( `/dynamic-pricing/rules/${rule.id}`, { status: "active", }, ); showToast( __("Pricing rule marked as active successfully"), "success", ); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"], }); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-statistics"], }); setConfirmDialog({ isOpen: false, rule: null, title: "", message: "", onConfirm: () => {}, }); } catch (error) { showToast( __("Failed to update pricing rule status"), "error", ); } }, }); }, condition: (rule: any) => rule.status !== "active" && rule.status !== "trash", }, { key: "inactive", label: __("Mark as Inactive"), icon: , onClick: (rule: any) => { setConfirmDialog({ isOpen: true, rule, title: __("Mark as Inactive"), message: __( "Are you sure you want to mark this pricing rule as inactive? It will no longer apply to trip pricing.", ), onConfirm: async () => { try { await apiClient.put( `/dynamic-pricing/rules/${rule.id}`, { status: "inactive", }, ); showToast( __( "Pricing rule marked as inactive successfully", ), "success", ); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"], }); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-statistics"], }); setConfirmDialog({ isOpen: false, rule: null, title: "", message: "", onConfirm: () => {}, }); } catch (error) { showToast( __("Failed to update pricing rule status"), "error", ); } }, }); }, condition: (rule: any) => rule.status === "active", }, { key: "restore", label: __("Restore"), icon: , onClick: (rule: any) => { setConfirmDialog({ isOpen: true, rule, title: __("Restore Rule"), message: __( "Are you sure you want to restore this pricing rule? It will be moved back to active status.", ), onConfirm: async () => { try { await apiClient.put( `/dynamic-pricing/rules/${rule.id}`, { status: "active", }, ); showToast( __("Pricing rule restored successfully"), "success", ); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"], }); queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-statistics"], }); setConfirmDialog({ isOpen: false, rule: null, title: "", message: "", onConfirm: () => {}, }); } catch (error) { showToast( __("Failed to restore pricing rule"), "error", ); } }, }); }, condition: (rule: any) => rule.status === "trash" || statusFilter === "trash", }, { key: "trash", label: __("Move to Trash"), icon: , onClick: (rule: any) => { setConfirmDialog({ isOpen: true, rule, title: __("Move Rule to Trash"), message: __( "Are you sure you want to move this pricing rule to Trash? It will stop applying to trip pricing immediately. You can restore it from the Trash filter.", ), confirmText: __("Move to Trash"), variant: "warning", onConfirm: async () => { setConfirmDialog((prev) => ({ ...prev, isLoading: true, })); try { await apiClient.put( `/dynamic-pricing/rules/${rule.id}`, { status: "trash" }, ); showToast( __("Pricing rule moved to Trash"), "success", ); invalidateRules(); closeConfirmDialog(); } catch (error) { setConfirmDialog((prev) => ({ ...prev, isLoading: false, })); showToast( __("Failed to move rule to Trash"), "error", ); } }, }); }, condition: (rule: any) => rule.status !== "trash" && statusFilter !== "trash", }, { key: "delete", label: __("Delete Permanently"), icon: , onClick: (rule: any) => { setConfirmDialog({ isOpen: true, rule, title: __("Delete Pricing Rule Permanently"), message: __( "This will permanently delete the pricing rule. This action cannot be undone. Continue?", ), confirmText: __("Delete Permanently"), variant: "danger", onConfirm: async () => { setConfirmDialog((prev) => ({ ...prev, isLoading: true, })); try { await apiClient.delete( `/dynamic-pricing/rules/${rule.id}`, ); showToast( __("Pricing rule deleted permanently"), "success", ); invalidateRules(); closeConfirmDialog(); } catch (error) { setConfirmDialog((prev) => ({ ...prev, isLoading: false, })); showToast(__("Failed to delete rule"), "error"); } }, }); }, condition: (rule: any) => rule.status === "trash" || statusFilter === "trash", variant: "destructive", }, ]} selectedItemIds={selectedIds} onSelectItem={(id, checked) => { if (checked) { setSelectedIds([...selectedIds, id as number]); } else { setSelectedIds( selectedIds.filter((selectedId) => selectedId !== id), ); } }} onSelectAll={(checked) => { if (checked) { setSelectedIds(filteredRules.map((rule: any) => rule.id)); } else { setSelectedIds([]); } }} isAllSelected={ selectedIds.length === filteredRules.length && filteredRules.length > 0 } getItemId={(rule: any) => rule.id} isLoading={isLoading} emptyText={__("No pricing rules found")} emptyDescription={__( 'Click "Create Rule" button above to get started', )} />
)} {/* Analytics Tab */} {activeTab === "analytics" && (
{/* Performance Metrics */} {analyticsLoading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : (

{__("Total Rules")}

{stats.total_rules || 0}

{__("All pricing rules")}

{__("Active Rules")}

{stats.active_rules || 0}

{__("Currently active")}

{__("Inactive Rules")}

{stats.inactive_rules || 0}

{__("Paused rules")}

{__("Rule Types Used")}

{stats.rule_types_used || 0}

{__("Different types")}

)} {/* Pricing History Metrics (what powers analytics) */} {analyticsLoading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : (

{__("Pricing Events")}

{stats.pricing_history_total || 0}

{__("Recorded price adjustments")}

{__("Trips Affected")}

{stats.pricing_history_trips_affected || 0}

{__("Unique trips with adjustments")}

{__("Avg Adjustment")}

{typeof stats.pricing_history_avg_adjustment_percentage === "number" ? `${stats.pricing_history_avg_adjustment_percentage.toFixed(2)}%` : "0%"}

{__("Average % change")}

{__("Last 30 Days")}

{stats.pricing_history_last_30_days || 0}

{__("Recent pricing events")}

)} {/* Rule Performance */} {__("Rule Performance")} {__("How each pricing rule is performing")} {analyticsLoading ? (
{[1, 2, 3].map((i) => (
))}
) : (
{ruleImpact.length > 0 ? ( ruleImpact.map((ri: any) => (

{ri.name || `${__("Rule")} #${ri.rule_id}`}

{(ri.rule_type || "").replace("_", " ")} {ri.adjustment_type || ""}
{__("Events")}: {ri.events || 0} {__("Avg Adjustment")}:{" "} {typeof ri.avg_adjustment_value === "number" ? ri.avg_adjustment_value.toFixed(2) : Number(ri.avg_adjustment_value || 0).toFixed( 2, )}

{__("Impact (30d)")}

{formatCurrencyAmount( Number(ri.total_adjustment_amount) || 0, )}

)) ) : (
{rules.length > 0 ? ( <>

{__( "No rule-level impact recorded in the last 30 days", )}

{__( "Charts use dynamic pricing history when a booking applies an adjusted price. Complete a checkout (or recalculate pricing) after rules are active to populate metrics.", )}

) : ( <>

{__("No pricing rules created yet")}

{__( "Create your first rule to see performance metrics", )}

)}
)}
)}
{/* Revenue Trend Chart */} {__("Revenue Impact Trend")} {__( "30-day revenue comparison with and without dynamic pricing", )} {!analyticsLoading && statsErrorMessage ? (
{__("Failed to load statistics:")} {statsErrorMessage}
) : null} {analyticsLoading ? (
) : trendData.length > 0 ? (
v ? toDateValue(String(v)).toLocaleDateString() : "" } /> formatCurrencyAmount(Number(v) || 0) } /> { if (name === "totalAdjustmentAmount") { return [ formatCurrencyAmount(Number(value) || 0), __("Revenue Impact"), ]; } if (name === "events") { return [Number(value) || 0, __("Events")]; } if (name === "avgAdjustmentPercentage") { return [ `${Number(value || 0).toFixed(2)}%`, __("Avg Adjustment"), ]; } return [value, name]; }} />
{trendData.map((row: any) => ( ))}
{__("Date")} {__("Events")} {__("Revenue Impact")} {__("Avg Adjustment")}
{row.day ? toDateValue( String(row.day), ).toLocaleDateString() : ""} {row.events ?? 0} {formatCurrencyAmount( Number(row.totalAdjustmentAmount) || 0, )} {`${Number(row.avgAdjustmentPercentage || 0).toFixed(2)}%`}
) : (

{Number(stats.pricing_history_total) > 0 && Number(stats.pricing_history_last_30_days) === 0 ? __( "No pricing events in the last 30 days (older history exists).", ) : __("No pricing history in the last 30 days")}

{rules.length > 0 && Number(stats.pricing_history_total) === 0 ? (

{__( "Revenue impact is recorded when a booking uses a trip price adjusted by your rules. Place a test booking to see this chart fill in.", )}

) : null}
)}
)} {/* Settings Tab */} {activeTab === "settings" && (
{/* General Settings */} {__("General Settings")} {__("Configure global dynamic pricing behavior")} {isLoading ? (
{[1, 2, 3].map((i) => (
))}
) : (

{__( "Choose whether percentage and fixed rules use the discounted trip price or the regular list price as their starting point. Bookings and availability pass both values when possible.", )}

{__( "How to combine matching rules. Caps below still apply afterwards. Per-rule numeric Priority is used as a tie-breaker when multiple rules tie for largest / best.", )}

handleSettingChange( "maximum_markup_percent", parseInt(e.target.value), ) } min="0" max="100" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500" />

{__( "Hard cap on upward adjustments (markup) across all matched rules to avoid surprising customers with sudden surges.", )}

handleSettingChange( "maximum_discount_percent", parseInt(e.target.value), ) } min="0" max="100" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500" />

{__( "Hard cap on downward adjustments (discount) so dynamic pricing can never push a trip below this fraction of its base price.", )}

)}
{/* Demand Calculation Settings */} {__("Demand Calculation")} {__("Configure how booking demand is calculated")} {isLoading ? (
{[1, 2].map((i) => (
))}
) : (
handleSettingChange( "calculation_period", parseInt(e.target.value), ) } min="1" max="30" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500" />

{__("Number of days to analyze for demand trends")}

{__("How often to recalculate demand scores")}

)}
{/* Display Settings */} {__("Display Settings")} {__("How dynamic pricing is shown to customers")} {isLoading ? (
{[1, 2].map((i) => (
))}
) : (

{__("Show Original Price")}

{__( "Display crossed-out original price when discount applied", )}

handleSettingChange("show_original_price", checked) } />

{__("Show Savings Badge")}

{__('Display "Save X%" badge on discounted trips')}

handleSettingChange("show_savings_badge", checked) } />

{__("Show Urgency Messages")}

{__( 'Display messages like "Price increases soon" for demand-based rules', )}

handleSettingChange("show_urgency_messages", checked) } />
)}
{/* Save Button */} {!isLoading && (
)}
)} {/* Rule Type Selection Modal */} setShowRuleTypeModal(false)} onSelectType={handleSelectRuleType} /> {/* Confirmation Dialog */} confirmDialog.onConfirm?.()} title={confirmDialog.title || ""} message={confirmDialog.message || ""} confirmText={confirmDialog.confirmText} variant={confirmDialog.variant ?? "danger"} isLoading={confirmDialog.isLoading ?? false} />
); }; export default DynamicPricingPage;