PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/DepartureForm.tsx +101 -4 3.0.4 → 3.0.17 View file →
@@ -22,10 +22,36 @@
22 22 time?: string;
23 23 max_capacity: string;
24 24 price_override?: string;
25 25 notes?: string;
26 + trip_id: string;
26 27 }
27 28
29 +interface TripOption {
30 + id: number;
31 + title?: string;
32 + name?: string;
33 +}
34 +
35 +// Trips endpoint returns either data[] or data.data[] depending on the caller;
36 +// normalise both to a plain array (mirrors Departures.tsx).
37 +function normalizeTripsForDropdown(raw: unknown): TripOption[] {
38 + if (Array.isArray(raw)) return raw as TripOption[];
39 + if (raw && typeof raw === "object") {
40 + const r = raw as Record<string, unknown>;
41 + if (Array.isArray(r.data)) return r.data as TripOption[];
42 + const inner = r.data;
43 + if (
44 + inner &&
45 + typeof inner === "object" &&
46 + Array.isArray((inner as Record<string, unknown>).data)
47 + ) {
48 + return (inner as Record<string, unknown>).data as TripOption[];
49 + }
50 + }
51 + return [];
52 +}
53 +
28 54 const DepartureForm: React.FC = () => {
29 55 const queryClient = useQueryClient();
30 56 const { showToast } = useToast();
31 57
@@ -42,8 +68,9 @@
42 68 time: "",
43 69 max_capacity: "",
44 70 price_override: "",
45 71 notes: "",
72 + trip_id: tripId ? String(tripId) : "",
46 73 });
47 74
48 75 const [errors, setErrors] = useState<
49 76 Partial<Record<keyof DepartureFormData, string>>
@@ -48,8 +75,21 @@
48 75 const [errors, setErrors] = useState<
49 76 Partial<Record<keyof DepartureFormData, string>>
50 77 >({});
51 78
79 + // Tours for the "Tour" selector so an existing departure can be reassigned
80 + // from one tour to another (e.g. to correct a wrong assignment).
81 + const { data: tripsData } = useQuery({
82 + queryKey: ["trips", "departure-form-tour-select", "v1"],
83 + queryFn: async () => {
84 + const response = await apiClient.get("/trips", {
85 + params: { per_page: 1000 },
86 + });
87 + return normalizeTripsForDropdown(response);
88 + },
89 + });
90 + const tripsList = normalizeTripsForDropdown(tripsData);
91 +
52 92 // Fetch existing departure data if editing
53 93 const { data: departureData, isLoading: isLoadingDeparture } = useQuery({
54 94 queryKey: ["departure", departureId],
55 95 queryFn: async () => {
@@ -70,11 +110,13 @@
70 110 time: departureData.time || "",
71 111 max_capacity: departureData.max_capacity?.toString() || "",
72 112 price_override: departureData.price_override?.toString() || "",
73 113 notes: departureData.notes || "",
114 + trip_id:
115 + departureData.trip_id?.toString() || (tripId ? String(tripId) : ""),
74 116 });
75 117 }
76 - }, [departureData]);
118 + }, [departureData, tripId]);
77 119
78 120 // Create/Update mutation
79 121 const saveMutation = useMutation({
80 122 mutationFn: async (data: DepartureFormData) => {
@@ -79,10 +121,15 @@
79 121 const saveMutation = useMutation({
80 122 mutationFn: async (data: DepartureFormData) => {
81 123 if (!tripId) throw new Error("Trip ID is required");
82 124
125 + // In edit mode the operator may reassign the departure to a different
126 + // tour via the selector; fall back to the URL trip otherwise.
127 + const targetTripId =
128 + isEditMode && data.trip_id ? parseInt(data.trip_id) : tripId;
129 +
83 130 const payload: any = {
84 - trip_id: tripId,
131 + trip_id: targetTripId,
85 132 date: data.date,
86 133 max_capacity: parseInt(data.max_capacity),
87 134 source: "manual",
88 135 };
@@ -101,9 +148,16 @@
101 148 return await apiClient.post(`/trips/${tripId}/departures`, payload);
102 149 }
103 150 },
104 151 onSuccess: () => {
152 + // The departure may have been reassigned to another tour, so refresh
153 + // both the old and new tour's lists and land on the new tour.
154 + const destTripId =
155 + isEditMode && formData.trip_id ? parseInt(formData.trip_id) : tripId;
105 156 queryClient.invalidateQueries({ queryKey: ["departures", tripId] });
157 + if (destTripId !== tripId) {
158 + queryClient.invalidateQueries({ queryKey: ["departures", destTripId] });
159 + }
106 160 showToast(
107 161 isEditMode
108 162 ? __("Departure updated successfully", "yatra")
109 163 : __("Departure created successfully", "yatra"),
@@ -108,9 +162,14 @@
108 162 ? __("Departure updated successfully", "yatra")
109 163 : __("Departure created successfully", "yatra"),
110 164 "success",
111 165 );
112 - window.location.href = `?page=yatra&subpage=trips&tab=departures&trip_id=${tripId}`;
166 + // Return to the standalone Departures page (where the user came
167 + // from). Previously this sent operators to the Trips page with a
168 + // departures-tab query string, which surprised anyone who reached
169 + // the form via the Departures sidebar entry — the table they
170 + // expected to see post-save was nowhere on the trip page.
171 + window.location.href = `?page=yatra&subpage=departures${destTripId ? `&trip_id=${destTripId}` : ""}`;
113 172 },
114 173 onError: (error: any) => {
115 174 showToast(
116 175 error?.message || __("Failed to save departure", "yatra"),
@@ -143,9 +202,11 @@
143 202 }
144 203 };
145 204
146 205 const handleBack = () => {
147 - window.location.href = `?page=yatra&subpage=trips&tab=departures${tripId ? `&trip_id=${tripId}` : ""}`;
206 + // Mirror the save redirect — return to the Departures page rather
207 + // than the Trips page so back-from-form lands where the user was.
208 + window.location.href = `?page=yatra&subpage=departures${tripId ? `&trip_id=${tripId}` : ""}`;
148 209 };
149 210
150 211 if (isLoadingDeparture) {
151 212 return (
@@ -180,8 +241,44 @@
180 241
181 242 <form onSubmit={handleSubmit}>
182 243 <Card>
183 244 <CardContent className="pt-6 space-y-6">
245 + {/* Tour (edit mode only): allows reassigning an existing departure
246 + from one tour to another. In create mode the tour comes from the
247 + page the operator started on. */}
248 + {isEditMode && (
249 + <div>
250 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
251 + {__("Tour", "yatra")}{" "}
252 + <span className="text-red-500">*</span>
253 + </label>
254 + <select
255 + value={formData.trip_id}
256 + onChange={(e) =>
257 + setFormData({ ...formData, trip_id: e.target.value })
258 + }
259 + className="w-full rounded-md border px-3 py-2 text-sm bg-white dark:bg-gray-800 dark:text-white border-gray-300 dark:border-gray-600"
260 + >
261 + {tripsList.length === 0 && formData.trip_id && (
262 + <option value={formData.trip_id}>
263 + {__("Loading tours…", "yatra")}
264 + </option>
265 + )}
266 + {tripsList.map((t) => (
267 + <option key={t.id} value={String(t.id)}>
268 + {t.title || t.name || `#${t.id}`}
269 + </option>
270 + ))}
271 + </select>
272 + <HelpText
273 + text={__(
274 + "Move this departure to a different tour. Any bookings on it move with it.",
275 + "yatra",
276 + )}
277 + />
278 + </div>
279 + )}
280 +
184 281 {/* Date */}
185 282 <div>
186 283 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
187 284 {__("Departure Date", "yatra")}{" "}