PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / hooks / useEmailSettingsManager.ts

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

375 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Load and save core email settings (SMTP, from addresses, transactional templates)
3 * via the same /settings API as the main Settings page.
4 */
5
6 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7 import { useCallback, useMemo, useState, useEffect } from "react";
8 import { fetchSettings, saveSettings } from "../api/settings-api";
9 import { useToast } from "../components/ui/toast";
10 import { usePermissions } from "./usePermissions";
11 import { __ } from "../lib/i18n";
12 import type {
13 EmailFieldChangeHandler,
14 EmailSettingsValues,
15 } from "../components/settings/email-settings-types";
16
17 const EMAIL_KEYS: (keyof EmailSettingsValues)[] = [
18 "admin_email",
19 "from_email",
20 "from_name",
21 "email_template_booking",
22 "email_template_confirmation",
23 "email_template_cancellation",
24 "email_template_reminder",
25 "email_template_admin_new_booking",
26 "email_template_admin_payment",
27 "email_template_admin_cancellation",
28 "email_template_trip_consent",
29 "email_template_customer_verification",
30 "email_template_guest_verification",
31 "smtp_enabled",
32 "smtp_host",
33 "smtp_port",
34 "smtp_username",
35 "smtp_password",
36 "smtp_encryption",
37 "email_tpl_booking_subject",
38 "email_tpl_booking_body",
39 "email_tpl_payment_subject",
40 "email_tpl_payment_body",
41 "email_tpl_cancellation_subject",
42 "email_tpl_cancellation_body",
43 "email_tpl_reminder_subject",
44 "email_tpl_reminder_body",
45 "email_tpl_admin_booking_subject",
46 "email_tpl_admin_booking_body",
47 "email_tpl_admin_payment_subject",
48 "email_tpl_admin_payment_body",
49 "email_tpl_admin_cancellation_subject",
50 "email_tpl_admin_cancellation_body",
51 "email_tpl_trip_consent_subject",
52 "email_tpl_trip_consent_body",
53 "email_tpl_customer_verification_subject",
54 "email_tpl_customer_verification_body",
55 "email_tpl_guest_verification_subject",
56 "email_tpl_guest_verification_body",
57 "email_template_booking_completed",
58 "email_template_booking_expired_customer",
59 "email_template_admin_booking_expired",
60 "email_template_scheduled_payment_reminder",
61 "email_template_scheduled_payment_succeeded",
62 "email_template_scheduled_payment_failed",
63 "email_template_admin_scheduled_payment_failed",
64 "email_template_enquiry_received",
65 "email_template_enquiry_admin",
66 "email_template_enquiry_response",
67 "email_template_review_request",
68 "email_template_abandoned_booking_recovery_first",
69 "email_template_abandoned_booking_recovery_second",
70 "email_template_abandoned_booking_recovery_final",
71 "email_tpl_booking_completed_subject",
72 "email_tpl_booking_completed_body",
73 "email_tpl_booking_expired_customer_subject",
74 "email_tpl_booking_expired_customer_body",
75 "email_tpl_admin_booking_expired_subject",
76 "email_tpl_admin_booking_expired_body",
77 "email_tpl_scheduled_payment_reminder_subject",
78 "email_tpl_scheduled_payment_reminder_body",
79 "email_tpl_scheduled_payment_succeeded_subject",
80 "email_tpl_scheduled_payment_succeeded_body",
81 "email_tpl_scheduled_payment_failed_subject",
82 "email_tpl_scheduled_payment_failed_body",
83 "email_tpl_admin_scheduled_payment_failed_subject",
84 "email_tpl_admin_scheduled_payment_failed_body",
85 "email_tpl_enquiry_received_subject",
86 "email_tpl_enquiry_received_body",
87 "email_tpl_enquiry_admin_subject",
88 "email_tpl_enquiry_admin_body",
89 "email_tpl_enquiry_response_subject",
90 "email_tpl_enquiry_response_body",
91 "email_tpl_review_request_subject",
92 "email_tpl_review_request_body",
93 "email_tpl_abandoned_booking_recovery_first_subject",
94 "email_tpl_abandoned_booking_recovery_first_body",
95 "email_tpl_abandoned_booking_recovery_second_subject",
96 "email_tpl_abandoned_booking_recovery_second_body",
97 "email_tpl_abandoned_booking_recovery_final_subject",
98 "email_tpl_abandoned_booking_recovery_final_body",
99 ];
100
101 const BOOL_KEYS = new Set<keyof EmailSettingsValues>([
102 "email_template_booking",
103 "email_template_confirmation",
104 "email_template_cancellation",
105 "email_template_reminder",
106 "email_template_admin_new_booking",
107 "email_template_admin_payment",
108 "email_template_admin_cancellation",
109 "email_template_trip_consent",
110 "email_template_customer_verification",
111 "email_template_guest_verification",
112 "email_template_booking_completed",
113 "email_template_booking_expired_customer",
114 "email_template_admin_booking_expired",
115 "email_template_scheduled_payment_reminder",
116 "email_template_scheduled_payment_succeeded",
117 "email_template_scheduled_payment_failed",
118 "email_template_admin_scheduled_payment_failed",
119 "email_template_enquiry_received",
120 "email_template_enquiry_admin",
121 "email_template_enquiry_response",
122 "email_template_review_request",
123 "email_template_abandoned_booking_recovery_first",
124 "email_template_abandoned_booking_recovery_second",
125 "email_template_abandoned_booking_recovery_final",
126 "smtp_enabled",
127 ]);
128
129 export const EMAIL_SETTINGS_DEFAULTS: EmailSettingsValues = {
130 admin_email: "[email protected]",
131 from_email: "[email protected]",
132 from_name: "Yatra Travel",
133 email_template_booking: true,
134 email_template_confirmation: true,
135 email_template_cancellation: true,
136 email_template_reminder: true,
137 email_template_admin_new_booking: true,
138 email_template_admin_payment: true,
139 email_template_admin_cancellation: true,
140 email_template_trip_consent: true,
141 email_template_customer_verification: true,
142 email_template_guest_verification: true,
143 email_template_booking_completed: true,
144 email_template_booking_expired_customer: true,
145 email_template_admin_booking_expired: true,
146 email_template_scheduled_payment_reminder: true,
147 email_template_scheduled_payment_succeeded: true,
148 email_template_scheduled_payment_failed: true,
149 email_template_admin_scheduled_payment_failed: true,
150 email_template_enquiry_received: true,
151 email_template_enquiry_admin: true,
152 email_template_enquiry_response: true,
153 email_template_review_request: true,
154 email_template_abandoned_booking_recovery_first: true,
155 email_template_abandoned_booking_recovery_second: true,
156 email_template_abandoned_booking_recovery_final: true,
157 smtp_enabled: false,
158 smtp_host: "smtp.gmail.com",
159 smtp_port: 587,
160 smtp_username: "",
161 smtp_password: "",
162 smtp_encryption: "tls",
163 email_tpl_booking_subject: "",
164 email_tpl_booking_body: "",
165 email_tpl_payment_subject: "",
166 email_tpl_payment_body: "",
167 email_tpl_cancellation_subject: "",
168 email_tpl_cancellation_body: "",
169 email_tpl_reminder_subject: "",
170 email_tpl_reminder_body: "",
171 email_tpl_admin_booking_subject: "",
172 email_tpl_admin_booking_body: "",
173 email_tpl_admin_payment_subject: "",
174 email_tpl_admin_payment_body: "",
175 email_tpl_admin_cancellation_subject: "",
176 email_tpl_admin_cancellation_body: "",
177 email_tpl_trip_consent_subject: "",
178 email_tpl_trip_consent_body: "",
179 email_tpl_customer_verification_subject: "",
180 email_tpl_customer_verification_body: "",
181 email_tpl_guest_verification_subject: "",
182 email_tpl_guest_verification_body: "",
183 email_tpl_booking_completed_subject: "",
184 email_tpl_booking_completed_body: "",
185 email_tpl_booking_expired_customer_subject: "",
186 email_tpl_booking_expired_customer_body: "",
187 email_tpl_admin_booking_expired_subject: "",
188 email_tpl_admin_booking_expired_body: "",
189 email_tpl_scheduled_payment_reminder_subject: "",
190 email_tpl_scheduled_payment_reminder_body: "",
191 email_tpl_scheduled_payment_succeeded_subject: "",
192 email_tpl_scheduled_payment_succeeded_body: "",
193 email_tpl_scheduled_payment_failed_subject: "",
194 email_tpl_scheduled_payment_failed_body: "",
195 email_tpl_admin_scheduled_payment_failed_subject: "",
196 email_tpl_admin_scheduled_payment_failed_body: "",
197 email_tpl_enquiry_received_subject: "",
198 email_tpl_enquiry_received_body: "",
199 email_tpl_enquiry_admin_subject: "",
200 email_tpl_enquiry_admin_body: "",
201 email_tpl_enquiry_response_subject: "",
202 email_tpl_enquiry_response_body: "",
203 email_tpl_review_request_subject: "",
204 email_tpl_review_request_body: "",
205 email_tpl_abandoned_booking_recovery_first_subject: "",
206 email_tpl_abandoned_booking_recovery_first_body: "",
207 email_tpl_abandoned_booking_recovery_second_subject: "",
208 email_tpl_abandoned_booking_recovery_second_body: "",
209 email_tpl_abandoned_booking_recovery_final_subject: "",
210 email_tpl_abandoned_booking_recovery_final_body: "",
211 };
212
213 function coerceEmailValues(
214 raw: Record<string, unknown> | undefined,
215 ): EmailSettingsValues {
216 const out: EmailSettingsValues = { ...EMAIL_SETTINGS_DEFAULTS };
217 if (!raw) return out;
218
219 for (const key of EMAIL_KEYS) {
220 const v = raw[key as string];
221 if (v === undefined || v === null) continue;
222
223 if (key === "smtp_port") {
224 const n = typeof v === "number" ? v : parseInt(String(v), 10);
225 out.smtp_port = Number.isFinite(n)
226 ? n
227 : EMAIL_SETTINGS_DEFAULTS.smtp_port;
228 } else if (BOOL_KEYS.has(key)) {
229 (out as Record<string, unknown>)[key as string] =
230 v === true || v === "true" || v === 1 || v === "1";
231 } else {
232 (out as Record<string, unknown>)[key as string] = String(v);
233 }
234 }
235 return out;
236 }
237
238 function sliceEqual(a: EmailSettingsValues, b: EmailSettingsValues): boolean {
239 for (const key of EMAIL_KEYS) {
240 if (a[key] !== b[key]) return false;
241 }
242 return true;
243 }
244
245 function applyEmailSlice(
246 base: Record<string, unknown>,
247 slice: EmailSettingsValues,
248 ): Record<string, unknown> {
249 const next = { ...base };
250 for (const key of EMAIL_KEYS) {
251 next[key as string] = slice[key] as unknown;
252 }
253 return next;
254 }
255
256 export function useEmailSettingsManager() {
257 const queryClient = useQueryClient();
258 const { showToast } = useToast();
259 const { can } = usePermissions();
260
261 const { data: settings, isLoading } = useQuery({
262 queryKey: ["settings"],
263 queryFn: async () => {
264 try {
265 return await fetchSettings();
266 } catch (error: unknown) {
267 const message =
268 error instanceof Error
269 ? error.message
270 : __("Failed to load settings", "yatra");
271 showToast(message, "error");
272 throw error;
273 }
274 },
275 // Was `manage_yatra` — that's not a registered Yatra cap, so the
276 // check returned false for everyone except admins-via-fallback,
277 // which silently broke the Email page on installs where the
278 // admin fallback wasn't yet wired (free, or Pro pre-Team). The
279 // canonical cap for the Email hub is `yatra_manage_emails`,
280 // registered in the Team module's CapabilityRegistry and held
281 // by Marketing role + Owner / Manager.
282 enabled: can("yatra_manage_emails"),
283 });
284
285 const serverSlice = useMemo(
286 () => coerceEmailValues(settings as Record<string, unknown> | undefined),
287 [settings],
288 );
289
290 const [localSlice, setLocalSlice] = useState<EmailSettingsValues | null>(
291 null,
292 );
293
294 useEffect(() => {
295 if (settings) {
296 setLocalSlice((prev) =>
297 prev === null
298 ? coerceEmailValues(settings as Record<string, unknown>)
299 : prev,
300 );
301 }
302 }, [settings]);
303
304 const ready = Boolean(settings) && !isLoading;
305 const values = localSlice ?? serverSlice;
306 const isDirty = localSlice ? !sliceEqual(localSlice, serverSlice) : false;
307
308 const handleFieldChange: EmailFieldChangeHandler = useCallback(
309 (e) => {
310 const field = (e.target.name || e.target.id) as keyof EmailSettingsValues;
311 if (!(EMAIL_KEYS as readonly string[]).includes(field as string)) return;
312
313 let value: string | number | boolean;
314 if (e.target.type === "checkbox") {
315 value = (e.target as HTMLInputElement).checked;
316 } else if (e.target.type === "number") {
317 const num = parseFloat(e.target.value);
318 value = Number.isFinite(num) ? num : 0;
319 } else {
320 value = e.target.value;
321 }
322
323 setLocalSlice((prev) => {
324 const base =
325 prev ??
326 (settings
327 ? coerceEmailValues(settings as Record<string, unknown>)
328 : { ...EMAIL_SETTINGS_DEFAULTS });
329 return { ...base, [field]: value } as EmailSettingsValues;
330 });
331 },
332 [settings],
333 );
334
335 const saveMutation = useMutation({
336 mutationFn: async () => {
337 if (!settings || !localSlice) {
338 throw new Error(__("Settings not loaded", "yatra"));
339 }
340 const payload = applyEmailSlice(
341 settings as Record<string, unknown>,
342 localSlice,
343 );
344 await saveSettings(payload);
345 return payload;
346 },
347 onSuccess: (saved) => {
348 queryClient.setQueryData(["settings"], saved);
349 setLocalSlice(coerceEmailValues(saved));
350 showToast(__("Email settings saved", "yatra"), "success");
351 },
352 onError: (error: unknown) => {
353 const message =
354 error instanceof Error
355 ? error.message
356 : __("Failed to save settings", "yatra");
357 showToast(message, "error");
358 },
359 });
360
361 const save = useCallback(() => {
362 saveMutation.mutate();
363 }, [saveMutation]);
364
365 return {
366 values,
367 handleFieldChange,
368 save,
369 isLoading,
370 ready,
371 isSaving: saveMutation.isPending,
372 canSave: ready && Boolean(localSlice) && isDirty && !saveMutation.isPending,
373 };
374 }
375