PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / DynamicPricing.tsx

DynamicPricing.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at resources/js/pages/DynamicPricing.tsx

1,617 lines 64.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Dynamic Pricing Page
3 *
4 * Premium module for intelligent price adjustments.
5 * This page only loads when Yatra Pro is active and module is enabled.
6 * Premium upgrade content is handled by premium-pages/DynamicPricing.tsx
7 *
8 * @package Yatra
9 * @since 3.0.0
10 */
11
12 import React, { useState, useMemo } from "react";
13 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
14 import {
15 Card,
16 CardContent,
17 CardHeader,
18 CardTitle,
19 CardDescription,
20 } from "../components/ui/card";
21 import { Button } from "../components/ui/button";
22 import { Badge } from "../components/ui/badge";
23 import { PageHeader } from "../components/common/PageHeader";
24 import { RuleTypeSelectionModal } from "../components/modals/RuleTypeSelectionModal";
25 import { Toggle } from "../components/ui/toggle";
26 import { useToast } from "../components/ui/toast";
27 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
28 import { Table as SharedTable } from "../components/shared/Table";
29 import { SearchFilterToolbar, BulkActionToolbar } from "../components/shared";
30 import { apiClient } from "../lib/api-client";
31 import { __ } from "../lib/i18n";
32 import { getCurrencySymbol } from "../data/currencies";
33 import PremiumUpgradeCard from "./premium-pages/DynamicPricing";
34 import {
35 ResponsiveContainer,
36 LineChart,
37 Line,
38 XAxis,
39 YAxis,
40 CartesianGrid,
41 Tooltip,
42 } from "recharts";
43 import {
44 Clock,
45 Target,
46 Check,
47 Package,
48 BarChart,
49 Settings,
50 Plus,
51 AlertCircle,
52 Edit,
53 Trash2,
54 DollarSign,
55 CheckCircle,
56 Save,
57 } from "lucide-react";
58
59 // Check if module is available (Pro active)
60 const isModuleAvailable = (): boolean => {
61 const yatraAdmin = (window as any)?.yatraAdmin;
62 return Boolean(yatraAdmin?.isPro);
63 };
64
65 // Main Component
66 const DynamicPricingPage: React.FC = () => {
67 const [activeTab, setActiveTab] = useState("rules");
68 const [showRuleTypeModal, setShowRuleTypeModal] = useState(false);
69 const [settings, setSettings] = useState({
70 rule_priority_mode: "highest",
71 maximum_markup_percent: 50,
72 maximum_discount_percent: 30,
73 calculation_period: 7,
74 update_frequency: "hourly",
75 show_original_price: true,
76 show_savings_badge: true,
77 show_urgency_messages: false,
78 });
79 const { showToast } = useToast();
80 const queryClient = useQueryClient();
81
82 // Search and filter state
83 const [searchTerm, setSearchTerm] = useState("");
84 const [statusFilter, setStatusFilter] = useState("all");
85 const [sortBy, setSortBy] = useState("name");
86 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
87 const [selectedIds, setSelectedIds] = useState<number[]>([]);
88 const [bulkAction, setBulkAction] = useState("");
89 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
90 const [confirmDialog, setConfirmDialog] = useState<{
91 isOpen: boolean;
92 rule: any | null;
93 title?: string;
94 message?: string;
95 onConfirm?: () => void;
96 }>({
97 isOpen: false,
98 rule: null,
99 });
100 const [isSaving, setIsSaving] = useState(false);
101
102 const handleSettingChange = (key: string, value: any) => {
103 setSettings((prev) => ({ ...prev, [key]: value }));
104 };
105
106 const handleSaveSettings = async () => {
107 setIsSaving(true);
108 try {
109 await apiClient.post("/dynamic-pricing/settings", settings);
110 showToast(__("Settings saved successfully"), "success");
111 } catch (error) {
112 console.error("Failed to save settings:", error);
113 showToast(__("Failed to save settings. Please try again."), "error");
114 } finally {
115 setIsSaving(false);
116 }
117 };
118
119 const handleSelectRuleType = (ruleType: string) => {
120 const baseUrl = window.location.href.split("&action=")[0];
121 window.location.href = `${baseUrl}&action=create-pricing-rule&rule_type=${ruleType}`;
122 };
123
124 // Show premium upgrade content if module is not available
125 if (!isModuleAvailable()) return <PremiumUpgradeCard />;
126
127 // Fetch settings from backend
128 const { data: settingsData } = useQuery({
129 queryKey: ["dynamic-pricing-settings"],
130 queryFn: async () => {
131 const response = await apiClient.get("/dynamic-pricing/settings");
132 return response.data || response;
133 },
134 });
135
136 // Update settings state when data is loaded
137 React.useEffect(() => {
138 if (settingsData) {
139 setSettings(settingsData);
140 }
141 }, [settingsData]);
142
143 // Fetch pricing rules
144 const { data: rulesData, isLoading } = useQuery({
145 queryKey: ["dynamic-pricing-rules"],
146 queryFn: async () => {
147 const response = await apiClient.get("/dynamic-pricing/rules");
148
149 return response;
150 },
151 });
152
153 // Fetch statistics
154 const {
155 data: statsData,
156 isLoading: isStatsLoading,
157 error: statsError,
158 } = useQuery({
159 queryKey: ["dynamic-pricing-statistics"],
160 queryFn: async () => {
161 const response = await apiClient.get("/dynamic-pricing/statistics");
162
163 return response;
164 },
165 });
166
167 // The API client returns the decoded JSON body.
168 // Some endpoints return { data: {...} } while others might return { data: { data: {...} } }.
169 const rulesPayload =
170 (rulesData as any)?.data?.data ?? (rulesData as any)?.data ?? [];
171 const statsPayload =
172 (statsData as any)?.data?.data ?? (statsData as any)?.data ?? {};
173
174 const rules = rulesPayload || [];
175 const stats = statsPayload || {};
176
177 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
178 const currencyPosition =
179 (window as any)?.yatraAdmin?.currencyPosition ||
180 (window as any)?.yatraAdmin?.currency_position ||
181 "before";
182 const decimalPlaces = Number(
183 (window as any)?.yatraAdmin?.decimalPlaces ||
184 (window as any)?.yatraAdmin?.currency_decimals ||
185 2,
186 );
187 const thousandSeparator =
188 (window as any)?.yatraAdmin?.thousandSeparator || ",";
189 const decimalSeparator = (window as any)?.yatraAdmin?.decimalSeparator || ".";
190
191 const formatCurrencyAmount = (amount: number) => {
192 const numPrice = Number(amount) || 0;
193 const formattedAmount = new Intl.NumberFormat(undefined, {
194 minimumFractionDigits: decimalPlaces,
195 maximumFractionDigits: decimalPlaces,
196 })
197 .format(numPrice)
198 .replace(/,/g, "TEMP_THOUSAND")
199 .replace(/\./g, decimalSeparator)
200 .replace(/TEMP_THOUSAND/g, thousandSeparator);
201
202 const currencySymbol = getCurrencySymbol(globalCurrency);
203 if (currencyPosition === "after" || currencyPosition === "right") {
204 return `${formattedAmount} ${currencySymbol}`;
205 }
206 return `${currencySymbol}${formattedAmount}`;
207 };
208
209 const trendData = Array.isArray(
210 (stats as any)?.pricing_history_trend_last_30_days,
211 )
212 ? (stats as any).pricing_history_trend_last_30_days.map((row: any) => {
213 const day = String(row.day || "");
214 const dateLabel = day ? new Date(day).toLocaleDateString() : "";
215 return {
216 day,
217 dateLabel,
218 events: Number(row.events) || 0,
219 totalAdjustmentAmount: Number(row.total_adjustment_amount) || 0,
220 avgAdjustmentPercentage: Number(row.avg_adjustment_percentage) || 0,
221 };
222 })
223 : [];
224
225 const ruleImpact = Array.isArray(
226 (stats as any)?.pricing_history_rule_impact_last_30_days,
227 )
228 ? (stats as any).pricing_history_rule_impact_last_30_days
229 : [];
230
231 const analyticsLoading = isLoading || isStatsLoading;
232 const statsErrorMessage = statsError
233 ? (statsError as any)?.message || String(statsError)
234 : "";
235
236 // Filter and sort rules
237 const filteredRules = useMemo(() => {
238 let filtered = [...rules];
239
240 // Apply search filter
241 if (searchTerm) {
242 filtered = filtered.filter(
243 (rule: any) =>
244 rule.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
245 rule.rule_type?.toLowerCase().includes(searchTerm.toLowerCase()),
246 );
247 }
248
249 // Apply status filter
250 if (statusFilter !== "all") {
251 filtered = filtered.filter((rule: any) => rule.status === statusFilter);
252 }
253
254 // Apply sorting
255 filtered.sort((a: any, b: any) => {
256 let aVal = a[sortBy];
257 let bVal = b[sortBy];
258
259 if (sortBy === "created_at" || sortBy === "updated_at") {
260 aVal = new Date(aVal).getTime();
261 bVal = new Date(bVal).getTime();
262 }
263
264 if (sortOrder === "asc") {
265 return aVal > bVal ? 1 : -1;
266 } else {
267 return aVal < bVal ? 1 : -1;
268 }
269 });
270
271 return filtered;
272 }, [rules, searchTerm, statusFilter, sortBy, sortOrder]);
273
274 // Bulk action mutation
275 const bulkMutation = useMutation({
276 mutationFn: async ({ action, ids }: { action: string; ids: number[] }) => {
277 const promises = ids.map((id) => {
278 if (action === "delete") {
279 return apiClient.delete(`/dynamic-pricing/rules/${id}`);
280 } else if (action === "restore") {
281 return apiClient.put(`/dynamic-pricing/rules/${id}`, {
282 status: "active",
283 });
284 } else if (action === "trash") {
285 return apiClient.put(`/dynamic-pricing/rules/${id}`, {
286 status: "trash",
287 });
288 } else if (action === "active" || action === "inactive") {
289 return apiClient.put(`/dynamic-pricing/rules/${id}`, {
290 status: action,
291 });
292 }
293 return Promise.resolve();
294 });
295 return Promise.all(promises);
296 },
297 onSuccess: () => {
298 queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"] });
299 queryClient.invalidateQueries({
300 queryKey: ["dynamic-pricing-statistics"],
301 });
302 setSelectedIds([]);
303 setBulkAction("");
304 showToast(__("Bulk action completed successfully"), "success");
305 },
306 onError: () => {
307 showToast(__("Failed to complete bulk action"), "error");
308 },
309 });
310
311 const handleBulkApply = () => {
312 if (!bulkAction) {
313 showToast(__("Select a bulk action first"), "warning");
314 return;
315 }
316 if (selectedIds.length === 0) {
317 showToast(__("Select at least one rule"), "warning");
318 return;
319 }
320 bulkMutation.mutate({ action: bulkAction, ids: selectedIds });
321 };
322
323 return (
324 <div className="space-y-6">
325 <PageHeader
326 title={__("Dynamic Pricing")}
327 description={__(
328 "Intelligent price adjustments based on demand, seasonality, and booking patterns",
329 )}
330 />
331
332 {/* Tabs with Create Button */}
333 <div className="border-b border-gray-200 dark:border-gray-700">
334 <div className="flex items-center justify-between">
335 <nav className="-mb-px flex space-x-8">
336 <button
337 onClick={() => setActiveTab("rules")}
338 className={`py-4 px-1 border-b-2 font-medium text-sm ${
339 activeTab === "rules"
340 ? "border-blue-500 text-blue-600 dark:text-blue-400"
341 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
342 }`}
343 >
344 <Target className="w-5 h-5 inline-block mr-2" />
345 {__("Pricing Rules")}
346 </button>
347 <button
348 onClick={() => setActiveTab("analytics")}
349 className={`py-4 px-1 border-b-2 font-medium text-sm ${
350 activeTab === "analytics"
351 ? "border-blue-500 text-blue-600 dark:text-blue-400"
352 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
353 }`}
354 >
355 <BarChart className="w-5 h-5 inline-block mr-2" />
356 {__("Analytics")}
357 </button>
358 <button
359 onClick={() => setActiveTab("settings")}
360 className={`py-4 px-1 border-b-2 font-medium text-sm ${
361 activeTab === "settings"
362 ? "border-blue-500 text-blue-600 dark:text-blue-400"
363 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
364 }`}
365 >
366 <Settings className="w-5 h-5 inline-block mr-2" />
367 {__("Settings")}
368 </button>
369 </nav>
370
371 {activeTab === "rules" && (
372 <Button onClick={() => setShowRuleTypeModal(true)} className="mb-4">
373 <Plus className="w-4 h-4 mr-2" />
374 {__("Create Rule")}
375 </Button>
376 )}
377 </div>
378 </div>
379
380 {/* Rules Tab */}
381 {activeTab === "rules" && (
382 <div className="space-y-6">
383 {/* Stats Cards */}
384 {isLoading ? (
385 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
386 {[1, 2, 3, 4].map((i) => (
387 <Card key={i}>
388 <CardContent className="pt-6">
389 <div className="flex items-center justify-between">
390 <div className="flex-1">
391 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
392 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16 animate-pulse"></div>
393 </div>
394 <div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
395 </div>
396 </CardContent>
397 </Card>
398 ))}
399 </div>
400 ) : (
401 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
402 <Card>
403 <CardContent className="pt-6">
404 <div className="flex items-center justify-between">
405 <div>
406 <p className="text-sm text-gray-600 dark:text-gray-400">
407 {__("Total Rules")}
408 </p>
409 <p className="text-3xl font-bold">
410 {stats.total_rules || 0}
411 </p>
412 </div>
413 <Target className="w-8 h-8 text-blue-600 dark:text-blue-400" />
414 </div>
415 </CardContent>
416 </Card>
417
418 <Card>
419 <CardContent className="pt-6">
420 <div className="flex items-center justify-between">
421 <div>
422 <p className="text-sm text-gray-600 dark:text-gray-400">
423 {__("Active Rules")}
424 </p>
425 <p className="text-3xl font-bold text-green-600 dark:text-green-400">
426 {stats.active_rules || 0}
427 </p>
428 </div>
429 <CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
430 </div>
431 </CardContent>
432 </Card>
433
434 <Card>
435 <CardContent className="pt-6">
436 <div className="flex items-center justify-between">
437 <div>
438 <p className="text-sm text-gray-600 dark:text-gray-400">
439 {__("Inactive Rules")}
440 </p>
441 <p className="text-3xl font-bold text-gray-600 dark:text-gray-400">
442 {stats.inactive_rules || 0}
443 </p>
444 </div>
445 <AlertCircle className="w-8 h-8 text-gray-600 dark:text-gray-400" />
446 </div>
447 </CardContent>
448 </Card>
449
450 <Card>
451 <CardContent className="pt-6">
452 <div className="flex items-center justify-between">
453 <div>
454 <p className="text-sm text-gray-600 dark:text-gray-400">
455 {__("Rule Types")}
456 </p>
457 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
458 {stats.rule_types_used || 0}
459 </p>
460 </div>
461 <Package className="w-8 h-8 text-purple-600 dark:text-purple-400" />
462 </div>
463 </CardContent>
464 </Card>
465 </div>
466 )}
467
468 {/* Search and Filter Toolbar */}
469 <Card>
470 <CardContent className="pt-6">
471 <SearchFilterToolbar
472 searchTerm={searchTerm}
473 onSearchChange={setSearchTerm}
474 statusFilter={statusFilter}
475 onStatusChange={setStatusFilter}
476 statusOptions={[
477 { value: "all", label: __("All Status") },
478 { value: "active", label: __("Active") },
479 { value: "inactive", label: __("Inactive") },
480 { value: "trash", label: __("Trash") },
481 ]}
482 sortBy={sortBy}
483 onSortByChange={setSortBy}
484 sortOrder={sortOrder}
485 onSortOrderChange={setSortOrder}
486 sortOptions={[
487 { value: "name", label: __("Name") },
488 { value: "priority", label: __("Priority") },
489 { value: "created_at", label: __("Created Date") },
490 { value: "updated_at", label: __("Updated Date") },
491 ]}
492 onResetFilters={() => {
493 setSearchTerm("");
494 setStatusFilter("all");
495 setSortBy("name");
496 setSortOrder("asc");
497 }}
498 hasFilters={
499 !!searchTerm ||
500 statusFilter !== "all" ||
501 sortBy !== "name" ||
502 sortOrder !== "asc"
503 }
504 placeholder={__("Search rules...")}
505 />
506 </CardContent>
507 </Card>
508
509 {/* Bulk Action Toolbar */}
510 <BulkActionToolbar
511 selectedIds={selectedIds}
512 bulkAction={bulkAction}
513 setBulkAction={setBulkAction}
514 onApply={handleBulkApply}
515 onClearSelection={() => setSelectedIds([])}
516 statusFilter={statusFilter}
517 setStatusFilter={setStatusFilter}
518 statusOptions={[
519 { key: "all", label: __("All"), count: rules.length },
520 {
521 key: "active",
522 label: __("Active"),
523 count: stats.active_rules || 0,
524 },
525 {
526 key: "inactive",
527 label: __("Inactive"),
528 count: stats.inactive_rules || 0,
529 },
530 {
531 key: "trash",
532 label: __("Trash"),
533 count: stats.trash_rules || 0,
534 },
535 ]}
536 showColumnsDropdown={showColumnsDropdown}
537 setShowColumnsDropdown={setShowColumnsDropdown}
538 columnOptions={[
539 { key: "name", label: __("Rule Name"), visible: true },
540 { key: "adjustment", label: __("Adjustment"), visible: true },
541 {
542 key: "applicable_trips",
543 label: __("Applicable To"),
544 visible: true,
545 },
546 { key: "priority", label: __("Priority"), visible: true },
547 { key: "status", label: __("Status"), visible: true },
548 { key: "created_at", label: __("Created"), visible: true },
549 ]}
550 onToggleColumn={() => {}}
551 bulkMutationPending={bulkMutation.isPending}
552 totalItems={filteredRules.length}
553 bulkActionOptions={
554 statusFilter === "trash"
555 ? [
556 { value: "restore", label: __("Restore") },
557 { value: "delete", label: __("Delete Permanently") },
558 ]
559 : [
560 { value: "active", label: __("Mark as Active") },
561 { value: "inactive", label: __("Mark as Inactive") },
562 { value: "trash", label: __("Move to Trash") },
563 ]
564 }
565 />
566
567 {/* Rules List */}
568 <Card>
569 <CardHeader>
570 <CardTitle>{__("Pricing Rules")}</CardTitle>
571 <CardDescription>
572 {__("Manage your dynamic pricing rules")}
573 </CardDescription>
574 </CardHeader>
575 <CardContent className="p-0">
576 <SharedTable
577 data={filteredRules}
578 columns={[
579 {
580 key: "name",
581 label: __("Rule Name"),
582 sortable: true,
583 visible: true,
584 render: (rule: any) => (
585 <div>
586 <a
587 href={`${window.location.href.split("&action=")[0]}&action=edit-pricing-rule&id=${rule.id}`}
588 className={`font-medium hover:underline transition-colors cursor-pointer ${
589 rule.status === "trash" || statusFilter === "trash"
590 ? "text-gray-400 dark:text-gray-600"
591 : "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
592 }`}
593 >
594 {rule.name}
595 </a>
596 <div
597 className={`text-sm ${
598 rule.status === "trash" || statusFilter === "trash"
599 ? "text-gray-400 dark:text-gray-600"
600 : "text-gray-500 dark:text-gray-400"
601 }`}
602 >
603 {rule.rule_type?.replace("_", " ")}
604 </div>
605 </div>
606 ),
607 },
608 {
609 key: "adjustment",
610 label: __("Adjustment"),
611 visible: true,
612 render: (rule: any) => (
613 <span className="font-medium text-gray-900 dark:text-white">
614 {rule.adjustment_type === "percentage"
615 ? `${rule.adjustment_value > 0 ? "+" : ""}${rule.adjustment_value}%`
616 : `$${rule.adjustment_value}`}
617 </span>
618 ),
619 },
620 {
621 key: "applicable_trips",
622 label: __("Applicable To"),
623 visible: true,
624 render: (rule: any) => (
625 <span className="text-sm text-gray-600 dark:text-gray-400">
626 {rule.applicable_trips === "all"
627 ? __("All Trips")
628 : __("Specific Trips")}
629 </span>
630 ),
631 },
632 {
633 key: "priority",
634 label: __("Priority"),
635 sortable: true,
636 visible: true,
637 render: (rule: any) => (
638 <Badge variant="outline">{rule.priority}</Badge>
639 ),
640 },
641 {
642 key: "status",
643 label: __("Status"),
644 sortable: true,
645 visible: true,
646 render: (rule: any) => (
647 <Badge
648 variant={
649 rule.status === "active" ? "success" : "outline"
650 }
651 >
652 {rule.status}
653 </Badge>
654 ),
655 },
656 {
657 key: "created_at",
658 label: __("Created"),
659 sortable: true,
660 visible: true,
661 render: (rule: any) => (
662 <span className="text-sm text-gray-500 dark:text-gray-400">
663 {new Date(rule.created_at).toLocaleDateString()}
664 </span>
665 ),
666 },
667 ]}
668 actions={[
669 {
670 key: "edit",
671 label: __("Edit"),
672 icon: <Edit className="w-4 h-4" />,
673 onClick: (rule: any) => {
674 const baseUrl = window.location.href.split("&action=")[0];
675 window.location.href = `${baseUrl}&action=edit-pricing-rule&id=${rule.id}`;
676 },
677 },
678 {
679 key: "active",
680 label: __("Mark as Active"),
681 icon: <Check className="w-4 h-4" />,
682 onClick: (rule: any) => {
683 setConfirmDialog({
684 isOpen: true,
685 rule,
686 title: __("Mark as Active"),
687 message: __(
688 "Are you sure you want to mark this pricing rule as active? It will start applying to trip pricing.",
689 ),
690 onConfirm: async () => {
691 try {
692 await apiClient.put(
693 `/dynamic-pricing/rules/${rule.id}`,
694 {
695 status: "active",
696 },
697 );
698 showToast(
699 __("Pricing rule marked as active successfully"),
700 "success",
701 );
702 queryClient.invalidateQueries({
703 queryKey: ["dynamic-pricing-rules"],
704 });
705 queryClient.invalidateQueries({
706 queryKey: ["dynamic-pricing-statistics"],
707 });
708 setConfirmDialog({
709 isOpen: false,
710 rule: null,
711 title: "",
712 message: "",
713 onConfirm: () => {},
714 });
715 } catch (error) {
716 showToast(
717 __("Failed to update pricing rule status"),
718 "error",
719 );
720 }
721 },
722 });
723 },
724 condition: (rule: any) =>
725 rule.status !== "active" && rule.status !== "trash",
726 },
727 {
728 key: "inactive",
729 label: __("Mark as Inactive"),
730 icon: <AlertCircle className="w-4 h-4" />,
731 onClick: (rule: any) => {
732 setConfirmDialog({
733 isOpen: true,
734 rule,
735 title: __("Mark as Inactive"),
736 message: __(
737 "Are you sure you want to mark this pricing rule as inactive? It will no longer apply to trip pricing.",
738 ),
739 onConfirm: async () => {
740 try {
741 await apiClient.put(
742 `/dynamic-pricing/rules/${rule.id}`,
743 {
744 status: "inactive",
745 },
746 );
747 showToast(
748 __(
749 "Pricing rule marked as inactive successfully",
750 ),
751 "success",
752 );
753 queryClient.invalidateQueries({
754 queryKey: ["dynamic-pricing-rules"],
755 });
756 queryClient.invalidateQueries({
757 queryKey: ["dynamic-pricing-statistics"],
758 });
759 setConfirmDialog({
760 isOpen: false,
761 rule: null,
762 title: "",
763 message: "",
764 onConfirm: () => {},
765 });
766 } catch (error) {
767 showToast(
768 __("Failed to update pricing rule status"),
769 "error",
770 );
771 }
772 },
773 });
774 },
775 condition: (rule: any) => rule.status === "active",
776 },
777 {
778 key: "restore",
779 label: __("Restore"),
780 icon: <Check className="w-4 h-4" />,
781 onClick: (rule: any) => {
782 setConfirmDialog({
783 isOpen: true,
784 rule,
785 title: __("Restore Rule"),
786 message: __(
787 "Are you sure you want to restore this pricing rule? It will be moved back to active status.",
788 ),
789 onConfirm: async () => {
790 try {
791 await apiClient.put(
792 `/dynamic-pricing/rules/${rule.id}`,
793 {
794 status: "active",
795 },
796 );
797 showToast(
798 __("Pricing rule restored successfully"),
799 "success",
800 );
801 queryClient.invalidateQueries({
802 queryKey: ["dynamic-pricing-rules"],
803 });
804 queryClient.invalidateQueries({
805 queryKey: ["dynamic-pricing-statistics"],
806 });
807 setConfirmDialog({
808 isOpen: false,
809 rule: null,
810 title: "",
811 message: "",
812 onConfirm: () => {},
813 });
814 } catch (error) {
815 showToast(
816 __("Failed to restore pricing rule"),
817 "error",
818 );
819 }
820 },
821 });
822 },
823 condition: (rule: any) =>
824 rule.status === "trash" || statusFilter === "trash",
825 },
826 {
827 key: "trash",
828 label: __("Move to Trash"),
829 icon: <Trash2 className="w-4 h-4" />,
830 onClick: (rule: any) => {
831 setConfirmDialog({
832 isOpen: true,
833 rule,
834 title: __("Confirm Action"),
835 message: __(
836 "Are you sure you want to perform this action?",
837 ),
838 onConfirm: () => {
839 // Handle action
840 },
841 });
842 },
843 condition: (rule: any) =>
844 rule.status !== "trash" && statusFilter !== "trash",
845 },
846 {
847 key: "delete",
848 label: __("Delete Permanently"),
849 icon: <Trash2 className="w-4 h-4" />,
850 onClick: (rule: any) => {
851 setConfirmDialog({
852 isOpen: true,
853 rule,
854 title: __("Confirm Action"),
855 message: __(
856 "Are you sure you want to perform this action?",
857 ),
858 onConfirm: () => {
859 // Handle action
860 },
861 });
862 },
863 condition: (rule: any) =>
864 rule.status === "trash" || statusFilter === "trash",
865 variant: "destructive",
866 },
867 ]}
868 selectedItemIds={selectedIds}
869 onSelectItem={(id, checked) => {
870 if (checked) {
871 setSelectedIds([...selectedIds, id as number]);
872 } else {
873 setSelectedIds(
874 selectedIds.filter((selectedId) => selectedId !== id),
875 );
876 }
877 }}
878 onSelectAll={(checked) => {
879 if (checked) {
880 setSelectedIds(filteredRules.map((rule: any) => rule.id));
881 } else {
882 setSelectedIds([]);
883 }
884 }}
885 isAllSelected={
886 selectedIds.length === filteredRules.length &&
887 filteredRules.length > 0
888 }
889 getItemId={(rule: any) => rule.id}
890 isLoading={isLoading}
891 emptyText={__("No pricing rules found")}
892 emptyDescription={__(
893 'Click "Create Rule" button above to get started',
894 )}
895 />
896 </CardContent>
897 </Card>
898 </div>
899 )}
900
901 {/* Analytics Tab */}
902 {activeTab === "analytics" && (
903 <div className="space-y-6">
904 {/* Performance Metrics */}
905 {analyticsLoading ? (
906 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
907 {[1, 2, 3, 4].map((i) => (
908 <Card key={i}>
909 <CardContent className="pt-6">
910 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
911 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2 animate-pulse"></div>
912 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-20 animate-pulse"></div>
913 </CardContent>
914 </Card>
915 ))}
916 </div>
917 ) : (
918 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
919 <Card>
920 <CardContent className="pt-6">
921 <div className="flex items-center justify-between">
922 <div>
923 <p className="text-sm text-gray-600 dark:text-gray-400">
924 {__("Total Rules")}
925 </p>
926 <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
927 {stats.total_rules || 0}
928 </p>
929 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
930 {__("All pricing rules")}
931 </p>
932 </div>
933 <Package className="w-8 h-8 text-blue-600 dark:text-blue-400" />
934 </div>
935 </CardContent>
936 </Card>
937
938 <Card>
939 <CardContent className="pt-6">
940 <div className="flex items-center justify-between">
941 <div>
942 <p className="text-sm text-gray-600 dark:text-gray-400">
943 {__("Active Rules")}
944 </p>
945 <p className="text-3xl font-bold text-green-600 dark:text-green-400">
946 {stats.active_rules || 0}
947 </p>
948 <p className="text-xs text-green-600 dark:text-green-400 mt-1">
949 {__("Currently active")}
950 </p>
951 </div>
952 <CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
953 </div>
954 </CardContent>
955 </Card>
956
957 <Card>
958 <CardContent className="pt-6">
959 <div className="flex items-center justify-between">
960 <div>
961 <p className="text-sm text-gray-600 dark:text-gray-400">
962 {__("Inactive Rules")}
963 </p>
964 <p className="text-3xl font-bold text-orange-600 dark:text-orange-400">
965 {stats.inactive_rules || 0}
966 </p>
967 <p className="text-xs text-orange-600 dark:text-orange-400 mt-1">
968 {__("Paused rules")}
969 </p>
970 </div>
971 <AlertCircle className="w-8 h-8 text-orange-600 dark:text-orange-400" />
972 </div>
973 </CardContent>
974 </Card>
975
976 <Card>
977 <CardContent className="pt-6">
978 <div className="flex items-center justify-between">
979 <div>
980 <p className="text-sm text-gray-600 dark:text-gray-400">
981 {__("Rule Types Used")}
982 </p>
983 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
984 {stats.rule_types_used || 0}
985 </p>
986 <p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
987 {__("Different types")}
988 </p>
989 </div>
990 <Target className="w-8 h-8 text-purple-600 dark:text-purple-400" />
991 </div>
992 </CardContent>
993 </Card>
994 </div>
995 )}
996
997 {/* Pricing History Metrics (what powers analytics) */}
998 {analyticsLoading ? (
999 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
1000 {[1, 2, 3, 4].map((i) => (
1001 <Card key={i}>
1002 <CardContent className="pt-6">
1003 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-28 mb-2 animate-pulse"></div>
1004 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2 animate-pulse"></div>
1005 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-32 animate-pulse"></div>
1006 </CardContent>
1007 </Card>
1008 ))}
1009 </div>
1010 ) : (
1011 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
1012 <Card>
1013 <CardContent className="pt-6">
1014 <div className="flex items-center justify-between">
1015 <div>
1016 <p className="text-sm text-gray-600 dark:text-gray-400">
1017 {__("Pricing Events")}
1018 </p>
1019 <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
1020 {stats.pricing_history_total || 0}
1021 </p>
1022 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1023 {__("Recorded price adjustments")}
1024 </p>
1025 </div>
1026 <BarChart className="w-8 h-8 text-blue-600 dark:text-blue-400" />
1027 </div>
1028 </CardContent>
1029 </Card>
1030
1031 <Card>
1032 <CardContent className="pt-6">
1033 <div className="flex items-center justify-between">
1034 <div>
1035 <p className="text-sm text-gray-600 dark:text-gray-400">
1036 {__("Trips Affected")}
1037 </p>
1038 <p className="text-3xl font-bold text-orange-600 dark:text-orange-400">
1039 {stats.pricing_history_trips_affected || 0}
1040 </p>
1041 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1042 {__("Unique trips with adjustments")}
1043 </p>
1044 </div>
1045 <Package className="w-8 h-8 text-orange-600 dark:text-orange-400" />
1046 </div>
1047 </CardContent>
1048 </Card>
1049
1050 <Card>
1051 <CardContent className="pt-6">
1052 <div className="flex items-center justify-between">
1053 <div>
1054 <p className="text-sm text-gray-600 dark:text-gray-400">
1055 {__("Avg Adjustment")}
1056 </p>
1057 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
1058 {typeof stats.pricing_history_avg_adjustment_percentage ===
1059 "number"
1060 ? `${stats.pricing_history_avg_adjustment_percentage.toFixed(2)}%`
1061 : "0%"}
1062 </p>
1063 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1064 {__("Average % change")}
1065 </p>
1066 </div>
1067 <DollarSign className="w-8 h-8 text-purple-600 dark:text-purple-400" />
1068 </div>
1069 </CardContent>
1070 </Card>
1071
1072 <Card>
1073 <CardContent className="pt-6">
1074 <div className="flex items-center justify-between">
1075 <div>
1076 <p className="text-sm text-gray-600 dark:text-gray-400">
1077 {__("Last 30 Days")}
1078 </p>
1079 <p className="text-3xl font-bold text-green-600 dark:text-green-400">
1080 {stats.pricing_history_last_30_days || 0}
1081 </p>
1082 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1083 {__("Recent pricing events")}
1084 </p>
1085 </div>
1086 <Clock className="w-8 h-8 text-green-600 dark:text-green-400" />
1087 </div>
1088 </CardContent>
1089 </Card>
1090 </div>
1091 )}
1092
1093 {/* Rule Performance */}
1094 <Card>
1095 <CardHeader>
1096 <CardTitle>{__("Rule Performance")}</CardTitle>
1097 <CardDescription>
1098 {__("How each pricing rule is performing")}
1099 </CardDescription>
1100 </CardHeader>
1101 <CardContent>
1102 {analyticsLoading ? (
1103 <div className="space-y-4">
1104 {[1, 2, 3].map((i) => (
1105 <div
1106 key={i}
1107 className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
1108 >
1109 <div className="flex-1 space-y-2">
1110 <div className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-48 animate-pulse"></div>
1111 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 animate-pulse"></div>
1112 </div>
1113 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-24 animate-pulse"></div>
1114 </div>
1115 ))}
1116 </div>
1117 ) : (
1118 <div className="space-y-4">
1119 {ruleImpact.length > 0 ? (
1120 ruleImpact.map((ri: any) => (
1121 <div
1122 key={ri.rule_id}
1123 className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
1124 >
1125 <div className="flex-1">
1126 <div className="flex items-center gap-3 mb-2">
1127 <h4 className="font-semibold text-gray-900 dark:text-white">
1128 {ri.name || `${__("Rule")} #${ri.rule_id}`}
1129 </h4>
1130 <Badge
1131 variant="default"
1132 className="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300"
1133 >
1134 {(ri.rule_type || "").replace("_", " ")}
1135 </Badge>
1136 <Badge variant="outline">
1137 {ri.adjustment_type || ""}
1138 </Badge>
1139 </div>
1140 <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
1141 <span>
1142 {__("Events")}: {ri.events || 0}
1143 </span>
1144 <span>•</span>
1145 <span>
1146 {__("Avg Adjustment")}:{" "}
1147 {typeof ri.avg_adjustment_value === "number"
1148 ? ri.avg_adjustment_value.toFixed(2)
1149 : Number(ri.avg_adjustment_value || 0).toFixed(
1150 2,
1151 )}
1152 </span>
1153 </div>
1154 </div>
1155 <div className="text-right">
1156 <p className="text-sm text-gray-500 dark:text-gray-400">
1157 {__("Impact (30d)")}
1158 </p>
1159 <p className="text-sm font-semibold text-gray-900 dark:text-white">
1160 {formatCurrencyAmount(
1161 Number(ri.total_adjustment_amount) || 0,
1162 )}
1163 </p>
1164 </div>
1165 </div>
1166 ))
1167 ) : (
1168 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1169 <p>{__("No pricing rules created yet")}</p>
1170 <p className="text-sm mt-2">
1171 {__(
1172 "Create your first rule to see performance metrics",
1173 )}
1174 </p>
1175 </div>
1176 )}
1177 </div>
1178 )}
1179 </CardContent>
1180 </Card>
1181
1182 {/* Revenue Trend Chart */}
1183 <Card>
1184 <CardHeader>
1185 <CardTitle>{__("Revenue Impact Trend")}</CardTitle>
1186 <CardDescription>
1187 {__(
1188 "30-day revenue comparison with and without dynamic pricing",
1189 )}
1190 </CardDescription>
1191 </CardHeader>
1192 <CardContent>
1193 {!analyticsLoading && statsErrorMessage ? (
1194 <div className="mb-4 text-sm text-red-600 dark:text-red-400">
1195 {__("Failed to load statistics:")} {statsErrorMessage}
1196 </div>
1197 ) : null}
1198
1199 {analyticsLoading ? (
1200 <div className="h-80 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1201 ) : trendData.length > 0 ? (
1202 <div className="space-y-4">
1203 <div className="w-full" style={{ height: 280 }}>
1204 <ResponsiveContainer width="100%" height="100%">
1205 <LineChart
1206 data={trendData}
1207 margin={{ top: 10, right: 20, left: 0, bottom: 0 }}
1208 >
1209 <CartesianGrid strokeDasharray="3 3" />
1210 <XAxis
1211 dataKey="day"
1212 tick={{ fontSize: 12 }}
1213 tickFormatter={(v: any) =>
1214 v ? new Date(String(v)).toLocaleDateString() : ""
1215 }
1216 />
1217 <YAxis
1218 tick={{ fontSize: 12 }}
1219 tickFormatter={(v: any) =>
1220 formatCurrencyAmount(Number(v) || 0)
1221 }
1222 />
1223 <Tooltip
1224 formatter={(value: any, name: any) => {
1225 if (name === "totalAdjustmentAmount") {
1226 return [
1227 formatCurrencyAmount(Number(value) || 0),
1228 __("Revenue Impact"),
1229 ];
1230 }
1231 if (name === "events") {
1232 return [Number(value) || 0, __("Events")];
1233 }
1234 if (name === "avgAdjustmentPercentage") {
1235 return [
1236 `${Number(value || 0).toFixed(2)}%`,
1237 __("Avg Adjustment"),
1238 ];
1239 }
1240 return [value, name];
1241 }}
1242 />
1243 <Line
1244 type="monotone"
1245 dataKey="totalAdjustmentAmount"
1246 stroke="#22c55e"
1247 strokeWidth={2}
1248 dot={{ r: 2 }}
1249 activeDot={{ r: 4 }}
1250 />
1251 </LineChart>
1252 </ResponsiveContainer>
1253 </div>
1254
1255 <div className="w-full overflow-x-auto">
1256 <div className="max-h-[420px] overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-700">
1257 <table className="w-full table-fixed border-collapse text-sm">
1258 <thead className="sticky top-0 bg-white dark:bg-gray-900">
1259 <tr className="text-left text-gray-600 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
1260 <th className="py-3 px-4 w-1/4">{__("Date")}</th>
1261 <th className="py-3 px-4 w-1/6 text-right">
1262 {__("Events")}
1263 </th>
1264 <th className="py-3 px-4 w-1/3 text-right">
1265 {__("Revenue Impact")}
1266 </th>
1267 <th className="py-3 px-4 w-1/4 text-right">
1268 {__("Avg Adjustment")}
1269 </th>
1270 </tr>
1271 </thead>
1272 <tbody>
1273 {trendData.map((row: any) => (
1274 <tr
1275 key={row.day}
1276 className="border-b border-gray-100 dark:border-gray-800"
1277 >
1278 <td className="py-3 px-4 text-gray-900 dark:text-white">
1279 {row.day
1280 ? new Date(
1281 String(row.day),
1282 ).toLocaleDateString()
1283 : ""}
1284 </td>
1285 <td className="py-3 px-4 text-right text-gray-900 dark:text-white">
1286 {row.events ?? 0}
1287 </td>
1288 <td className="py-3 px-4 text-right text-gray-900 dark:text-white tabular-nums">
1289 {formatCurrencyAmount(
1290 Number(row.totalAdjustmentAmount) || 0,
1291 )}
1292 </td>
1293 <td className="py-3 px-4 text-right text-gray-900 dark:text-white tabular-nums">{`${Number(row.avgAdjustmentPercentage || 0).toFixed(2)}%`}</td>
1294 </tr>
1295 ))}
1296 </tbody>
1297 </table>
1298 </div>
1299 </div>
1300 </div>
1301 ) : (
1302 <div className="h-40 flex items-center justify-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
1303 <div className="text-center">
1304 <p className="text-gray-500 dark:text-gray-400">
1305 {__("No pricing history in the last 30 days")}
1306 </p>
1307 </div>
1308 </div>
1309 )}
1310 </CardContent>
1311 </Card>
1312 </div>
1313 )}
1314
1315 {/* Settings Tab */}
1316 {activeTab === "settings" && (
1317 <div className="space-y-6">
1318 {/* General Settings */}
1319 <Card>
1320 <CardHeader>
1321 <CardTitle>{__("General Settings")}</CardTitle>
1322 <CardDescription>
1323 {__("Configure global dynamic pricing behavior")}
1324 </CardDescription>
1325 </CardHeader>
1326 <CardContent>
1327 {isLoading ? (
1328 <div className="space-y-6">
1329 {[1, 2, 3].map((i) => (
1330 <div key={i} className="space-y-2">
1331 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 animate-pulse"></div>
1332 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1333 </div>
1334 ))}
1335 </div>
1336 ) : (
1337 <div className="space-y-6">
1338 <div>
1339 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1340 {__("Rule Priority Mode")}
1341 </label>
1342 <select
1343 value={settings.rule_priority_mode}
1344 onChange={(e) =>
1345 handleSettingChange(
1346 "rule_priority_mode",
1347 e.target.value,
1348 )
1349 }
1350 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"
1351 >
1352 <option value="highest">
1353 {__("Apply Highest Priority Rule Only")}
1354 </option>
1355 <option value="cumulative">
1356 {__("Apply All Matching Rules (Cumulative)")}
1357 </option>
1358 <option value="best">
1359 {__("Apply Best Price for Customer")}
1360 </option>
1361 </select>
1362 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1363 {__(
1364 "How to handle multiple rules matching the same booking",
1365 )}
1366 </p>
1367 </div>
1368
1369 <div>
1370 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1371 {__("Maximum Price Increase (%)")}
1372 </label>
1373 <input
1374 type="number"
1375 value={settings.maximum_markup_percent}
1376 onChange={(e) =>
1377 handleSettingChange(
1378 "maximum_markup_percent",
1379 parseInt(e.target.value),
1380 )
1381 }
1382 min="0"
1383 max="100"
1384 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"
1385 />
1386 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1387 {__("Cap maximum price increase to protect customers")}
1388 </p>
1389 </div>
1390
1391 <div>
1392 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1393 {__("Maximum Price Decrease (%)")}
1394 </label>
1395 <input
1396 type="number"
1397 value={settings.maximum_discount_percent}
1398 onChange={(e) =>
1399 handleSettingChange(
1400 "maximum_discount_percent",
1401 parseInt(e.target.value),
1402 )
1403 }
1404 min="0"
1405 max="100"
1406 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"
1407 />
1408 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1409 {__("Cap maximum discount to maintain profitability")}
1410 </p>
1411 </div>
1412 </div>
1413 )}
1414 </CardContent>
1415 </Card>
1416
1417 {/* Demand Calculation Settings */}
1418 <Card>
1419 <CardHeader>
1420 <CardTitle>{__("Demand Calculation")}</CardTitle>
1421 <CardDescription>
1422 {__("Configure how booking demand is calculated")}
1423 </CardDescription>
1424 </CardHeader>
1425 <CardContent>
1426 {isLoading ? (
1427 <div className="space-y-6">
1428 {[1, 2].map((i) => (
1429 <div key={i} className="space-y-2">
1430 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 animate-pulse"></div>
1431 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1432 </div>
1433 ))}
1434 </div>
1435 ) : (
1436 <div className="space-y-6">
1437 <div>
1438 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1439 {__("Calculation Period (Days)")}
1440 </label>
1441 <input
1442 type="number"
1443 value={settings.calculation_period}
1444 onChange={(e) =>
1445 handleSettingChange(
1446 "calculation_period",
1447 parseInt(e.target.value),
1448 )
1449 }
1450 min="1"
1451 max="30"
1452 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"
1453 />
1454 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1455 {__("Number of days to analyze for demand trends")}
1456 </p>
1457 </div>
1458
1459 <div>
1460 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1461 {__("Update Frequency")}
1462 </label>
1463 <select
1464 value={settings.update_frequency}
1465 onChange={(e) =>
1466 handleSettingChange("update_frequency", e.target.value)
1467 }
1468 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"
1469 >
1470 <option value="hourly">{__("Every Hour")}</option>
1471 <option value="daily">{__("Once Daily")}</option>
1472 <option value="realtime">
1473 {__("Real-time (On Each Booking)")}
1474 </option>
1475 </select>
1476 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1477 {__("How often to recalculate demand scores")}
1478 </p>
1479 </div>
1480 </div>
1481 )}
1482 </CardContent>
1483 </Card>
1484
1485 {/* Display Settings */}
1486 <Card>
1487 <CardHeader>
1488 <CardTitle>{__("Display Settings")}</CardTitle>
1489 <CardDescription>
1490 {__("How dynamic pricing is shown to customers")}
1491 </CardDescription>
1492 </CardHeader>
1493 <CardContent>
1494 {isLoading ? (
1495 <div className="space-y-6">
1496 {[1, 2].map((i) => (
1497 <div key={i} className="space-y-2">
1498 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 animate-pulse"></div>
1499 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1500 </div>
1501 ))}
1502 </div>
1503 ) : (
1504 <div className="space-y-6">
1505 <div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1506 <div>
1507 <h4 className="font-semibold text-gray-900 dark:text-white">
1508 {__("Show Original Price")}
1509 </h4>
1510 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1511 {__(
1512 "Display crossed-out original price when discount applied",
1513 )}
1514 </p>
1515 </div>
1516 <Toggle
1517 checked={settings.show_original_price}
1518 onChange={(checked) =>
1519 handleSettingChange("show_original_price", checked)
1520 }
1521 />
1522 </div>
1523
1524 <div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1525 <div>
1526 <h4 className="font-semibold text-gray-900 dark:text-white">
1527 {__("Show Savings Badge")}
1528 </h4>
1529 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1530 {__('Display "Save X%" badge on discounted trips')}
1531 </p>
1532 </div>
1533 <Toggle
1534 checked={settings.show_savings_badge}
1535 onChange={(checked) =>
1536 handleSettingChange("show_savings_badge", checked)
1537 }
1538 />
1539 </div>
1540
1541 <div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1542 <div>
1543 <h4 className="font-semibold text-gray-900 dark:text-white">
1544 {__("Show Urgency Messages")}
1545 </h4>
1546 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
1547 {__(
1548 'Display messages like "Price increases soon" for demand-based rules',
1549 )}
1550 </p>
1551 </div>
1552 <Toggle
1553 checked={settings.show_urgency_messages}
1554 onChange={(checked) =>
1555 handleSettingChange("show_urgency_messages", checked)
1556 }
1557 />
1558 </div>
1559 </div>
1560 )}
1561 </CardContent>
1562 </Card>
1563
1564 {/* Save Button */}
1565 {!isLoading && (
1566 <div className="flex justify-end">
1567 <Button
1568 onClick={handleSaveSettings}
1569 disabled={isSaving}
1570 className="px-6"
1571 >
1572 {isSaving ? (
1573 <>
1574 <Save className="w-4 h-4 mr-2 animate-spin" />
1575 {__("Saving...")}
1576 </>
1577 ) : (
1578 <>
1579 <Save className="w-4 h-4 mr-2" />
1580 {__("Save Settings")}
1581 </>
1582 )}
1583 </Button>
1584 </div>
1585 )}
1586 </div>
1587 )}
1588
1589 {/* Rule Type Selection Modal */}
1590 <RuleTypeSelectionModal
1591 isOpen={showRuleTypeModal}
1592 onClose={() => setShowRuleTypeModal(false)}
1593 onSelectType={handleSelectRuleType}
1594 />
1595
1596 {/* Confirmation Dialog */}
1597 <ConfirmationDialog
1598 isOpen={confirmDialog.isOpen}
1599 onClose={() =>
1600 setConfirmDialog({
1601 isOpen: false,
1602 rule: null,
1603 title: "",
1604 message: "",
1605 onConfirm: () => {},
1606 })
1607 }
1608 onConfirm={() => confirmDialog.onConfirm?.()}
1609 title={confirmDialog.title || ""}
1610 message={confirmDialog.message || ""}
1611 />
1612 </div>
1613 );
1614 };
1615
1616 export default DynamicPricingPage;
1617