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

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

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