PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
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 2.0.10 All 83 releases
yatra / resources / js / hooks / useItineraryFormSave.ts

useItineraryFormSave.ts in Yatra – Travel Booking & Tour Operator Software 3.0.2.8, at resources/js/hooks/useItineraryFormSave.ts

576 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Custom hook for ItineraryForm save/mutation logic
3 */
4
5 import { useMutation, useQueryClient } from "@tanstack/react-query";
6 import { apiClient } from "../lib/api-client";
7 import { API_ENDPOINTS } from "../lib/api-endpoints";
8 import { useToast } from "../components/ui/toast";
9 import { __ } from "../lib/i18n";
10 import { ItineraryFormData, ActivityForm } from "./useItineraryFormValidation";
11
12 interface UseItineraryFormSaveParams {
13 isAddDayMode: boolean;
14 isEditMode: boolean;
15 entryId: number | null;
16 activityForms: ActivityForm[];
17 formData: ItineraryFormData;
18 calculateDuration: (
19 startTime?: string,
20 endTime?: string,
21 timeType?: string,
22 ) => string;
23 entryData?: any; // Entry data to check if it's a day entry
24 dayTripData?: any; // Trip data to find day entry
25 tripsData?: any[]; // Trips data to check trip type
26 }
27
28 export const useItineraryFormSave = ({
29 isAddDayMode,
30 isEditMode,
31 entryId,
32 activityForms,
33 formData,
34 calculateDuration,
35 entryData,
36 dayTripData,
37 tripsData,
38 }: UseItineraryFormSaveParams) => {
39 const queryClient = useQueryClient();
40 const { showToast } = useToast();
41
42 const saveMutation = useMutation({
43 mutationFn: async (data: ItineraryFormData) => {
44 // For day mode, create day and all activities
45 if (isAddDayMode) {
46 const tripId = parseInt(data.trip_id);
47 const day = parseInt(data.day);
48 const dayTitle = data.day_title?.trim() || null;
49
50 // In edit mode, update the day entry itself first (only day info, not activities)
51 if (isEditMode && entryId) {
52 // Only send day-specific fields to avoid updating activity titles
53 // The backend requires 'title' field, so we set it to day_title
54 const dayPayload: any = {
55 trip_id: tripId,
56 day: day,
57 day_title: dayTitle,
58 day_description: data.day_description?.trim() || null,
59 title: dayTitle || `Day ${day}`, // Backend requires title field
60 item_type_id: null,
61 item_id: null,
62 status: data.status || "draft",
63 };
64
65 // When editing, we MUST find the day entry ID - never use POST for updates
66 // Find the day entry ID from the data we already have
67 // IMPORTANT: Use the ORIGINAL day number from entryData, not the new day from formData
68 let dayEntryId: number | null = null;
69
70 // First, check if entryData is a day entry (has null item_type_id and item_id)
71 if (
72 entryData &&
73 (entryData.item_type_id === null ||
74 entryData.item_type_id === undefined) &&
75 (entryData.item_id === null || entryData.item_id === undefined)
76 ) {
77 dayEntryId = entryData.id
78 ? parseInt(entryData.id.toString())
79 : null;
80 }
81
82 // If entryData has day_id, query the backend to get the day entry ID
83 // This is the most reliable way when entryData is an activity
84 if (!dayEntryId && entryData && entryData.day_id) {
85 try {
86 const dayEntryResponse = await apiClient.get(
87 `/itinerary/day-entry-by-day-id/${entryData.day_id}`,
88 );
89 const dayEntryData = dayEntryResponse?.data || dayEntryResponse;
90 if (dayEntryData && dayEntryData.day_entry_id) {
91 dayEntryId = parseInt(dayEntryData.day_entry_id.toString());
92 }
93 } catch (error) {
94 // Silently fail and try other methods
95 // Error is logged by the API client if needed
96 }
97 }
98
99 // If still not found and we have dayTripData, try to find from dayTripData
100 if (!dayEntryId && entryData && entryData.day_id && dayTripData) {
101 const itineraryDays = dayTripData.itinerary_days || [];
102 // Find the day that matches entryData.day_id
103 for (const dayData of itineraryDays) {
104 // Check if this day's id matches entryData.day_id
105 if (
106 dayData.id === entryData.day_id ||
107 dayData.day_id === entryData.day_id
108 ) {
109 if (dayData.entries) {
110 // Find entry with null item_type_id/item_id (the day entry)
111 const dayEntry = dayData.entries.find(
112 (e: any) =>
113 (e.item_type_id === null ||
114 e.item_type_id === undefined) &&
115 (e.item_id === null || e.item_id === undefined),
116 );
117 if (dayEntry && dayEntry.id) {
118 dayEntryId = parseInt(dayEntry.id.toString());
119 break;
120 }
121 }
122 }
123 }
124 }
125
126 // If still not found, try to find from dayTripData using ORIGINAL day number
127 // Use entryData.day (original) to find the day entry, not formData.day (new)
128 const originalDay = entryData?.day
129 ? parseInt(entryData.day.toString())
130 : null;
131 if (!dayEntryId && dayTripData && originalDay) {
132 const itineraryDays = dayTripData.itinerary_days || [];
133 const dayData = itineraryDays.find(
134 (d: any) => (d.day_number || d.day) === originalDay,
135 );
136 if (dayData && dayData.entries) {
137 const dayEntry = dayData.entries.find(
138 (e: any) =>
139 (e.item_type_id === null || e.item_type_id === undefined) &&
140 (e.item_id === null || e.item_id === undefined),
141 );
142 if (dayEntry && dayEntry.id) {
143 dayEntryId = parseInt(dayEntry.id.toString());
144 }
145 }
146 }
147
148 // In edit mode, we MUST have a day entry ID - never use POST
149 // If day entry doesn't exist, it's an error (should have been created when day was created)
150 if (!dayEntryId) {
151 throw new Error(
152 "Day entry not found. The day entry should exist when editing. Please refresh the page and try again, or contact support if the issue persists.",
153 );
154 }
155
156 // In edit mode, ALWAYS use PUT - never POST
157 // Days always use mode=day
158 const endpoint = API_ENDPOINTS.ITINERARY_GET(dayEntryId, "day");
159 const dayResponse = await apiClient.put(endpoint, dayPayload);
160
161 const buildActivityPayload = (
162 activityData: ActivityForm["data"],
163 ) => ({
164 trip_id: tripId,
165 day: day,
166 day_title: dayTitle,
167 item_type_id: parseInt(activityData.item_type_id!),
168 item_id: parseInt(activityData.item_id!),
169 title: activityData.title!.trim(),
170 description: (activityData.description || "").trim(),
171 location: (activityData.location || "").trim(),
172 duration:
173 (activityData.duration || "").trim() ||
174 (activityData.start_time &&
175 activityData.end_time &&
176 activityData.time_type === "exact"
177 ? calculateDuration(
178 activityData.start_time,
179 activityData.end_time,
180 activityData.time_type,
181 )
182 : null),
183 start_time: activityData.start_time || "08:00",
184 end_time: activityData.end_time || "17:00",
185 time_type: activityData.time_type || "exact",
186 cost: activityData.cost ? parseFloat(activityData.cost) : null,
187 cost_per_person: activityData.cost_per_person !== false,
188 notes: (activityData.notes || "").trim(),
189 included_items: Array.isArray(activityData.included_items)
190 ? activityData.included_items
191 : [],
192 excluded_items: Array.isArray(activityData.excluded_items)
193 ? activityData.excluded_items
194 : [],
195 gallery: activityData.gallery || [],
196 video_url: activityData.video_url || "",
197 status: activityData.status || "draft",
198 });
199
200 const existingActivities = activityForms.filter((af) => af.entryId);
201 for (let index = 0; index < existingActivities.length; index++) {
202 const activityForm = existingActivities[index];
203 const activityData = activityForm.data;
204
205 if (
206 !activityData.item_type_id ||
207 !activityData.item_id ||
208 !activityData.title?.trim()
209 ) {
210 throw new Error(
211 `Activity ${index + 1} is missing required fields (item type, item, or title)`,
212 );
213 }
214
215 const payload = buildActivityPayload(activityData);
216 const activityEntryId =
217 typeof activityForm.entryId === "string"
218 ? parseInt(activityForm.entryId, 10)
219 : activityForm.entryId;
220
221 if (!activityEntryId) {
222 throw new Error(
223 `Activity ${index + 1} is missing an entry ID for update.`,
224 );
225 }
226
227 try {
228 // Activities always use mode=activity
229 const endpoint = API_ENDPOINTS.ITINERARY_GET(
230 activityEntryId,
231 "activity",
232 );
233 await apiClient.put(endpoint, payload);
234 } catch (error: any) {
235 throw new Error(
236 `Failed to update activity ${index + 1}: ${error?.message || "Unknown error"}`,
237 );
238 }
239 }
240
241 const newActivities = activityForms.filter((af) => !af.entryId);
242
243 if (newActivities.length > 0) {
244 const responses = [dayResponse.data || dayResponse];
245 for (let index = 0; index < newActivities.length; index++) {
246 const activityForm = newActivities[index];
247 const activityData = activityForm.data;
248
249 if (
250 !activityData.item_type_id ||
251 !activityData.item_id ||
252 !activityData.title?.trim()
253 ) {
254 throw new Error(
255 `Activity ${index + 1} is missing required fields (item type, item, or title)`,
256 );
257 }
258
259 const payload = buildActivityPayload(activityData);
260
261 try {
262 const response = await apiClient.post("/itinerary", payload);
263 responses.push(response.data || response);
264 } catch (error: any) {
265 throw new Error(
266 `Failed to save activity ${index + 1}: ${error?.message || "Unknown error"}`,
267 );
268 }
269 }
270 return responses[0];
271 }
272
273 return dayResponse.data || dayResponse;
274 }
275
276 // Create mode: create day and all activities
277 // Filter out empty/invalid activities (those missing required fields)
278 const validActivities = activityForms.filter((af) => {
279 const activityData = af.data;
280 return (
281 activityData.item_type_id &&
282 activityData.item_id &&
283 activityData.title?.trim()
284 );
285 });
286
287 // If no valid activities, create only the day entry
288 if (validActivities.length === 0) {
289 const payload = {
290 trip_id: tripId,
291 day: day,
292 day_title: dayTitle,
293 day_description: data.day_description?.trim() || null,
294 item_type_id: null,
295 item_id: null,
296 title: dayTitle || `Day ${day}`,
297 description: "",
298 location: null,
299 duration: null,
300 start_time: null,
301 end_time: null,
302 time_type: "exact",
303 cost: null,
304 cost_per_person: false,
305 notes: null,
306 included_items: [],
307 excluded_items: [],
308 status: data.status || "draft",
309 };
310
311 const response = await apiClient.post("/itinerary", payload);
312 return response.data || response;
313 }
314
315 // Save all valid activities sequentially to ensure proper ordering and day creation
316 const responses = [];
317 for (let index = 0; index < validActivities.length; index++) {
318 const activityForm = validActivities[index];
319 const activityData = activityForm.data;
320
321 const payload = {
322 trip_id: tripId,
323 day: day,
324 day_title: dayTitle,
325 item_type_id: parseInt(activityData.item_type_id!),
326 item_id: parseInt(activityData.item_id!),
327 title: activityData.title!.trim(),
328 description: (activityData.description || "").trim(),
329 location: (activityData.location || "").trim(),
330 location_latitude: activityData.location_latitude
331 ? parseFloat(activityData.location_latitude)
332 : null,
333 location_longitude: activityData.location_longitude
334 ? parseFloat(activityData.location_longitude)
335 : null,
336 duration:
337 (activityData.duration || "").trim() ||
338 (activityData.start_time &&
339 activityData.end_time &&
340 activityData.time_type === "exact"
341 ? calculateDuration(
342 activityData.start_time,
343 activityData.end_time,
344 activityData.time_type,
345 )
346 : null),
347 start_time: activityData.start_time || "08:00",
348 end_time: activityData.end_time || "17:00",
349 time_type: activityData.time_type || "exact",
350 cost: activityData.cost ? parseFloat(activityData.cost) : null,
351 cost_per_person: activityData.cost_per_person !== false,
352 notes: (activityData.notes || "").trim(),
353 included_items: Array.isArray(activityData.included_items)
354 ? activityData.included_items
355 : [],
356 excluded_items: Array.isArray(activityData.excluded_items)
357 ? activityData.excluded_items
358 : [],
359 gallery: activityData.gallery || [],
360 video_url: activityData.video_url || "",
361 status: activityData.status || "draft",
362 };
363
364 try {
365 const response = await apiClient.post("/itinerary", payload);
366 responses.push(response.data || response);
367 } catch (error: any) {
368 throw new Error(
369 `Failed to save activity ${index + 1}: ${error?.message || "Unknown error"}`,
370 );
371 }
372 }
373
374 // Return the first response (contains day info)
375 return responses[0] || responses;
376 }
377
378 // For activity mode, use all fields
379 const payload = {
380 trip_id: parseInt(data.trip_id),
381 day: parseInt(data.day),
382 day_title: data.day_title?.trim() || null,
383 item_type_id: parseInt(data.item_type_id),
384 item_id: parseInt(data.item_id),
385 title: data.title.trim(),
386 description: data.description.trim(),
387 location: data.location.trim(),
388 location_latitude: data.location_latitude
389 ? parseFloat(data.location_latitude)
390 : null,
391 location_longitude: data.location_longitude
392 ? parseFloat(data.location_longitude)
393 : null,
394 duration: data.duration.trim() || calculateDuration(),
395 start_time: data.start_time,
396 end_time: data.end_time,
397 time_type: data.time_type,
398 cost: data.cost ? parseFloat(data.cost) : null,
399 cost_per_person: data.cost_per_person,
400 notes: data.notes.trim(),
401 included_items: data.included_items,
402 excluded_items: data.excluded_items,
403 gallery: data.gallery || [],
404 video_url: data.video_url || "",
405 status: data.status || "draft", // Ensure status is always included
406 };
407
408 if (isEditMode && entryId) {
409 // Ensure status is explicitly included in update payload
410 const updatePayload = {
411 ...payload,
412 status: data.status || "draft", // Explicitly include status for updates
413 };
414 // Pass mode parameter to specify we're updating an activity entry
415 const endpoint = API_ENDPOINTS.ITINERARY_GET(entryId, "activity");
416 const response = await apiClient.put(endpoint, updatePayload);
417 return response.data || response;
418 } else {
419 const response = await apiClient.post("/itinerary", payload);
420 return response.data || response;
421 }
422 },
423 onSuccess: () => {
424 // For day mode, show success message and navigate
425 if (isAddDayMode) {
426 // Count only valid activities (those with required fields filled)
427 const validActivitiesCount = activityForms.filter((af) => {
428 const activityData = af.data;
429 return (
430 activityData.item_type_id &&
431 activityData.item_id &&
432 activityData.title?.trim()
433 );
434 }).length;
435
436 // Check if it's a single-day trip for dynamic messages
437 const selectedTrip = tripsData?.find(
438 (t: any) => t.id.toString() === formData.trip_id,
439 );
440 const isSingleDay = selectedTrip?.trip_type === "single_day";
441
442 if (isEditMode) {
443 // Edit mode messages - dynamic based on trip type
444 if (validActivitiesCount === 0) {
445 const message = isSingleDay
446 ? __("Entry updated successfully!", "yatra")
447 : __("Day updated successfully!", "yatra");
448 showToast(message, "success");
449 } else {
450 const message = isSingleDay
451 ? __(
452 "Entry and activities updated successfully!",
453 "yatra",
454 ).replace(
455 "activities",
456 `${validActivitiesCount} ${validActivitiesCount === 1 ? "activity" : "activities"}`,
457 )
458 : __("Day and activities updated successfully!", "yatra").replace(
459 "activities",
460 `${validActivitiesCount} ${validActivitiesCount === 1 ? "activity" : "activities"}`,
461 );
462 showToast(message, "success");
463 }
464 } else {
465 // Create mode messages - dynamic based on trip type
466 if (validActivitiesCount === 0) {
467 const message = isSingleDay
468 ? __("Entry created successfully!", "yatra")
469 : __("Day created successfully!", "yatra");
470 showToast(message, "success");
471 } else {
472 const message = isSingleDay
473 ? __(
474 "Entry and activities created successfully!",
475 "yatra",
476 ).replace(
477 "activities",
478 `${validActivitiesCount} ${validActivitiesCount === 1 ? "activity" : "activities"}`,
479 )
480 : __("Day and activities created successfully!", "yatra").replace(
481 "activities",
482 `${validActivitiesCount} ${validActivitiesCount === 1 ? "activity" : "activities"}`,
483 );
484 showToast(message, "success");
485 }
486 }
487
488 // Invalidate all itinerary-related queries
489 queryClient.invalidateQueries({ queryKey: ["itinerary"] });
490 queryClient.invalidateQueries({ queryKey: ["itinerary-entries"] });
491 queryClient.invalidateQueries({ queryKey: ["trips"] });
492 queryClient.invalidateQueries({ queryKey: ["trips-simple"] });
493
494 // Also invalidate the specific trip query if we have trip_id
495 if (formData.trip_id) {
496 queryClient.invalidateQueries({
497 queryKey: ["trips", parseInt(formData.trip_id)],
498 });
499 queryClient.invalidateQueries({
500 queryKey: ["trip", parseInt(formData.trip_id)],
501 });
502 }
503
504 // Only redirect for new days, not for editing existing days
505 if (!isEditMode) {
506 const day = parseInt(formData.day);
507 const tripId = formData.trip_id;
508 setTimeout(() => {
509 const params = new URLSearchParams({
510 trip_id: tripId,
511 day: day.toString(),
512 });
513 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=itinerary&tab=itinerary&${params.toString()}`;
514 }, 1500);
515 }
516 // For editing, stay on the same page - no redirect
517 return;
518 }
519
520 // For activity mode or edit mode
521 showToast(
522 isEditMode
523 ? __("Itinerary entry updated successfully!", "yatra")
524 : __("Itinerary entry created successfully!", "yatra"),
525 "success",
526 );
527
528 // Invalidate all itinerary-related queries
529 queryClient.invalidateQueries({ queryKey: ["itinerary"] });
530 queryClient.invalidateQueries({ queryKey: ["itinerary-entries"] });
531 queryClient.invalidateQueries({ queryKey: ["trips"] });
532 queryClient.invalidateQueries({ queryKey: ["trips-simple"] });
533
534 // Also invalidate the specific trip query if we have trip_id
535 if (formData.trip_id) {
536 queryClient.invalidateQueries({
537 queryKey: ["trips", parseInt(formData.trip_id)],
538 });
539 queryClient.invalidateQueries({
540 queryKey: ["trip", parseInt(formData.trip_id)],
541 });
542 // Force refetch the trip data
543 queryClient.refetchQueries({
544 queryKey: ["trips", parseInt(formData.trip_id)],
545 });
546 }
547
548 // Force refetch all trips to ensure fresh data
549 queryClient.refetchQueries({ queryKey: ["trips"] });
550 queryClient.refetchQueries({ queryKey: ["trips-simple"] });
551
552 // Only redirect for new entries, not for editing existing entries
553 if (!isEditMode) {
554 setTimeout(() => {
555 const tripIdParam = formData.trip_id
556 ? `&trip_id=${formData.trip_id}`
557 : "";
558 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=itinerary&tab=itinerary${tripIdParam}`;
559 }, 1000);
560 }
561 // For editing, stay on the same page - no redirect
562 },
563 onError: (error: any) => {
564 const errorMessage =
565 error?.message || __("An error occurred while saving", "yatra");
566 showToast(errorMessage, "error");
567 },
568 onSettled: () => {
569 // Reset submitting state when mutation completes (success or error)
570 // This ensures the loading state is properly cleared
571 },
572 });
573
574 return { saveMutation };
575 };
576