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 / DepartureForm.tsx

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

331 lines 10.7 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 window.location.href = `?page=yatra&subpage=trips&tab=departures&trip_id=${tripId}`;
113 },
114 onError: (error: any) => {
115 showToast(
116 error?.message || __("Failed to save departure", "yatra"),
117 "error",
118 );
119 },
120 });
121
122 const validateForm = (): boolean => {
123 const newErrors: Partial<Record<keyof DepartureFormData, string>> = {};
124
125 if (!formData.date) {
126 newErrors.date = __("Date is required", "yatra");
127 }
128
129 if (!formData.max_capacity) {
130 newErrors.max_capacity = __("Max capacity is required", "yatra");
131 } else if (parseInt(formData.max_capacity) < 1) {
132 newErrors.max_capacity = __("Max capacity must be at least 1", "yatra");
133 }
134
135 setErrors(newErrors);
136 return Object.keys(newErrors).length === 0;
137 };
138
139 const handleSubmit = (e: React.FormEvent) => {
140 e.preventDefault();
141 if (validateForm()) {
142 saveMutation.mutate(formData);
143 }
144 };
145
146 const handleBack = () => {
147 window.location.href = `?page=yatra&subpage=trips&tab=departures${tripId ? `&trip_id=${tripId}` : ""}`;
148 };
149
150 if (isLoadingDeparture) {
151 return (
152 <div className="space-y-6">
153 <div className="animate-pulse">
154 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
155 <div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
156 </div>
157 </div>
158 );
159 }
160
161 return (
162 <div className="space-y-6">
163 <PageHeader
164 title={
165 isEditMode
166 ? __("Edit Departure", "yatra")
167 : __("Add Departure", "yatra")
168 }
169 description={__(
170 "Create or update a departure date for this trip",
171 "yatra",
172 )}
173 actions={
174 <Button variant="ghost" onClick={handleBack}>
175 <ArrowLeft className="w-4 h-4 mr-2" />
176 {__("Back", "yatra")}
177 </Button>
178 }
179 />
180
181 <form onSubmit={handleSubmit}>
182 <Card>
183 <CardContent className="pt-6 space-y-6">
184 {/* Date */}
185 <div>
186 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
187 {__("Departure Date", "yatra")}{" "}
188 <span className="text-red-500">*</span>
189 </label>
190 <DatePicker
191 value={formData.date}
192 onChange={(value) => setFormData({ ...formData, date: value })}
193 minDate={new Date()}
194 />
195 {errors.date && (
196 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
197 {errors.date}
198 </p>
199 )}
200 <HelpText
201 text={__("Select the departure date for this trip", "yatra")}
202 />
203 </div>
204
205 {/* Time (Optional) */}
206 <div>
207 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
208 {__("Departure Time", "yatra")}{" "}
209 <span className="text-gray-400">
210 ({__("Optional", "yatra")})
211 </span>
212 </label>
213 <TimePicker
214 value={formData.time || ""}
215 onChange={(value) => setFormData({ ...formData, time: value })}
216 />
217 <HelpText
218 text={__("Optional departure time (e.g., 09:00 AM)", "yatra")}
219 />
220 </div>
221
222 {/* Max Capacity */}
223 <div>
224 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
225 {__("Max Capacity", "yatra")}{" "}
226 <span className="text-red-500">*</span>
227 </label>
228 <Input
229 type="number"
230 min="1"
231 value={formData.max_capacity}
232 onChange={(e) =>
233 setFormData({ ...formData, max_capacity: e.target.value })
234 }
235 placeholder={__("e.g., 20", "yatra")}
236 />
237 {errors.max_capacity && (
238 <p className="mt-1 text-sm text-red-600 dark:text-red-400">
239 {errors.max_capacity}
240 </p>
241 )}
242 <HelpText
243 text={__(
244 "Maximum number of travelers for this departure",
245 "yatra",
246 )}
247 />
248 </div>
249
250 {/* Price Override (Optional) */}
251 <div>
252 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
253 {__("Price Override", "yatra")}{" "}
254 <span className="text-gray-400">
255 ({__("Optional", "yatra")})
256 </span>
257 </label>
258 <Input
259 type="number"
260 step="0.01"
261 min="0"
262 value={formData.price_override || ""}
263 onChange={(e) =>
264 setFormData({ ...formData, price_override: e.target.value })
265 }
266 placeholder={__("e.g., 150.00", "yatra")}
267 />
268 <HelpText
269 text={__(
270 "Override the default trip price for this specific departure",
271 "yatra",
272 )}
273 />
274 </div>
275
276 {/* Notes (Optional) */}
277 <div>
278 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
279 {__("Notes", "yatra")}{" "}
280 <span className="text-gray-400">
281 ({__("Optional", "yatra")})
282 </span>
283 </label>
284 <textarea
285 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"
286 rows={4}
287 value={formData.notes || ""}
288 onChange={(e) =>
289 setFormData({ ...formData, notes: e.target.value })
290 }
291 placeholder={__(
292 "Additional notes about this departure...",
293 "yatra",
294 )}
295 />
296 <HelpText
297 text={__(
298 "Internal notes about this departure (not visible to customers)",
299 "yatra",
300 )}
301 />
302 </div>
303
304 {/* Submit Button */}
305 <div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
306 <Button type="button" variant="outline" onClick={handleBack}>
307 {__("Cancel", "yatra")}
308 </Button>
309 <Button type="submit" disabled={saveMutation.isPending}>
310 {saveMutation.isPending ? (
311 <>
312 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
313 {__("Saving...", "yatra")}
314 </>
315 ) : (
316 <>
317 <Save className="w-4 h-4 mr-2" />
318 {__("Save Departure", "yatra")}
319 </>
320 )}
321 </Button>
322 </div>
323 </CardContent>
324 </Card>
325 </form>
326 </div>
327 );
328 };
329
330 export default DepartureForm;
331