PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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 / RecurringRuleForm.tsx

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

2,539 lines 121.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Recurring Availability Rule Form
3 * Create and edit recurring availability patterns
4 */
5
6 import React, { useState, useEffect } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Save,
11 Clock,
12 DollarSign,
13 RefreshCw,
14 Plus,
15 X,
16 AlertCircle,
17 Eye,
18 MapPin,
19 CheckCircle2,
20 } from "lucide-react";
21 import { __ } from "../lib/i18n";
22 import { Button } from "../components/ui/button";
23 import { Input } from "../components/ui/input";
24 import { Select } from "../components/ui/select";
25 import { SearchableSelect } from "../components/ui/searchable-select";
26 import { DatePicker } from "../components/ui/date-picker";
27 import { TimePicker } from "../components/ui/time-picker";
28 import { PageHeader } from "../components/common/PageHeader";
29 import {
30 Card,
31 CardContent,
32 CardHeader,
33 CardTitle,
34 CardDescription,
35 } from "../components/ui/card";
36 import { Badge } from "../components/ui/badge";
37 import { Alert } from "../components/ui/alert";
38 import { useNavigate } from "../hooks/useNavigate";
39 import { apiClient } from "../lib/api-client";
40 import { useToast } from "../components/ui/toast";
41 import { RecurringRuleFormSkeleton } from "../components/availability/RecurringRuleFormSkeleton";
42 import { LocationPicker } from "../components/trip-form/LocationPicker";
43
44 function coordFromApi(v: unknown): string {
45 if (v == null || v === "") return "";
46 return String(v);
47 }
48
49 interface Trip {
50 id: number;
51 title: string;
52 trip_type?: "single_day" | "multi_day";
53 duration_days?: number;
54 starting_location?: string;
55 ending_location?: string;
56 starting_latitude?: string | number;
57 starting_longitude?: string | number;
58 ending_latitude?: string | number;
59 ending_longitude?: string | number;
60 pricing_type?: "regular" | "traveler_based";
61 }
62
63 interface TravelerCategory {
64 id: number;
65 name?: string;
66 label?: string;
67 description?: string;
68 min_age?: number;
69 max_age?: number;
70 age_min?: number;
71 age_max?: number;
72 status?: string;
73 }
74
75 interface TravelerPricing {
76 category_id: number;
77 original_price: number;
78 sale_price?: number;
79 }
80
81 interface TimeSlot {
82 departure_time: string;
83 arrival_time: string;
84 seats: number;
85 price: number;
86 sale_price?: number;
87 traveler_pricing?: TravelerPricing[];
88 }
89
90 interface RecurringRule {
91 id?: number;
92 trip_id: number;
93 name: string;
94 rule_type: "weekly" | "monthly" | "interval";
95 days_of_week: number[];
96 week_of_month?: "first" | "second" | "third" | "fourth" | "last";
97 day_of_week?: number;
98 interval_days?: number;
99 start_date: string;
100 end_date?: string;
101 excluded_dates: string[];
102 months: number[]; // Array of month numbers (1-12) to filter by
103 time_slots: TimeSlot[]; // For single-day trips with multiple slots
104 pricing_type: "regular" | "traveler_based"; // Allow override of trip's pricing type
105 original_price?: number;
106 sale_price?: number;
107 traveler_pricing?: TravelerPricing[]; // For traveler-based pricing
108 seats_total: number;
109 departure_time?: string;
110 arrival_time?: string;
111 from_location?: string;
112 to_location?: string;
113 from_latitude?: string;
114 from_longitude?: string;
115 to_latitude?: string;
116 to_longitude?: string;
117 cutoff_hours: number;
118 alert_threshold: number;
119 status: "active" | "inactive";
120 }
121
122 const dayOptions = [
123 { value: 0, label: "Sunday" },
124 { value: 1, label: "Monday" },
125 { value: 2, label: "Tuesday" },
126 { value: 3, label: "Wednesday" },
127 { value: 4, label: "Thursday" },
128 { value: 5, label: "Friday" },
129 { value: 6, label: "Saturday" },
130 ];
131
132 const weekOptions = [
133 { value: "first", label: "First" },
134 { value: "second", label: "Second" },
135 { value: "third", label: "Third" },
136 { value: "fourth", label: "Fourth" },
137 { value: "last", label: "Last" },
138 ];
139
140 const RecurringRuleForm: React.FC = () => {
141 const { navigate } = useNavigate();
142 const queryClient = useQueryClient();
143 const { showToast } = useToast();
144
145 // Get parameters from URL
146 const params = new URLSearchParams(window.location.search);
147 const ruleId = params.get("id");
148 const tripIdFromUrl = params.get("trip_id");
149 const isEditing = !!ruleId;
150
151 // Form state
152 const [formData, setFormData] = useState<RecurringRule>({
153 trip_id: tripIdFromUrl ? parseInt(tripIdFromUrl) : 0,
154 name: "",
155 rule_type: "weekly",
156 days_of_week: [0], // Sunday by default
157 week_of_month: "first",
158 day_of_week: 0,
159 interval_days: 7,
160 start_date: new Date().toISOString().split("T")[0],
161 end_date: "",
162 excluded_dates: [],
163 months: [], // Empty = all months, otherwise specific months (1-12)
164 time_slots: [], // For single-day trips with multiple slots
165 pricing_type: "regular", // Will be updated based on trip's pricing type
166 original_price: undefined,
167 sale_price: undefined,
168 traveler_pricing: [], // For traveler-based pricing
169 seats_total: 20,
170 departure_time: "",
171 arrival_time: "",
172 from_location: "",
173 to_location: "",
174 from_latitude: "",
175 from_longitude: "",
176 to_latitude: "",
177 to_longitude: "",
178 cutoff_hours: 24,
179 alert_threshold: 5,
180 status: "active",
181 });
182
183 const [newExcludedDate, setNewExcludedDate] = useState("");
184 const [previewData, setPreviewData] = useState<{
185 total: number;
186 dates: any[];
187 } | null>(null);
188 const [showCategorySelector, setShowCategorySelector] = useState(false);
189
190 // Fetch trips for dropdown
191 const { data: tripsData } = useQuery({
192 queryKey: ["trips", "all"],
193 queryFn: async () => {
194 const response = await apiClient.get("/trips", {
195 params: { per_page: 100, status: "publish" },
196 });
197 return {
198 trips: (response?.data || []).map((trip: any) => ({
199 id: Number(trip.id) || 0,
200 title: trip.title,
201 trip_type:
202 trip.trip_type ||
203 (trip.duration_days <= 1 ? "single_day" : "multi_day"),
204 duration_days: trip.duration_days || 1,
205 starting_location: trip.starting_location,
206 ending_location: trip.ending_location,
207 pricing_type: trip.pricing_type || "regular",
208 })) as Trip[],
209 };
210 },
211 });
212
213 // Fetch traveler categories
214 const { data: travelerCategories = [] } = useQuery({
215 queryKey: ["traveler-categories"],
216 queryFn: async () => {
217 const response = await apiClient.get("/traveler-categories", {
218 params: { per_page: 100 },
219 });
220 return (response?.data || []) as TravelerCategory[];
221 },
222 });
223
224 // Get selected trip details
225 const selectedTrip = tripsData?.trips.find((t) => t.id === formData.trip_id);
226 const { data: tripForLocations } = useQuery({
227 queryKey: ["trip", formData.trip_id, "recurring-rule-locations"],
228 queryFn: async () => {
229 const response = await apiClient.get(`/trips/${formData.trip_id}`);
230 return response?.data || response || null;
231 },
232 enabled: formData.trip_id > 0,
233 staleTime: 5 * 60 * 1000,
234 });
235 const { data: fallbackTripData } = useQuery({
236 queryKey: ["trip", formData.trip_id, "recurring-rule-form"],
237 queryFn: async () => {
238 if (!formData.trip_id || selectedTrip) {
239 return null;
240 }
241 const response = await apiClient.get(`/trips/${formData.trip_id}`);
242 return response?.data || response || null;
243 },
244 enabled: !!formData.trip_id && !selectedTrip,
245 staleTime: 5 * 60 * 1000,
246 });
247 const effectiveTrip =
248 selectedTrip ||
249 (fallbackTripData
250 ? {
251 id: Number(fallbackTripData.id) || formData.trip_id,
252 title: fallbackTripData.title,
253 }
254 : null);
255 const tripNameLabel = effectiveTrip?.title || __("Unnamed Trip", "yatra");
256 let headerDescription = __(
257 "Set up automatic availability patterns for your trips",
258 "yatra",
259 );
260 if (effectiveTrip) {
261 headerDescription = `${isEditing ? __("Edit", "yatra") : __("Add", "yatra")} ${__("availability rule for", "yatra")} ${tripNameLabel} (Trip ID: ${effectiveTrip.id})`;
262 } else if (formData.trip_id) {
263 headerDescription = `${isEditing ? __("Edit", "yatra") : __("Add", "yatra")} ${__("availability rule for Trip ID:", "yatra")} ${formData.trip_id}`;
264 }
265 const isSingleDayTrip =
266 selectedTrip?.trip_type === "single_day" ||
267 (selectedTrip?.duration_days || 1) <= 1;
268 // Use form's pricing_type which defaults to trip's pricing type but can be overridden
269 const isTravelerBasedPricing = formData.pricing_type === "traveler_based";
270
271 // Fetch existing rule if editing
272 const { data: existingRule, isLoading: isLoadingRule } = useQuery({
273 queryKey: ["recurring-availability", ruleId],
274 queryFn: async () => {
275 if (!ruleId) return null;
276 const response = await apiClient.get(`/recurring-availability/${ruleId}`);
277 return response?.data || response; // Unwrap data property from API response
278 },
279 enabled: !!ruleId,
280 });
281
282 // Update form when existing rule is loaded
283 useEffect(() => {
284 if (existingRule && tripsData) {
285 // Parse days_of_week - handle both array and string formats
286 let daysOfWeek = [0]; // Default to Sunday
287 if (
288 existingRule.days_of_week_array &&
289 Array.isArray(existingRule.days_of_week_array)
290 ) {
291 daysOfWeek = existingRule.days_of_week_array;
292 } else if (existingRule.days_of_week) {
293 if (typeof existingRule.days_of_week === "string") {
294 daysOfWeek = existingRule.days_of_week
295 .split(",")
296 .map(Number)
297 .filter((n: number) => !isNaN(n));
298 } else if (Array.isArray(existingRule.days_of_week)) {
299 daysOfWeek = existingRule.days_of_week.map(Number);
300 }
301 }
302
303 // Ensure rule_type is properly typed
304 const ruleType = (existingRule.rule_type || "weekly") as
305 | "weekly"
306 | "monthly"
307 | "interval";
308
309 setFormData({
310 trip_id: existingRule.trip_id || 0,
311 name: existingRule.name || "",
312 rule_type: ruleType,
313 days_of_week: daysOfWeek.length > 0 ? daysOfWeek : [0],
314 week_of_month: existingRule.week_of_month || "first",
315 day_of_week: existingRule.day_of_week ?? 0,
316 interval_days: existingRule.interval_days || 7,
317 start_date:
318 existingRule.start_date || new Date().toISOString().split("T")[0],
319 end_date: existingRule.end_date || "",
320 excluded_dates: Array.isArray(existingRule.excluded_dates)
321 ? existingRule.excluded_dates
322 : [],
323 months: Array.isArray(existingRule.months) ? existingRule.months : [],
324 time_slots: Array.isArray(existingRule.time_slots)
325 ? existingRule.time_slots
326 : [],
327 pricing_type: existingRule.pricing_type || "regular",
328 original_price: existingRule.original_price,
329 sale_price: existingRule.sale_price,
330 traveler_pricing: Array.isArray(existingRule.traveler_pricing)
331 ? existingRule.traveler_pricing
332 : [],
333 seats_total: existingRule.seats_total || 20,
334 departure_time: existingRule.departure_time || "",
335 arrival_time: existingRule.arrival_time || "",
336 from_location: existingRule.from_location || "",
337 to_location: existingRule.to_location || "",
338 from_latitude: coordFromApi(existingRule.from_latitude),
339 from_longitude: coordFromApi(existingRule.from_longitude),
340 to_latitude: coordFromApi(existingRule.to_latitude),
341 to_longitude: coordFromApi(existingRule.to_longitude),
342 cutoff_hours: existingRule.cutoff_hours || 24,
343 alert_threshold: existingRule.alert_threshold || 5,
344 status: existingRule.status || "active",
345 });
346 }
347 }, [existingRule, tripsData]);
348
349 // Set pricing type based on selected trip when not editing
350 useEffect(() => {
351 if (!isEditing && !existingRule) {
352 setFormData((prev) => ({
353 ...prev,
354 ...(selectedTrip || tripForLocations
355 ? {
356 pricing_type: (selectedTrip?.pricing_type ||
357 (tripForLocations as Trip)?.pricing_type ||
358 "regular" ||
359 "regular") as "regular" | "traveler_based",
360 }
361 : {}),
362 ...(tripForLocations
363 ? {
364 from_location:
365 prev.from_location ||
366 (tripForLocations as Trip).starting_location ||
367 "",
368 to_location:
369 prev.to_location ||
370 (tripForLocations as Trip).ending_location ||
371 "",
372 from_latitude:
373 prev.from_latitude ||
374 coordFromApi((tripForLocations as Trip).starting_latitude),
375 from_longitude:
376 prev.from_longitude ||
377 coordFromApi((tripForLocations as Trip).starting_longitude),
378 to_latitude:
379 prev.to_latitude ||
380 coordFromApi((tripForLocations as Trip).ending_latitude),
381 to_longitude:
382 prev.to_longitude ||
383 coordFromApi((tripForLocations as Trip).ending_longitude),
384 }
385 : {}),
386 }));
387 }
388 }, [isEditing, selectedTrip, tripForLocations, existingRule]);
389
390 // Create mutation
391 const createMutation = useMutation({
392 mutationFn: async (data: RecurringRule) => {
393 return await apiClient.post("/recurring-availability", {
394 ...data,
395 days_of_week: data.days_of_week.join(","),
396 time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
397 traveler_pricing:
398 data.traveler_pricing && data.traveler_pricing.length > 0
399 ? data.traveler_pricing
400 : undefined,
401 });
402 },
403 onSuccess: () => {
404 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
405 showToast(__("Recurring rule created successfully", "yatra"), "success");
406 navigate({
407 subpage: "trips",
408 tab: "availability",
409 trip_id: formData.trip_id.toString(),
410 });
411 },
412 onError: (error: any) => {
413 showToast(
414 error?.message || __("Failed to create rule", "yatra"),
415 "error",
416 );
417 },
418 });
419
420 // Update mutation
421 const updateMutation = useMutation({
422 mutationFn: async (data: RecurringRule) => {
423 return await apiClient.put(`/recurring-availability/${ruleId}`, {
424 ...data,
425 days_of_week: data.days_of_week.join(","),
426 time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
427 traveler_pricing:
428 data.traveler_pricing && data.traveler_pricing.length > 0
429 ? data.traveler_pricing
430 : undefined,
431 });
432 },
433 onSuccess: () => {
434 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
435 showToast(__("Recurring rule updated successfully", "yatra"), "success");
436 navigate({
437 subpage: "trips",
438 tab: "availability",
439 trip_id: formData.trip_id.toString(),
440 });
441 },
442 onError: (error: any) => {
443 showToast(
444 error?.message || __("Failed to update rule", "yatra"),
445 "error",
446 );
447 },
448 });
449
450 // Preview mutation
451 const previewMutation = useMutation({
452 mutationFn: async (data: RecurringRule) => {
453 return await apiClient.post("/recurring-availability/preview", {
454 ...data,
455 days_of_week: data.days_of_week.join(","),
456 time_slots: data.time_slots.length > 0 ? data.time_slots : undefined,
457 traveler_pricing:
458 data.traveler_pricing && data.traveler_pricing.length > 0
459 ? data.traveler_pricing
460 : undefined,
461 preview_limit: 20,
462 });
463 },
464 onSuccess: (response) => {
465 // Unwrap the data property from API response
466 const previewResult = response?.data || response;
467 setPreviewData(previewResult);
468 },
469 onError: (error: any) => {
470 showToast(
471 error?.message || __("Failed to generate preview", "yatra"),
472 "error",
473 );
474 },
475 });
476
477 // Handle form submit
478 const handleSubmit = (e: React.FormEvent) => {
479 e.preventDefault();
480
481 if (!formData.trip_id) {
482 showToast(__("Please select a trip", "yatra"), "error");
483 return;
484 }
485
486 if (formData.rule_type === "weekly" && formData.days_of_week.length === 0) {
487 showToast(
488 __("Please select at least one day of the week", "yatra"),
489 "error",
490 );
491 return;
492 }
493
494 if (isEditing) {
495 updateMutation.mutate(formData);
496 } else {
497 createMutation.mutate(formData);
498 }
499 };
500
501 // Handle day toggle for weekly rules
502 const toggleDay = (day: number) => {
503 setFormData((prev) => ({
504 ...prev,
505 days_of_week: prev.days_of_week.includes(day)
506 ? prev.days_of_week.filter((d) => d !== day)
507 : [...prev.days_of_week, day].sort((a, b) => a - b),
508 }));
509 };
510
511 // Add excluded date
512 const addExcludedDate = () => {
513 if (newExcludedDate && !formData.excluded_dates.includes(newExcludedDate)) {
514 setFormData((prev) => ({
515 ...prev,
516 excluded_dates: [...prev.excluded_dates, newExcludedDate].sort(),
517 }));
518 setNewExcludedDate("");
519 }
520 };
521
522 // Remove excluded date
523 const removeExcludedDate = (date: string) => {
524 setFormData((prev) => ({
525 ...prev,
526 excluded_dates: prev.excluded_dates.filter((d) => d !== date),
527 }));
528 };
529
530 const isLoading = createMutation.isPending || updateMutation.isPending;
531
532 if (isEditing && isLoadingRule) {
533 return <RecurringRuleFormSkeleton />;
534 }
535
536 return (
537 <div className="space-y-6">
538 <PageHeader
539 title={
540 isEditing
541 ? __("Edit Recurring Rule", "yatra")
542 : __("Create Recurring Rule", "yatra")
543 }
544 description={headerDescription}
545 actions={
546 <Button
547 variant="outline"
548 onClick={() =>
549 navigate({
550 subpage: "trips",
551 tab: "availability",
552 trip_id: formData.trip_id?.toString() || tripIdFromUrl || "",
553 })
554 }
555 >
556 <ArrowLeft className="w-4 h-4 mr-2" />
557 {__("Back to Availability", "yatra")}
558 </Button>
559 }
560 />
561
562 <form onSubmit={handleSubmit}>
563 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
564 {/* Main Form */}
565 <div className="lg:col-span-2 space-y-6">
566 {/* Basic Info */}
567 <Card>
568 <CardHeader>
569 <CardTitle>{__("Basic Information", "yatra")}</CardTitle>
570 </CardHeader>
571 <CardContent className="space-y-4">
572 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
573 <div>
574 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
575 {__("Trip", "yatra")}{" "}
576 <span className="text-red-500">*</span>
577 </label>
578 <SearchableSelect
579 value={formData.trip_id?.toString() || ""}
580 onChange={(value) =>
581 setFormData((prev) => ({
582 ...prev,
583 trip_id: parseInt(value) || 0,
584 // Reset pricing when trip changes and set pricing_type based on new trip
585 pricing_type: (tripsData?.trips.find(
586 (t) => t.id === parseInt(value),
587 )?.pricing_type || "regular") as
588 | "regular"
589 | "traveler_based",
590 traveler_pricing: [],
591 original_price: undefined,
592 sale_price: undefined,
593 }))
594 }
595 options={[
596 { value: "", label: __("-- Select Trip --", "yatra") },
597 ...(tripsData?.trips.map((trip) => ({
598 value: trip.id.toString(),
599 label: `${trip.title} (${trip.pricing_type === "traveler_based" ? "Traveler-Based" : "Regular"})`,
600 })) || []),
601 ]}
602 placeholder={__("Select a trip", "yatra")}
603 disabled={isEditing}
604 />
605 </div>
606 <div>
607 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
608 {__("Rule Name", "yatra")}
609 </label>
610 <Input
611 type="text"
612 value={formData.name}
613 onChange={(e) =>
614 setFormData((prev) => ({
615 ...prev,
616 name: e.target.value,
617 }))
618 }
619 placeholder={__("e.g., Weekend Departures", "yatra")}
620 />
621 </div>
622 </div>
623
624 <div>
625 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
626 {__("Rule Type", "yatra")}{" "}
627 <span className="text-red-500">*</span>
628 </label>
629 <Select
630 value={formData.rule_type}
631 onChange={(e) =>
632 setFormData((prev) => ({
633 ...prev,
634 rule_type: e.target.value as
635 | "weekly"
636 | "monthly"
637 | "interval",
638 }))
639 }
640 >
641 <option value="weekly">
642 {__("Weekly (specific days)", "yatra")}
643 </option>
644 <option value="monthly">
645 {__("Monthly (e.g., first Sunday)", "yatra")}
646 </option>
647 <option value="interval">
648 {__("Interval (every X days)", "yatra")}
649 </option>
650 </Select>
651 </div>
652 </CardContent>
653 </Card>
654
655 {/* Pattern Configuration */}
656 <Card>
657 <CardHeader>
658 <CardTitle className="flex items-center gap-2">
659 <RefreshCw className="w-5 h-5" />
660 {__("Recurrence Pattern", "yatra")}
661 </CardTitle>
662 </CardHeader>
663 <CardContent className="space-y-4">
664 {formData.rule_type === "weekly" && (
665 <div>
666 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
667 {__("Days of Week", "yatra")}{" "}
668 <span className="text-red-500">*</span>
669 </label>
670 <div className="flex flex-wrap gap-2">
671 {dayOptions.map((day) => (
672 <button
673 key={day.value}
674 type="button"
675 onClick={() => toggleDay(day.value)}
676 className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
677 formData.days_of_week.includes(day.value)
678 ? "bg-blue-600 text-white"
679 : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
680 }`}
681 >
682 {day.label}
683 </button>
684 ))}
685 </div>
686 </div>
687 )}
688
689 {formData.rule_type === "monthly" && (
690 <div className="grid grid-cols-2 gap-4">
691 <div>
692 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
693 {__("Week of Month", "yatra")}
694 </label>
695 <Select
696 value={formData.week_of_month || "first"}
697 onChange={(e) =>
698 setFormData((prev) => ({
699 ...prev,
700 week_of_month: e.target.value as any,
701 }))
702 }
703 >
704 {weekOptions.map((week) => (
705 <option key={week.value} value={week.value}>
706 {week.label}
707 </option>
708 ))}
709 </Select>
710 </div>
711 <div>
712 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
713 {__("Day of Week", "yatra")}
714 </label>
715 <Select
716 value={formData.day_of_week?.toString() || "0"}
717 onChange={(e) =>
718 setFormData((prev) => ({
719 ...prev,
720 day_of_week: parseInt(e.target.value),
721 }))
722 }
723 >
724 {dayOptions.map((day) => (
725 <option key={day.value} value={day.value}>
726 {day.label}
727 </option>
728 ))}
729 </Select>
730 </div>
731 </div>
732 )}
733
734 {formData.rule_type === "interval" && (
735 <div>
736 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
737 {__("Every X Days", "yatra")}
738 </label>
739 <Input
740 type="number"
741 min={1}
742 max={365}
743 value={formData.interval_days || 7}
744 onChange={(e) =>
745 setFormData((prev) => ({
746 ...prev,
747 interval_days: parseInt(e.target.value) || 7,
748 }))
749 }
750 />
751 </div>
752 )}
753
754 {/* Date Range */}
755 <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
756 <div>
757 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
758 {__("Start Date", "yatra")}{" "}
759 <span className="text-red-500">*</span>
760 </label>
761 <DatePicker
762 value={formData.start_date}
763 onChange={(value: string) =>
764 setFormData((prev) => ({ ...prev, start_date: value }))
765 }
766 placeholder={__("Select start date", "yatra")}
767 />
768 </div>
769 <div>
770 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
771 {__("End Date", "yatra")}{" "}
772 <span className="text-gray-400">
773 ({__("optional", "yatra")})
774 </span>
775 </label>
776 <DatePicker
777 value={formData.end_date || ""}
778 onChange={(value: string) =>
779 setFormData((prev) => ({ ...prev, end_date: value }))
780 }
781 minDate={
782 formData.start_date
783 ? new Date(formData.start_date)
784 : undefined
785 }
786 placeholder={__("Select end date (optional)", "yatra")}
787 />
788 </div>
789 </div>
790
791 {/* Month Filter */}
792 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
793 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
794 {__("Specific Months", "yatra")}{" "}
795 <span className="text-gray-400">
796 ({__("optional - leave empty for all months", "yatra")})
797 </span>
798 </label>
799 <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
800 {[
801 { value: 1, label: __("January", "yatra") },
802 { value: 2, label: __("February", "yatra") },
803 { value: 3, label: __("March", "yatra") },
804 { value: 4, label: __("April", "yatra") },
805 { value: 5, label: __("May", "yatra") },
806 { value: 6, label: __("June", "yatra") },
807 { value: 7, label: __("July", "yatra") },
808 { value: 8, label: __("August", "yatra") },
809 { value: 9, label: __("September", "yatra") },
810 { value: 10, label: __("October", "yatra") },
811 { value: 11, label: __("November", "yatra") },
812 { value: 12, label: __("December", "yatra") },
813 ].map((month) => (
814 <label
815 key={month.value}
816 className={`flex items-center justify-center px-3 py-2 rounded-md border cursor-pointer transition-colors ${
817 formData.months.includes(month.value)
818 ? "bg-blue-50 border-blue-500 text-blue-700 dark:bg-blue-900/20 dark:border-blue-600 dark:text-blue-300"
819 : "border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800"
820 }`}
821 >
822 <input
823 type="checkbox"
824 checked={formData.months.includes(month.value)}
825 onChange={(e) => {
826 const checked = e.target.checked;
827 setFormData((prev) => ({
828 ...prev,
829 months: checked
830 ? [...prev.months, month.value].sort(
831 (a, b) => a - b,
832 )
833 : prev.months.filter((m) => m !== month.value),
834 }));
835 }}
836 className="sr-only"
837 />
838 <span className="text-sm font-medium">
839 {month.label.slice(0, 3)}
840 </span>
841 </label>
842 ))}
843 </div>
844 {formData.months.length > 0 && (
845 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
846 {__("Selected:", "yatra")} {formData.months.length}{" "}
847 {formData.months.length === 1
848 ? __("month", "yatra")
849 : __("months", "yatra")}
850 </p>
851 )}
852 </div>
853
854 {/* Excluded Dates */}
855 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
856 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
857 {__("Excluded Dates", "yatra")}{" "}
858 <span className="text-gray-400">
859 ({__("holidays, etc.", "yatra")})
860 </span>
861 </label>
862 <div className="flex gap-2 mb-2">
863 <div className="flex-1">
864 <DatePicker
865 value={newExcludedDate}
866 onChange={(value: string) => setNewExcludedDate(value)}
867 placeholder={__("Select date to exclude", "yatra")}
868 />
869 </div>
870 <Button
871 type="button"
872 variant="outline"
873 onClick={addExcludedDate}
874 disabled={!newExcludedDate}
875 >
876 <Plus className="w-4 h-4" />
877 </Button>
878 </div>
879 {formData.excluded_dates.length > 0 && (
880 <div className="flex flex-wrap gap-2">
881 {formData.excluded_dates.map((date) => (
882 <Badge
883 key={date}
884 variant="outline"
885 className="flex items-center gap-1"
886 >
887 {new Date(date + "T00:00:00").toLocaleDateString(
888 "en-US",
889 {
890 weekday: "short",
891 month: "short",
892 day: "numeric",
893 year: "numeric",
894 },
895 )}
896 <button
897 type="button"
898 onClick={() => removeExcludedDate(date)}
899 className="ml-1 hover:text-red-500"
900 >
901 <X className="w-3 h-3" />
902 </button>
903 </Badge>
904 ))}
905 </div>
906 )}
907 </div>
908 </CardContent>
909 </Card>
910
911 {/* Pricing & Capacity */}
912 <Card>
913 <CardHeader>
914 <CardTitle className="flex items-center gap-2">
915 <DollarSign className="w-5 h-5" />
916 {__("Pricing & Availability", "yatra")}
917 </CardTitle>
918 <CardDescription>
919 {__(
920 "Set pricing for traveler categories and seat availability for this rule",
921 "yatra",
922 )}
923 </CardDescription>
924 </CardHeader>
925 <CardContent className="space-y-6">
926 {/* Pricing Override Info */}
927 <div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-4">
928 <div className="flex items-start gap-3">
929 <div className="p-2 rounded-full bg-amber-100 dark:bg-amber-900/30">
930 <svg
931 className="w-5 h-5 text-amber-600 dark:text-amber-400"
932 fill="none"
933 stroke="currentColor"
934 viewBox="0 0 24 24"
935 >
936 <path
937 strokeLinecap="round"
938 strokeLinejoin="round"
939 strokeWidth={2}
940 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
941 />
942 </svg>
943 </div>
944 <div className="flex-1">
945 <p className="text-sm font-medium text-gray-900 dark:text-white mb-1">
946 {__("Pricing Override (Optional)", "yatra")}
947 </p>
948 <p className="text-xs text-gray-600 dark:text-gray-400">
949 {__(
950 "Leave pricing fields empty to use the trip's default pricing. Fill them in only if you want to override the default pricing for dates generated by this rule.",
951 "yatra",
952 )}
953 </p>
954 </div>
955 </div>
956 </div>
957
958 {/* Pricing Type Info - Inherited from Trip */}
959 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
960 <div className="flex items-center gap-3">
961 <div
962 className={`p-2 rounded-full ${isTravelerBasedPricing ? "bg-green-100 dark:bg-green-900/30" : "bg-blue-100 dark:bg-blue-900/30"}`}
963 >
964 {isTravelerBasedPricing ? (
965 <svg
966 className="w-5 h-5 text-green-600 dark:text-green-400"
967 fill="none"
968 stroke="currentColor"
969 viewBox="0 0 24 24"
970 >
971 <path
972 strokeLinecap="round"
973 strokeLinejoin="round"
974 strokeWidth={2}
975 d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
976 />
977 </svg>
978 ) : (
979 <svg
980 className="w-5 h-5 text-blue-600 dark:text-blue-400"
981 fill="none"
982 stroke="currentColor"
983 viewBox="0 0 24 24"
984 >
985 <path
986 strokeLinecap="round"
987 strokeLinejoin="round"
988 strokeWidth={2}
989 d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
990 />
991 </svg>
992 )}
993 </div>
994 <div>
995 <p className="text-sm font-medium text-gray-900 dark:text-white">
996 {isTravelerBasedPricing
997 ? __("Traveler-Based Pricing", "yatra")
998 : __("Regular Pricing", "yatra")}
999 </p>
1000 <p className="text-xs text-gray-600 dark:text-gray-400">
1001 {isTravelerBasedPricing
1002 ? __(
1003 "This trip uses traveler category pricing. Set prices for each category below.",
1004 "yatra",
1005 )
1006 : __(
1007 "This trip uses regular pricing. Set a single price for all travelers below.",
1008 "yatra",
1009 )}
1010 </p>
1011 </div>
1012 </div>
1013 </div>
1014
1015 {/* Regular Pricing Fields */}
1016 {!isTravelerBasedPricing && (
1017 <div className="space-y-4">
1018 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1019 <div>
1020 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1021 {__("Original Price", "yatra")}{" "}
1022 <span className="text-gray-400 text-xs">
1023 ({__("Optional", "yatra")})
1024 </span>
1025 </label>
1026 <Input
1027 type="number"
1028 min={0}
1029 step="0.01"
1030 value={formData.original_price || ""}
1031 onChange={(e) =>
1032 setFormData((prev) => ({
1033 ...prev,
1034 original_price:
1035 parseFloat(e.target.value) || undefined,
1036 }))
1037 }
1038 placeholder="0.00"
1039 />
1040 </div>
1041 <div>
1042 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1043 {__("Sale Price", "yatra")}
1044 </label>
1045 <Input
1046 type="number"
1047 min={0}
1048 step="0.01"
1049 value={formData.sale_price || ""}
1050 onChange={(e) =>
1051 setFormData((prev) => ({
1052 ...prev,
1053 sale_price:
1054 parseFloat(e.target.value) || undefined,
1055 }))
1056 }
1057 placeholder="0.00"
1058 />
1059 <p className="mt-1 text-xs text-gray-500">
1060 {__("Leave empty if no discount", "yatra")}
1061 </p>
1062 </div>
1063 </div>
1064 </div>
1065 )}
1066
1067 {/* Traveler-Based Pricing */}
1068 {isTravelerBasedPricing && (
1069 <div className="space-y-4">
1070 <div>
1071 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1072 {__("Traveler Category Pricing", "yatra")}{" "}
1073 <span className="text-red-500">*</span>
1074 </label>
1075 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
1076 {__(
1077 "Add pricing for traveler categories. Categories are managed in Traveler Categories page.",
1078 "yatra",
1079 )}
1080 </p>
1081 </div>
1082
1083 {/* Active Categories Filter */}
1084 {(() => {
1085 const activeCategories = travelerCategories.filter(
1086 (cat: TravelerCategory) =>
1087 cat.status === "active" || cat.status === "publish",
1088 );
1089
1090 if (activeCategories.length === 0) {
1091 return (
1092 <div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg text-center">
1093 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
1094 {__(
1095 "No active traveler categories found.",
1096 "yatra",
1097 )}
1098 </p>
1099 <Button
1100 type="button"
1101 variant="outline"
1102 onClick={() =>
1103 (window.location.href =
1104 "?page=yatra&subpage=traveler-categories&action=create")
1105 }
1106 className="flex items-center gap-2 mx-auto"
1107 >
1108 <Plus className="w-4 h-4" />
1109 {__("Create Category", "yatra")}
1110 </Button>
1111 </div>
1112 );
1113 }
1114
1115 return (
1116 <div className="space-y-4">
1117 {/* Add Pricing Button with Dropdown */}
1118 <div className="relative">
1119 <Button
1120 type="button"
1121 variant="outline"
1122 onClick={() =>
1123 setShowCategorySelector(!showCategorySelector)
1124 }
1125 className="flex items-center gap-2"
1126 disabled={
1127 activeCategories.filter(
1128 (cat) =>
1129 !formData.traveler_pricing?.some(
1130 (tp) => tp.category_id === cat.id,
1131 ),
1132 ).length === 0
1133 }
1134 >
1135 <Plus className="w-4 h-4" />
1136 {__("Add Pricing", "yatra")}
1137 </Button>
1138
1139 {/* Category Selection Dropdown */}
1140 {showCategorySelector && (
1141 <>
1142 <div
1143 className="fixed inset-0 z-10"
1144 onClick={() => setShowCategorySelector(false)}
1145 />
1146 <div className="absolute top-full left-0 mt-2 w-full max-w-md bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20 max-h-96 overflow-y-auto">
1147 <div className="p-2">
1148 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
1149 {__(
1150 "Select a category to add pricing",
1151 "yatra",
1152 )}
1153 </div>
1154 {activeCategories.filter(
1155 (cat) =>
1156 !formData.traveler_pricing?.some(
1157 (tp) => tp.category_id === cat.id,
1158 ),
1159 ).length === 0 ? (
1160 <div className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
1161 {__(
1162 "All categories have pricing added",
1163 "yatra",
1164 )}
1165 </div>
1166 ) : (
1167 <div className="space-y-1">
1168 {activeCategories
1169 .filter(
1170 (cat) =>
1171 !formData.traveler_pricing?.some(
1172 (tp) =>
1173 tp.category_id === cat.id,
1174 ),
1175 )
1176 .map((category: TravelerCategory) => {
1177 const minAge =
1178 category.age_min ??
1179 category.min_age;
1180 const maxAge =
1181 category.age_max ??
1182 category.max_age;
1183 const ageRange =
1184 minAge !== undefined ||
1185 maxAge !== undefined
1186 ? minAge !== undefined &&
1187 maxAge !== undefined
1188 ? `${minAge}-${maxAge} ${__("years", "yatra")}`
1189 : minAge !== undefined
1190 ? `${minAge}+ ${__("years", "yatra")}`
1191 : maxAge !== undefined
1192 ? `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}`
1193 : ""
1194 : null;
1195 const categoryName =
1196 category.label ||
1197 category.name ||
1198 `Category ${category.id}`;
1199
1200 return (
1201 <button
1202 key={category.id}
1203 type="button"
1204 onClick={() => {
1205 setFormData((prev) => ({
1206 ...prev,
1207 traveler_pricing: [
1208 ...(prev.traveler_pricing ||
1209 []),
1210 {
1211 category_id:
1212 category.id,
1213 original_price: 0,
1214 sale_price: undefined,
1215 },
1216 ],
1217 }));
1218 setShowCategorySelector(
1219 false,
1220 );
1221 }}
1222 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
1223 >
1224 <div className="font-medium text-sm text-gray-900 dark:text-white">
1225 {categoryName}
1226 {ageRange && (
1227 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1228 ({ageRange})
1229 </span>
1230 )}
1231 </div>
1232 {category.description && (
1233 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1234 {category.description}
1235 </div>
1236 )}
1237 </button>
1238 );
1239 })}
1240 </div>
1241 )}
1242 </div>
1243 </div>
1244 </>
1245 )}
1246 </div>
1247
1248 {/* Added Pricing List */}
1249 {formData.traveler_pricing &&
1250 formData.traveler_pricing.length > 0 && (
1251 <div className="space-y-3">
1252 {formData.traveler_pricing.map(
1253 (pricing, index) => {
1254 const category = activeCategories.find(
1255 (cat) => cat.id === pricing.category_id,
1256 );
1257 if (!category) return null;
1258
1259 const minAge =
1260 category.age_min ?? category.min_age;
1261 const maxAge =
1262 category.age_max ?? category.max_age;
1263 const categoryName =
1264 category.label ||
1265 category.name ||
1266 `Category ${pricing.category_id}`;
1267
1268 return (
1269 <div
1270 key={pricing.category_id}
1271 className="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
1272 >
1273 <div className="flex items-start justify-between mb-3">
1274 <div className="flex-1">
1275 <div className="flex items-center gap-2 mb-1">
1276 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1277 {categoryName}
1278 {(minAge !== undefined ||
1279 maxAge !== undefined) && (
1280 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1281 (
1282 {minAge !== undefined &&
1283 maxAge !== undefined
1284 ? `${minAge}-${maxAge} ${__("years", "yatra")}`
1285 : minAge !== undefined
1286 ? `${minAge}+ ${__("years", "yatra")}`
1287 : maxAge !== undefined
1288 ? `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}`
1289 : ""}
1290 )
1291 </span>
1292 )}
1293 </h4>
1294 </div>
1295 {category.description && (
1296 <p className="text-xs text-gray-600 dark:text-gray-400">
1297 {category.description}
1298 </p>
1299 )}
1300 </div>
1301 <button
1302 type="button"
1303 onClick={() =>
1304 setFormData((prev) => ({
1305 ...prev,
1306 traveler_pricing:
1307 prev.traveler_pricing?.filter(
1308 (_, i) => i !== index,
1309 ) || [],
1310 }))
1311 }
1312 className="p-1 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1313 title={__(
1314 "Remove Pricing",
1315 "yatra",
1316 )}
1317 >
1318 <X className="w-4 h-4" />
1319 </button>
1320 </div>
1321 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1322 <div>
1323 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1324 {__("Original Price", "yatra")}{" "}
1325 <span className="text-red-500">
1326 *
1327 </span>
1328 </label>
1329 <Input
1330 type="number"
1331 min={0}
1332 step="0.01"
1333 value={
1334 pricing.original_price || ""
1335 }
1336 onChange={(e) => {
1337 const newPricing = [
1338 ...(formData.traveler_pricing ||
1339 []),
1340 ];
1341 newPricing[
1342 index
1343 ].original_price =
1344 parseFloat(e.target.value) ||
1345 0;
1346 setFormData((prev) => ({
1347 ...prev,
1348 traveler_pricing: newPricing,
1349 }));
1350 }}
1351 placeholder="0.00"
1352 />
1353 </div>
1354 <div>
1355 <label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1356 {__("Sale Price", "yatra")} (
1357 {__("Optional", "Optional")})
1358 </label>
1359 <Input
1360 type="number"
1361 min={0}
1362 step="0.01"
1363 value={pricing.sale_price || ""}
1364 onChange={(e) => {
1365 const newPricing = [
1366 ...(formData.traveler_pricing ||
1367 []),
1368 ];
1369 newPricing[index].sale_price =
1370 parseFloat(e.target.value) ||
1371 undefined;
1372 setFormData((prev) => ({
1373 ...prev,
1374 traveler_pricing: newPricing,
1375 }));
1376 }}
1377 className="text-sm"
1378 placeholder="0.00"
1379 />
1380 </div>
1381 </div>
1382 </div>
1383 );
1384 },
1385 )}
1386 </div>
1387 )}
1388 </div>
1389 );
1390 })()}
1391 </div>
1392 )}
1393
1394 {/* Inventory Management - Common for both pricing types */}
1395 {selectedTrip && (
1396 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
1397 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
1398 {__("Inventory Management", "yatra")}
1399 </h4>
1400 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1401 <div>
1402 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1403 {__("Total Capacity", "yatra")}{" "}
1404 <span className="text-red-500">*</span>
1405 </label>
1406 <Input
1407 type="number"
1408 min={1}
1409 value={formData.seats_total}
1410 onChange={(e) =>
1411 setFormData((prev) => ({
1412 ...prev,
1413 seats_total: parseInt(e.target.value) || 1,
1414 }))
1415 }
1416 />
1417 <p className="mt-1 text-xs text-gray-500">
1418 {__(
1419 "Maximum number of seats available for this rule",
1420 "yatra",
1421 )}
1422 </p>
1423 </div>
1424 <div>
1425 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1426 {__("Alert Threshold", "yatra")}
1427 </label>
1428 <Input
1429 type="number"
1430 min={0}
1431 value={formData.alert_threshold}
1432 onChange={(e) =>
1433 setFormData((prev) => ({
1434 ...prev,
1435 alert_threshold: parseInt(e.target.value) || 0,
1436 }))
1437 }
1438 />
1439 <p className="mt-1 text-xs text-gray-500">
1440 {__(
1441 "Alert when available seats drop below this number",
1442 "yatra",
1443 )}
1444 </p>
1445 </div>
1446 </div>
1447 </div>
1448 )}
1449
1450 {/* No trip selected message */}
1451 {!selectedTrip && (
1452 <div className="text-center py-6 bg-gray-50 dark:bg-gray-800 rounded-lg">
1453 <AlertCircle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
1454 <p className="text-sm text-gray-500 dark:text-gray-400">
1455 {__("Select a trip above to configure pricing", "yatra")}
1456 </p>
1457 </div>
1458 )}
1459 </CardContent>
1460 </Card>
1461
1462 {/* Time & Location */}
1463 <Card>
1464 <CardHeader>
1465 <CardTitle className="flex items-center gap-2">
1466 <Clock className="w-5 h-5" />
1467 {__("Time & Location", "yatra")}
1468 </CardTitle>
1469 {selectedTrip && (
1470 <CardDescription>
1471 <Badge
1472 variant={isSingleDayTrip ? "default" : "outline"}
1473 className="mr-2"
1474 >
1475 {isSingleDayTrip
1476 ? __("Single-Day Trip", "yatra")
1477 : __("Multi-Day Trip", "yatra")}
1478 </Badge>
1479 {!isSingleDayTrip && selectedTrip.duration_days && (
1480 <span className="text-gray-500">
1481 ({selectedTrip.duration_days} {__("days", "yatra")})
1482 </span>
1483 )}
1484 </CardDescription>
1485 )}
1486 </CardHeader>
1487 <CardContent className="space-y-4">
1488 {/* Single-Day Trip: Multiple Time Slots */}
1489 {isSingleDayTrip && formData.trip_id > 0 && (
1490 <div className="space-y-4">
1491 <div className="flex items-center justify-between">
1492 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
1493 {__("Time Slots", "yatra")}
1494 <span className="text-gray-400 ml-1">
1495 ({__("for each recurring day", "yatra")})
1496 </span>
1497 </label>
1498 <Button
1499 type="button"
1500 variant="outline"
1501 size="sm"
1502 onClick={() =>
1503 setFormData((prev) => ({
1504 ...prev,
1505 time_slots: [
1506 ...prev.time_slots,
1507 {
1508 departure_time: "09:00",
1509 arrival_time: "17:00",
1510 seats: 20,
1511 price: 0,
1512 traveler_pricing: isTravelerBasedPricing
1513 ? []
1514 : undefined,
1515 },
1516 ],
1517 }))
1518 }
1519 >
1520 <Plus className="w-4 h-4 mr-1" />
1521 {__("Add Slot", "yatra")}
1522 </Button>
1523 </div>
1524
1525 {formData.time_slots.length === 0 ? (
1526 <div className="text-center py-6 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-200 dark:border-gray-700">
1527 <Clock className="w-8 h-8 text-gray-400 mx-auto mb-2" />
1528 <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
1529 {__("No time slots added yet", "yatra")}
1530 </p>
1531 <p className="text-xs text-gray-400 dark:text-gray-500">
1532 {__(
1533 "Add multiple time slots for tours throughout the day (e.g., morning, afternoon, evening)",
1534 "yatra",
1535 )}
1536 </p>
1537 </div>
1538 ) : (
1539 <div className="space-y-3">
1540 {formData.time_slots.map((slot, index) => (
1541 <div
1542 key={index}
1543 className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
1544 >
1545 <div className="flex items-center justify-between mb-3">
1546 <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
1547 {__("Slot", "yatra")} {index + 1}
1548 </span>
1549 <Button
1550 type="button"
1551 variant="ghost"
1552 size="sm"
1553 onClick={() =>
1554 setFormData((prev) => ({
1555 ...prev,
1556 time_slots: prev.time_slots.filter(
1557 (_, i) => i !== index,
1558 ),
1559 }))
1560 }
1561 className="text-red-500 hover:text-red-600 h-8 w-8 p-0"
1562 >
1563 <X className="w-4 h-4" />
1564 </Button>
1565 </div>
1566 <div className="space-y-3">
1567 {/* Time fields on first line */}
1568 <div className="grid grid-cols-2 gap-3">
1569 <div>
1570 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1571 {__("Departure Time", "yatra")}
1572 </label>
1573 <TimePicker
1574 value={slot.departure_time || ""}
1575 onChange={(value: string) => {
1576 const newSlots = [...formData.time_slots];
1577 newSlots[index].departure_time = value;
1578 setFormData((prev) => ({
1579 ...prev,
1580 time_slots: newSlots,
1581 }));
1582 }}
1583 placeholder="09:00"
1584 />
1585 </div>
1586 <div>
1587 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1588 {__("Arrival Time", "yatra")}
1589 </label>
1590 <TimePicker
1591 value={slot.arrival_time || ""}
1592 onChange={(value: string) => {
1593 const newSlots = [...formData.time_slots];
1594 newSlots[index].arrival_time = value;
1595 setFormData((prev) => ({
1596 ...prev,
1597 time_slots: newSlots,
1598 }));
1599 }}
1600 placeholder="17:00"
1601 />
1602 </div>
1603 </div>
1604
1605 {/* Seats, Price and Sale Price on second line */}
1606 {!isTravelerBasedPricing && (
1607 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
1608 <div>
1609 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1610 {__("Seats", "yatra")}
1611 </label>
1612 <Input
1613 type="number"
1614 min={1}
1615 value={slot.seats}
1616 onChange={(e) => {
1617 const newSlots = [
1618 ...formData.time_slots,
1619 ];
1620 newSlots[index].seats =
1621 parseInt(e.target.value) || 1;
1622 setFormData((prev) => ({
1623 ...prev,
1624 time_slots: newSlots,
1625 }));
1626 }}
1627 className="text-sm"
1628 />
1629 </div>
1630 <div>
1631 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1632 {__("Price", "yatra")}
1633 </label>
1634 <Input
1635 type="number"
1636 min={0}
1637 step="0.01"
1638 value={slot.price || ""}
1639 onChange={(e) => {
1640 const newSlots = [
1641 ...formData.time_slots,
1642 ];
1643 newSlots[index].price =
1644 parseFloat(e.target.value) || 0;
1645 setFormData((prev) => ({
1646 ...prev,
1647 time_slots: newSlots,
1648 }));
1649 }}
1650 className="text-sm"
1651 placeholder="0.00"
1652 />
1653 </div>
1654 <div>
1655 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1656 {__("Sale Price", "yatra")}
1657 </label>
1658 <Input
1659 type="number"
1660 min={0}
1661 step="0.01"
1662 value={slot.sale_price || ""}
1663 onChange={(e) => {
1664 const newSlots = [
1665 ...formData.time_slots,
1666 ];
1667 newSlots[index].sale_price =
1668 parseFloat(e.target.value) ||
1669 undefined;
1670 setFormData((prev) => ({
1671 ...prev,
1672 time_slots: newSlots,
1673 }));
1674 }}
1675 className="text-sm"
1676 placeholder="0.00"
1677 />
1678 </div>
1679 </div>
1680 )}
1681
1682 {/* For traveler-based pricing, only show seats on second line */}
1683 {isTravelerBasedPricing && (
1684 <div>
1685 <label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
1686 {__("Seats", "yatra")}
1687 </label>
1688 <Input
1689 type="number"
1690 min={1}
1691 value={slot.seats}
1692 onChange={(e) => {
1693 const newSlots = [...formData.time_slots];
1694 newSlots[index].seats =
1695 parseInt(e.target.value) || 1;
1696 setFormData((prev) => ({
1697 ...prev,
1698 time_slots: newSlots,
1699 }));
1700 }}
1701 className="text-sm"
1702 />
1703 </div>
1704 )}
1705 </div>
1706
1707 {/* Traveler Pricing for Time Slot - only show when traveler-based */}
1708 {isTravelerBasedPricing && (
1709 <div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
1710 <div className="flex items-center justify-between mb-3">
1711 <div>
1712 <span className="text-xs font-medium text-gray-700 dark:text-gray-300">
1713 {__("Traveler Category Pricing", "yatra")}
1714 </span>
1715 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1716 {__(
1717 "Set pricing for each traveler category for this time slot",
1718 "yatra",
1719 )}
1720 </p>
1721 </div>
1722 </div>
1723
1724 {(() => {
1725 const activeCategories =
1726 travelerCategories.filter(
1727 (cat: TravelerCategory) =>
1728 cat.status === "active" ||
1729 cat.status === "publish",
1730 );
1731 const slotTravelerPricing =
1732 slot.traveler_pricing || [];
1733 const availableCategories =
1734 activeCategories.filter(
1735 (cat) =>
1736 !slotTravelerPricing.some(
1737 (tp) => tp.category_id === cat.id,
1738 ),
1739 );
1740
1741 return (
1742 <div className="space-y-3">
1743 {/* Add Pricing Button with Dropdown */}
1744 <div className="relative">
1745 <Button
1746 type="button"
1747 variant="outline"
1748 size="sm"
1749 onClick={() => {
1750 // Toggle dropdown for this specific slot
1751 const dropdownId = `slot-${index}-category-dropdown`;
1752 const dropdown =
1753 document.getElementById(
1754 dropdownId,
1755 );
1756 if (dropdown) {
1757 dropdown.classList.toggle(
1758 "hidden",
1759 );
1760 }
1761 }}
1762 disabled={
1763 availableCategories.length === 0
1764 }
1765 >
1766 <Plus className="w-3 h-3 mr-1" />
1767 {__("Add Pricing", "yatra")}
1768 </Button>
1769
1770 {/* Category Selection Dropdown */}
1771 <div
1772 id={`slot-${index}-category-dropdown`}
1773 className="hidden absolute top-full left-0 mt-2 w-full max-w-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20 max-h-64 overflow-y-auto"
1774 >
1775 <div className="p-2">
1776 <div className="text-xs font-medium text-gray-700 dark:text-gray-300 px-3 py-2 mb-1">
1777 {__(
1778 "Select a category to add pricing",
1779 "yatra",
1780 )}
1781 </div>
1782 {availableCategories.length ===
1783 0 ? (
1784 <div className="px-3 py-3 text-xs text-gray-500 dark:text-gray-400 text-center">
1785 {__(
1786 "All categories have pricing added",
1787 "yatra",
1788 )}
1789 </div>
1790 ) : (
1791 <div className="space-y-1">
1792 {availableCategories.map(
1793 (
1794 category: TravelerCategory,
1795 ) => {
1796 const minAge =
1797 category.age_min ??
1798 category.min_age;
1799 const maxAge =
1800 category.age_max ??
1801 category.max_age;
1802 const ageRange =
1803 minAge !== undefined ||
1804 maxAge !== undefined
1805 ? minAge !==
1806 undefined &&
1807 maxAge !== undefined
1808 ? `${minAge}-${maxAge} ${__("years", "yatra")}`
1809 : minAge !== undefined
1810 ? `${minAge}+ ${__("years", "yatra")}`
1811 : `${__("Under", "yatra")} ${maxAge} ${__("years", "yatra")}`
1812 : null;
1813 const categoryName =
1814 category.label ||
1815 category.name ||
1816 `Category ${category.id}`;
1817
1818 return (
1819 <button
1820 key={category.id}
1821 type="button"
1822 onClick={() => {
1823 const newSlots = [
1824 ...formData.time_slots,
1825 ];
1826 if (
1827 !newSlots[index]
1828 .traveler_pricing
1829 ) {
1830 newSlots[
1831 index
1832 ].traveler_pricing =
1833 [];
1834 }
1835 newSlots[
1836 index
1837 ].traveler_pricing = [
1838 ...newSlots[index]
1839 .traveler_pricing!,
1840 {
1841 category_id:
1842 category.id,
1843 original_price: 0,
1844 sale_price:
1845 undefined,
1846 },
1847 ];
1848 setFormData(
1849 (prev) => ({
1850 ...prev,
1851 time_slots:
1852 newSlots,
1853 }),
1854 );
1855 // Hide dropdown
1856 const dropdown =
1857 document.getElementById(
1858 `slot-${index}-category-dropdown`,
1859 );
1860 if (dropdown)
1861 dropdown.classList.add(
1862 "hidden",
1863 );
1864 }}
1865 className="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
1866 >
1867 <div className="font-medium text-xs text-gray-900 dark:text-white">
1868 {categoryName}
1869 {ageRange && (
1870 <span className="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
1871 ({ageRange})
1872 </span>
1873 )}
1874 </div>
1875 {category.description && (
1876 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">
1877 {
1878 category.description
1879 }
1880 </div>
1881 )}
1882 </button>
1883 );
1884 },
1885 )}
1886 </div>
1887 )}
1888 </div>
1889 </div>
1890 </div>
1891
1892 {/* Added Pricing List */}
1893 {slotTravelerPricing.length > 0 && (
1894 <div className="space-y-2">
1895 {slotTravelerPricing.map(
1896 (tp, tpIndex) => {
1897 const category =
1898 activeCategories.find(
1899 (cat) =>
1900 cat.id === tp.category_id,
1901 );
1902 if (!category) return null;
1903
1904 const minAge =
1905 category.age_min ??
1906 category.min_age;
1907 const maxAge =
1908 category.age_max ??
1909 category.max_age;
1910 const categoryName =
1911 category.label ||
1912 category.name ||
1913 `Category ${tp.category_id}`;
1914
1915 return (
1916 <div
1917 key={tp.category_id}
1918 className="p-3 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg"
1919 >
1920 <div className="flex items-start justify-between mb-2">
1921 <div className="flex-1">
1922 <span className="text-xs font-semibold text-gray-900 dark:text-white">
1923 {categoryName}
1924 {(minAge !==
1925 undefined ||
1926 maxAge !==
1927 undefined) && (
1928 <span className="ml-1 text-xs font-normal text-gray-500 dark:text-gray-400">
1929 (
1930 {minAge !==
1931 undefined &&
1932 maxAge !== undefined
1933 ? `${minAge}-${maxAge}`
1934 : minAge !==
1935 undefined
1936 ? `${minAge}+`
1937 : `<${maxAge}`}{" "}
1938 {__(
1939 "years",
1940 "yatra",
1941 )}
1942 )
1943 </span>
1944 )}
1945 </span>
1946 </div>
1947 <button
1948 type="button"
1949 onClick={() => {
1950 const newSlots = [
1951 ...formData.time_slots,
1952 ];
1953 if (
1954 newSlots[index] &&
1955 newSlots[index]
1956 .traveler_pricing
1957 ) {
1958 newSlots[
1959 index
1960 ].traveler_pricing =
1961 newSlots[
1962 index
1963 ].traveler_pricing!.filter(
1964 (_, i) =>
1965 i !== tpIndex,
1966 );
1967 setFormData(
1968 (prev) => ({
1969 ...prev,
1970 time_slots:
1971 newSlots,
1972 }),
1973 );
1974 }
1975 }}
1976 className="p-0.5 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
1977 >
1978 <X className="w-3 h-3" />
1979 </button>
1980 </div>
1981 <div className="grid grid-cols-2 gap-2">
1982 <div>
1983 <label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">
1984 {__("Price", "yatra")}{" "}
1985 <span className="text-red-500">
1986 *
1987 </span>
1988 </label>
1989 <Input
1990 type="number"
1991 min={0}
1992 step="0.01"
1993 value={
1994 tp.original_price ||
1995 ""
1996 }
1997 onChange={(e) => {
1998 const newSlots = [
1999 ...formData.time_slots,
2000 ];
2001 if (
2002 newSlots[index] &&
2003 newSlots[index]
2004 .traveler_pricing
2005 ) {
2006 newSlots[
2007 index
2008 ].traveler_pricing![
2009 tpIndex
2010 ].original_price =
2011 parseFloat(
2012 e.target.value,
2013 ) || 0;
2014 setFormData(
2015 (prev) => ({
2016 ...prev,
2017 time_slots:
2018 newSlots,
2019 }),
2020 );
2021 }
2022 }}
2023 className="text-xs"
2024 placeholder="0.00"
2025 />
2026 </div>
2027 <div>
2028 <label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">
2029 {__("Sale", "yatra")}
2030 </label>
2031 <Input
2032 type="number"
2033 min={0}
2034 step="0.01"
2035 value={
2036 tp.sale_price || ""
2037 }
2038 onChange={(e) => {
2039 const newSlots = [
2040 ...formData.time_slots,
2041 ];
2042 if (
2043 newSlots[index] &&
2044 newSlots[index]
2045 .traveler_pricing
2046 ) {
2047 newSlots[
2048 index
2049 ].traveler_pricing![
2050 tpIndex
2051 ].sale_price =
2052 parseFloat(
2053 e.target.value,
2054 ) || undefined;
2055 setFormData(
2056 (prev) => ({
2057 ...prev,
2058 time_slots:
2059 newSlots,
2060 }),
2061 );
2062 }
2063 }}
2064 className="text-xs"
2065 placeholder="0.00"
2066 />
2067 </div>
2068 </div>
2069 </div>
2070 );
2071 },
2072 )}
2073 </div>
2074 )}
2075
2076 {slotTravelerPricing.length === 0 && (
2077 <div className="text-center py-3 bg-gray-100 dark:bg-gray-800 rounded border border-dashed border-gray-300 dark:border-gray-600">
2078 <p className="text-xs text-gray-500">
2079 {__(
2080 'Click "Add Pricing" to set prices for traveler categories',
2081 "yatra",
2082 )}
2083 </p>
2084 </div>
2085 )}
2086 </div>
2087 );
2088 })()}
2089 </div>
2090 )}
2091 </div>
2092 ))}
2093 </div>
2094 )}
2095
2096 {/* If no time slots, show default fields as fallback */}
2097 {formData.time_slots.length === 0 && (
2098 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
2099 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
2100 {__(
2101 "Or use default time (applies to all generated dates):",
2102 "yatra",
2103 )}
2104 </p>
2105 <div className="grid grid-cols-2 gap-4">
2106 <div>
2107 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2108 {__("Default Start Time", "yatra")}
2109 </label>
2110 <TimePicker
2111 value={formData.departure_time || ""}
2112 onChange={(value: string) =>
2113 setFormData((prev) => ({
2114 ...prev,
2115 departure_time: value,
2116 }))
2117 }
2118 placeholder="09:00"
2119 />
2120 </div>
2121 <div>
2122 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2123 {__("Default End Time", "yatra")}
2124 </label>
2125 <TimePicker
2126 value={formData.arrival_time || ""}
2127 onChange={(value: string) =>
2128 setFormData((prev) => ({
2129 ...prev,
2130 arrival_time: value,
2131 }))
2132 }
2133 placeholder="17:00"
2134 />
2135 </div>
2136 </div>
2137 </div>
2138 )}
2139 </div>
2140 )}
2141
2142 {/* Multi-Day Trip: Single Departure Time */}
2143 {!isSingleDayTrip && formData.trip_id > 0 && (
2144 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
2145 <div>
2146 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2147 {__("Departure Time", "yatra")}
2148 </label>
2149 <TimePicker
2150 value={formData.departure_time || ""}
2151 onChange={(value: string) =>
2152 setFormData((prev) => ({
2153 ...prev,
2154 departure_time: value,
2155 }))
2156 }
2157 placeholder="08:00"
2158 />
2159 </div>
2160 <div>
2161 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2162 {__("Return Time", "yatra")}{" "}
2163 <span className="text-gray-400">
2164 ({__("on final day", "yatra")})
2165 </span>
2166 </label>
2167 <TimePicker
2168 value={formData.arrival_time || ""}
2169 onChange={(value: string) =>
2170 setFormData((prev) => ({
2171 ...prev,
2172 arrival_time: value,
2173 }))
2174 }
2175 placeholder="18:00"
2176 />
2177 </div>
2178 </div>
2179 )}
2180
2181 {/* No trip selected */}
2182 {!formData.trip_id && (
2183 <div className="text-center py-6 bg-gray-50 dark:bg-gray-800 rounded-lg">
2184 <AlertCircle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
2185 <p className="text-sm text-gray-500 dark:text-gray-400">
2186 {__(
2187 "Select a trip above to configure time settings",
2188 "yatra",
2189 )}
2190 </p>
2191 </div>
2192 )}
2193
2194 {/* Location & Cutoff - always shown when trip selected */}
2195 {formData.trip_id > 0 && (
2196 <>
2197 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 pt-4 border-t border-gray-200 dark:border-gray-700">
2198 <div className="space-y-4">
2199 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
2200 <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
2201 <MapPin className="w-5 h-5 text-white" />
2202 </div>
2203 <div className="flex-1">
2204 <h4 className="text-base font-semibold text-blue-900 dark:text-blue-100">
2205 {__("Starting Point", "yatra")}
2206 </h4>
2207 <p className="text-xs text-blue-700 dark:text-blue-300">
2208 {__("Where the journey begins", "yatra")}
2209 </p>
2210 </div>
2211 {formData.from_latitude &&
2212 formData.from_longitude && (
2213 <div
2214 className="w-2 h-2 bg-blue-500 rounded-full"
2215 title={__("Coordinates set", "yatra")}
2216 />
2217 )}
2218 </div>
2219 <div className="space-y-2">
2220 <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
2221 {__("From location (departure)", "yatra")}
2222 </label>
2223 <LocationPicker
2224 value={{
2225 name: formData.from_location || "",
2226 latitude: formData.from_latitude || "",
2227 longitude: formData.from_longitude || "",
2228 }}
2229 onChange={(loc) =>
2230 setFormData((prev) => ({
2231 ...prev,
2232 from_location: loc.name,
2233 from_latitude: loc.latitude,
2234 from_longitude: loc.longitude,
2235 }))
2236 }
2237 label=""
2238 placeholder={__(
2239 "Search for starting location...",
2240 "yatra",
2241 )}
2242 helpText=""
2243 required={false}
2244 defaultMapCenter={
2245 formData.from_latitude && formData.from_longitude
2246 ? [
2247 parseFloat(formData.from_latitude),
2248 parseFloat(formData.from_longitude),
2249 ]
2250 : tripForLocations?.starting_latitude &&
2251 tripForLocations?.starting_longitude
2252 ? [
2253 parseFloat(
2254 String(
2255 tripForLocations.starting_latitude,
2256 ),
2257 ),
2258 parseFloat(
2259 String(
2260 tripForLocations.starting_longitude,
2261 ),
2262 ),
2263 ]
2264 : [-8.3405, 115.092]
2265 }
2266 defaultZoom={13}
2267 mapHeight="300px"
2268 showMapButton={false}
2269 searchLimit={8}
2270 __={__}
2271 className=""
2272 mapClassName="rounded-lg"
2273 showManualCoordinateFields
2274 />
2275 <p className="text-xs text-gray-500 dark:text-gray-400">
2276 {__(
2277 "Default: trip starting location. Set per rule to override.",
2278 "yatra",
2279 )}
2280 </p>
2281 </div>
2282 </div>
2283
2284 <div className="space-y-4">
2285 <div className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-xl border border-green-200 dark:border-green-800">
2286 <div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
2287 <CheckCircle2 className="w-5 h-5 text-white" />
2288 </div>
2289 <div className="flex-1">
2290 <h4 className="text-base font-semibold text-green-900 dark:text-green-100">
2291 {__("Ending Point", "yatra")}
2292 </h4>
2293 <p className="text-xs text-green-700 dark:text-green-300">
2294 {__("Where the journey concludes", "yatra")}
2295 </p>
2296 </div>
2297 {formData.to_latitude && formData.to_longitude && (
2298 <div
2299 className="w-2 h-2 bg-green-500 rounded-full"
2300 title={__("Coordinates set", "yatra")}
2301 />
2302 )}
2303 </div>
2304 <div className="space-y-2">
2305 <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
2306 {__("To location (destination)", "yatra")}
2307 </label>
2308 <LocationPicker
2309 value={{
2310 name: formData.to_location || "",
2311 latitude: formData.to_latitude || "",
2312 longitude: formData.to_longitude || "",
2313 }}
2314 onChange={(loc) =>
2315 setFormData((prev) => ({
2316 ...prev,
2317 to_location: loc.name,
2318 to_latitude: loc.latitude,
2319 to_longitude: loc.longitude,
2320 }))
2321 }
2322 label=""
2323 placeholder={__(
2324 "Search for ending location...",
2325 "yatra",
2326 )}
2327 helpText=""
2328 required={false}
2329 defaultMapCenter={
2330 formData.to_latitude && formData.to_longitude
2331 ? [
2332 parseFloat(formData.to_latitude),
2333 parseFloat(formData.to_longitude),
2334 ]
2335 : tripForLocations?.ending_latitude &&
2336 tripForLocations?.ending_longitude
2337 ? [
2338 parseFloat(
2339 String(
2340 tripForLocations.ending_latitude,
2341 ),
2342 ),
2343 parseFloat(
2344 String(
2345 tripForLocations.ending_longitude,
2346 ),
2347 ),
2348 ]
2349 : [-8.5069, 115.2625]
2350 }
2351 defaultZoom={13}
2352 mapHeight="300px"
2353 showMapButton={false}
2354 searchLimit={8}
2355 __={__}
2356 className=""
2357 mapClassName="rounded-lg"
2358 showManualCoordinateFields
2359 />
2360 <p className="text-xs text-gray-500 dark:text-gray-400">
2361 {__(
2362 "Default: trip ending location. Set per rule to override.",
2363 "yatra",
2364 )}
2365 </p>
2366 </div>
2367 </div>
2368 </div>
2369 <div>
2370 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2371 {__("Booking Cutoff (hours before)", "yatra")}
2372 </label>
2373 <Input
2374 type="number"
2375 min={0}
2376 value={formData.cutoff_hours}
2377 onChange={(e) =>
2378 setFormData((prev) => ({
2379 ...prev,
2380 cutoff_hours: parseInt(e.target.value) || 0,
2381 }))
2382 }
2383 />
2384 </div>
2385 </>
2386 )}
2387 </CardContent>
2388 </Card>
2389 </div>
2390
2391 {/* Sidebar */}
2392 <div className="space-y-6">
2393 {/* Status */}
2394 <Card>
2395 <CardHeader>
2396 <CardTitle>{__("Status", "yatra")}</CardTitle>
2397 </CardHeader>
2398 <CardContent>
2399 <Select
2400 value={formData.status}
2401 onChange={(e) =>
2402 setFormData((prev) => ({
2403 ...prev,
2404 status: e.target.value as "active" | "inactive",
2405 }))
2406 }
2407 >
2408 <option value="active">{__("Active", "yatra")}</option>
2409 <option value="inactive">{__("Inactive", "yatra")}</option>
2410 </Select>
2411 </CardContent>
2412 </Card>
2413
2414 {/* Preview */}
2415 <Card>
2416 <CardHeader>
2417 <CardTitle className="flex items-center gap-2">
2418 <Eye className="w-5 h-5" />
2419 {__("Preview", "yatra")}
2420 </CardTitle>
2421 <CardDescription>
2422 {__("See which dates will be generated", "yatra")}
2423 </CardDescription>
2424 </CardHeader>
2425 <CardContent>
2426 <Button
2427 type="button"
2428 variant="outline"
2429 className="w-full mb-4"
2430 onClick={() => previewMutation.mutate(formData)}
2431 disabled={previewMutation.isPending || !formData.trip_id}
2432 >
2433 {previewMutation.isPending ? (
2434 <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
2435 ) : (
2436 <Eye className="w-4 h-4 mr-2" />
2437 )}
2438 {__("Generate Preview", "yatra")}
2439 </Button>
2440
2441 {previewData && (
2442 <div className="space-y-3">
2443 <div className="flex items-center justify-between text-sm">
2444 <span className="text-gray-600 dark:text-gray-400">
2445 {__("Total dates:", "yatra")}
2446 </span>
2447 <Badge variant="success">{previewData.total}</Badge>
2448 </div>
2449 <div className="max-h-48 overflow-y-auto space-y-1">
2450 {previewData.dates &&
2451 Array.isArray(previewData.dates) &&
2452 previewData.dates.map((date: any, index: number) => (
2453 <div
2454 key={index}
2455 className="text-xs px-2 py-1 bg-gray-50 dark:bg-gray-800 rounded flex justify-between"
2456 >
2457 <span>
2458 {new Date(date.departure_date).toLocaleDateString(
2459 "en-US",
2460 {
2461 weekday: "short",
2462 month: "short",
2463 day: "numeric",
2464 year: "numeric",
2465 },
2466 )}
2467 </span>
2468 {date.departure_time && (
2469 <span className="text-gray-500">
2470 {date.departure_time}
2471 </span>
2472 )}
2473 </div>
2474 ))}
2475 {previewData.total > 20 && (
2476 <div className="text-xs text-gray-500 text-center py-1">
2477 +{previewData.total - 20} {__("more dates", "yatra")}
2478 </div>
2479 )}
2480 </div>
2481 </div>
2482 )}
2483 </CardContent>
2484 </Card>
2485
2486 {/* Actions */}
2487 <Card>
2488 <CardContent className="pt-6">
2489 <div className="space-y-3">
2490 <Button type="submit" className="w-full" disabled={isLoading}>
2491 {isLoading ? (
2492 <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
2493 ) : (
2494 <Save className="w-4 h-4 mr-2" />
2495 )}
2496 {isEditing
2497 ? __("Update Rule", "yatra")
2498 : __("Create Rule", "yatra")}
2499 </Button>
2500 <Button
2501 type="button"
2502 variant="outline"
2503 className="w-full"
2504 onClick={() =>
2505 navigate({
2506 subpage: "trips",
2507 tab: "availability",
2508 trip_id:
2509 formData.trip_id?.toString() || tripIdFromUrl || "",
2510 })
2511 }
2512 >
2513 {__("Cancel", "yatra")}
2514 </Button>
2515 </div>
2516 </CardContent>
2517 </Card>
2518
2519 {/* Help */}
2520 <Alert>
2521 <div className="ml-2">
2522 <h4 className="font-medium">{__("How it works", "yatra")}</h4>
2523 <p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
2524 {__(
2525 "Dates are generated automatically based on your pattern. Manually added specific dates will take priority over generated dates.",
2526 "yatra",
2527 )}
2528 </p>
2529 </div>
2530 </Alert>
2531 </div>
2532 </div>
2533 </form>
2534 </div>
2535 );
2536 };
2537
2538 export default RecurringRuleForm;
2539