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 / pages / EmailTemplateForm.tsx

EmailTemplateForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.11, at resources/js/pages/EmailTemplateForm.tsx

1,469 lines 59.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect, useMemo } from "react";
2 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3 import { __ } from "../lib/i18n";
4 import { useToast } from "../components/ui/toast";
5 import {
6 Card,
7 CardContent,
8 CardHeader,
9 CardTitle,
10 } from "../components/ui/card";
11 import { Button } from "../components/ui/button";
12 import { Input } from "../components/ui/input";
13 import { Badge } from "../components/ui/badge";
14 import {
15 Mail,
16 ArrowLeft,
17 Save,
18 Eye,
19 Send,
20 Loader2,
21 Code,
22 Copy,
23 Check,
24 Info,
25 Zap,
26 ChevronDown,
27 Sparkles,
28 } from "lucide-react";
29 import { AiEmailTemplateModal } from "../components/ai/AiEmailTemplateModal";
30 import { isAiEligible, isAiModuleEnabled } from "../lib/ai-availability";
31 import { Switch } from "../components/ui/switch";
32 import {
33 EMAIL_TEMPLATES_CATALOG,
34 getCatalogEntryByTemplateKey,
35 getCoreTemplateDefinition,
36 isCoreTemplateSlug,
37 } from "../lib/email-templates-catalog";
38 import {
39 isEmailAutomationModuleEnabled,
40 isModuleActive,
41 isProPluginActive,
42 } from "../lib/plugin-utils";
43 import {
44 fetchSettings,
45 previewCoreEmailTemplate,
46 saveSettings,
47 } from "../api/settings-api";
48 import { EmailPreviewModal } from "../components/email/EmailPreviewModal";
49 import {
50 createEmailTemplate,
51 fetchEmailTemplate,
52 fetchEmailTemplateEvents,
53 fetchEmailTemplateVariables,
54 previewEmailTemplate,
55 sendEmailTemplateTest,
56 updateEmailTemplate,
57 } from "../api/email-automation-api";
58
59 interface PreviewData {
60 subject: string;
61 body: string;
62 }
63
64 const EmailTemplateForm: React.FC = () => {
65 const { id, isCreateMode, coreTemplateSlug, coreReadOnlyParam } =
66 useMemo(() => {
67 const params = new URLSearchParams(window.location.search);
68 const templateId = params.get("id");
69 const action = params.get("action");
70 const core = params.get("core_template") || "";
71 return {
72 id: templateId,
73 isCreateMode: action === "create",
74 coreTemplateSlug: core,
75 coreReadOnlyParam: params.get("readonly") === "1",
76 };
77 }, []);
78
79 const isCoreSettingsEdit = isCoreTemplateSlug(coreTemplateSlug);
80 // Per-template BCC/CC settings keys are derived from the template's subject
81 // key (email_tpl_booking_subject -> _bcc / _cc), exactly as the PHP side does,
82 // so the catalog needs no extra entries and a new template type is covered
83 // automatically.
84 const coreAddressKey = (
85 subjectKey: string | undefined,
86 suffix: "bcc" | "cc",
87 ): string | null =>
88 subjectKey ? subjectKey.replace(/_subject$/, "") + "_" + suffix : null;
89
90 const coreDef = isCoreSettingsEdit
91 ? getCoreTemplateDefinition(coreTemplateSlug)
92 : undefined;
93
94 const catalogEntryForCore = useMemo(
95 () =>
96 coreTemplateSlug
97 ? getCatalogEntryByTemplateKey(coreTemplateSlug)
98 : undefined,
99 [coreTemplateSlug],
100 );
101
102 /** URL ?readonly=1 — no edits at all from this screen */
103 const isCoreStrictViewOnly = isCoreSettingsEdit && Boolean(coreReadOnlyParam);
104
105 /** Module required but inactive: subject/body/save locked; on/off can still be changed (site setting). */
106 const isCoreModuleBodyLocked = useMemo(() => {
107 if (!isCoreSettingsEdit || isCoreStrictViewOnly) {
108 return false;
109 }
110 const req = catalogEntryForCore?.requiresModule;
111 if (!req) {
112 return false;
113 }
114 return !isProPluginActive() || !isModuleActive(req);
115 }, [isCoreSettingsEdit, isCoreStrictViewOnly, catalogEntryForCore]);
116
117 /** Subject/body (and merge-tag insert) read-only; strict URL also locks status. */
118 const isCoreBodyReadOnly = isCoreStrictViewOnly || isCoreModuleBodyLocked;
119
120 const isCoreViewMode = isCoreStrictViewOnly || isCoreModuleBodyLocked;
121
122 const goBack = () => {
123 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates`;
124 };
125 const queryClient = useQueryClient();
126 const { showToast } = useToast();
127
128 const [formData, setFormData] = useState({
129 name: "",
130 description: "",
131 from_name: "",
132 from_email: "",
133 to_email: "",
134 reply_to: "",
135 bcc: "",
136 cc: "",
137 subject: "",
138 body: "",
139 event_key: "",
140 is_active: true,
141 });
142 const [showPreview, setShowPreview] = useState(false);
143 const [previewData, setPreviewData] = useState<PreviewData | null>(null);
144 const [testEmail, setTestEmail] = useState("");
145 const [copiedVar, setCopiedVar] = useState<string | null>(null);
146 const [showEventDropdown, setShowEventDropdown] = useState(false);
147 const [showAiModal, setShowAiModal] = useState(false);
148
149 useEffect(() => {
150 if (isCreateMode && !isEmailAutomationModuleEnabled()) {
151 window.location.href =
152 "admin.php?page=yatra&subpage=email-automation&tab=templates";
153 }
154 }, [isCreateMode]);
155
156 useEffect(() => {
157 const params = new URLSearchParams(window.location.search);
158 if (params.get("action") !== "edit") return;
159 const ct = params.get("core_template");
160 if (ct && !isCoreTemplateSlug(ct)) {
161 window.location.href =
162 "admin.php?page=yatra&subpage=email-automation&tab=templates";
163 }
164 }, []);
165
166 const { data: settings, isLoading: settingsLoading } = useQuery({
167 queryKey: ["settings"],
168 queryFn: () => fetchSettings(),
169 enabled: isCoreSettingsEdit,
170 });
171
172 const { data: templateData, isLoading } = useQuery({
173 queryKey: ["email-template", id],
174 queryFn: () => fetchEmailTemplate(id as string),
175 enabled: Boolean(id) && !isCoreSettingsEdit,
176 });
177
178 // Event-scoped variables: the sidebar re-fetches whenever the
179 // operator switches the trigger event so the "Available
180 // Variables" panel reflects only tags that will actually
181 // resolve at send-time for the chosen event. Empty event_key
182 // returns the full union (for the create-template starter
183 // state before they've picked an event).
184 const { data: variablesData } = useQuery({
185 queryKey: ["email-variables", formData.event_key || ""],
186 queryFn: () => fetchEmailTemplateVariables(formData.event_key || ""),
187 enabled: !isCoreSettingsEdit && isEmailAutomationModuleEnabled(),
188 });
189
190 // Trigger events. Fetch them live rather than trusting only the one-time
191 // window.yatraAdmin.emailEvents localize snapshot: that snapshot is written
192 // by the Pro module at PHP render time, but the module-enabled flag is
193 // refreshed client-side (useModules), so the two can diverge — the form
194 // opens (flag says enabled) while emailEvents is still empty, leaving the
195 // dropdown with "No Event to select". The live fetch corrects that; the
196 // localize snapshot is used as an instant fallback while it loads.
197 const { data: fetchedEvents } = useQuery({
198 queryKey: ["email-template-events"],
199 queryFn: fetchEmailTemplateEvents,
200 enabled: !isCoreSettingsEdit && isEmailAutomationModuleEnabled(),
201 });
202 // Resolve the trigger-event list from the first non-empty source. Both the
203 // live fetch AND the window.yatraAdmin.emailEvents localize are gated on the
204 // Pro Email Automation module actively delivering data (route + localize both
205 // only run when the module is enabled and the client flag agrees), so if the
206 // module gate isn't delivering — disabled, or a client/server flag drift —
207 // both are empty and the dropdown ended up unselectable. Fall back to the
208 // module-INDEPENDENT catalog (same source the templates list uses) so an event
209 // can always be chosen. `??` alone won't do — [] is not nullish.
210 const events = useMemo(() => {
211 if (Array.isArray(fetchedEvents) && fetchedEvents.length > 0) {
212 return fetchedEvents;
213 }
214 const localized = (window as any).yatraAdmin?.emailEvents;
215 if (Array.isArray(localized) && localized.length > 0) {
216 return localized;
217 }
218 // Derive a readable name from the event key (e.g. "booking.created" →
219 // "Booking Created") since the catalog keys events, not names.
220 const prettify = (key: string) =>
221 key.replace(/[._]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
222 return [
223 ...new Set(
224 EMAIL_TEMPLATES_CATALOG.map((e) => e.event_key).filter(Boolean),
225 ),
226 ].map((key) => ({ key, name: prettify(key) }));
227 }, [fetchedEvents]);
228
229 useEffect(() => {
230 if (!templateData || typeof templateData !== "object") return;
231 const t = templateData as Record<string, unknown>;
232 setFormData({
233 name: String(t.name ?? ""),
234 description: String(t.description ?? ""),
235 from_name: String(t.from_name ?? ""),
236 from_email: String(t.from_email ?? ""),
237 to_email: String(t.to_email ?? ""),
238 reply_to: String(t.reply_to ?? ""),
239 bcc: String(t.bcc ?? ""),
240 cc: String(t.cc ?? ""),
241 subject: String(t.subject ?? ""),
242 body: String(t.body ?? ""),
243 event_key: String(t.event_key ?? ""),
244 is_active: Boolean(t.is_active ?? true),
245 });
246 }, [templateData]);
247
248 useEffect(() => {
249 if (!isCoreSettingsEdit || !settings || !coreDef) return;
250 const flag = coreDef.settingsFlag;
251 const subj = coreDef.settingsSubject;
252 const bodyKey = coreDef.settingsBody;
253 if (!flag || !subj || !bodyKey) return;
254 const s = settings as Record<string, unknown>;
255 setFormData({
256 name: coreDef.name,
257 description: coreDef.description,
258 from_name: "",
259 from_email: "",
260 to_email: coreDef.to_email || "",
261 reply_to: "",
262 bcc: String(s[coreAddressKey(subj, "bcc") ?? ""] ?? ""),
263 cc: String(s[coreAddressKey(subj, "cc") ?? ""] ?? ""),
264 subject: String(s[subj] ?? ""),
265 body: String(s[bodyKey] ?? ""),
266 event_key: coreDef.event_key,
267 is_active: Boolean(s[flag]),
268 });
269 }, [isCoreSettingsEdit, settings, coreDef]);
270
271 const saveMutation = useMutation({
272 mutationFn: async (data: typeof formData) => {
273 if (isCreateMode) {
274 const response = await createEmailTemplate(data);
275 const r = response as {
276 success?: boolean;
277 message?: string;
278 data?: { id?: string | number };
279 };
280 if (r && r.success === false) {
281 throw new Error(r.message || __("Failed to create template"));
282 }
283 return response;
284 }
285 return await updateEmailTemplate(id as string, data);
286 },
287 onSuccess: (response) => {
288 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
289 if (isCreateMode) {
290 showToast(__("Template created successfully"), "success");
291 // Redirect to edit page after creation - response.data contains the template
292 const newId = (response as { data?: { id?: string | number } })?.data
293 ?.id;
294 if (newId) {
295 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=template&action=edit&id=${newId}`;
296 } else {
297 // Fallback to list page
298 window.location.href =
299 "admin.php?page=yatra&subpage=email-automation&tab=templates";
300 }
301 } else {
302 queryClient.invalidateQueries({ queryKey: ["email-template", id] });
303 showToast(__("Template saved successfully"), "success");
304 }
305 },
306 onError: (error: any) => {
307 const message =
308 error?.message ||
309 (isCreateMode
310 ? __("Failed to create template")
311 : __("Failed to save template"));
312 showToast(message, "error");
313 },
314 });
315
316 const saveCoreMutation = useMutation({
317 mutationFn: async () => {
318 if (isCoreStrictViewOnly) {
319 throw new Error(__("This template is view-only.", "yatra"));
320 }
321 if (
322 !coreDef?.settingsFlag ||
323 !coreDef.settingsSubject ||
324 !coreDef.settingsBody
325 ) {
326 throw new Error(__("Invalid template", "yatra"));
327 }
328 if (!settings) {
329 throw new Error(__("Settings not loaded", "yatra"));
330 }
331 const base = { ...(settings as Record<string, unknown>) };
332 if (isCoreModuleBodyLocked) {
333 base[coreDef.settingsFlag] = formData.is_active;
334 await saveSettings(base);
335 return base;
336 }
337 base[coreDef.settingsSubject] = formData.subject;
338 base[coreDef.settingsBody] = formData.body;
339 base[coreDef.settingsFlag] = formData.is_active;
340
341 const bccKey = coreAddressKey(coreDef.settingsSubject, "bcc");
342 const ccKey = coreAddressKey(coreDef.settingsSubject, "cc");
343 if (bccKey) base[bccKey] = formData.bcc;
344 if (ccKey) base[ccKey] = formData.cc;
345 await saveSettings(base);
346 return base;
347 },
348 onSuccess: (saved) => {
349 queryClient.setQueryData(["settings"], saved);
350 showToast(__("Template saved", "yatra"), "success");
351 },
352 onError: (error: unknown) => {
353 const message =
354 error instanceof Error ? error.message : __("Failed to save", "yatra");
355 showToast(message, "error");
356 },
357 });
358
359 const previewMutation = useMutation({
360 mutationFn: async () => {
361 return previewEmailTemplate(id as string);
362 },
363 onSuccess: (data) => {
364 setPreviewData(data);
365 setShowPreview(true);
366 },
367 onError: () => {
368 showToast(__("Failed to load preview"), "error");
369 },
370 });
371
372 const corePreviewMutation = useMutation({
373 mutationFn: async () => {
374 if (!coreTemplateSlug) {
375 throw new Error(__("Invalid template", "yatra"));
376 }
377 return previewCoreEmailTemplate({
378 template_key: coreTemplateSlug,
379 subject: formData.subject,
380 body: formData.body,
381 });
382 },
383 onSuccess: (data) => {
384 setPreviewData(data);
385 setShowPreview(true);
386 },
387 onError: (error: unknown) => {
388 const message =
389 error instanceof Error ? error.message : __("Preview failed", "yatra");
390 showToast(message, "error");
391 },
392 });
393
394 const testMutation = useMutation({
395 mutationFn: async (email: string) => {
396 return sendEmailTemplateTest(id as string, email);
397 },
398 onSuccess: () => {
399 showToast(__("Test email sent successfully"), "success");
400 setTestEmail("");
401 },
402 onError: () => {
403 showToast(__("Failed to send test email"), "error");
404 },
405 });
406
407 const handleSubmit = (e?: React.FormEvent | React.MouseEvent) => {
408 if (e) e.preventDefault();
409
410 if (isCoreSettingsEdit) {
411 if (isCoreStrictViewOnly) {
412 return;
413 }
414 saveCoreMutation.mutate();
415 return;
416 }
417
418 if (!formData.name.trim()) {
419 showToast(__("Template name is required"), "error");
420 return;
421 }
422 if (!formData.subject.trim()) {
423 showToast(__("Subject line is required"), "error");
424 return;
425 }
426
427 saveMutation.mutate(formData);
428 };
429
430 const copyVariable = (variable: string) => {
431 const text = `{{${variable}}}`;
432
433 const markCopied = () => {
434 setCopiedVar(variable);
435 setTimeout(() => setCopiedVar(null), 2000);
436 };
437
438 // Legacy fallback: navigator.clipboard is only defined in a secure context
439 // (HTTPS or localhost). On a plain-HTTP wp-admin it is undefined, so the
440 // previous call threw and nothing was copied. execCommand("copy") works
441 // there via a temporary off-screen textarea.
442 const legacyCopy = (): boolean => {
443 try {
444 const el = document.createElement("textarea");
445 el.value = text;
446 el.setAttribute("readonly", "");
447 el.style.position = "fixed";
448 el.style.top = "-9999px";
449 document.body.appendChild(el);
450 el.select();
451 const ok = document.execCommand("copy");
452 document.body.removeChild(el);
453 return ok;
454 } catch {
455 return false;
456 }
457 };
458
459 if (navigator?.clipboard?.writeText) {
460 navigator.clipboard.writeText(text).then(markCopied, () => {
461 if (legacyCopy()) {
462 markCopied();
463 }
464 });
465 } else if (legacyCopy()) {
466 markCopied();
467 }
468 };
469
470 const insertVariable = (variable: string) => {
471 if (isCoreBodyReadOnly) {
472 return;
473 }
474 const textarea = document.getElementById(
475 "email-body",
476 ) as HTMLTextAreaElement;
477 if (textarea) {
478 const start = textarea.selectionStart;
479 const end = textarea.selectionEnd;
480 const text = formData.body;
481 const newText =
482 text.substring(0, start) + `{{${variable}}}` + text.substring(end);
483 setFormData({ ...formData, body: newText });
484 }
485 };
486
487 const showEditSkeleton =
488 !isCreateMode && (isCoreSettingsEdit ? settingsLoading : isLoading);
489
490 if (showEditSkeleton) {
491 return (
492 <div className="space-y-6">
493 {/* Header Skeleton */}
494 <div className="flex items-center justify-between animate-pulse">
495 <div className="flex items-center gap-4">
496 <div className="h-10 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
497 <div>
498 <div className="h-8 w-56 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
499 <div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded"></div>
500 </div>
501 </div>
502 <div className="flex items-center gap-3">
503 <div className="h-10 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
504 <div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
505 </div>
506 </div>
507
508 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
509 {/* Main Form Skeleton */}
510 <div className="lg:col-span-2 space-y-6">
511 {/* Template Details Card */}
512 <Card>
513 <CardHeader>
514 <div className="flex items-center justify-between">
515 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
516 <div className="h-6 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
517 </div>
518 </CardHeader>
519 <CardContent className="space-y-4">
520 <div className="space-y-2">
521 <div className="h-4 w-28 bg-gray-200 dark:bg-gray-700 rounded"></div>
522 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
523 </div>
524 <div className="space-y-2">
525 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
526 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
527 </div>
528 <div className="space-y-2">
529 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
530 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
531 </div>
532 <div className="space-y-2">
533 <div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
534 <div className="h-64 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
535 </div>
536 </CardContent>
537 </Card>
538
539 {/* Test Email Card */}
540 <Card>
541 <CardHeader>
542 <div className="h-6 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
543 </CardHeader>
544 <CardContent>
545 <div className="flex gap-3">
546 <div className="h-10 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
547 <div className="h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
548 </div>
549 </CardContent>
550 </Card>
551 </div>
552
553 {/* Sidebar Skeleton */}
554 <div className="space-y-6">
555 {/* Variables Card */}
556 <Card>
557 <CardHeader>
558 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
559 </CardHeader>
560 <CardContent className="space-y-4">
561 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
562 {[1, 2, 3].map((group) => (
563 <div key={group} className="space-y-2">
564 <div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
565 {[1, 2, 3].map((item) => (
566 <div
567 key={item}
568 className="h-12 w-full bg-gray-200 dark:bg-gray-700 rounded"
569 ></div>
570 ))}
571 </div>
572 ))}
573 </CardContent>
574 </Card>
575
576 {/* Template Info Card */}
577 <Card>
578 <CardHeader>
579 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded"></div>
580 </CardHeader>
581 <CardContent className="space-y-3">
582 {[1, 2, 3].map((i) => (
583 <div key={i} className="flex justify-between">
584 <div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
585 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
586 </div>
587 ))}
588 </CardContent>
589 </Card>
590 </div>
591 </div>
592 </div>
593 );
594 }
595
596 return (
597 <div className="space-y-6">
598 {/* Header */}
599 <div className="flex items-center justify-between">
600 <div className="flex items-center gap-4">
601 <Button variant="ghost" onClick={() => goBack()}>
602 <ArrowLeft className="w-4 h-4 mr-2" />
603 {__("Back")}
604 </Button>
605 <div>
606 <h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
607 <Mail className="w-6 h-6 text-blue-500" />
608 {isCreateMode
609 ? __("Create Email Template")
610 : isCoreViewMode
611 ? __("View email template", "yatra")
612 : __("Edit Email Template")}
613 </h1>
614 <p className="text-gray-500 dark:text-gray-400 mt-1">
615 {isCreateMode
616 ? __("Create a new custom email template")
617 : isCoreSettingsEdit
618 ? isCoreViewMode
619 ? __(
620 "This template uses site settings. Enable Yatra Pro and the required module to edit subject and HTML body.",
621 "yatra",
622 )
623 : __(
624 "Core customer email (saved with site settings). From address is set under Email → Delivery.",
625 "yatra",
626 )
627 : String(templateData?.name ?? "")}
628 </p>
629 </div>
630 </div>
631 <div className="flex items-center gap-3">
632 {!isCreateMode && (
633 <Button
634 variant="outline"
635 onClick={() =>
636 isCoreSettingsEdit
637 ? corePreviewMutation.mutate()
638 : previewMutation.mutate()
639 }
640 disabled={
641 previewMutation.isPending || corePreviewMutation.isPending
642 }
643 >
644 {previewMutation.isPending || corePreviewMutation.isPending ? (
645 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
646 ) : (
647 <Eye className="w-4 h-4 mr-2" />
648 )}
649 {__("Preview")}
650 </Button>
651 )}
652 <Button
653 onClick={handleSubmit}
654 disabled={
655 saveMutation.isPending ||
656 saveCoreMutation.isPending ||
657 isCoreStrictViewOnly
658 }
659 className="bg-blue-600 hover:bg-blue-700 text-white"
660 >
661 {saveMutation.isPending || saveCoreMutation.isPending ? (
662 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
663 ) : (
664 <Save className="w-4 h-4 mr-2" />
665 )}
666 {isCreateMode ? __("Create Template") : __("Save Template")}
667 </Button>
668 </div>
669 </div>
670
671 {isCoreBodyReadOnly && (
672 <div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
673 <Info className="w-5 h-5 flex-shrink-0 mt-0.5" />
674 <p>
675 {isCoreStrictViewOnly
676 ? __(
677 "This link is view-only. Subject, body, and status cannot be changed from this screen.",
678 "yatra",
679 )
680 : __(
681 "Subject and HTML body are read-only until the required module is active. You can still turn this email on or off below; values reflect your site settings.",
682 "yatra",
683 )}
684 </p>
685 </div>
686 )}
687
688 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
689 {/* Main Form */}
690 <div className="lg:col-span-2 space-y-6">
691 <Card>
692 <CardHeader>
693 <CardTitle className="flex items-center justify-between">
694 <span>{__("Template Details")}</span>
695 <div className="flex items-center gap-2">
696 <span
697 className={`text-sm ${formData.is_active ? "text-green-600 dark:text-green-400" : "text-gray-500"}`}
698 >
699 {formData.is_active ? __("Active") : __("Inactive")}
700 </span>
701 <Switch
702 checked={formData.is_active}
703 disabled={isCoreStrictViewOnly}
704 onCheckedChange={(checked) =>
705 setFormData({ ...formData, is_active: checked })
706 }
707 />
708 </div>
709 </CardTitle>
710 </CardHeader>
711 <CardContent className="space-y-4">
712 <div>
713 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
714 {__("Template Name")}
715 </label>
716 <Input
717 value={formData.name}
718 onChange={(e) =>
719 setFormData({ ...formData, name: e.target.value })
720 }
721 placeholder={__("Enter template name")}
722 readOnly={isCoreSettingsEdit}
723 className={
724 isCoreSettingsEdit ? "bg-gray-50 dark:bg-gray-900/40" : ""
725 }
726 />
727 </div>
728
729 <div>
730 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
731 {__("Description")}
732 </label>
733 <Input
734 value={formData.description}
735 onChange={(e) =>
736 setFormData({ ...formData, description: e.target.value })
737 }
738 placeholder={__(
739 "Brief description of when this email is sent",
740 )}
741 readOnly={isCoreSettingsEdit}
742 className={
743 isCoreSettingsEdit ? "bg-gray-50 dark:bg-gray-900/40" : ""
744 }
745 />
746 </div>
747
748 {/* Event Selector */}
749 <div>
750 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
751 {__("Trigger Event")}
752 </label>
753 {Boolean(templateData?.is_system) || isCoreSettingsEdit ? (
754 <div className="space-y-2">
755 <div className="flex items-center gap-2">
756 <span className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800">
757 <Zap className="w-4 h-4" />
758 {formData.event_key || "-"}
759 </span>
760 <span className="text-xs text-gray-500 dark:text-gray-400">
761 {__("System templates cannot change events")}
762 </span>
763 </div>
764 {formData.event_key && (
765 <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
766 <p className="text-sm text-indigo-800 dark:text-indigo-300">
767 {events.find((e: any) => e.key === formData.event_key)
768 ?.description || ""}
769 </p>
770 </div>
771 )}
772 </div>
773 ) : (
774 <div className="relative">
775 {/* Dropdown Trigger Button */}
776 <button
777 type="button"
778 onClick={() => setShowEventDropdown(!showEventDropdown)}
779 className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-left flex items-center justify-between hover:border-gray-400 dark:hover:border-gray-500 transition-colors"
780 >
781 {formData.event_key ? (
782 <div className="flex items-center gap-2">
783 <Zap className="w-4 h-4 text-blue-500" />
784 <span className="text-sm font-medium text-gray-900 dark:text-white">
785 {events.find(
786 (e: any) => e.key === formData.event_key,
787 )?.name || formData.event_key}
788 </span>
789 </div>
790 ) : (
791 <div className="flex items-center gap-2">
792 <Mail className="w-4 h-4 text-gray-400" />
793 <span className="text-sm text-gray-500 dark:text-gray-400">
794 {__("No Event (Sequence Only)")}
795 </span>
796 </div>
797 )}
798 <ChevronDown
799 className={`w-4 h-4 text-gray-400 transition-transform ${showEventDropdown ? "rotate-180" : ""}`}
800 />
801 </button>
802
803 {/* Dropdown List */}
804 {showEventDropdown && (
805 <div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-80 overflow-y-auto">
806 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
807 <p className="text-xs text-gray-500 dark:text-gray-400 px-2">
808 {__(
809 "Select an event to trigger this email, or leave empty for sequence use",
810 )}
811 </p>
812 </div>
813 <div className="py-1">
814 {/* None option - for templates used in sequences */}
815 <button
816 type="button"
817 onClick={() => {
818 setFormData({ ...formData, event_key: "" });
819 setShowEventDropdown(false);
820 }}
821 className={`w-full px-3 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-700 ${
822 !formData.event_key
823 ? "bg-indigo-50 dark:bg-indigo-900/20"
824 : ""
825 }`}
826 >
827 <div className="flex items-start gap-2">
828 <Mail
829 className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
830 !formData.event_key
831 ? "text-blue-500"
832 : "text-gray-400"
833 }`}
834 />
835 <div className="flex-1 min-w-0">
836 <div className="flex items-center gap-2">
837 <span
838 className={`text-sm font-medium ${
839 !formData.event_key
840 ? "text-blue-800 dark:text-blue-400"
841 : "text-gray-900 dark:text-white"
842 }`}
843 >
844 {__("No Event (Sequence Only)")}
845 </span>
846 </div>
847 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
848 {__(
849 "This template will only be used in email sequences",
850 )}
851 </p>
852 </div>
853 {!formData.event_key && (
854 <Check className="w-4 h-4 text-blue-500 flex-shrink-0" />
855 )}
856 </div>
857 </button>
858 {events.map((event: any) => (
859 <button
860 key={event.key}
861 type="button"
862 onClick={() => {
863 setFormData({
864 ...formData,
865 event_key: event.key,
866 });
867 setShowEventDropdown(false);
868 }}
869 className={`w-full px-3 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-700 last:border-b-0 ${
870 formData.event_key === event.key
871 ? "bg-indigo-50 dark:bg-indigo-900/20"
872 : ""
873 }`}
874 >
875 <div className="flex items-start gap-2">
876 <Zap
877 className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
878 formData.event_key === event.key
879 ? "text-blue-500"
880 : "text-gray-400"
881 }`}
882 />
883 <div className="flex-1 min-w-0">
884 <div className="flex items-center gap-2">
885 <span
886 className={`text-sm font-medium ${
887 formData.event_key === event.key
888 ? "text-blue-800 dark:text-blue-400"
889 : "text-gray-900 dark:text-white"
890 }`}
891 >
892 {event.name}
893 </span>
894 <code className="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded">
895 {event.key}
896 </code>
897 </div>
898 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
899 {event.description}
900 </p>
901 </div>
902 {formData.event_key === event.key && (
903 <Check className="w-4 h-4 text-blue-500 flex-shrink-0" />
904 )}
905 </div>
906 </button>
907 ))}
908 {events.length === 0 && (
909 <div className="px-3 py-3 text-xs text-amber-600 dark:text-amber-400">
910 {__(
911 "No trigger events are available. Enable the Email Automation module under Yatra → Modules, then reload this page.",
912 )}
913 </div>
914 )}
915 </div>
916 </div>
917 )}
918
919 {/* Click outside to close */}
920 {showEventDropdown && (
921 <div
922 className="fixed inset-0 z-40"
923 onClick={() => setShowEventDropdown(false)}
924 />
925 )}
926 </div>
927 )}
928 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
929 {__(
930 "This email will be triggered when the selected event occurs",
931 )}
932 </p>
933 </div>
934
935 {!isCoreSettingsEdit && (
936 <>
937 {/* From/Reply-To Settings */}
938 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
939 <div>
940 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
941 {__("From Name")}
942 </label>
943 <Input
944 value={formData.from_name}
945 onChange={(e) =>
946 setFormData({
947 ...formData,
948 from_name: e.target.value,
949 })
950 }
951 placeholder={__("e.g., Travel Agency Name")}
952 />
953 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
954 {__("Leave empty to use site default")}
955 </p>
956 </div>
957 <div>
958 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
959 {__("From Email")}
960 </label>
961 <Input
962 type="email"
963 value={formData.from_email}
964 onChange={(e) =>
965 setFormData({
966 ...formData,
967 from_email: e.target.value,
968 })
969 }
970 placeholder={__("e.g., [email protected]")}
971 />
972 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
973 {__("Leave empty to use site default")}
974 </p>
975 </div>
976 </div>
977
978 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
979 <div>
980 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
981 {__("To Email")}
982 </label>
983 <Input
984 type="email"
985 value={formData.to_email}
986 onChange={(e) =>
987 setFormData({ ...formData, to_email: e.target.value })
988 }
989 placeholder={__("e.g., {{customer_email}}")}
990 />
991 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
992 {__(
993 "Recipient email. Use variables or leave empty for default",
994 )}
995 </p>
996 </div>
997 <div>
998 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
999 {__("Reply-To Email")}
1000 </label>
1001 <Input
1002 type="email"
1003 value={formData.reply_to}
1004 onChange={(e) =>
1005 setFormData({ ...formData, reply_to: e.target.value })
1006 }
1007 placeholder={__("e.g., [email protected]")}
1008 />
1009 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1010 {__(
1011 "Where replies will be sent. Leave empty to use From Email",
1012 )}
1013 </p>
1014 </div>
1015 </div>
1016 </>
1017 )}
1018
1019 {isCoreSettingsEdit && (
1020 <p className="text-sm text-gray-600 dark:text-gray-400 rounded-lg border border-gray-200 dark:border-gray-600 p-3 bg-gray-50 dark:bg-gray-800/50">
1021 {__(
1022 "From / reply addresses are configured under Email → Delivery. Recipient uses the customer on the booking.",
1023 "yatra",
1024 )}
1025 </p>
1026 )}
1027
1028 {/* Copy addresses — available for every template, core or Pro.
1029 Both are opt-in: left empty, no copy is sent. */}
1030 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1031 <div>
1032 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1033 {__("BCC", "yatra")}
1034 </label>
1035 <Input
1036 value={formData.bcc}
1037 onChange={(e) =>
1038 setFormData({ ...formData, bcc: e.target.value })
1039 }
1040 placeholder={__("e.g., [email protected]", "yatra")}
1041 disabled={isCoreViewMode}
1042 />
1043 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1044 {__(
1045 "Blind copy, hidden from the recipient. Separate several addresses with commas. Leave empty to send no copy.",
1046 "yatra",
1047 )}
1048 </p>
1049 </div>
1050 <div>
1051 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1052 {__("CC", "yatra")}
1053 </label>
1054 <Input
1055 value={formData.cc}
1056 onChange={(e) =>
1057 setFormData({ ...formData, cc: e.target.value })
1058 }
1059 placeholder={__("e.g., [email protected]", "yatra")}
1060 disabled={isCoreViewMode}
1061 />
1062 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1063 {__(
1064 "Visible copy — the recipient can see this address. Separate several addresses with commas.",
1065 "yatra",
1066 )}
1067 </p>
1068 </div>
1069 </div>
1070
1071 <div>
1072 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1073 {__("Subject Line")}
1074 </label>
1075 <Input
1076 value={formData.subject}
1077 onChange={(e) =>
1078 setFormData({ ...formData, subject: e.target.value })
1079 }
1080 placeholder={__("Email subject with {{variables}}")}
1081 readOnly={isCoreBodyReadOnly}
1082 className={`font-mono text-sm ${isCoreBodyReadOnly ? "bg-gray-50 dark:bg-gray-900/40" : ""}`}
1083 />
1084 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1085 {__("Use {{variable}} syntax to insert dynamic content")}
1086 </p>
1087 </div>
1088
1089 <div>
1090 {/* Label + AI sparkle on the same row — sparkle sits
1091 to the right of the "Email Body (HTML)" label so
1092 it's visually associated with the editor it
1093 affects, not floating above Subject Line. */}
1094 <div className="mb-1 flex items-center justify-between">
1095 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
1096 {__("Email Body (HTML)")}
1097 </label>
1098 {!isCoreBodyReadOnly &&
1099 isAiEligible() &&
1100 isAiModuleEnabled() && (
1101 <Button
1102 type="button"
1103 variant="outline"
1104 size="sm"
1105 onClick={() => setShowAiModal(true)}
1106 className="border-blue-300 text-blue-700 hover:bg-blue-50 dark:border-blue-500/40 dark:text-blue-200 dark:hover:bg-blue-900/30"
1107 >
1108 <Sparkles className="mr-1.5 h-3.5 w-3.5" />
1109 {__("Generate with AI", "yatra")}
1110 </Button>
1111 )}
1112 </div>
1113 <textarea
1114 id="email-body"
1115 value={formData.body}
1116 onChange={(e) =>
1117 setFormData({ ...formData, body: e.target.value })
1118 }
1119 rows={20}
1120 readOnly={isCoreBodyReadOnly}
1121 className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg font-mono text-sm ${
1122 isCoreBodyReadOnly
1123 ? "bg-gray-50 dark:bg-gray-900/40 text-gray-900 dark:text-white"
1124 : "bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
1125 }`}
1126 placeholder={__("HTML email content...")}
1127 />
1128 </div>
1129 </CardContent>
1130 </Card>
1131
1132 {/* Test Email - Only show in edit mode */}
1133 {!isCreateMode && !isCoreSettingsEdit && (
1134 <Card>
1135 <CardHeader>
1136 <CardTitle className="flex items-center gap-2">
1137 <Send className="w-5 h-5 text-blue-500" />
1138 {__("Send Test Email")}
1139 </CardTitle>
1140 </CardHeader>
1141 <CardContent>
1142 <div className="flex gap-3">
1143 <Input
1144 type="email"
1145 value={testEmail}
1146 onChange={(e) => setTestEmail(e.target.value)}
1147 placeholder={__("Enter email address")}
1148 className="flex-1"
1149 />
1150 <Button
1151 onClick={() => testMutation.mutate(testEmail)}
1152 disabled={!testEmail || testMutation.isPending}
1153 >
1154 {testMutation.isPending ? (
1155 <Loader2 className="w-4 h-4 animate-spin" />
1156 ) : (
1157 <Send className="w-4 h-4" />
1158 )}
1159 </Button>
1160 </div>
1161 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
1162 {__(
1163 "Send a test email with sample data to preview how it looks",
1164 )}
1165 </p>
1166 </CardContent>
1167 </Card>
1168 )}
1169 </div>
1170
1171 {/* Sidebar - Variables */}
1172 <div className="space-y-6">
1173 <Card>
1174 <CardHeader>
1175 <CardTitle className="flex items-center gap-2">
1176 <Code className="w-5 h-5 text-blue-500" />
1177 {__("Available Variables")}
1178 </CardTitle>
1179 </CardHeader>
1180 <CardContent className="space-y-4">
1181 {!isCoreSettingsEdit && (
1182 <>
1183 <p className="text-sm text-gray-500 dark:text-gray-400">
1184 {__("Click to copy, double-click to insert at cursor")}
1185 </p>
1186 {/*
1187 Subtitle showing which event the variables list is
1188 scoped to. Without this hint, operators see a
1189 different set of tags every time they switch the
1190 event dropdown and have no clear explanation why.
1191 */}
1192 {formData.event_key ? (
1193 // Show the technical event KEY (dot-notation, monospace)
1194 // as the primary label so it can't be confused with the
1195 // template's display title — earlier versions showed
1196 // the event's friendly name, which often matches what
1197 // operators name their templates ("Customer email
1198 // verification" appears as both the event name and a
1199 // common template title) and made it unclear what the
1200 // chip was scoped to. Friendly name still appears as
1201 // the parenthesised clarifier.
1202 <p className="text-xs text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30 rounded px-2 py-1.5">
1203 {__("Variables available for trigger event:", "yatra")}{" "}
1204 <code className="font-mono text-blue-800 dark:text-blue-200 bg-blue-100 dark:bg-blue-900/40 px-1 rounded">
1205 {formData.event_key}
1206 </code>
1207 {(() => {
1208 const match = events.find(
1209 (e: any) => e.key === formData.event_key,
1210 );
1211 return match?.name ? (
1212 <span className="text-blue-600/80 dark:text-blue-400/80">
1213 {" "}
1214 ({match.name})
1215 </span>
1216 ) : null;
1217 })()}
1218 </p>
1219 ) : (
1220 <p className="text-xs text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 rounded px-2 py-1.5">
1221 {__(
1222 "Pick a trigger event below to see the variables that apply to that event. Showing the full catalog until then.",
1223 "yatra",
1224 )}
1225 </p>
1226 )}
1227 </>
1228 )}
1229
1230 {isCoreSettingsEdit && coreDef?.mergeTags ? (
1231 <div className="space-y-2">
1232 <p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
1233 {__("Merge tags", "yatra")}
1234 </p>
1235 <p className="text-xs text-gray-500 dark:text-gray-400">
1236 {__(
1237 "Click a tag to copy; paste into subject or HTML body.",
1238 "yatra",
1239 )}
1240 </p>
1241 <div className="flex flex-wrap gap-1">
1242 {coreDef.mergeTags.split(",").map((raw) => {
1243 const tag = raw.trim();
1244 const inner = tag.replace(/\{\{|\}\}/g, "").trim();
1245 if (!inner) return null;
1246 return (
1247 <button
1248 key={tag}
1249 type="button"
1250 onClick={() => copyVariable(inner)}
1251 className="rounded border border-gray-200 dark:border-gray-600 px-2 py-1 text-xs font-mono text-blue-600 dark:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800"
1252 >
1253 {tag}
1254 </button>
1255 );
1256 })}
1257 </div>
1258 </div>
1259 ) : (
1260 variablesData &&
1261 Object.entries(variablesData).map(
1262 ([category, variables]: [string, any]) => (
1263 <div key={category}>
1264 <h4 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">
1265 {category}
1266 </h4>
1267 <div className="space-y-1">
1268 {variables.map((v: any) => (
1269 <button
1270 key={v.key}
1271 type="button"
1272 onClick={() => copyVariable(v.key)}
1273 onDoubleClick={() => insertVariable(v.key)}
1274 className="w-full text-left px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors group"
1275 >
1276 <div className="flex items-center justify-between">
1277 <code className="text-xs text-blue-600 dark:text-blue-400">
1278 {`{{${v.key}}}`}
1279 </code>
1280 {copiedVar === v.key ? (
1281 <Check className="w-3 h-3 text-green-500" />
1282 ) : (
1283 <Copy className="w-3 h-3 text-gray-400 opacity-0 group-hover:opacity-100" />
1284 )}
1285 </div>
1286 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1287 {v.description}
1288 </p>
1289 </button>
1290 ))}
1291 </div>
1292 </div>
1293 ),
1294 )
1295 )}
1296 </CardContent>
1297 </Card>
1298
1299 {/* Template Info - Different content for create vs edit */}
1300 <Card>
1301 <CardHeader>
1302 <CardTitle className="flex items-center gap-2">
1303 <Info className="w-5 h-5 text-gray-500" />
1304 {isCreateMode ? __("New Template") : __("Template Info")}
1305 </CardTitle>
1306 </CardHeader>
1307 <CardContent className="space-y-3 text-sm">
1308 <div className="space-y-2">
1309 <div className="flex justify-between items-center">
1310 <span className="text-gray-500 dark:text-gray-400">
1311 {__("Event")}
1312 </span>
1313 <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400">
1314 <Zap className="w-3 h-3" />
1315 {formData.event_key || "-"}
1316 </span>
1317 </div>
1318 {formData.event_key && (
1319 <p className="text-xs text-gray-500 dark:text-gray-400 italic">
1320 {events.find((e: any) => e.key === formData.event_key)
1321 ?.description || ""}
1322 </p>
1323 )}
1324 </div>
1325 {isCreateMode ? (
1326 <>
1327 <div className="flex justify-between">
1328 <span className="text-gray-500 dark:text-gray-400">
1329 {__("Type")}
1330 </span>
1331 <span className="text-gray-900 dark:text-white">
1332 {__("Custom")}
1333 </span>
1334 </div>
1335 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
1336 <p className="text-xs text-blue-700 dark:text-blue-300">
1337 {__(
1338 "Custom templates can be edited and deleted. Select an event to trigger this email automatically.",
1339 )}
1340 </p>
1341 </div>
1342 </>
1343 ) : (
1344 <>
1345 <div className="flex justify-between">
1346 <span className="text-gray-500 dark:text-gray-400">
1347 {__("Key")}
1348 </span>
1349 <code className="text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
1350 {isCoreSettingsEdit
1351 ? coreDef?.template_key
1352 : String(templateData?.template_key ?? "")}
1353 </code>
1354 </div>
1355 <div className="flex justify-between">
1356 <span className="text-gray-500 dark:text-gray-400">
1357 {__("Category")}
1358 </span>
1359 <Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
1360 {isCoreSettingsEdit
1361 ? coreDef?.category
1362 : String(templateData?.category ?? "")}
1363 </Badge>
1364 </div>
1365 <div className="flex justify-between">
1366 <span className="text-gray-500 dark:text-gray-400">
1367 {__("Recipient")}
1368 </span>
1369 <span
1370 className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
1371 (isCoreSettingsEdit
1372 ? coreDef?.recipient_type
1373 : String(templateData?.recipient_type ?? "")) ===
1374 "admin"
1375 ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
1376 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
1377 }`}
1378 >
1379 {(isCoreSettingsEdit
1380 ? coreDef?.recipient_type
1381 : String(templateData?.recipient_type ?? "")) ===
1382 "admin"
1383 ? __("Admin")
1384 : __("Customer")}
1385 </span>
1386 </div>
1387 <div className="flex justify-between">
1388 <span className="text-gray-500 dark:text-gray-400">
1389 {__("Type")}
1390 </span>
1391 <span className="text-gray-900 dark:text-white">
1392 {isCoreSettingsEdit || Boolean(templateData?.is_system)
1393 ? __("System")
1394 : __("Custom")}
1395 </span>
1396 </div>
1397 </>
1398 )}
1399 </CardContent>
1400 </Card>
1401 </div>
1402 </div>
1403
1404 <EmailPreviewModal
1405 open={showPreview && Boolean(previewData)}
1406 onClose={() => {
1407 setShowPreview(false);
1408 setPreviewData(null);
1409 }}
1410 subject={previewData?.subject ?? ""}
1411 body={previewData?.body ?? ""}
1412 />
1413
1414 {/* AI: subject + body generator. The merge-tag list comes from
1415 the catalog entry when it's a core template, falling back
1416 to a sensible default set when editing a custom template. */}
1417 <AiEmailTemplateModal
1418 open={showAiModal}
1419 onClose={() => setShowAiModal(false)}
1420 templateKey={
1421 coreTemplateSlug || formData.event_key || "custom_template"
1422 }
1423 templateName={coreDef?.name || formData.name || formData.event_key}
1424 templateDescription={coreDef?.description || formData.description}
1425 recipientType={
1426 (catalogEntryForCore?.recipient_type as "customer" | "admin") ||
1427 "customer"
1428 }
1429 mergeTags={(() => {
1430 // Catalog stores mergeTags as a comma-string like
1431 // "{{customer_name}}, {{booking_reference}}, …". Coerce to
1432 // the bare tag names ('customer_name') that the agent's
1433 // server-side normalizer accepts.
1434 const raw = catalogEntryForCore?.mergeTags ?? "";
1435 if (!raw) {
1436 return [
1437 "site_name",
1438 "site_url",
1439 "customer_name",
1440 "customer_email",
1441 "booking_reference",
1442 "trip_name",
1443 "travel_date",
1444 "total_amount",
1445 "currency",
1446 ];
1447 }
1448 return raw
1449 .split(",")
1450 .map((s) =>
1451 s
1452 .trim()
1453 .replace(/^\{\{|\}\}$/g, "")
1454 .trim(),
1455 )
1456 .filter(Boolean);
1457 })()}
1458 currentSubject={formData.subject}
1459 currentBody={formData.body}
1460 onAccept={({ subject, body }) =>
1461 setFormData({ ...formData, subject, body })
1462 }
1463 />
1464 </div>
1465 );
1466 };
1467
1468 export default EmailTemplateForm;
1469