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

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