PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 / DepartureForm.tsx

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

338 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Departure Form
3 * Add or edit a departure for a trip
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 { PageHeader } from "../components/common/PageHeader";
13 import { Card, CardContent } from "../components/ui/card";
14 import { HelpText } from "../components/ui/help-text";
15 import { DatePicker } from "../components/ui/date-picker";
16 import { TimePicker } from "../components/ui/time-picker";
17 import { apiClient } from "../lib/api-client";
18 import { useToast } from "../components/ui/toast";
19
20 interface DepartureFormData {
21 date: string;
22 time?: string;
23 max_capacity: string;
24 price_override?: string;
25 notes?: string;
26 }
27
28 const DepartureForm: React.FC = () => {
29 const queryClient = useQueryClient();
30 const { showToast } = useToast();
31
32 // Get trip_id and id from URL
33 const urlParams = new URLSearchParams(window.location.search);
34 const tripId = urlParams.get("trip_id")
35 ? parseInt(urlParams.get("trip_id")!)
36 : null;
37 const departureId = urlParams.get("id") || null;
38 const isEditMode = !!departureId;
39
40 const [formData, setFormData] = useState<DepartureFormData>({
41 date: "",
42 time: "",
43 max_capacity: "",
44 price_override: "",
45 notes: "",
46 });
47
48 const [errors, setErrors] = useState<
49 Partial<Record<keyof DepartureFormData, string>>
50 >({});
51
52 // Fetch existing departure data if editing
53 const { data: departureData, isLoading: isLoadingDeparture } = useQuery({
54 queryKey: ["departure", departureId],
55 queryFn: async () => {
56 if (!departureId || !tripId) return null;
57 const response = await apiClient.get(
58 `/trips/${tripId}/departures/${departureId}`,
59 );
60 return response?.data || response;
61 },
62 enabled: isEditMode && !!departureId && !!tripId,
63 });
64
65 // Load form data when departure data is available
66 useEffect(() => {
67 if (departureData) {
68 setFormData({
69 date: departureData.date || departureData.start_date || "",
70 time: departureData.time || "",
71 max_capacity: departureData.max_capacity?.toString() || "",
72 price_override: departureData.price_override?.toString() || "",
73 notes: departureData.notes || "",
74 });
75 }
76 }, [departureData]);
77
78 // Create/Update mutation
79 const saveMutation = useMutation({
80 mutationFn: async (data: DepartureFormData) => {
81 if (!tripId) throw new Error("Trip ID is required");
82
83 const payload: any = {
84 trip_id: tripId,
85 date: data.date,
86 max_capacity: parseInt(data.max_capacity),
87 source: "manual",
88 };
89
90 if (data.time) payload.time = data.time;
91 if (data.price_override)
92 payload.price_override = parseFloat(data.price_override);
93 if (data.notes) payload.notes = data.notes;
94
95 if (isEditMode && departureId) {
96 return await apiClient.put(
97 `/trips/${tripId}/departures/${departureId}`,
98 payload,
99 );
100 } else {
101 return await apiClient.post(`/trips/${tripId}/departures`, payload);
102 }
103 },
104 onSuccess: () => {
105 queryClient.invalidateQueries({ queryKey: ["departures", tripId] });
106 showToast(
107 isEditMode
108 ? __("Departure updated successfully", "yatra")
109 : __("Departure created successfully", "yatra"),
110 "success",
111 );
112 // Return to the standalone Departures page (where the user came
113 // from). Previously this sent operators to the Trips page with a
114 // departures-tab query string, which surprised anyone who reached
115 // the form via the Departures sidebar entry — the table they
116 // expected to see post-save was nowhere on the trip page.
117 window.location.href = `?page=yatra&subpage=departures${tripId ? `&trip_id=${tripId}` : ""}`;
118 },
119 onError: (error: any) => {
120 showToast(
121 error?.message || __("Failed to save departure", "yatra"),
122 "error",
123 );
124 },
125 });
126
127 const validateForm = (): boolean => {
128 const newErrors: Partial<Record<keyof DepartureFormData, string>> = {};
129
130 if (!formData.date) {
131 newErrors.date = __("Date is required", "yatra");
132 }
133
134 if (!formData.max_capacity) {
135 newErrors.max_capacity = __("Max capacity is required", "yatra");
136 } else if (parseInt(formData.max_capacity) < 1) {
137 newErrors.max_capacity = __("Max capacity must be at least 1", "yatra");
138 }
139
140 setErrors(newErrors);
141 return Object.keys(newErrors).length === 0;
142 };
143
144 const handleSubmit = (e: React.FormEvent) => {
145 e.preventDefault();
146 if (validateForm()) {
147 saveMutation.mutate(formData);
148 }
149 };
150
151 const handleBack = () => {
152 // Mirror the save redirect — return to the Departures page rather
153 // than the Trips page so back-from-form lands where the user was.
154 window.location.href = `?page=yatra&subpage=departures${tripId ? `&trip_id=${tripId}` : ""}`;
155 };
156
157 if (isLoadingDeparture) {
158 return (
159 <div className="space-y-6">
160 <div className="animate-pulse">
161 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
162 <div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
163 </div>
164 </div>
165 );
166 }
167
168 return (
169 <div className="space-y-6">
170 <PageHeader
171 title={
172 isEditMode
173 ? __("Edit Departure", "yatra")
174 : __("Add Departure", "yatra")
175 }
176 description={__(
177 "Create or update a departure date for this trip",
178 "yatra",
179 )}
180 actions={
181 <Button variant="ghost" onClick={handleBack}>
182 <ArrowLeft className="w-4 h-4 mr-2" />
183 {__("Back", "yatra")}
184 </Button>
185 }
186 />
187
188 <form onSubmit={handleSubmit}>
189 <Card>
190 <CardContent className="pt-6 space-y-6">
191 {/* Date */}
192 <div>
193 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
194 {__("Departure Date", "yatra")}{" "}
195 <span className="text-red-500">*</span>
196 </label>
197 <DatePicker
198 value={formData.date}
199 onChange={(value) => setFormData({ ...formData, date: value })}
200 minDate={new Date()}
201 />
202 {errors.date && (
203 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
204 {errors.date}
205 </p>
206 )}
207 <HelpText
208 text={__("Select the departure date for this trip", "yatra")}
209 />
210 </div>
211
212 {/* Time (Optional) */}
213 <div>
214 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
215 {__("Departure Time", "yatra")}{" "}
216 <span className="text-gray-400">
217 ({__("Optional", "yatra")})
218 </span>
219 </label>
220 <TimePicker
221 value={formData.time || ""}
222 onChange={(value) => setFormData({ ...formData, time: value })}
223 />
224 <HelpText
225 text={__("Optional departure time (e.g., 09:00 AM)", "yatra")}
226 />
227 </div>
228
229 {/* Max Capacity */}
230 <div>
231 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
232 {__("Max Capacity", "yatra")}{" "}
233 <span className="text-red-500">*</span>
234 </label>
235 <Input
236 type="number"
237 min="1"
238 value={formData.max_capacity}
239 onChange={(e) =>
240 setFormData({ ...formData, max_capacity: e.target.value })
241 }
242 placeholder={__("e.g., 20", "yatra")}
243 />
244 {errors.max_capacity && (
245 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
246 {errors.max_capacity}
247 </p>
248 )}
249 <HelpText
250 text={__(
251 "Maximum number of travelers for this departure",
252 "yatra",
253 )}
254 />
255 </div>
256
257 {/* Price Override (Optional) */}
258 <div>
259 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
260 {__("Price Override", "yatra")}{" "}
261 <span className="text-gray-400">
262 ({__("Optional", "yatra")})
263 </span>
264 </label>
265 <Input
266 type="number"
267 step="0.01"
268 min="0"
269 value={formData.price_override || ""}
270 onChange={(e) =>
271 setFormData({ ...formData, price_override: e.target.value })
272 }
273 placeholder={__("e.g., 150.00", "yatra")}
274 />
275 <HelpText
276 text={__(
277 "Override the default trip price for this specific departure",
278 "yatra",
279 )}
280 />
281 </div>
282
283 {/* Notes (Optional) */}
284 <div>
285 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
286 {__("Notes", "yatra")}{" "}
287 <span className="text-gray-400">
288 ({__("Optional", "yatra")})
289 </span>
290 </label>
291 <textarea
292 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
293 rows={4}
294 value={formData.notes || ""}
295 onChange={(e) =>
296 setFormData({ ...formData, notes: e.target.value })
297 }
298 placeholder={__(
299 "Additional notes about this departure...",
300 "yatra",
301 )}
302 />
303 <HelpText
304 text={__(
305 "Internal notes about this departure (not visible to customers)",
306 "yatra",
307 )}
308 />
309 </div>
310
311 {/* Submit Button */}
312 <div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
313 <Button type="button" variant="outline" onClick={handleBack}>
314 {__("Cancel", "yatra")}
315 </Button>
316 <Button type="submit" disabled={saveMutation.isPending}>
317 {saveMutation.isPending ? (
318 <>
319 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
320 {__("Saving...", "yatra")}
321 </>
322 ) : (
323 <>
324 <Save className="w-4 h-4 mr-2" />
325 {__("Save Departure", "yatra")}
326 </>
327 )}
328 </Button>
329 </div>
330 </CardContent>
331 </Card>
332 </form>
333 </div>
334 );
335 };
336
337 export default DepartureForm;
338