PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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.2.6, at resources/js/pages/DynamicPricingRuleForm.tsx

1,216 lines 47.5 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
62 apply_on_weekends?: boolean;
63 apply_on_weekdays?: boolean;
64 weekend_days?: string[];
65 }
66
67 const RULE_TYPES = [
68 {
69 id: "early_bird",
70 name: __("Early Bird Discount"),
71 description: __("Reward customers who book well in advance"),
72 icon: Calendar,
73 color: "blue",
74 example: __("10% off for bookings 30+ days early"),
75 },
76 {
77 id: "last_minute",
78 name: __("Last Minute Deals"),
79 description: __("Fill remaining spots close to departure"),
80 icon: Clock,
81 color: "orange",
82 example: __("15% off for bookings within 7 days"),
83 },
84 {
85 id: "demand",
86 name: __("Demand-Based Pricing"),
87 description: __("Adjust prices based on booking velocity"),
88 icon: TrendingUp,
89 color: "green",
90 example: __("Increase price for hot trips, decrease for slow"),
91 },
92 {
93 id: "inventory",
94 name: __("Inventory-Based"),
95 description: __("Price changes based on remaining capacity"),
96 icon: Package,
97 color: "purple",
98 example: __("Higher price when few spots left"),
99 },
100 {
101 id: "seasonal",
102 name: __("Seasonal Pricing"),
103 description: __("Adjust for peak and off-peak seasons"),
104 icon: Sun,
105 color: "yellow",
106 example: __("25% premium during summer months"),
107 },
108 {
109 id: "time_based",
110 name: __("Time-Based"),
111 description: __("Different prices for specific days and date ranges"),
112 icon: Target,
113 color: "indigo",
114 example: __("15% premium on weekends within specific dates"),
115 },
116 ];
117
118 const DynamicPricingRuleForm: React.FC = () => {
119 const { showToast } = useToast();
120 const queryClient = useQueryClient();
121 const urlParams = new URLSearchParams(window.location.search);
122 const ruleId = urlParams.get("id");
123 const ruleTypeFromUrl = urlParams.get("rule_type") || "";
124 const isEdit = !!ruleId;
125
126 const [formData, setFormData] = useState<RuleFormData>({
127 name: "",
128 rule_type: ruleTypeFromUrl,
129 adjustment_type: "percentage",
130 adjustment_value: 0,
131 min_days_before: 0,
132 max_days_before: 0,
133 min_inventory: 0,
134 max_inventory: 0,
135 start_date: "",
136 end_date: "",
137 applicable_trips: "all",
138 trip_ids: [],
139 status: "active",
140 priority: 1,
141 demand_threshold_high: 80,
142 demand_threshold_low: 30,
143 apply_on_weekends: false,
144 apply_on_weekdays: false,
145 weekend_days: [],
146 });
147
148 const [showTripDropdown, setShowTripDropdown] = useState(false);
149 const [tripSearchQuery, setTripSearchQuery] = useState("");
150 const [debouncedTripSearch, setDebouncedTripSearch] = useState("");
151 const [showRuleTypeModal, setShowRuleTypeModal] = useState(false);
152
153 const handleSelectRuleType = (ruleType: string) => {
154 handleChange("rule_type", ruleType);
155 setShowRuleTypeModal(false);
156 };
157
158 // Debounce trip search
159 useEffect(() => {
160 const timer = setTimeout(() => {
161 setDebouncedTripSearch(tripSearchQuery);
162 }, 300);
163 return () => clearTimeout(timer);
164 }, [tripSearchQuery]);
165
166 // Fetch trips for selection
167 const { data: tripsData } = useQuery({
168 queryKey: ["trips-list", debouncedTripSearch],
169 queryFn: async () => {
170 const params = new URLSearchParams();
171 if (debouncedTripSearch) {
172 params.append("search", debouncedTripSearch);
173 }
174 params.append("per_page", "50");
175 const response = await apiClient.get(`/trips?${params.toString()}`);
176 return response.data || [];
177 },
178 });
179
180 const tripOptions = (tripsData || []).map((trip: any) => ({
181 value: trip.id,
182 label: trip.title || `Trip #${trip.id}`,
183 }));
184
185 // Fetch existing rule if editing
186 const {
187 data: ruleData,
188 isLoading,
189 isError,
190 error,
191 } = useQuery({
192 queryKey: ["dynamic-pricing-rule", ruleId],
193 queryFn: async () => {
194 if (!ruleId) return null;
195 const response = await apiClient.get(`/dynamic-pricing/rules/${ruleId}`);
196
197 return response;
198 },
199 enabled: isEdit,
200 retry: 1,
201 });
202
203 // Log loading state
204
205 // Populate form with existing data
206 useEffect(() => {
207 if (ruleData?.data) {
208 const ruleDetails = ruleData.data;
209
210 setFormData({
211 name: ruleDetails.name || "",
212 rule_type: ruleDetails.rule_type || "",
213 adjustment_type: ruleDetails.adjustment_type || "percentage",
214 adjustment_value: Number(ruleDetails.adjustment_value) || 0,
215 min_days_before: Number(ruleDetails.min_days_before) || 0,
216 max_days_before: Number(ruleDetails.max_days_before) || 0,
217 min_inventory: Number(ruleDetails.min_inventory) || 0,
218 max_inventory: Number(ruleDetails.max_inventory) || 0,
219 start_date: ruleDetails.start_date || "",
220 end_date: ruleDetails.end_date || "",
221 applicable_trips: ruleDetails.applicable_trips || "all",
222 trip_ids: Array.isArray(ruleDetails.trip_ids)
223 ? ruleDetails.trip_ids
224 : ruleDetails.trip_ids
225 ? JSON.parse(ruleDetails.trip_ids)
226 : [],
227 status: ruleDetails.status || "active",
228 priority: Number(ruleDetails.priority) || 1,
229 demand_threshold_high: ruleDetails.demand_threshold_high
230 ? Number(ruleDetails.demand_threshold_high)
231 : 80,
232 demand_threshold_low: ruleDetails.demand_threshold_low
233 ? Number(ruleDetails.demand_threshold_low)
234 : 30,
235 apply_on_weekends: Boolean(ruleDetails.apply_on_weekends),
236 apply_on_weekdays: Boolean(ruleDetails.apply_on_weekdays),
237 weekend_days: Array.isArray(ruleDetails.weekend_days)
238 ? ruleDetails.weekend_days
239 : ruleDetails.weekend_days
240 ? JSON.parse(ruleDetails.weekend_days)
241 : [],
242 });
243 }
244 }, [ruleData]);
245
246 // Create/Update mutation
247 const saveMutation = useMutation({
248 mutationFn: async (data: RuleFormData) => {
249 if (isEdit) {
250 return await apiClient.put(`/dynamic-pricing/rules/${ruleId}`, data);
251 } else {
252 return await apiClient.post("/dynamic-pricing/rules", data);
253 }
254 },
255 onSuccess: (response) => {
256 showToast(
257 isEdit
258 ? __("Pricing rule updated successfully")
259 : __("Pricing rule created successfully"),
260 "success",
261 );
262 queryClient.invalidateQueries({ queryKey: ["dynamic-pricing-rules"] });
263
264 if (!isEdit && response?.data?.id) {
265 // For new rules, redirect to edit page
266 const baseUrl = window.location.href.split("&action=")[0];
267 const editUrl = `${baseUrl}&action=edit-pricing-rule&id=${response.data.id}`;
268 window.location.href = editUrl;
269 }
270 // For updates, stay on the same page (no redirect)
271 },
272 onError: (error: any) => {
273 showToast(error?.message || __("Failed to save pricing rule"), "error");
274 },
275 });
276
277 const handleSubmit = (e: React.FormEvent) => {
278 e.preventDefault();
279 saveMutation.mutate(formData);
280 };
281
282 const handleCancel = () => {
283 window.location.href = window.location.href.split("&action=")[0];
284 };
285
286 const handleChange = (field: keyof RuleFormData, value: any) => {
287 setFormData((prev) => ({ ...prev, [field]: value }));
288 };
289
290 // Show error state if API call fails
291 if (isError) {
292 return (
293 <div className="space-y-6">
294 <PageHeader
295 title={__("Edit Pricing Rule")}
296 description={__(
297 "Configure dynamic pricing rules to automatically adjust trip prices",
298 )}
299 />
300 <Card>
301 <CardContent className="pt-6">
302 <div className="text-center py-12">
303 <p className="text-red-600 dark:text-red-400 mb-4">
304 {__("Failed to load pricing rule")}
305 </p>
306 <p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
307 {error?.message ||
308 __("An error occurred while fetching the rule data")}
309 </p>
310 <Button
311 onClick={() =>
312 (window.location.href =
313 window.location.href.split("&action=")[0])
314 }
315 >
316 {__("Back to Rules")}
317 </Button>
318 </div>
319 </CardContent>
320 </Card>
321 </div>
322 );
323 }
324
325 if (isLoading) {
326 return (
327 <div className="space-y-6">
328 <PageHeader
329 title={__("Edit Pricing Rule")}
330 description={__(
331 "Configure dynamic pricing rules to automatically adjust trip prices",
332 )}
333 />
334
335 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
336 {/* Main Content Skeleton - Left Side (2/3) */}
337 <div className="lg:col-span-2 space-y-6">
338 {/* Rule Type Badge Skeleton */}
339 <Card>
340 <CardContent className="pt-6">
341 <div className="flex items-center gap-3">
342 <Skeleton className="w-5 h-5 rounded" />
343 <div className="flex-1">
344 <Skeleton className="h-3 w-16 mb-2" />
345 <Skeleton className="h-5 w-48 mb-1" />
346 <Skeleton className="h-3 w-64" />
347 </div>
348 </div>
349 </CardContent>
350 </Card>
351
352 {/* Rule Details Skeleton */}
353 <Card>
354 <CardHeader>
355 <Skeleton className="h-6 w-32 mb-2" />
356 <Skeleton className="h-4 w-64" />
357 </CardHeader>
358 <CardContent className="space-y-4">
359 <div>
360 <Skeleton className="h-4 w-24 mb-2" />
361 <Skeleton className="h-10 w-full" />
362 </div>
363 </CardContent>
364 </Card>
365
366 {/* Price Adjustment Skeleton */}
367 <Card>
368 <CardHeader>
369 <Skeleton className="h-6 w-40 mb-2" />
370 <Skeleton className="h-4 w-48" />
371 </CardHeader>
372 <CardContent className="space-y-4">
373 <div className="grid grid-cols-2 gap-4">
374 <div>
375 <Skeleton className="h-4 w-32 mb-2" />
376 <Skeleton className="h-10 w-full" />
377 </div>
378 <div>
379 <Skeleton className="h-4 w-24 mb-2" />
380 <Skeleton className="h-10 w-full" />
381 </div>
382 </div>
383 </CardContent>
384 </Card>
385
386 {/* Conditions Skeleton */}
387 <Card>
388 <CardHeader>
389 <Skeleton className="h-6 w-32 mb-2" />
390 <Skeleton className="h-4 w-56" />
391 </CardHeader>
392 <CardContent className="space-y-4">
393 <div className="grid grid-cols-2 gap-4">
394 <div>
395 <Skeleton className="h-4 w-32 mb-2" />
396 <Skeleton className="h-10 w-full" />
397 </div>
398 <div>
399 <Skeleton className="h-4 w-32 mb-2" />
400 <Skeleton className="h-10 w-full" />
401 </div>
402 </div>
403 </CardContent>
404 </Card>
405
406 {/* Applicable Trips Skeleton */}
407 <Card>
408 <CardHeader>
409 <Skeleton className="h-6 w-40 mb-2" />
410 <Skeleton className="h-4 w-64" />
411 </CardHeader>
412 <CardContent className="space-y-4">
413 <div>
414 <Skeleton className="h-4 w-32 mb-2" />
415 <Skeleton className="h-10 w-full" />
416 </div>
417 </CardContent>
418 </Card>
419 </div>
420
421 {/* Sidebar Skeleton - Right Side (1/3) */}
422 <div className="space-y-6">
423 {/* Status & Priority Skeleton */}
424 <Card>
425 <CardHeader>
426 <Skeleton className="h-6 w-32 mb-2" />
427 </CardHeader>
428 <CardContent className="space-y-4">
429 <div>
430 <Skeleton className="h-4 w-16 mb-2" />
431 <Skeleton className="h-10 w-full" />
432 </div>
433 <div>
434 <Skeleton className="h-4 w-16 mb-2" />
435 <Skeleton className="h-10 w-full" />
436 </div>
437 </CardContent>
438 </Card>
439
440 {/* Actions Skeleton */}
441 <Card>
442 <CardContent className="pt-6 space-y-3">
443 <Skeleton className="h-10 w-full" />
444 <Skeleton className="h-10 w-full" />
445 </CardContent>
446 </Card>
447 </div>
448 </div>
449 </div>
450 );
451 }
452
453 const selectedRuleType = RULE_TYPES.find(
454 (rt) => rt.id === formData.rule_type,
455 );
456
457 return (
458 <div className="space-y-6">
459 <PageHeader
460 title={isEdit ? __("Edit Pricing Rule") : __("Create Pricing Rule")}
461 description={__(
462 "Configure dynamic pricing rules to automatically adjust trip prices",
463 )}
464 />
465
466 <form onSubmit={handleSubmit}>
467 {/* Two Column Layout: Main Content + Sidebar */}
468 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
469 {/* Main Content - Left Side (2/3) */}
470 <div className="lg:col-span-2 space-y-6">
471 {/* Step 1: Select Rule Type (Card-Based Selection) */}
472 {!isEdit && !formData.rule_type && (
473 <Card>
474 <CardHeader>
475 <CardTitle>{__("Step 1: Select Rule Type")}</CardTitle>
476 <CardDescription>
477 {__(
478 "Choose the type of dynamic pricing rule you want to create",
479 )}
480 </CardDescription>
481 </CardHeader>
482 <CardContent>
483 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
484 {RULE_TYPES.map((ruleType) => {
485 const Icon = ruleType.icon;
486 const colorClasses = {
487 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",
488 orange:
489 "border-orange-200 dark:border-orange-800 hover:border-orange-400 dark:hover:border-orange-600 bg-orange-50 dark:bg-orange-900/20",
490 green:
491 "border-green-200 dark:border-green-800 hover:border-green-400 dark:hover:border-green-600 bg-green-50 dark:bg-green-900/20",
492 purple:
493 "border-purple-200 dark:border-purple-800 hover:border-purple-400 dark:hover:border-purple-600 bg-purple-50 dark:bg-purple-900/20",
494 yellow:
495 "border-yellow-200 dark:border-yellow-800 hover:border-yellow-400 dark:hover:border-yellow-600 bg-yellow-50 dark:bg-yellow-900/20",
496 indigo:
497 "border-indigo-200 dark:border-indigo-800 hover:border-indigo-400 dark:hover:border-indigo-600 bg-indigo-50 dark:bg-indigo-900/20",
498 };
499 return (
500 <button
501 key={ruleType.id}
502 type="button"
503 onClick={() => handleChange("rule_type", ruleType.id)}
504 className={`p-4 border-2 rounded-lg text-left transition-all cursor-pointer ${colorClasses[ruleType.color as keyof typeof colorClasses]}`}
505 >
506 <div className="flex items-start gap-3">
507 <div className="flex-shrink-0">
508 <Icon className="w-6 h-6 text-gray-700 dark:text-gray-300" />
509 </div>
510 <div className="flex-1 min-w-0">
511 <h3 className="font-semibold text-gray-900 dark:text-white mb-1">
512 {ruleType.name}
513 </h3>
514 <p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
515 {ruleType.description}
516 </p>
517 <p className="text-xs text-gray-500 dark:text-gray-500 italic">
518 {__("Example:")} {ruleType.example}
519 </p>
520 </div>
521 </div>
522 </button>
523 );
524 })}
525 </div>
526 </CardContent>
527 </Card>
528 )}
529
530 {/* Show selected rule type badge if editing or type selected */}
531 {(isEdit || formData.rule_type) && selectedRuleType && (
532 <Card>
533 <CardContent className="pt-6">
534 <div className="flex items-center justify-between">
535 <div className="flex items-center gap-3">
536 {React.createElement(selectedRuleType.icon, {
537 className: "w-5 h-5 text-gray-600 dark:text-gray-400",
538 })}
539 <div>
540 <p className="text-sm text-gray-500 dark:text-gray-400">
541 {__("Rule Type")}
542 </p>
543 <p className="font-semibold text-gray-900 dark:text-white">
544 {selectedRuleType.name}
545 </p>
546 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
547 {selectedRuleType.description}
548 </p>
549 </div>
550 </div>
551 <Button
552 type="button"
553 variant="outline"
554 size="sm"
555 onClick={() => setShowRuleTypeModal(true)}
556 >
557 {__("Change")}
558 </Button>
559 </div>
560 </CardContent>
561 </Card>
562 )}
563
564 {/* Step 2: Basic Information (Only show after rule type selected) */}
565 {formData.rule_type && (
566 <>
567 <Card>
568 <CardHeader>
569 <CardTitle>{__("Rule Details")}</CardTitle>
570 <CardDescription>
571 {__("Name and description for this pricing rule")}
572 </CardDescription>
573 </CardHeader>
574 <CardContent className="space-y-4">
575 <div>
576 <Label htmlFor="name">{__("Rule Name")} *</Label>
577 <Input
578 id="name"
579 value={formData.name}
580 onChange={(e) => handleChange("name", e.target.value)}
581 placeholder={__("e.g., Summer Early Bird Discount")}
582 required
583 />
584 </div>
585 </CardContent>
586 </Card>
587
588 <Card>
589 <CardHeader>
590 <CardTitle>{__("Price Adjustment")}</CardTitle>
591 <CardDescription>
592 {__("How much to adjust the price")}
593 </CardDescription>
594 </CardHeader>
595 <CardContent className="space-y-4">
596 <div>
597 <Label htmlFor="adjustment_type">
598 {__("Adjustment Type")} *
599 </Label>
600 <Select
601 id="adjustment_type"
602 value={formData.adjustment_type}
603 onChange={(e) =>
604 handleChange("adjustment_type", e.target.value)
605 }
606 required
607 >
608 <option value="percentage">{__("Percentage")}</option>
609 <option value="fixed">{__("Fixed Amount")}</option>
610 </Select>
611 </div>
612
613 <div>
614 <Label htmlFor="adjustment_value">
615 {__("Adjustment Value")} *
616 </Label>
617 <Input
618 id="adjustment_value"
619 type="number"
620 step="0.01"
621 value={formData.adjustment_value}
622 onChange={(e) =>
623 handleChange(
624 "adjustment_value",
625 parseFloat(e.target.value),
626 )
627 }
628 placeholder={
629 formData.adjustment_type === "percentage"
630 ? "10"
631 : "100"
632 }
633 required
634 />
635 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
636 {formData.adjustment_type === "percentage"
637 ? __(
638 "Enter percentage (e.g., 10 for 10% discount, -10 for 10% increase)",
639 )
640 : __(
641 "Enter fixed amount (e.g., 100 for $100 discount, -100 for $100 increase)",
642 )}
643 </p>
644 </div>
645 </CardContent>
646 </Card>
647
648 <Card>
649 <CardHeader>
650 <CardTitle>{__("Conditions")}</CardTitle>
651 <CardDescription>
652 {__("When this rule should apply")}
653 </CardDescription>
654 </CardHeader>
655 <CardContent className="space-y-4">
656 {/* Early Bird & Last Minute - Days before departure */}
657 {(formData.rule_type === "early_bird" ||
658 formData.rule_type === "last_minute") && (
659 <>
660 <div>
661 <Label htmlFor="min_days_before">
662 {__("Minimum Days Before Departure")}
663 </Label>
664 <Input
665 id="min_days_before"
666 type="number"
667 value={formData.min_days_before}
668 onChange={(e) =>
669 handleChange(
670 "min_days_before",
671 parseInt(e.target.value),
672 )
673 }
674 min="0"
675 />
676 </div>
677
678 <div>
679 <Label htmlFor="max_days_before">
680 {__("Maximum Days Before Departure")}
681 </Label>
682 <Input
683 id="max_days_before"
684 type="number"
685 value={formData.max_days_before}
686 onChange={(e) =>
687 handleChange(
688 "max_days_before",
689 parseInt(e.target.value),
690 )
691 }
692 min="0"
693 />
694 </div>
695
696 <div>
697 <Label htmlFor="start_date">
698 {__("Start Date (Optional)")}
699 </Label>
700 <DatePicker
701 value={formData.start_date}
702 onChange={(value) =>
703 handleChange("start_date", value)
704 }
705 placeholder={__("Select start date")}
706 />
707 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
708 {__("Leave empty for no date restriction")}
709 </p>
710 </div>
711
712 <div>
713 <Label htmlFor="end_date">
714 {__("End Date (Optional)")}
715 </Label>
716 <DatePicker
717 value={formData.end_date}
718 onChange={(value) =>
719 handleChange("end_date", value)
720 }
721 placeholder={__("Select end date")}
722 />
723 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
724 {__("Leave empty for no date restriction")}
725 </p>
726 </div>
727 </>
728 )}
729
730 {formData.rule_type === "inventory" && (
731 <>
732 <div>
733 <Label htmlFor="min_inventory">
734 {__("Minimum Available Seats")}
735 </Label>
736 <Input
737 id="min_inventory"
738 type="number"
739 value={formData.min_inventory}
740 onChange={(e) =>
741 handleChange(
742 "min_inventory",
743 parseInt(e.target.value),
744 )
745 }
746 min="0"
747 />
748 </div>
749
750 <div>
751 <Label htmlFor="max_inventory">
752 {__("Maximum Available Seats")}
753 </Label>
754 <Input
755 id="max_inventory"
756 type="number"
757 value={formData.max_inventory}
758 onChange={(e) =>
759 handleChange(
760 "max_inventory",
761 parseInt(e.target.value),
762 )
763 }
764 min="0"
765 />
766 </div>
767 </>
768 )}
769
770 {/* Seasonal - Date range */}
771 {formData.rule_type === "seasonal" && (
772 <>
773 <div>
774 <Label htmlFor="start_date">{__("Start Date")}</Label>
775 <DatePicker
776 value={formData.start_date}
777 onChange={(value) =>
778 handleChange("start_date", value)
779 }
780 placeholder={__("Select start date")}
781 />
782 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
783 {__("When this seasonal pricing starts")}
784 </p>
785 </div>
786
787 <div>
788 <Label htmlFor="end_date">{__("End Date")}</Label>
789 <DatePicker
790 value={formData.end_date}
791 onChange={(value) =>
792 handleChange("end_date", value)
793 }
794 placeholder={__("Select end date")}
795 />
796 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
797 {__("When this seasonal pricing ends")}
798 </p>
799 </div>
800 </>
801 )}
802
803 {/* Demand-Based - Booking velocity thresholds */}
804 {formData.rule_type === "demand" && (
805 <>
806 <div>
807 <Label htmlFor="demand_threshold_high">
808 {__("High Demand Threshold (%)")}
809 </Label>
810 <Input
811 id="demand_threshold_high"
812 type="number"
813 value={formData.demand_threshold_high}
814 onChange={(e) =>
815 handleChange(
816 "demand_threshold_high",
817 parseInt(e.target.value),
818 )
819 }
820 min="0"
821 max="100"
822 />
823 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
824 {__(
825 "Apply pricing when booking velocity exceeds this percentage (e.g., 80 = high demand)",
826 )}
827 </p>
828 </div>
829
830 <div>
831 <Label htmlFor="demand_threshold_low">
832 {__("Low Demand Threshold (%)")}
833 </Label>
834 <Input
835 id="demand_threshold_low"
836 type="number"
837 value={formData.demand_threshold_low}
838 onChange={(e) =>
839 handleChange(
840 "demand_threshold_low",
841 parseInt(e.target.value),
842 )
843 }
844 min="0"
845 max="100"
846 />
847 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
848 {__(
849 "Apply pricing when booking velocity falls below this percentage (e.g., 30 = low demand)",
850 )}
851 </p>
852 </div>
853
854 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
855 <p className="text-xs text-blue-700 dark:text-blue-300">
856 {__(
857 "💡 Demand scores are calculated automatically via cron job based on booking velocity.",
858 )}
859 </p>
860 </div>
861 </>
862 )}
863
864 {/* Time-Based - Day of week selection */}
865 {formData.rule_type === "time_based" && (
866 <>
867 <div>
868 <Label>{__("Apply Pricing On")}</Label>
869 <div className="grid grid-cols-2 gap-2 mt-2">
870 {[
871 "monday",
872 "tuesday",
873 "wednesday",
874 "thursday",
875 "friday",
876 "saturday",
877 "sunday",
878 ].map((day) => (
879 <div
880 key={day}
881 className="flex items-center gap-2"
882 >
883 <input
884 type="checkbox"
885 id={`day_${day}`}
886 checked={formData.weekend_days?.includes(day)}
887 onChange={(e) => {
888 const current = formData.weekend_days || [];
889 if (e.target.checked) {
890 handleChange("weekend_days", [
891 ...current,
892 day,
893 ]);
894 } else {
895 handleChange(
896 "weekend_days",
897 current.filter((d) => d !== day),
898 );
899 }
900 }}
901 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
902 />
903 <Label
904 htmlFor={`day_${day}`}
905 className="mb-0 capitalize"
906 >
907 {__(day)}
908 </Label>
909 </div>
910 ))}
911 </div>
912 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
913 {__(
914 "Select the days of the week when this pricing rule applies",
915 )}
916 </p>
917 </div>
918
919 <div>
920 <Label htmlFor="start_date">
921 {__("Start Date (Optional)")}
922 </Label>
923 <DatePicker
924 value={formData.start_date}
925 onChange={(value) =>
926 handleChange("start_date", value)
927 }
928 placeholder={__("Select start date")}
929 />
930 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
931 {__("Leave empty for no date restriction")}
932 </p>
933 </div>
934
935 <div>
936 <Label htmlFor="end_date">
937 {__("End Date (Optional)")}
938 </Label>
939 <DatePicker
940 value={formData.end_date}
941 onChange={(value) =>
942 handleChange("end_date", value)
943 }
944 placeholder={__("Select end date")}
945 />
946 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
947 {__("Leave empty for no date restriction")}
948 </p>
949 </div>
950
951 <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg">
952 <p className="text-xs text-indigo-700 dark:text-indigo-300">
953 {__(
954 "💡 Use positive values for premium pricing (weekends) or negative for discounts (weekdays).",
955 )}
956 </p>
957 </div>
958 </>
959 )}
960 </CardContent>
961 </Card>
962 </>
963 )}
964 </div>
965
966 {/* Sidebar - Right Side (1/3) */}
967 {formData.rule_type && (
968 <div className="space-y-4">
969 {/* Status */}
970 <Card>
971 <CardHeader className="pb-3">
972 <CardTitle className="text-base">{__("Status")}</CardTitle>
973 </CardHeader>
974 <CardContent className="space-y-3">
975 <Select
976 value={formData.status}
977 onChange={(e) => handleChange("status", e.target.value)}
978 >
979 <option value="active">{__("Active")}</option>
980 <option value="inactive">{__("Inactive")}</option>
981 </Select>
982 <p className="text-xs text-gray-500 dark:text-gray-400">
983 {__("Only active rules will be applied to trips.")}
984 </p>
985 </CardContent>
986 </Card>
987
988 {/* Priority */}
989 <Card>
990 <CardHeader className="pb-3">
991 <CardTitle className="text-base">{__("Priority")}</CardTitle>
992 </CardHeader>
993 <CardContent className="space-y-3">
994 <Input
995 type="number"
996 value={formData.priority}
997 onChange={(e) =>
998 handleChange("priority", parseInt(e.target.value))
999 }
1000 min="1"
1001 max="100"
1002 />
1003 <p className="text-xs text-gray-500 dark:text-gray-400">
1004 {__("Higher priority rules are applied first (1-100)")}
1005 </p>
1006 </CardContent>
1007 </Card>
1008
1009 {/* Applicable Trips */}
1010 <Card>
1011 <CardHeader className="pb-3">
1012 <CardTitle className="text-base">
1013 {__("Applicable To")}
1014 </CardTitle>
1015 </CardHeader>
1016 <CardContent className="space-y-3">
1017 <Select
1018 value={formData.applicable_trips}
1019 onChange={(e) =>
1020 handleChange("applicable_trips", e.target.value)
1021 }
1022 >
1023 <option value="all">{__("All Trips")}</option>
1024 <option value="specific">{__("Specific Trips")}</option>
1025 </Select>
1026
1027 {formData.applicable_trips === "specific" && (
1028 <div className="mt-3 space-y-2 relative">
1029 <div
1030 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"
1031 onClick={() => setShowTripDropdown(!showTripDropdown)}
1032 >
1033 {formData.trip_ids.length > 0 ? (
1034 <div className="flex flex-wrap gap-1.5">
1035 {formData.trip_ids.slice(0, 2).map((tripId) => {
1036 const trip = tripOptions.find(
1037 (t: any) => t.value === tripId,
1038 );
1039 return (
1040 <span
1041 key={tripId}
1042 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"
1043 >
1044 {trip?.label || `Trip #${tripId}`}{" "}
1045 <span className="text-blue-600 dark:text-blue-400">
1046 #{tripId}
1047 </span>
1048 <button
1049 type="button"
1050 onClick={(e) => {
1051 e.stopPropagation();
1052 handleChange(
1053 "trip_ids",
1054 formData.trip_ids.filter(
1055 (id) => id !== tripId,
1056 ),
1057 );
1058 }}
1059 className="hover:text-blue-600 dark:hover:text-blue-200"
1060 >
1061 ×
1062 </button>
1063 </span>
1064 );
1065 })}
1066 {formData.trip_ids.length > 2 && (
1067 <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">
1068 +{formData.trip_ids.length - 2} {__("more")}
1069 </span>
1070 )}
1071 </div>
1072 ) : (
1073 <span className="text-sm text-gray-500 dark:text-gray-400">
1074 {__("Click to select trips...")}
1075 </span>
1076 )}
1077 </div>
1078
1079 {showTripDropdown && (
1080 <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">
1081 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
1082 <Input
1083 type="text"
1084 placeholder={__("Search trips...")}
1085 value={tripSearchQuery}
1086 onChange={(e) =>
1087 setTripSearchQuery(e.target.value)
1088 }
1089 className="w-full text-sm"
1090 onClick={(e) => e.stopPropagation()}
1091 />
1092 </div>
1093 <div className="max-h-48 overflow-y-auto">
1094 {tripOptions.map((trip: any) => {
1095 const isSelected = formData.trip_ids.includes(
1096 trip.value,
1097 );
1098 return (
1099 <div
1100 key={trip.value}
1101 className="px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center gap-2"
1102 onClick={(e) => {
1103 e.stopPropagation();
1104 if (isSelected) {
1105 handleChange(
1106 "trip_ids",
1107 formData.trip_ids.filter(
1108 (id) => id !== trip.value,
1109 ),
1110 );
1111 } else {
1112 handleChange("trip_ids", [
1113 ...formData.trip_ids,
1114 trip.value,
1115 ]);
1116 }
1117 }}
1118 >
1119 <div
1120 className={`w-4 h-4 border-2 rounded flex items-center justify-center ${
1121 isSelected
1122 ? "bg-blue-600 border-blue-600"
1123 : "border-gray-300 dark:border-gray-600"
1124 }`}
1125 >
1126 {isSelected && (
1127 <Check className="w-3 h-3 text-white" />
1128 )}
1129 </div>
1130 <span className="flex-1 text-sm text-gray-900 dark:text-white">
1131 {trip.label}
1132 </span>
1133 <span className="text-xs text-gray-500 dark:text-gray-400">
1134 #{trip.value}
1135 </span>
1136 </div>
1137 );
1138 })}
1139 </div>
1140 </div>
1141 )}
1142 </div>
1143 )}
1144 </CardContent>
1145 </Card>
1146
1147 {/* Create Rule Button - Below Applicable To */}
1148 <div className="flex flex-col gap-3">
1149 <Button
1150 type="submit"
1151 disabled={saveMutation.isPending}
1152 className="w-full"
1153 >
1154 {saveMutation.isPending ? (
1155 <>
1156 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
1157 {__("Saving...")}
1158 </>
1159 ) : (
1160 <>
1161 <Save className="w-4 h-4 mr-2" />
1162 {isEdit ? __("Update Rule") : __("Create Rule")}
1163 </>
1164 )}
1165 </Button>
1166 <Button
1167 type="button"
1168 variant="outline"
1169 onClick={handleCancel}
1170 className="w-full"
1171 >
1172 <ArrowLeft className="w-4 h-4 mr-2" />
1173 {__("Cancel")}
1174 </Button>
1175 </div>
1176 </div>
1177 )}
1178 </div>
1179
1180 {/* Remove old actions section */}
1181 {false && formData.rule_type && (
1182 <div className="flex items-center justify-between mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
1183 <Button type="button" variant="outline" onClick={handleCancel}>
1184 <ArrowLeft className="w-4 h-4 mr-2" />
1185 {__("Cancel")}
1186 </Button>
1187
1188 <Button type="submit" disabled={saveMutation.isPending}>
1189 {saveMutation.isPending ? (
1190 <>
1191 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
1192 {__("Saving...")}
1193 </>
1194 ) : (
1195 <>
1196 <Save className="w-4 h-4 mr-2" />
1197 {isEdit ? __("Update Rule") : __("Create Rule")}
1198 </>
1199 )}
1200 </Button>
1201 </div>
1202 )}
1203 </form>
1204
1205 {/* Rule Type Selection Modal */}
1206 <RuleTypeSelectionModal
1207 isOpen={showRuleTypeModal}
1208 onClose={() => setShowRuleTypeModal(false)}
1209 onSelectType={handleSelectRuleType}
1210 />
1211 </div>
1212 );
1213 };
1214
1215 export default DynamicPricingRuleForm;
1216