PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / DepartureRecurringRuleForm.tsx

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

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