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

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

442 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Departure Recurring Rule Form
3 * Create and edit recurring rules for departures
4 */
5
6 import React, { useState, useEffect } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2 } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { toDateValue } from "../lib/dateFormat";
11 import { Button } from "../components/ui/button";
12 import { Input } from "../components/ui/input";
13 import { Select } from "../components/ui/select";
14 import { PageHeader } from "../components/common/PageHeader";
15 import { Card, CardContent } from "../components/ui/card";
16 import { HelpText } from "../components/ui/help-text";
17 import { DatePicker } from "../components/ui/date-picker";
18 import { apiClient } from "../lib/api-client";
19 import { useToast } from "../components/ui/toast";
20
21 interface RecurringRuleFormData {
22 recurrence_type: "daily" | "weekly" | "monthly" | "custom_days";
23 weekdays: number[];
24 start_date?: string;
25 end_date?: string;
26 max_capacity: string;
27 base_price?: string;
28 is_active: boolean;
29 }
30
31 const dayOptions = [
32 { value: 0, label: __("Sunday", "yatra") },
33 { value: 1, label: __("Monday", "yatra") },
34 { value: 2, label: __("Tuesday", "yatra") },
35 { value: 3, label: __("Wednesday", "yatra") },
36 { value: 4, label: __("Thursday", "yatra") },
37 { value: 5, label: __("Friday", "yatra") },
38 { value: 6, label: __("Saturday", "yatra") },
39 ];
40
41 const DepartureRecurringRuleForm: React.FC = () => {
42 const queryClient = useQueryClient();
43 const { showToast } = useToast();
44
45 // Get trip_id and id from URL
46 const urlParams = new URLSearchParams(window.location.search);
47 const tripId = urlParams.get("trip_id")
48 ? parseInt(urlParams.get("trip_id")!)
49 : null;
50 const ruleId = urlParams.get("id") || null;
51 const isEditMode = !!ruleId;
52
53 const [formData, setFormData] = useState<RecurringRuleFormData>({
54 recurrence_type: "weekly",
55 weekdays: [],
56 start_date: "",
57 end_date: "",
58 max_capacity: "",
59 base_price: "",
60 is_active: true,
61 });
62
63 const [errors, setErrors] = useState<
64 Partial<Record<keyof RecurringRuleFormData, string>>
65 >({});
66
67 // Fetch existing rule data if editing
68 const { data: ruleData, isLoading: isLoadingRule } = useQuery({
69 queryKey: ["recurring-rule", ruleId],
70 queryFn: async () => {
71 if (!ruleId || !tripId) return null;
72 const response = await apiClient.get(
73 `/trips/${tripId}/recurring-rules/${ruleId}`,
74 );
75 return response?.data || response;
76 },
77 enabled: isEditMode && !!ruleId && !!tripId,
78 });
79
80 // Load form data when rule data is available
81 useEffect(() => {
82 if (ruleData) {
83 setFormData({
84 recurrence_type: ruleData.recurrence_type || "weekly",
85 weekdays: ruleData.weekdays || [],
86 start_date: ruleData.start_date || "",
87 end_date: ruleData.end_date || "",
88 max_capacity: ruleData.max_capacity?.toString() || "",
89 base_price: ruleData.base_price?.toString() || "",
90 is_active: ruleData.is_active !== false,
91 });
92 }
93 }, [ruleData]);
94
95 // Create/Update mutation
96 const saveMutation = useMutation({
97 mutationFn: async (data: RecurringRuleFormData) => {
98 if (!tripId) throw new Error("Trip ID is required");
99
100 const payload: any = {
101 trip_id: tripId,
102 recurrence_type: data.recurrence_type,
103 max_capacity: parseInt(data.max_capacity),
104 is_active: data.is_active,
105 };
106
107 if (
108 data.recurrence_type === "weekly" ||
109 data.recurrence_type === "custom_days"
110 ) {
111 payload.weekdays = data.weekdays;
112 }
113
114 if (data.start_date) payload.start_date = data.start_date;
115 if (data.end_date) payload.end_date = data.end_date;
116 if (data.base_price) payload.base_price = parseFloat(data.base_price);
117
118 if (isEditMode && ruleId) {
119 return await apiClient.put(
120 `/trips/${tripId}/recurring-rules/${ruleId}`,
121 payload,
122 );
123 } else {
124 return await apiClient.post(
125 `/trips/${tripId}/recurring-rules`,
126 payload,
127 );
128 }
129 },
130 onSuccess: () => {
131 queryClient.invalidateQueries({ queryKey: ["recurring-rules", tripId] });
132 showToast(
133 isEditMode
134 ? __("Recurring rule updated successfully", "yatra")
135 : __("Recurring rule created successfully", "yatra"),
136 "success",
137 );
138 window.location.href = `?page=yatra&subpage=trips&tab=departures&action=rules&trip_id=${tripId}`;
139 },
140 onError: (error: any) => {
141 showToast(error?.message || __("Failed to save rule", "yatra"), "error");
142 },
143 });
144
145 const validateForm = (): boolean => {
146 const newErrors: Partial<Record<keyof RecurringRuleFormData, string>> = {};
147
148 if (!formData.max_capacity) {
149 newErrors.max_capacity = __("Max capacity is required", "yatra");
150 } else if (parseInt(formData.max_capacity) < 1) {
151 newErrors.max_capacity = __("Max capacity must be at least 1", "yatra");
152 }
153
154 if (
155 (formData.recurrence_type === "weekly" ||
156 formData.recurrence_type === "custom_days") &&
157 formData.weekdays.length === 0
158 ) {
159 newErrors.weekdays = __("At least one weekday must be selected", "yatra");
160 }
161
162 setErrors(newErrors);
163 return Object.keys(newErrors).length === 0;
164 };
165
166 const handleSubmit = (e: React.FormEvent) => {
167 e.preventDefault();
168 if (validateForm()) {
169 saveMutation.mutate(formData);
170 }
171 };
172
173 const handleBack = () => {
174 window.location.href = `?page=yatra&subpage=trips&tab=departures&action=rules${tripId ? `&trip_id=${tripId}` : ""}`;
175 };
176
177 const toggleWeekday = (day: number) => {
178 setFormData({
179 ...formData,
180 weekdays: formData.weekdays.includes(day)
181 ? formData.weekdays.filter((d) => d !== day)
182 : [...formData.weekdays, day].sort(),
183 });
184 };
185
186 if (isLoadingRule) {
187 return (
188 <div className="space-y-6">
189 <div className="animate-pulse">
190 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
191 <div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
192 </div>
193 </div>
194 );
195 }
196
197 return (
198 <div className="space-y-6">
199 <PageHeader
200 title={
201 isEditMode
202 ? __("Edit Recurring Rule", "yatra")
203 : __("Add Recurring Rule", "yatra")
204 }
205 description={__(
206 "Create a recurring pattern for automatic departure date generation",
207 "yatra",
208 )}
209 actions={
210 <Button variant="ghost" onClick={handleBack}>
211 <ArrowLeft className="w-4 h-4 mr-2" />
212 {__("Back", "yatra")}
213 </Button>
214 }
215 />
216
217 <form onSubmit={handleSubmit}>
218 <Card>
219 <CardContent className="pt-6 space-y-6">
220 {/* Recurrence Type */}
221 <div>
222 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
223 {__("Recurrence Type", "yatra")}{" "}
224 <span className="text-red-500">*</span>
225 </label>
226 <Select
227 value={formData.recurrence_type}
228 onChange={(e) => {
229 setFormData({
230 ...formData,
231 recurrence_type: e.target.value as any,
232 weekdays: [],
233 });
234 }}
235 >
236 <option value="daily">{__("Daily", "yatra")}</option>
237 <option value="weekly">{__("Weekly", "yatra")}</option>
238 <option value="monthly">{__("Monthly", "yatra")}</option>
239 <option value="custom_days">
240 {__("Custom Days", "yatra")}
241 </option>
242 </Select>
243 <HelpText
244 text={__(
245 "Select how often departures should be generated",
246 "yatra",
247 )}
248 />
249 </div>
250
251 {/* Weekdays (for weekly/custom_days) */}
252 {(formData.recurrence_type === "weekly" ||
253 formData.recurrence_type === "custom_days") && (
254 <div>
255 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
256 {__("Days of Week", "yatra")}{" "}
257 <span className="text-red-500">*</span>
258 </label>
259 <div className="flex flex-wrap gap-2">
260 {dayOptions.map((day) => (
261 <button
262 key={day.value}
263 type="button"
264 onClick={() => toggleWeekday(day.value)}
265 className={`px-4 py-2 rounded-md border ${
266 formData.weekdays.includes(day.value)
267 ? "bg-blue-500 text-white border-blue-500"
268 : "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
269 }`}
270 >
271 {day.label}
272 </button>
273 ))}
274 </div>
275 {errors.weekdays && (
276 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
277 {errors.weekdays}
278 </p>
279 )}
280 <HelpText
281 text={__(
282 "Select which days of the week departures should be generated",
283 "yatra",
284 )}
285 />
286 </div>
287 )}
288
289 {/* Start Date */}
290 <div>
291 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
292 {__("Start Date", "yatra")}{" "}
293 <span className="text-gray-400">
294 ({__("Optional", "yatra")})
295 </span>
296 </label>
297 <DatePicker
298 value={formData.start_date || ""}
299 onChange={(value) =>
300 setFormData({ ...formData, start_date: value })
301 }
302 />
303 <HelpText
304 text={__(
305 "When should this rule start generating dates?",
306 "yatra",
307 )}
308 />
309 </div>
310
311 {/* End Date */}
312 <div>
313 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
314 {__("End Date", "yatra")}{" "}
315 <span className="text-gray-400">
316 ({__("Optional", "yatra")})
317 </span>
318 </label>
319 <DatePicker
320 value={formData.end_date || ""}
321 onChange={(value) =>
322 setFormData({ ...formData, end_date: value })
323 }
324 minDate={
325 formData.start_date
326 ? toDateValue(formData.start_date)
327 : undefined
328 }
329 />
330 <HelpText
331 text={__(
332 "When should this rule stop generating dates? Leave empty for no end date",
333 "yatra",
334 )}
335 />
336 </div>
337
338 {/* Max Capacity */}
339 <div>
340 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
341 {__("Max Capacity", "yatra")}{" "}
342 <span className="text-red-500">*</span>
343 </label>
344 <Input
345 type="number"
346 min="1"
347 value={formData.max_capacity}
348 onChange={(e) =>
349 setFormData({ ...formData, max_capacity: e.target.value })
350 }
351 placeholder={__("e.g., 20", "yatra")}
352 />
353 {errors.max_capacity && (
354 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
355 {errors.max_capacity}
356 </p>
357 )}
358 <HelpText
359 text={__(
360 "Maximum number of travelers for generated departures",
361 "yatra",
362 )}
363 />
364 </div>
365
366 {/* Base Price (Optional) */}
367 <div>
368 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
369 {__("Base Price", "yatra")}{" "}
370 <span className="text-gray-400">
371 ({__("Optional", "yatra")})
372 </span>
373 </label>
374 <Input
375 type="number"
376 step="0.01"
377 min="0"
378 value={formData.base_price || ""}
379 onChange={(e) =>
380 setFormData({ ...formData, base_price: e.target.value })
381 }
382 placeholder={__("e.g., 150.00", "yatra")}
383 />
384 <HelpText
385 text={__(
386 "Base price per person for generated departures (optional)",
387 "yatra",
388 )}
389 />
390 </div>
391
392 {/* Active Status */}
393 <div>
394 <label className="flex items-center gap-2">
395 <input
396 type="checkbox"
397 checked={formData.is_active}
398 onChange={(e) =>
399 setFormData({ ...formData, is_active: e.target.checked })
400 }
401 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
402 />
403 <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
404 {__("Active", "yatra")}
405 </span>
406 </label>
407 <HelpText
408 text={__(
409 "Only active rules will generate departure dates",
410 "yatra",
411 )}
412 />
413 </div>
414
415 {/* Submit Button */}
416 <div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
417 <Button type="button" variant="outline" onClick={handleBack}>
418 {__("Cancel", "yatra")}
419 </Button>
420 <Button type="submit" disabled={saveMutation.isPending}>
421 {saveMutation.isPending ? (
422 <>
423 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
424 {__("Saving...", "yatra")}
425 </>
426 ) : (
427 <>
428 <Save className="w-4 h-4 mr-2" />
429 {__("Save Rule", "yatra")}
430 </>
431 )}
432 </Button>
433 </div>
434 </CardContent>
435 </Card>
436 </form>
437 </div>
438 );
439 };
440
441 export default DepartureRecurringRuleForm;
442