PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / DynamicPricingRuleForm.tsx

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

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