PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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.11, at resources/js/hooks/useEmailSettingsManager.ts

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