PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / DynamicPricingRuleForm.tsx

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

1,529 lines 62.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Dynamic Pricing Rule Form Page
3 *
4 * Create and edit dynamic pricing rules with sidebar layout
5 *
6 * @package Yatra
7 * @since 3.0.0
8 */
9
10 import React, { useState, useEffect } from "react";
11 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
12 import {
13 Card,
14 CardContent,
15 CardHeader,
16 CardTitle,
17 CardDescription,
18 } from "../components/ui/card";
19 import { Button } from "../components/ui/button";
20 import { Label } from "../components/ui/label";
21 import { Input } from "../components/ui/input";
22 import { Select } from "../components/ui/select";
23 import { Skeleton } from "../components/ui/skeleton";
24 import { DatePicker } from "../components/ui/date-picker";
25 import { PageHeader } from "../components/common/PageHeader";
26 import { RuleTypeSelectionModal } from "../components/modals/RuleTypeSelectionModal";
27 import { useToast } from "../components/ui/toast";
28 import { apiClient } from "../lib/api-client";
29 import { __ } from "../lib/i18n";
30 import {
31 ArrowLeft,
32 Save,
33 Loader2,
34 Calendar,
35 Clock,
36 TrendingUp,
37 Package,
38 Sun,
39 Target,
40 Check,
41 } from "lucide-react";
42
43 interface RuleFormData {
44 name: string;
45 rule_type: string;
46 adjustment_type: string;
47 adjustment_value: number;
48 min_days_before: number;
49 max_days_before: number;
50 min_inventory: number;
51 max_inventory: number;
52 start_date: string;
53 end_date: string;
54 applicable_trips: string;
55 trip_ids: number[];
56 status: string;
57 priority: number;
58 // Demand-based conditions
59 demand_threshold_high?: number;
60 demand_threshold_low?: number;
61 // Time-based conditions. `weekend_days` is the source of truth — the engine
62 // evaluates the rule only on those weekdays. The `apply_on_weekends` /
63 // `apply_on_weekdays` columns still exist in the DB schema for legacy reasons
64 // but the engine never reads them, so we don't surface them in the form to
65 // avoid the impression they do anything.
66 weekend_days?: string[];
67 }
68
69 const RULE_TYPES = [
70 {
71 id: "early_bird",
72 name: __("Early Bird Discount"),
73 description: __(
74 "Reward customers who book well in advance. Always behaves as a discount.",
75 ),
76 icon: Calendar,
77 color: "blue",
78 example: __(
79 "10% off when departure is at least 30 days away (set Min Days Before = 30).",
80 ),
81 },
82 {
83 id: "last_minute",
84 name: __("Last Minute Deals"),
85 description: __(
86 "Discount close to departure to fill remaining seats. Always behaves as a discount.",
87 ),
88 icon: Clock,
89 color: "orange",
90 example: __(
91 "15% off when departure is within 7 days (set Max Days Before = 7).",
92 ),
93 },
94 {
95 id: "demand",
96 name: __("Demand-Based Pricing"),
97 description: __(
98 "Adjusts price by booking velocity score (recomputed by cron / on each booking).",
99 ),
100 icon: TrendingUp,
101 color: "green",
102 example: __(
103 "Score above 70 → markup; below 30 → discount. Magnitude scales with how far the score is outside the band.",
104 ),
105 },
106 {
107 id: "inventory",
108 name: __("Inventory-Based"),
109 description: __(
110 "Reacts to seats remaining on the chosen departure (works with time slots too).",
111 ),
112 icon: Package,
113 color: "purple",
114 example: __(
115 "Seats ≤ 5 → +10% scarcity markup; seats ≥ 20 → 50% of magnitude as discount.",
116 ),
117 },
118 {
119 id: "seasonal",
120 name: __("Seasonal Pricing"),
121 description: __(
122 "Applies when the DEPARTURE date falls within the season window.",
123 ),
124 icon: Sun,
125 color: "yellow",
126 example: __(
127 "+25% on departures between Jul 1 – Aug 31. Use a negative value for off-peak discounts.",
128 ),
129 },
130 {
131 id: "time_based",
132 name: __("Time-Based (Weekday/Weekend)"),
133 description: __(
134 "Applies on selected weekdays of the DEPARTURE date, optionally limited to a date window.",
135 ),
136 icon: Target,
137 color: "indigo",
138 example: __(
139 "+15% premium on Saturday & Sunday departures. Add a date range to make it effective only for that period.",
140 ),
141 },
142 ];
143
144 const DynamicPricingRuleForm: React.FC = () => {
145 const { showToast } = useToast();
146 const queryClient = useQueryClient();
147 const urlParams = new URLSearchParams(window.location.search);
148 const ruleId = urlParams.get("id");
149 const ruleTypeFromUrl = urlParams.get("rule_type") || "";
150 const isEdit = !!ruleId;
151
152 const [formData, setFormData] = useState<RuleFormData>({
153 name: "",
154 rule_type: ruleTypeFromUrl,
155 adjustment_type: "percentage",
156 adjustment_value: 0,
157 min_days_before: 0,
158 max_days_before: 0,
159 min_inventory: 0,
160 max_inventory: 0,
161 start_date: "",
162 end_date: "",
163 applicable_trips: "all",
164 trip_ids: [],
165 status: "active",
166 priority: 1,
167 demand_threshold_high: 80,
168 demand_threshold_low: 30,
169 weekend_days: [],
170 });
171
172 const [showTripDropdown, setShowTripDropdown] = useState(false);
173 const [tripSearchQuery, setTripSearchQuery] = useState("");
174 const [debouncedTripSearch, setDebouncedTripSearch] = useState("");
175 const [showRuleTypeModal, setShowRuleTypeModal] = useState(false);
176
177 // Rule simulation state — drives the "When will this apply?" panel.
178 // We keep a small base price input so admins can see absolute dollar amounts
179 // for the discount/markup rather than just percentages.
180 const [previewBasePrice, setPreviewBasePrice] = useState<number>(100);
181 const [previewLoading, setPreviewLoading] = useState(false);
182 const [previewError, setPreviewError] = useState<string | null>(null);
183 const [previewResult, setPreviewResult] = useState<{
184 rule_type: string;
185 base_price: number;
186 current_date: string;
187 scenarios: Array<{
188 label: string;
189 departure_date: string | null;
190 spots_remaining: number | null;
191 applies: boolean;
192 base_price: number;
193 final_price: number;
194 adjustment: number;
195 adjustment_percent: number;
196 }>;
197 } | null>(null);
198
199 const handleSelectRuleType = (ruleType: string) => {
200 handleChange("rule_type", ruleType);
201 setShowRuleTypeModal(false);
202 // A new rule type changes which scenarios are meaningful — drop the old
203 // preview so stale results don't mislead the admin.
204 setPreviewResult(null);
205 setPreviewError(null);
206 };
207
208 const handleRunSimulation = async () => {
209 if (!formData.rule_type) {
210 setPreviewError(__("Pick a rule type first."));
211 return;
212 }
213 setPreviewLoading(true);
214 setPreviewError(null);
215 try {
216 const response = await apiClient.post("/dynamic-pricing/simulate-rule", {
217 rule_type: formData.rule_type,
218 adjustment_type: formData.adjustment_type,
219 adjustment_value: formData.adjustment_value,
220 min_days_before: formData.min_days_before,
221 max_days_before: formData.max_days_before,
222 min_inventory: formData.min_inventory,
223 max_inventory: formData.max_inventory,
224 start_date: formData.start_date || null,
225 end_date: formData.end_date || null,
226 weekend_days: formData.weekend_days || [],
227 demand_threshold_low: formData.demand_threshold_low,
228 demand_threshold_high: formData.demand_threshold_high,
229 base_price: previewBasePrice > 0 ? previewBasePrice : 100,
230 });
231 // Controller returns { success: true, data: {...scenarios...} } — checking
232 // .success on .data would always be undefined and throw a false negative.
233 const envelope = response as any;
234 if (envelope?.success === false) {
235 throw new Error(envelope?.message || __("Simulation failed."));
236 }
237 const result = envelope?.data ?? envelope;
238 if (!result || !Array.isArray(result.scenarios)) {
239 throw new Error(__("Simulation returned an unexpected response."));
240 }
241 setPreviewResult(result);
242 } catch (e: any) {
243 setPreviewError(e?.message || __("Simulation failed."));
244 } finally {
245 setPreviewLoading(false);
246 }
247 };
248
249 // Debounce trip search
250 useEffect(() => {
251 const timer = setTimeout(() => {
252 setDebouncedTripSearch(tripSearchQuery);
253 }, 300);
254 return () => clearTimeout(timer);
255 }, [tripSearchQuery]);
256
257 // Fetch trips for selection
258 const { data: tripsData } = useQuery({
259 queryKey: ["trips-list", debouncedTripSearch],
260 queryFn: async () => {
261 const params = new URLSearchParams();
262 if (debouncedTripSearch) {
263 params.append("search", debouncedTripSearch);
264 }
265 params.append("per_page", "50");
266 const response = await apiClient.get(`/trips?${params.toString()}`);
267 return response.data || [];
268 },
269 });
270
271 const tripOptions = (tripsData || []).map((trip: any) => ({
272 value: trip.id,
273 label: trip.title || `Trip #${trip.id}`,
274 }));
275
276 // Fetch existing rule if editing
277 const {
278 data: ruleData,
279 isLoading,
280 isError,
281 error,
282 } = useQuery({
283 queryKey: ["dynamic-pricing-rule", ruleId],
284 queryFn: async () => {
285 if (!ruleId) return null;
286 const response = await apiClient.get(`/dynamic-pricing/rules/${ruleId}`);
287
288 return response;
289 },
290 enabled: isEdit,
291 retry: 1,
292 });
293
294 // Log loading state
295
296 // Populate form with existing data
297 useEffect(() => {
298 if (ruleData?.data) {
299 const ruleDetails = ruleData.data;
300
301 setFormData({
302 name: ruleDetails.name || "",
303 rule_type: ruleDetails.rule_type || "",
304 adjustment_type: ruleDetails.adjustment_type || "percentage",
305 adjustment_value: Number(ruleDetails.adjustment_value) || 0,
306 min_days_before: Number(ruleDetails.min_days_before) || 0,
307 max_days_before: Number(ruleDetails.max_days_before) || 0,
308 min_inventory: Number(ruleDetails.min_inventory) || 0,
309 max_inventory: Number(ruleDetails.max_inventory) || 0,
310 start_date: ruleDetails.start_date || "",
311 end_date: ruleDetails.end_date || "",
312 applicable_trips: ruleDetails.applicable_trips || "all",
313 trip_ids: Array.isArray(ruleDetails.trip_ids)
314 ? ruleDetails.trip_ids
315 : ruleDetails.trip_ids
316 ? JSON.parse(ruleDetails.trip_ids)
317 : [],
318 status: ruleDetails.status || "active",
319 priority: Number(ruleDetails.priority) || 1,
320 demand_threshold_high: ruleDetails.demand_threshold_high
321 ? Number(ruleDetails.demand_threshold_high)
322 : 80,
323 demand_threshold_low: ruleDetails.demand_threshold_low
324 ? Number(ruleDetails.demand_threshold_low)
325 : 30,
326 weekend_days: Array.isArray(ruleDetails.weekend_days)
327 ? ruleDetails.weekend_days
328 : ruleDetails.weekend_days
329 ? JSON.parse(ruleDetails.weekend_days)
330 : [],
331 });
332 }
333 }, [ruleData]);
334
335 // Create/Update mutation
336 const saveMutation = useMutation({
337 mutationFn: async (data: RuleFormData) => {
338 if (isEdit) {
339 return await apiClient.put(`/dynamic-pricing/rules/${ruleId}`, data);
340 } else {
341 return await apiClient.post("/dynamic-pricing/rules", data);
342 }
343 },
344 onSuccess: (response) => {
345 showToast(
346 isEdit
347 ? __("Pricing rule updated successfully")
348 : __("Pricing rule created successfully"),
349 "success",
350 );
351 queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"] });
352
353 if (!isEdit && response?.data?.id) {
354 // For new rules, redirect to edit page
355 const baseUrl = window.location.href.split("&action=")[0];
356 const editUrl = `${baseUrl}&action=edit-pricing-rule&id=${response.data.id}`;
357 window.location.href = editUrl;
358 }
359 // For updates, stay on the same page (no redirect)
360 },
361 onError: (error: any) => {
362 showToast(error?.message || __("Failed to save pricing rule"), "error");
363 },
364 });
365
366 const handleSubmit = (e: React.FormEvent) => {
367 e.preventDefault();
368 saveMutation.mutate(formData);
369 };
370
371 const handleCancel = () => {
372 window.location.href = window.location.href.split("&action=")[0];
373 };
374
375 const handleChange = (field: keyof RuleFormData, value: any) => {
376 setFormData((prev) => ({ ...prev, [field]: value }));
377 };
378
379 // Show error state if API call fails
380 if (isError) {
381 return (
382 <div className="space-y-6">
383 <PageHeader
384 title={__("Edit Pricing Rule")}
385 description={__(
386 "Configure dynamic pricing rules to automatically adjust trip prices",
387 )}
388 />
389 <Card>
390 <CardContent className="pt-6">
391 <div className="text-center py-12">
392 <p className="text-red-600 dark:text-red-400 mb-4">
393 {__("Failed to load pricing rule")}
394 </p>
395 <p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
396 {error?.message ||
397 __("An error occurred while fetching the rule data")}
398 </p>
399 <Button
400 onClick={() =>
401 (window.location.href =
402 window.location.href.split("&action=")[0])
403 }
404 >
405 {__("Back to Rules")}
406 </Button>
407 </div>
408 </CardContent>
409 </Card>
410 </div>
411 );
412 }
413
414 if (isLoading) {
415 return (
416 <div className="space-y-6">
417 <PageHeader
418 title={__("Edit Pricing Rule")}
419 description={__(
420 "Configure dynamic pricing rules to automatically adjust trip prices",
421 )}
422 />
423
424 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
425 {/* Main Content Skeleton - Left Side (2/3) */}
426 <div className="lg:col-span-2 space-y-6">
427 {/* Rule Type Badge Skeleton */}
428 <Card>
429 <CardContent className="pt-6">
430 <div className="flex items-center gap-3">
431 <Skeleton className="w-5 h-5 rounded" />
432 <div className="flex-1">
433 <Skeleton className="h-3 w-16 mb-2" />
434 <Skeleton className="h-5 w-48 mb-1" />
435 <Skeleton className="h-3 w-64" />
436 </div>
437 </div>
438 </CardContent>
439 </Card>
440
441 {/* Rule Details Skeleton */}
442 <Card>
443 <CardHeader>
444 <Skeleton className="h-6 w-32 mb-2" />
445 <Skeleton className="h-4 w-64" />
446 </CardHeader>
447 <CardContent className="space-y-4">
448 <div>
449 <Skeleton className="h-4 w-24 mb-2" />
450 <Skeleton className="h-10 w-full" />
451 </div>
452 </CardContent>
453 </Card>
454
455 {/* Price Adjustment Skeleton */}
456 <Card>
457 <CardHeader>
458 <Skeleton className="h-6 w-40 mb-2" />
459 <Skeleton className="h-4 w-48" />
460 </CardHeader>
461 <CardContent className="space-y-4">
462 <div className="grid grid-cols-2 gap-4">
463 <div>
464 <Skeleton className="h-4 w-32 mb-2" />
465 <Skeleton className="h-10 w-full" />
466 </div>
467 <div>
468 <Skeleton className="h-4 w-24 mb-2" />
469 <Skeleton className="h-10 w-full" />
470 </div>
471 </div>
472 </CardContent>
473 </Card>
474
475 {/* Conditions Skeleton */}
476 <Card>
477 <CardHeader>
478 <Skeleton className="h-6 w-32 mb-2" />
479 <Skeleton className="h-4 w-56" />
480 </CardHeader>
481 <CardContent className="space-y-4">
482 <div className="grid grid-cols-2 gap-4">
483 <div>
484 <Skeleton className="h-4 w-32 mb-2" />
485 <Skeleton className="h-10 w-full" />
486 </div>
487 <div>
488 <Skeleton className="h-4 w-32 mb-2" />
489 <Skeleton className="h-10 w-full" />
490 </div>
491 </div>
492 </CardContent>
493 </Card>
494
495 {/* Applicable Trips Skeleton */}
496 <Card>
497 <CardHeader>
498 <Skeleton className="h-6 w-40 mb-2" />
499 <Skeleton className="h-4 w-64" />
500 </CardHeader>
501 <CardContent className="space-y-4">
502 <div>
503 <Skeleton className="h-4 w-32 mb-2" />
504 <Skeleton className="h-10 w-full" />
505 </div>
506 </CardContent>
507 </Card>
508 </div>
509
510 {/* Sidebar Skeleton - Right Side (1/3) */}
511 <div className="space-y-6">
512 {/* Status & Priority Skeleton */}
513 <Card>
514 <CardHeader>
515 <Skeleton className="h-6 w-32 mb-2" />
516 </CardHeader>
517 <CardContent className="space-y-4">
518 <div>
519 <Skeleton className="h-4 w-16 mb-2" />
520 <Skeleton className="h-10 w-full" />
521 </div>
522 <div>
523 <Skeleton className="h-4 w-16 mb-2" />
524 <Skeleton className="h-10 w-full" />
525 </div>
526 </CardContent>
527 </Card>
528
529 {/* Actions Skeleton */}
530 <Card>
531 <CardContent className="pt-6 space-y-3">
532 <Skeleton className="h-10 w-full" />
533 <Skeleton className="h-10 w-full" />
534 </CardContent>
535 </Card>
536 </div>
537 </div>
538 </div>
539 );
540 }
541
542 const selectedRuleType = RULE_TYPES.find(
543 (rt) => rt.id === formData.rule_type,
544 );
545
546 return (
547 <div className="space-y-6">
548 <PageHeader
549 title={isEdit ? __("Edit Pricing Rule") : __("Create Pricing Rule")}
550 description={__(
551 "Configure dynamic pricing rules to automatically adjust trip prices",
552 )}
553 />
554
555 <form onSubmit={handleSubmit}>
556 {/* Two Column Layout: Main Content + Sidebar */}
557 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
558 {/* Main Content - Left Side (2/3) */}
559 <div className="lg:col-span-2 space-y-6">
560 {/* Step 1: Select Rule Type (Card-Based Selection) */}
561 {!isEdit && !formData.rule_type && (
562 <Card>
563 <CardHeader>
564 <CardTitle>{__("Step 1: Select Rule Type")}</CardTitle>
565 <CardDescription>
566 {__(
567 "Choose the type of dynamic pricing rule you want to create",
568 )}
569 </CardDescription>
570 </CardHeader>
571 <CardContent>
572 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
573 {RULE_TYPES.map((ruleType) => {
574 const Icon = ruleType.icon;
575 const colorClasses = {
576 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",
577 orange:
578 "border-orange-200 dark:border-orange-800 hover:border-orange-400 dark:hover:border-orange-600 bg-orange-50 dark:bg-orange-900/20",
579 green:
580 "border-green-200 dark:border-green-800 hover:border-green-400 dark:hover:border-green-600 bg-green-50 dark:bg-green-900/20",
581 purple:
582 "border-purple-200 dark:border-purple-800 hover:border-purple-400 dark:hover:border-purple-600 bg-purple-50 dark:bg-purple-900/20",
583 yellow:
584 "border-yellow-200 dark:border-yellow-800 hover:border-yellow-400 dark:hover:border-yellow-600 bg-yellow-50 dark:bg-yellow-900/20",
585 indigo:
586 "border-indigo-200 dark:border-indigo-800 hover:border-indigo-400 dark:hover:border-indigo-600 bg-indigo-50 dark:bg-indigo-900/20",
587 };
588 return (
589 <button
590 key={ruleType.id}
591 type="button"
592 onClick={() => handleChange("rule_type", ruleType.id)}
593 className={`p-4 border-2 rounded-lg text-left transition-all cursor-pointer ${colorClasses[ruleType.color as keyof typeof colorClasses]}`}
594 >
595 <div className="flex items-start gap-3">
596 <div className="flex-shrink-0">
597 <Icon className="w-6 h-6 text-gray-700 dark:text-gray-300" />
598 </div>
599 <div className="flex-1 min-w-0">
600 <h3 className="font-semibold text-gray-900 dark:text-white mb-1">
601 {ruleType.name}
602 </h3>
603 <p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
604 {ruleType.description}
605 </p>
606 <p className="text-xs text-gray-500 dark:text-gray-500 italic">
607 {__("Example:")} {ruleType.example}
608 </p>
609 </div>
610 </div>
611 </button>
612 );
613 })}
614 </div>
615 </CardContent>
616 </Card>
617 )}
618
619 {/* Show selected rule type badge if editing or type selected */}
620 {(isEdit || formData.rule_type) && selectedRuleType && (
621 <Card>
622 <CardContent className="pt-6">
623 <div className="flex items-center justify-between">
624 <div className="flex items-center gap-3">
625 {React.createElement(selectedRuleType.icon, {
626 className: "w-5 h-5 text-gray-600 dark:text-gray-400",
627 })}
628 <div>
629 <p className="text-sm text-gray-500 dark:text-gray-400">
630 {__("Rule Type")}
631 </p>
632 <p className="font-semibold text-gray-900 dark:text-white">
633 {selectedRuleType.name}
634 </p>
635 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
636 {selectedRuleType.description}
637 </p>
638 </div>
639 </div>
640 <Button
641 type="button"
642 variant="outline"
643 size="sm"
644 onClick={() => setShowRuleTypeModal(true)}
645 >
646 {__("Change")}
647 </Button>
648 </div>
649 </CardContent>
650 </Card>
651 )}
652
653 {/* Step 2: Basic Information (Only show after rule type selected) */}
654 {formData.rule_type && (
655 <>
656 <Card>
657 <CardHeader>
658 <CardTitle>{__("Rule Details")}</CardTitle>
659 <CardDescription>
660 {__(
661 "Internal name for this pricing rule (visible to admins only).",
662 )}
663 </CardDescription>
664 </CardHeader>
665 <CardContent className="space-y-4">
666 <div>
667 <Label htmlFor="name">{__("Rule Name")} *</Label>
668 <Input
669 id="name"
670 value={formData.name}
671 onChange={(e) => handleChange("name", e.target.value)}
672 placeholder={__("e.g., Summer Early Bird Discount")}
673 required
674 />
675 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
676 {__(
677 "Used in admin lists, analytics, and price-history exports. Customers never see this name.",
678 )}
679 </p>
680 </div>
681 </CardContent>
682 </Card>
683
684 <Card>
685 <CardHeader>
686 <CardTitle>{__("Price Adjustment")}</CardTitle>
687 <CardDescription>
688 {__("How much to adjust the price")}
689 </CardDescription>
690 </CardHeader>
691 <CardContent className="space-y-4">
692 <div>
693 <Label htmlFor="adjustment_type">
694 {__("Adjustment Type")} *
695 </Label>
696 <Select
697 id="adjustment_type"
698 value={formData.adjustment_type}
699 onChange={(e) =>
700 handleChange("adjustment_type", e.target.value)
701 }
702 required
703 >
704 <option value="percentage">{__("Percentage")}</option>
705 <option value="fixed">{__("Fixed Amount")}</option>
706 </Select>
707 </div>
708
709 <div>
710 <Label htmlFor="adjustment_value">
711 {__("Adjustment Value")} *
712 </Label>
713 <Input
714 id="adjustment_value"
715 type="number"
716 step="0.01"
717 value={formData.adjustment_value}
718 onChange={(e) =>
719 handleChange(
720 "adjustment_value",
721 parseFloat(e.target.value),
722 )
723 }
724 placeholder={
725 formData.adjustment_type === "percentage"
726 ? "10"
727 : "100"
728 }
729 required
730 />
731 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
732 {(() => {
733 const rt = formData.rule_type;
734 // Early bird & last minute always behave as discounts:
735 // enter a positive magnitude.
736 if (rt === "early_bird" || rt === "last_minute") {
737 return formData.adjustment_type === "percentage"
738 ? __(
739 "Enter the discount magnitude as a positive number (e.g. 10 = 10% off). The rule always applies as a discount.",
740 )
741 : __(
742 "Enter the discount magnitude as a positive number (e.g. 100 = 100 off). The rule always applies as a discount.",
743 );
744 }
745 // Inventory & demand auto-derive direction from
746 // capacity / demand score; admins enter only the
747 // magnitude.
748 if (rt === "inventory" || rt === "demand") {
749 return formData.adjustment_type === "percentage"
750 ? __(
751 "Enter the magnitude (positive). Direction is derived automatically: scarce inventory / high demand → markup, plenty / low demand → discount.",
752 )
753 : __(
754 "Enter the magnitude (positive). Direction is derived automatically: scarce inventory / high demand → markup, plenty / low demand → discount.",
755 );
756 }
757 // Seasonal & time-based use the sign as entered.
758 return formData.adjustment_type === "percentage"
759 ? __(
760 "Use a positive value to increase price (e.g. 10 = +10%) and a negative value to discount (e.g. -10 = 10% off).",
761 )
762 : __(
763 "Use a positive value to increase price (e.g. 100 = +100) and a negative value to discount (e.g. -100 = 100 off).",
764 );
765 })()}
766 </p>
767 </div>
768 </CardContent>
769 </Card>
770
771 <Card>
772 <CardHeader>
773 <CardTitle>{__("Conditions")}</CardTitle>
774 <CardDescription>
775 {__("When this rule should apply")}
776 </CardDescription>
777 </CardHeader>
778 <CardContent className="space-y-4">
779 {/* Early Bird & Last Minute - Days before departure */}
780 {(formData.rule_type === "early_bird" ||
781 formData.rule_type === "last_minute") && (
782 <>
783 <div>
784 <Label htmlFor="min_days_before">
785 {__("Minimum Days Before Departure")}
786 </Label>
787 <Input
788 id="min_days_before"
789 type="number"
790 value={formData.min_days_before}
791 onChange={(e) =>
792 handleChange(
793 "min_days_before",
794 parseInt(e.target.value),
795 )
796 }
797 min="0"
798 />
799 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
800 {__(
801 "Apply only when the selected departure date is at least this many days away from today. Example: 30 means 30+ days before departure.",
802 )}
803 </p>
804 </div>
805
806 <div>
807 <Label htmlFor="max_days_before">
808 {__("Maximum Days Before Departure")}
809 </Label>
810 <Input
811 id="max_days_before"
812 type="number"
813 value={formData.max_days_before}
814 onChange={(e) =>
815 handleChange(
816 "max_days_before",
817 parseInt(e.target.value),
818 )
819 }
820 min="0"
821 />
822 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
823 {__(
824 "Apply only when the selected departure date is within this many days from today. Example: 7 means 0–7 days before departure.",
825 )}
826 </p>
827 </div>
828
829 <div className="p-3 bg-gray-50 dark:bg-gray-900/40 border border-gray-200 dark:border-gray-800 rounded-lg">
830 <p className="text-xs text-gray-700 dark:text-gray-300">
831 {__(
832 "If you set both Minimum and Maximum, the rule applies only inside that window (Minimum ≤ days before departure ≤ Maximum).",
833 )}
834 </p>
835 </div>
836
837 <div>
838 <Label htmlFor="start_date">
839 {__("Start Date (Optional)")}
840 </Label>
841 <DatePicker
842 value={formData.start_date}
843 onChange={(value) =>
844 handleChange("start_date", value)
845 }
846 placeholder={__("Select start date")}
847 />
848 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
849 {__(
850 "Optional lifetime limit. If set, this rule will run only on or after this date. Leave empty for no restriction.",
851 )}
852 </p>
853 </div>
854
855 <div>
856 <Label htmlFor="end_date">
857 {__("End Date (Optional)")}
858 </Label>
859 <DatePicker
860 value={formData.end_date}
861 onChange={(value) =>
862 handleChange("end_date", value)
863 }
864 placeholder={__("Select end date")}
865 />
866 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
867 {__(
868 "Optional lifetime limit. If set, this rule will run only on or before this date. Leave empty for no restriction.",
869 )}
870 </p>
871 </div>
872 </>
873 )}
874
875 {formData.rule_type === "inventory" && (
876 <>
877 <div>
878 <Label htmlFor="min_inventory">
879 {__("Low-inventory threshold (seats)")}
880 </Label>
881 <Input
882 id="min_inventory"
883 type="number"
884 value={formData.min_inventory}
885 onChange={(e) =>
886 handleChange(
887 "min_inventory",
888 parseInt(e.target.value),
889 )
890 }
891 min="0"
892 />
893 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
894 {__(
895 "When seats remaining ≤ this number, the rule applies a markup (scarcity).",
896 )}
897 </p>
898 </div>
899
900 <div>
901 <Label htmlFor="max_inventory">
902 {__("High-inventory threshold (seats)")}
903 </Label>
904 <Input
905 id="max_inventory"
906 type="number"
907 value={formData.max_inventory}
908 onChange={(e) =>
909 handleChange(
910 "max_inventory",
911 parseInt(e.target.value),
912 )
913 }
914 min="0"
915 />
916 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
917 {__(
918 "When seats remaining ≥ this number, the rule applies a partial discount (50% of the magnitude).",
919 )}
920 </p>
921 </div>
922 </>
923 )}
924
925 {/* Seasonal - Departure window */}
926 {formData.rule_type === "seasonal" && (
927 <>
928 <div>
929 <Label htmlFor="start_date">
930 {__("Season Start (Departure Date)")}
931 </Label>
932 <DatePicker
933 value={formData.start_date}
934 onChange={(value) =>
935 handleChange("start_date", value)
936 }
937 placeholder={__("Select start date")}
938 />
939 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
940 {__(
941 "Earliest departure date this seasonal rule applies to.",
942 )}
943 </p>
944 </div>
945
946 <div>
947 <Label htmlFor="end_date">
948 {__("Season End (Departure Date)")}
949 </Label>
950 <DatePicker
951 value={formData.end_date}
952 onChange={(value) =>
953 handleChange("end_date", value)
954 }
955 placeholder={__("Select end date")}
956 />
957 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
958 {__(
959 "Latest departure date this seasonal rule applies to. The rule matches whenever the booking's departure date falls between these two dates (inclusive).",
960 )}
961 </p>
962 </div>
963
964 <div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
965 <p className="text-xs text-yellow-700 dark:text-yellow-300">
966 {__(
967 "💡 Tip: Use a positive Adjustment Value for peak-season markup (e.g. 25% during summer) or a negative value for an off-peak discount.",
968 )}
969 </p>
970 </div>
971 </>
972 )}
973
974 {/* Demand-Based - Booking velocity thresholds */}
975 {formData.rule_type === "demand" && (
976 <>
977 <div>
978 <Label htmlFor="demand_threshold_high">
979 {__("High Demand Threshold (%)")}
980 </Label>
981 <Input
982 id="demand_threshold_high"
983 type="number"
984 value={formData.demand_threshold_high}
985 onChange={(e) =>
986 handleChange(
987 "demand_threshold_high",
988 parseInt(e.target.value),
989 )
990 }
991 min="0"
992 max="100"
993 />
994 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
995 {__(
996 "Apply pricing when booking velocity exceeds this percentage (e.g., 80 = high demand)",
997 )}
998 </p>
999 </div>
1000
1001 <div>
1002 <Label htmlFor="demand_threshold_low">
1003 {__("Low Demand Threshold (%)")}
1004 </Label>
1005 <Input
1006 id="demand_threshold_low"
1007 type="number"
1008 value={formData.demand_threshold_low}
1009 onChange={(e) =>
1010 handleChange(
1011 "demand_threshold_low",
1012 parseInt(e.target.value),
1013 )
1014 }
1015 min="0"
1016 max="100"
1017 />
1018 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1019 {__(
1020 "Apply pricing when booking velocity falls below this percentage (e.g., 30 = low demand)",
1021 )}
1022 </p>
1023 </div>
1024
1025 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
1026 <p className="text-xs text-blue-700 dark:text-blue-300">
1027 {__(
1028 "💡 Demand scores are calculated automatically via cron job based on booking velocity.",
1029 )}
1030 </p>
1031 </div>
1032 </>
1033 )}
1034
1035 {/* Time-Based - Day of week selection */}
1036 {formData.rule_type === "time_based" && (
1037 <>
1038 <div>
1039 <Label>{__("Apply Pricing On")}</Label>
1040 <div className="grid grid-cols-2 gap-2 mt-2">
1041 {[
1042 "monday",
1043 "tuesday",
1044 "wednesday",
1045 "thursday",
1046 "friday",
1047 "saturday",
1048 "sunday",
1049 ].map((day) => (
1050 <div
1051 key={day}
1052 className="flex items-center gap-2"
1053 >
1054 <input
1055 type="checkbox"
1056 id={`day_${day}`}
1057 checked={formData.weekend_days?.includes(day)}
1058 onChange={(e) => {
1059 const current = formData.weekend_days || [];
1060 if (e.target.checked) {
1061 handleChange("weekend_days", [
1062 ...current,
1063 day,
1064 ]);
1065 } else {
1066 handleChange(
1067 "weekend_days",
1068 current.filter((d) => d !== day),
1069 );
1070 }
1071 }}
1072 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1073 />
1074 <Label
1075 htmlFor={`day_${day}`}
1076 className="mb-0 capitalize"
1077 >
1078 {__(day)}
1079 </Label>
1080 </div>
1081 ))}
1082 </div>
1083 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
1084 {__(
1085 "Selected days are matched against the booking's DEPARTURE date — e.g. tick Saturday + Sunday to apply a weekend premium to Saturday/Sunday departures, regardless of when the customer books.",
1086 )}
1087 </p>
1088 </div>
1089
1090 <div>
1091 <Label htmlFor="start_date">
1092 {__("Start Date (Optional)")}
1093 </Label>
1094 <DatePicker
1095 value={formData.start_date}
1096 onChange={(value) =>
1097 handleChange("start_date", value)
1098 }
1099 placeholder={__("Select start date")}
1100 />
1101 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1102 {__(
1103 "Limits the rule to departures on or after this date. Leave empty for no restriction.",
1104 )}
1105 </p>
1106 </div>
1107
1108 <div>
1109 <Label htmlFor="end_date">
1110 {__("End Date (Optional)")}
1111 </Label>
1112 <DatePicker
1113 value={formData.end_date}
1114 onChange={(value) =>
1115 handleChange("end_date", value)
1116 }
1117 placeholder={__("Select end date")}
1118 />
1119 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1120 {__(
1121 "Limits the rule to departures on or before this date. Leave empty for no restriction.",
1122 )}
1123 </p>
1124 </div>
1125
1126 <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg">
1127 <p className="text-xs text-indigo-700 dark:text-indigo-300">
1128 {__(
1129 "💡 Sign matters: a positive Adjustment Value adds a premium on the selected days; a negative value gives a discount.",
1130 )}
1131 </p>
1132 </div>
1133 </>
1134 )}
1135 </CardContent>
1136 </Card>
1137 </>
1138 )}
1139 </div>
1140
1141 {/* Sidebar - Right Side (1/3) */}
1142 {formData.rule_type && (
1143 <div className="space-y-4">
1144 {/* Status */}
1145 <Card>
1146 <CardHeader className="pb-3">
1147 <CardTitle className="text-base">{__("Status")}</CardTitle>
1148 </CardHeader>
1149 <CardContent className="space-y-3">
1150 <Select
1151 value={formData.status}
1152 onChange={(e) => handleChange("status", e.target.value)}
1153 >
1154 <option value="active">{__("Active")}</option>
1155 <option value="inactive">{__("Inactive")}</option>
1156 </Select>
1157 <p className="text-xs text-gray-500 dark:text-gray-400">
1158 {__("Only active rules will be applied to trips.")}
1159 </p>
1160 </CardContent>
1161 </Card>
1162
1163 {/* Priority */}
1164 <Card>
1165 <CardHeader className="pb-3">
1166 <CardTitle className="text-base">{__("Priority")}</CardTitle>
1167 </CardHeader>
1168 <CardContent className="space-y-3">
1169 <Input
1170 type="number"
1171 value={formData.priority}
1172 onChange={(e) =>
1173 handleChange("priority", parseInt(e.target.value))
1174 }
1175 min="1"
1176 max="100"
1177 />
1178 <p className="text-xs text-gray-500 dark:text-gray-400">
1179 {__(
1180 "A higher number = higher priority (1–100). When more than one rule matches, the global Rule Priority Mode in Settings decides whether the highest-priority rule wins, every rule stacks, or the customer gets the best price.",
1181 )}
1182 </p>
1183 </CardContent>
1184 </Card>
1185
1186 {/* Applicable Trips */}
1187 <Card>
1188 <CardHeader className="pb-3">
1189 <CardTitle className="text-base">
1190 {__("Applicable To")}
1191 </CardTitle>
1192 </CardHeader>
1193 <CardContent className="space-y-3">
1194 <Select
1195 value={formData.applicable_trips}
1196 onChange={(e) =>
1197 handleChange("applicable_trips", e.target.value)
1198 }
1199 >
1200 <option value="all">{__("All Trips")}</option>
1201 <option value="specific">{__("Specific Trips")}</option>
1202 </Select>
1203
1204 {formData.applicable_trips === "specific" && (
1205 <div className="mt-3 space-y-2 relative">
1206 <div
1207 className="border border-gray-300 dark:border-gray-600 rounded-lg p-2 cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 bg-white dark:bg-gray-800"
1208 onClick={() => setShowTripDropdown(!showTripDropdown)}
1209 >
1210 {formData.trip_ids.length > 0 ? (
1211 <div className="flex flex-wrap gap-1.5">
1212 {formData.trip_ids.slice(0, 2).map((tripId) => {
1213 const trip = tripOptions.find(
1214 (t: any) => t.value === tripId,
1215 );
1216 return (
1217 <span
1218 key={tripId}
1219 className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded"
1220 >
1221 {trip?.label || `Trip #${tripId}`}{" "}
1222 <span className="text-blue-600 dark:text-blue-400">
1223 #{tripId}
1224 </span>
1225 <button
1226 type="button"
1227 onClick={(e) => {
1228 e.stopPropagation();
1229 handleChange(
1230 "trip_ids",
1231 formData.trip_ids.filter(
1232 (id) => id !== tripId,
1233 ),
1234 );
1235 }}
1236 className="hover:text-blue-600 dark:hover:text-blue-200"
1237 >
1238 ×
1239 </button>
1240 </span>
1241 );
1242 })}
1243 {formData.trip_ids.length > 2 && (
1244 <span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded">
1245 +{formData.trip_ids.length - 2} {__("more")}
1246 </span>
1247 )}
1248 </div>
1249 ) : (
1250 <span className="text-sm text-gray-500 dark:text-gray-400">
1251 {__("Click to select trips...")}
1252 </span>
1253 )}
1254 </div>
1255
1256 {showTripDropdown && (
1257 <div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-64 overflow-hidden">
1258 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
1259 <Input
1260 type="text"
1261 placeholder={__("Search trips...")}
1262 value={tripSearchQuery}
1263 onChange={(e) =>
1264 setTripSearchQuery(e.target.value)
1265 }
1266 className="w-full text-sm"
1267 onClick={(e) => e.stopPropagation()}
1268 />
1269 </div>
1270 <div className="max-h-48 overflow-y-auto">
1271 {tripOptions.map((trip: any) => {
1272 const isSelected = formData.trip_ids.includes(
1273 trip.value,
1274 );
1275 return (
1276 <div
1277 key={trip.value}
1278 className="px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center gap-2"
1279 onClick={(e) => {
1280 e.stopPropagation();
1281 if (isSelected) {
1282 handleChange(
1283 "trip_ids",
1284 formData.trip_ids.filter(
1285 (id) => id !== trip.value,
1286 ),
1287 );
1288 } else {
1289 handleChange("trip_ids", [
1290 ...formData.trip_ids,
1291 trip.value,
1292 ]);
1293 }
1294 }}
1295 >
1296 <div
1297 className={`w-4 h-4 border-2 rounded flex items-center justify-center ${
1298 isSelected
1299 ? "bg-blue-600 border-blue-600"
1300 : "border-gray-300 dark:border-gray-600"
1301 }`}
1302 >
1303 {isSelected && (
1304 <Check className="w-3 h-3 text-white" />
1305 )}
1306 </div>
1307 <span className="flex-1 text-sm text-gray-900 dark:text-white">
1308 {trip.label}
1309 </span>
1310 <span className="text-xs text-gray-500 dark:text-gray-400">
1311 #{trip.value}
1312 </span>
1313 </div>
1314 );
1315 })}
1316 </div>
1317 </div>
1318 )}
1319 </div>
1320 )}
1321 </CardContent>
1322 </Card>
1323
1324 {/* "When will this apply?" preview panel.
1325 Runs the current (unsaved) form data through the engine
1326 against rule-type-appropriate sample scenarios so admins can
1327 see exactly when their rule fires, before saving. */}
1328 <Card>
1329 <CardHeader>
1330 <CardTitle className="text-base">
1331 {__("When will this rule apply?")}
1332 </CardTitle>
1333 <CardDescription>
1334 {__(
1335 "Run a simulation with the current settings to see which scenarios trigger the discount/markup. Nothing is saved.",
1336 )}
1337 </CardDescription>
1338 </CardHeader>
1339 <CardContent>
1340 <div className="flex items-end gap-3 mb-3">
1341 <div className="flex-1">
1342 <Label htmlFor="preview-base-price">
1343 {__("Sample base price")}
1344 </Label>
1345 <Input
1346 id="preview-base-price"
1347 type="number"
1348 min={1}
1349 step="0.01"
1350 value={previewBasePrice}
1351 onChange={(e) =>
1352 setPreviewBasePrice(Number(e.target.value) || 0)
1353 }
1354 />
1355 </div>
1356 <Button
1357 type="button"
1358 onClick={handleRunSimulation}
1359 disabled={previewLoading || !formData.rule_type}
1360 >
1361 {previewLoading ? (
1362 <>
1363 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
1364 {__("Running...")}
1365 </>
1366 ) : (
1367 <>
1368 <Target className="w-4 h-4 mr-2" />
1369 {__("Run Preview")}
1370 </>
1371 )}
1372 </Button>
1373 </div>
1374
1375 {previewError && (
1376 <p className="text-sm text-red-600 dark:text-red-400 mb-2">
1377 {previewError}
1378 </p>
1379 )}
1380
1381 {previewResult && (
1382 <div className="overflow-x-auto rounded-md border border-gray-200 dark:border-gray-700">
1383 <table className="w-full text-sm">
1384 <thead className="bg-gray-50 dark:bg-gray-900">
1385 <tr className="text-left">
1386 <th className="px-3 py-2 font-medium">
1387 {__("Scenario")}
1388 </th>
1389 <th className="px-3 py-2 font-medium">
1390 {__("Final Price")}
1391 </th>
1392 <th className="px-3 py-2 font-medium">
1393 {__("Change")}
1394 </th>
1395 <th className="px-3 py-2 font-medium">
1396 {__("Rule")}
1397 </th>
1398 </tr>
1399 </thead>
1400 <tbody>
1401 {previewResult.scenarios.map((s, i) => (
1402 <tr
1403 key={i}
1404 className="border-t border-gray-200 dark:border-gray-700"
1405 >
1406 <td className="px-3 py-2">
1407 <div>{s.label}</div>
1408 {s.departure_date && (
1409 <div className="text-xs text-gray-500 dark:text-gray-400">
1410 {s.departure_date}
1411 </div>
1412 )}
1413 </td>
1414 <td className="px-3 py-2 font-medium">
1415 {previewResult.base_price > 0
1416 ? `$${s.final_price.toFixed(2)}`
1417 : "—"}
1418 </td>
1419 <td className="px-3 py-2">
1420 {s.applies ? (
1421 <span
1422 className={
1423 s.adjustment < 0
1424 ? "text-green-700 dark:text-green-400"
1425 : "text-orange-700 dark:text-orange-400"
1426 }
1427 >
1428 {s.adjustment > 0 ? "+" : ""}$
1429 {s.adjustment.toFixed(2)} (
1430 {s.adjustment_percent > 0 ? "+" : ""}
1431 {s.adjustment_percent.toFixed(2)}%)
1432 </span>
1433 ) : (
1434 <span className="text-gray-500 dark:text-gray-400">
1435 —
1436 </span>
1437 )}
1438 </td>
1439 <td className="px-3 py-2">
1440 {s.applies ? (
1441 <span className="inline-flex items-center gap-1 text-green-700 dark:text-green-400">
1442 <Check className="w-3.5 h-3.5" />
1443 {__("Fires")}
1444 </span>
1445 ) : (
1446 <span className="text-gray-500 dark:text-gray-400">
1447 {__("Skipped")}
1448 </span>
1449 )}
1450 </td>
1451 </tr>
1452 ))}
1453 </tbody>
1454 </table>
1455 </div>
1456 )}
1457 </CardContent>
1458 </Card>
1459
1460 {/* Create Rule Button - Below Applicable To */}
1461 <div className="flex flex-col gap-3">
1462 <Button
1463 type="submit"
1464 disabled={saveMutation.isPending}
1465 className="w-full"
1466 >
1467 {saveMutation.isPending ? (
1468 <>
1469 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
1470 {__("Saving...")}
1471 </>
1472 ) : (
1473 <>
1474 <Save className="w-4 h-4 mr-2" />
1475 {isEdit ? __("Update Rule") : __("Create Rule")}
1476 </>
1477 )}
1478 </Button>
1479 <Button
1480 type="button"
1481 variant="outline"
1482 onClick={handleCancel}
1483 className="w-full"
1484 >
1485 <ArrowLeft className="w-4 h-4 mr-2" />
1486 {__("Cancel")}
1487 </Button>
1488 </div>
1489 </div>
1490 )}
1491 </div>
1492
1493 {/* Remove old actions section */}
1494 {false && formData.rule_type && (
1495 <div className="flex items-center justify-between mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1496 <Button type="button" variant="outline" onClick={handleCancel}>
1497 <ArrowLeft className="w-4 h-4 mr-2" />
1498 {__("Cancel")}
1499 </Button>
1500
1501 <Button type="submit" disabled={saveMutation.isPending}>
1502 {saveMutation.isPending ? (
1503 <>
1504 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
1505 {__("Saving...")}
1506 </>
1507 ) : (
1508 <>
1509 <Save className="w-4 h-4 mr-2" />
1510 {isEdit ? __("Update Rule") : __("Create Rule")}
1511 </>
1512 )}
1513 </Button>
1514 </div>
1515 )}
1516 </form>
1517
1518 {/* Rule Type Selection Modal */}
1519 <RuleTypeSelectionModal
1520 isOpen={showRuleTypeModal}
1521 onClose={() => setShowRuleTypeModal(false)}
1522 onSelectType={handleSelectRuleType}
1523 />
1524 </div>
1525 );
1526 };
1527
1528 export default DynamicPricingRuleForm;
1529