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 +93 -3 3.0.13 → 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"),
@@ -113,9 +167,9 @@
113 167 // from). Previously this sent operators to the Trips page with a
114 168 // departures-tab query string, which surprised anyone who reached
115 169 // the form via the Departures sidebar entry — the table they
116 170 // expected to see post-save was nowhere on the trip page.
117 - window.location.href = `?page=yatra&subpage=departures${tripId ? `&trip_id=${tripId}` : ""}`;
171 + window.location.href = `?page=yatra&subpage=departures${destTripId ? `&trip_id=${destTripId}` : ""}`;
118 172 },
119 173 onError: (error: any) => {
120 174 showToast(
121 175 error?.message || __("Failed to save departure", "yatra"),
@@ -187,8 +241,44 @@
187 241
188 242 <form onSubmit={handleSubmit}>
189 243 <Card>
190 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 +
191 281 {/* Date */}
192 282 <div>
193 283 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
194 284 {__("Departure Date", "yatra")}{" "}