PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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 / components / email / EmailTemplatesList.tsx

EmailTemplatesList.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.16, at resources/js/components/email/EmailTemplatesList.tsx

1,399 lines 48.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Unified email templates table: Pro + Email Automation uses REST data;
3 * otherwise the same layout with settings-backed core rows and locked Pro-only rows.
4 */
5
6 import React, { useState, useMemo, useCallback } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { __, sprintf } from "../../lib/i18n";
9 import { useToast } from "../ui/toast";
10 import { Card, CardContent } from "../ui/card";
11 import { Button } from "../ui/button";
12 import { Input } from "../ui/input";
13 import { Select } from "../ui/select";
14 import { ConfirmationDialog } from "../ui/confirmation-dialog";
15 import { Switch } from "../ui/switch";
16 import { Table as SharedTable, BulkActionToolbar, Pagination } from "../shared";
17 import {
18 Mail,
19 Edit,
20 Search,
21 FileText,
22 CreditCard,
23 MessageSquare,
24 Bell,
25 Megaphone,
26 Plus,
27 Trash2,
28 Zap,
29 Copy,
30 Lock,
31 Eye,
32 } from "lucide-react";
33 import { isProPluginActive, isModuleActive } from "../../lib/plugin-utils";
34 import {
35 buildLocalTemplateRows,
36 EMAIL_TEMPLATES_CATALOG,
37 getCatalogEntryByTemplateKey,
38 type UnifiedEmailTemplate,
39 } from "../../lib/email-templates-catalog";
40 import type { EmailSettingsValues } from "../settings/email-settings-types";
41 import type { EmailFieldChangeHandler } from "../settings/email-settings-types";
42 import {
43 deleteEmailTemplate,
44 duplicateEmailTemplate,
45 fetchEmailTemplates,
46 previewEmailTemplate,
47 setEmailTemplateActive,
48 } from "../../api/email-automation-api";
49 import { previewCoreEmailTemplate } from "../../api/settings-api";
50 import { EmailPreviewModal } from "./EmailPreviewModal";
51 import { EmailOverrideCreateModal } from "./EmailOverrideCreateModal";
52 import { SearchableSelect } from "../ui/searchable-select";
53 import {
54 reorderEmailTemplateOverrides,
55 resolveEmailTemplatesForTrip,
56 } from "../../api/email-automation-api";
57 import { describeTargets, useTripTargets } from "../../hooks/useTripTargets";
58 import { ArrowDown, ArrowUp, Compass, Globe } from "lucide-react";
59
60 const EMAIL_TEMPLATE_VISIBLE_COLUMNS_DEFAULT: Record<string, boolean> = {
61 name: true,
62 event: true,
63 subject: true,
64 body_preview: true,
65 description: true,
66 category: true,
67 recipient_type: true,
68 is_active: true,
69 };
70
71 function plainTextEmailPreview(html: string, maxLen = 100): string {
72 if (!html) return "";
73 const plain = html
74 .replace(/<script[\s\S]*?<\/script>/gi, " ")
75 .replace(/<style[\s\S]*?<\/style>/gi, " ")
76 .replace(/<[^>]+>/g, " ")
77 .replace(/\s+/g, " ")
78 .trim();
79 if (plain.length <= maxLen) return plain;
80 return `${plain.slice(0, maxLen)}…`;
81 }
82
83 export type EmailTemplatesListProps = {
84 automationModuleActive: boolean;
85 settingsBridge?: {
86 values: EmailSettingsValues;
87 onFieldChange: EmailFieldChangeHandler;
88 };
89 };
90
91 /** Module required by catalog but not active — template stays visible; content is view-only in free UI. */
92 function isModuleGatedLocked(template: UnifiedEmailTemplate): boolean {
93 const entry = getCatalogEntryByTemplateKey(template.template_key);
94 if (!entry?.requiresModule) {
95 return false;
96 }
97 return !isProPluginActive() || !isModuleActive(entry.requiresModule);
98 }
99
100 const getEffectiveRecipientType = (
101 template: UnifiedEmailTemplate,
102 ): "customer" | "admin" => {
103 const catalog = getCatalogEntryByTemplateKey(template.template_key);
104 if (catalog?.recipient_type) {
105 return catalog.recipient_type;
106 }
107 const fromApi = template.recipient_type;
108 if (fromApi === "admin" || fromApi === "customer") {
109 return fromApi;
110 }
111 const toEmail = template.to_email || "";
112 if (toEmail.includes("{{admin_email}}")) {
113 return "admin";
114 }
115 if (
116 toEmail.includes("{customer_email}") ||
117 toEmail.includes("{{customer_email}}")
118 ) {
119 return "customer";
120 }
121 return "customer";
122 };
123
124 const categoryIcons: Record<string, React.ElementType> = {
125 booking: FileText,
126 payment: CreditCard,
127 enquiry: MessageSquare,
128 reminder: Bell,
129 marketing: Megaphone,
130 };
131
132 const isApiTemplate = (t: UnifiedEmailTemplate): boolean => {
133 if (t.is_locked) {
134 return false;
135 }
136 if (typeof t.id === "number") {
137 return true;
138 }
139 return typeof t.id === "string" && /^[0-9]+$/.test(String(t.id).trim());
140 };
141
142 /** Pro DB templates use numeric id; the API may return id as a string (e.g. "42"). */
143 function toNumericTemplateId(id: string | number | undefined): number | null {
144 if (id === undefined) {
145 return null;
146 }
147 if (typeof id === "number" && Number.isFinite(id)) {
148 return id;
149 }
150 if (typeof id === "string" && /^[0-9]+$/.test(id.trim())) {
151 return Number(id);
152 }
153 return null;
154 }
155
156 export const EmailTemplatesList: React.FC<EmailTemplatesListProps> = ({
157 automationModuleActive,
158 settingsBridge,
159 }) => {
160 const queryClient = useQueryClient();
161 const { showToast } = useToast();
162
163 const [searchTerm, setSearchTerm] = useState("");
164 const [categoryFilter, setCategoryFilter] = useState("all");
165 const [recipientFilter, setRecipientFilter] = useState("all");
166 const [eventFilter, setEventFilter] = useState("all");
167 // Default to the "Active" view so the list shows only in-use templates and
168 // disabled/unused system templates stay decluttered out of sight. The "All"
169 // and "Inactive" tabs remain one click away to review or re-enable them.
170 const [statusFilter, setStatusFilter] = useState("active");
171 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
172 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
173 const [bulkAction, setBulkAction] = useState("");
174 const [currentPage, setCurrentPage] = useState(1);
175 const itemsPerPage = 20;
176
177 const [previewOpen, setPreviewOpen] = useState(false);
178 const [previewPayload, setPreviewPayload] = useState<{
179 subject: string;
180 body: string;
181 } | null>(null);
182
183 const [visibleColumns, setVisibleColumns] = useState(() => {
184 const saved = localStorage.getItem("yatra-email-templates-columns");
185 const parsed = saved ? JSON.parse(saved) : {};
186 return { ...EMAIL_TEMPLATE_VISIBLE_COLUMNS_DEFAULT, ...parsed };
187 });
188
189 const toggleColumn = (columnKey: string) => {
190 const newVisibleColumns = {
191 ...visibleColumns,
192 [columnKey]: !visibleColumns[columnKey],
193 };
194 setVisibleColumns(newVisibleColumns);
195 localStorage.setItem(
196 "yatra-email-templates-columns",
197 JSON.stringify(newVisibleColumns),
198 );
199 };
200
201 // Trip-specific overrides (Pro Email Automation ≥ the build that sets this
202 // flag). Without it the tab is exactly the pre-override list.
203 const overridesSupported =
204 automationModuleActive &&
205 !!(window as any).yatraAdmin?.emailTemplateOverridesEnabled;
206 const [section, setSection] = useState<"global" | "override">("global");
207 const [parentFilter, setParentFilter] = useState("");
208 const [asTrip, setAsTrip] = useState<number | null>(null);
209 const [overrideModal, setOverrideModal] = useState<{
210 open: boolean;
211 parent: UnifiedEmailTemplate | null;
212 }>({ open: false, parent: null });
213 const { options: tripTargetOptions } = useTripTargets(overridesSupported);
214 const tripOptions = useMemo(
215 () =>
216 tripTargetOptions
217 .filter((o) => String(o.value).startsWith("trip:"))
218 .map((o) => ({
219 id: Number(String(o.value).slice(5)),
220 label: o.label.replace(/^[^:]+: /, ""),
221 })),
222 [tripTargetOptions],
223 );
224 const { data: resolution } = useQuery({
225 queryKey: ["email-templates-resolve", asTrip],
226 queryFn: () => resolveEmailTemplatesForTrip(asTrip as number),
227 enabled: overridesSupported && !!asTrip,
228 });
229
230 const { data: templatesData, isLoading: apiLoading } = useQuery({
231 queryKey: ["email-templates"],
232 queryFn: () => fetchEmailTemplates(),
233 enabled: automationModuleActive,
234 });
235
236 const apiTemplates: UnifiedEmailTemplate[] = useMemo(() => {
237 const raw = Array.isArray(templatesData) ? templatesData : [];
238 return (raw as UnifiedEmailTemplate[]).map((t) => ({
239 ...t,
240 is_locked: false,
241 }));
242 }, [templatesData]);
243
244 const localTemplates = useMemo(() => {
245 if (automationModuleActive || !settingsBridge) return [];
246 return buildLocalTemplateRows(settingsBridge.values, {
247 isProPluginActive: isProPluginActive(),
248 isModuleActive,
249 });
250 }, [automationModuleActive, settingsBridge]);
251
252 const allTemplates = automationModuleActive ? apiTemplates : localTemplates;
253 const overrideTemplates = useMemo(
254 () => (overridesSupported ? allTemplates.filter((t) => !!t.overrides) : []),
255 [allTemplates, overridesSupported],
256 );
257 const globalTemplates = useMemo(
258 () =>
259 overridesSupported
260 ? allTemplates.filter((t) => !t.overrides)
261 : allTemplates,
262 [allTemplates, overridesSupported],
263 );
264 // The list the filters / table operate on: one section at a time.
265 const templates =
266 overridesSupported && section === "override"
267 ? overrideTemplates
268 : globalTemplates;
269 const globalByKey = useMemo(() => {
270 const m = new Map<string, UnifiedEmailTemplate>();
271 globalTemplates.forEach((t) => m.set(t.template_key, t));
272 return m;
273 }, [globalTemplates]);
274 const overridesByParent = useMemo(() => {
275 const m = new Map<string, UnifiedEmailTemplate[]>();
276 overrideTemplates.forEach((t) => {
277 const k = t.overrides || "";
278 m.set(k, [...(m.get(k) || []), t]);
279 });
280 m.forEach((arr) =>
281 arr.sort(
282 (a, b) =>
283 (a.priority || 0) - (b.priority || 0) || Number(a.id) - Number(b.id),
284 ),
285 );
286 return m;
287 }, [overrideTemplates]);
288
289 const isLoading = automationModuleActive ? apiLoading : false;
290
291 const reorderMutation = useMutation({
292 mutationFn: ({ parentKey, ids }: { parentKey: string; ids: number[] }) =>
293 reorderEmailTemplateOverrides(parentKey, ids),
294 onSuccess: () => {
295 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
296 queryClient.invalidateQueries({ queryKey: ["email-templates-resolve"] });
297 },
298 onError: (error: any) => {
299 showToast(
300 error?.message || __("Failed to reorder overrides", "yatra"),
301 "error",
302 );
303 },
304 });
305 const moveOverride = (t: UnifiedEmailTemplate, delta: number) => {
306 const siblings = overridesByParent.get(t.overrides || "") || [];
307 const ids = siblings.map((x) => Number(x.id));
308 const i = ids.indexOf(Number(t.id));
309 const j = i + delta;
310 if (i < 0 || j < 0 || j >= ids.length) return;
311 [ids[i], ids[j]] = [ids[j], ids[i]];
312 reorderMutation.mutate({ parentKey: t.overrides || "", ids });
313 };
314 /** "View as a trip": which row is sent for the chosen trip. */
315 const sentForTrip = (t: UnifiedEmailTemplate): "hit" | "dim" | null => {
316 if (!asTrip || !resolution) return null;
317 if (t.overrides) {
318 return resolution.resolved[t.overrides]?.id === Number(t.id)
319 ? "hit"
320 : "dim";
321 }
322 if (!resolution.overridable.includes(t.template_key)) return null; // not booking-bound: unaffected
323 return resolution.resolved[t.template_key] ? "dim" : "hit";
324 };
325
326 const toggleMutation = useMutation({
327 mutationFn: async ({
328 id,
329 is_active,
330 }: {
331 id: number;
332 is_active: boolean;
333 }) => {
334 return await setEmailTemplateActive(id, is_active);
335 },
336 onSuccess: () => {
337 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
338 showToast(__("Template updated"), "success");
339 },
340 });
341
342 const deleteMutation = useMutation({
343 mutationFn: async (id: number) => {
344 return await deleteEmailTemplate(id);
345 },
346 onSuccess: () => {
347 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
348 showToast(__("Template deleted"), "success");
349 },
350 onError: (error: any) => {
351 showToast(error?.message || __("Failed to delete template"), "error");
352 },
353 });
354
355 const duplicateMutation = useMutation({
356 mutationFn: async (id: number) => {
357 return await duplicateEmailTemplate(id);
358 },
359 onSuccess: () => {
360 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
361 showToast(__("Template duplicated successfully"), "success");
362 },
363 onError: (error: any) => {
364 showToast(error?.message || __("Failed to duplicate template"), "error");
365 },
366 });
367
368 const proPreviewMutation = useMutation({
369 mutationFn: (templateId: number) => previewEmailTemplate(templateId),
370 onSuccess: (data) => {
371 setPreviewPayload(data);
372 setPreviewOpen(true);
373 },
374 onError: () => {
375 showToast(__("Failed to load preview", "yatra"), "error");
376 },
377 });
378
379 const corePreviewMutation = useMutation({
380 mutationFn: (t: UnifiedEmailTemplate) =>
381 previewCoreEmailTemplate({
382 template_key: t.template_key,
383 subject: t.subject || "",
384 body: t.body || "",
385 }),
386 onSuccess: (data) => {
387 setPreviewPayload(data);
388 setPreviewOpen(true);
389 },
390 onError: () => {
391 showToast(__("Failed to load preview", "yatra"), "error");
392 },
393 });
394
395 const bulkDeleteMutation = useMutation({
396 mutationFn: async (ids: (string | number)[]) => {
397 const customTemplateIds = ids
398 .map((id) => toNumericTemplateId(id))
399 .filter((id): id is number => id !== null)
400 .filter((id) => {
401 const template = templates.find(
402 (t) => toNumericTemplateId(t.id) === id,
403 );
404 return template && !template.is_system && isApiTemplate(template);
405 });
406
407 if (customTemplateIds.length === 0) {
408 throw new Error(
409 __(
410 "No custom templates selected. System templates cannot be deleted.",
411 ),
412 );
413 }
414
415 await Promise.all(customTemplateIds.map((id) => deleteEmailTemplate(id)));
416 return {
417 deleted: customTemplateIds.length,
418 skipped: ids.length - customTemplateIds.length,
419 };
420 },
421 onSuccess: (data) => {
422 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
423 setSelectedIds([]);
424 setBulkAction("");
425 if (data.skipped > 0) {
426 showToast(
427 __(
428 `Deleted ${data.deleted} template(s). ${data.skipped} system template(s) were skipped.`,
429 ),
430 "success",
431 );
432 } else {
433 showToast(__(`Deleted ${data.deleted} template(s)`), "success");
434 }
435 },
436 onError: (error: any) => {
437 showToast(error?.message || __("Failed to delete templates"), "error");
438 },
439 });
440
441 const categories: string[] = useMemo(
442 () => [...new Set(templates.map((t) => t.category))] as string[],
443 [templates],
444 );
445
446 const events = useMemo(() => {
447 if (automationModuleActive) {
448 return (window as any).yatraAdmin?.emailEvents || [];
449 }
450 return [...new Set(EMAIL_TEMPLATES_CATALOG.map((e) => e.event_key))].map(
451 (key) => ({ key, name: key }),
452 );
453 }, [automationModuleActive]);
454
455 const filteredTemplates = useMemo(() => {
456 return templates.filter((t) => {
457 const q = searchTerm.toLowerCase();
458 const bodyPlain = plainTextEmailPreview(t.body || "", 500).toLowerCase();
459 const matchesSearch =
460 !searchTerm ||
461 t.name.toLowerCase().includes(q) ||
462 (t.description || "").toLowerCase().includes(q) ||
463 (t.subject || "").toLowerCase().includes(q) ||
464 bodyPlain.includes(q);
465 const matchesCategory =
466 categoryFilter === "all" || t.category === categoryFilter;
467 const matchesRecipient =
468 recipientFilter === "all" ||
469 getEffectiveRecipientType(t) === recipientFilter;
470 const matchesEvent =
471 eventFilter === "all" ||
472 (t.effective_event_key || t.event_key) === eventFilter;
473 const matchesParent =
474 !parentFilter || section !== "override" || t.overrides === parentFilter;
475 const matchesStatus =
476 statusFilter === "all" ||
477 (statusFilter === "active" && t.is_active) ||
478 (statusFilter === "inactive" && !t.is_active);
479 return (
480 matchesSearch &&
481 matchesCategory &&
482 matchesRecipient &&
483 matchesEvent &&
484 matchesStatus &&
485 matchesParent
486 );
487 });
488 }, [
489 templates,
490 searchTerm,
491 categoryFilter,
492 recipientFilter,
493 eventFilter,
494 statusFilter,
495 parentFilter,
496 section,
497 ]);
498
499 const totalFilteredItems = filteredTemplates.length;
500 const totalPages = Math.ceil(totalFilteredItems / itemsPerPage);
501 const paginatedTemplates = useMemo(() => {
502 const startIndex = (currentPage - 1) * itemsPerPage;
503 return filteredTemplates.slice(startIndex, startIndex + itemsPerPage);
504 }, [filteredTemplates, currentPage, itemsPerPage]);
505
506 useMemo(() => {
507 setCurrentPage(1);
508 // eslint-disable-next-line react-hooks/exhaustive-deps
509 }, [searchTerm, categoryFilter, recipientFilter, eventFilter, statusFilter]);
510
511 const handleLockedNavigate = useCallback(() => {
512 if (isProPluginActive()) {
513 window.location.href = "admin.php?page=yatra&subpage=modules";
514 } else {
515 window.open(
516 "https://wpyatra.com/pricing?module=email-automation",
517 "_blank",
518 );
519 }
520 }, []);
521
522 const handleEdit = (template: UnifiedEmailTemplate) => {
523 if (!template.is_locked) {
524 const proId = toNumericTemplateId(template.id);
525 if (automationModuleActive && proId !== null) {
526 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${proId}`;
527 return;
528 }
529 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&core_template=${encodeURIComponent(template.template_key)}`;
530 return;
531 }
532 if (isModuleGatedLocked(template)) {
533 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&core_template=${encodeURIComponent(template.template_key)}`;
534 return;
535 }
536 handleLockedNavigate();
537 };
538
539 const handlePreview = (template: UnifiedEmailTemplate) => {
540 if (template.is_locked && !isModuleGatedLocked(template)) {
541 return;
542 }
543 const numericId = toNumericTemplateId(template.id);
544
545 if (automationModuleActive && numericId !== null) {
546 proPreviewMutation.mutate(numericId);
547 } else {
548 corePreviewMutation.mutate(template);
549 }
550 };
551
552 const [deleteDialog, setDeleteDialog] = useState<{
553 isOpen: boolean;
554 template: UnifiedEmailTemplate | null;
555 }>({ isOpen: false, template: null });
556
557 const handleDelete = useCallback((template: UnifiedEmailTemplate) => {
558 setDeleteDialog({ isOpen: true, template });
559 }, []);
560
561 const confirmDelete = useCallback(() => {
562 const t = deleteDialog.template;
563 if (!t) {
564 setDeleteDialog({ isOpen: false, template: null });
565 return;
566 }
567 const id = toNumericTemplateId(t.id);
568 if (id !== null) {
569 deleteMutation.mutate(id);
570 }
571 setDeleteDialog({ isOpen: false, template: null });
572 }, [deleteDialog.template, deleteMutation]);
573
574 const cancelDelete = useCallback(() => {
575 setDeleteDialog({ isOpen: false, template: null });
576 }, []);
577
578 const handleDuplicate = (template: UnifiedEmailTemplate) => {
579 const id = toNumericTemplateId(template.id);
580 if (id !== null) {
581 duplicateMutation.mutate(id);
582 }
583 };
584
585 const handleCreate = () => {
586 window.location.href =
587 "admin.php?page=yatra&subpage=email-automation&tab=template&action=create";
588 };
589
590 const handleBulkAction = () => {
591 // Report the input that is actually missing. The Apply button is disabled
592 // whenever nothing is selected, so this can only be reached with a non-empty
593 // selection — the combined message blamed the selection and left operators
594 // re-selecting rows that were already ticked.
595 if (selectedIds.length === 0) {
596 showToast(__("Please select at least one template.", "yatra"), "error");
597 return;
598 }
599
600 if (!bulkAction) {
601 showToast(__("Please choose an action to apply.", "yatra"), "error");
602 return;
603 }
604 if (bulkAction === "delete") {
605 bulkDeleteMutation.mutate(selectedIds);
606 }
607 };
608
609 const handleSelectAll = (checked: boolean) => {
610 if (checked) {
611 setSelectedIds(filteredTemplates.map((t) => t.id));
612 } else {
613 setSelectedIds([]);
614 }
615 };
616
617 const handleSelectOne = (id: string | number, checked: boolean) => {
618 if (checked) {
619 setSelectedIds([...selectedIds, id]);
620 } else {
621 setSelectedIds(selectedIds.filter((i) => i !== id));
622 }
623 };
624
625 const formatEventKey = (eventKey: string) => eventKey || "-";
626
627 const emitSettingsCheckbox = (
628 flagKey: keyof EmailSettingsValues,
629 checked: boolean,
630 ) => {
631 if (!settingsBridge) return;
632 settingsBridge.onFieldChange({
633 target: {
634 name: flagKey,
635 type: "checkbox",
636 checked,
637 },
638 } as React.ChangeEvent<HTMLInputElement>);
639 };
640
641 const columns = [
642 {
643 key: "name",
644 label: __("Template"),
645 visible: visibleColumns.name,
646 render: (template: UnifiedEmailTemplate) => {
647 const CategoryIcon = categoryIcons[template.category] || Mail;
648 return (
649 <div className="flex items-center gap-3">
650 <div className="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0">
651 <CategoryIcon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
652 </div>
653 <div>
654 {template.is_locked ? (
655 <button
656 type="button"
657 onClick={() => handleEdit(template)}
658 className="text-left font-medium text-gray-600 dark:text-gray-300 hover:text-amber-700 dark:hover:text-amber-400"
659 >
660 {template.name}
661 <Lock className="inline-block w-3.5 h-3.5 ml-1.5 -mt-0.5 opacity-70" />
662 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200">
663 {isModuleGatedLocked(template)
664 ? __("View only", "yatra")
665 : __("Pro", "yatra")}
666 </span>
667 </button>
668 ) : (
669 <button
670 type="button"
671 onClick={() => handleEdit(template)}
672 className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline text-left"
673 >
674 {template.name}
675 </button>
676 )}
677 {template.overrides ? (
678 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
679 {__("Override", "yatra")}
680 </span>
681 ) : template.is_system ? (
682 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
683 {overridesSupported ? __("Global", "yatra") : __("System")}
684 </span>
685 ) : (
686 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
687 {__("Custom")}
688 </span>
689 )}
690 {(() => {
691 const mark = sentForTrip(template);
692 const tripName =
693 tripOptions.find((t) => t.id === asTrip)?.label || "";
694 if (mark === "hit") {
695 return (
696 <span
697 className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300"
698 data-testid="sent-for-trip"
699 >
700 {sprintf(__("sent for %s", "yatra"), tripName)}
701 {template.overrides &&
702 resolution?.resolved[template.overrides]?.reason
703 ? ` · ${resolution.resolved[template.overrides]?.reason}`
704 : ""}
705 </span>
706 );
707 }
708 if (mark === "dim" && !template.overrides) {
709 const ov = resolution?.resolved[template.template_key];
710 return (
711 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
712 {sprintf(
713 __("overridden for %1$s → %2$s", "yatra"),
714 tripName,
715 ov?.name || "",
716 )}
717 </span>
718 );
719 }
720 return null;
721 })()}
722 {template.overrides && !template.is_active && (
723 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
724 {__("off — its trips get the global template", "yatra")}
725 </span>
726 )}
727 {template.overrides &&
728 template.is_active &&
729 !String(template.body || "").trim() && (
730 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
731 {__(
732 "no body yet — its trips get the global template",
733 "yatra",
734 )}
735 </span>
736 )}
737 </div>
738 </div>
739 );
740 },
741 },
742 ...(overridesSupported && section === "override"
743 ? [
744 {
745 key: "overrides_of",
746 label: __("Overrides", "yatra"),
747 visible: true,
748 render: (template: UnifiedEmailTemplate) => {
749 const parent = globalByKey.get(template.overrides || "");
750 const siblings =
751 overridesByParent.get(template.overrides || "") || [];
752 const pos = siblings.findIndex((x) => x.id === template.id) + 1;
753 return (
754 <div>
755 <button
756 type="button"
757 className="text-blue-600 dark:text-blue-400 hover:underline font-medium text-left"
758 onClick={() => parent && handleEdit(parent)}
759 >
760 {parent?.name || template.overrides}
761 </button>
762 {siblings.length > 1 && (
763 <div className="text-xs text-gray-400">
764 {sprintf(
765 __("priority %1$d of %2$d", "yatra"),
766 pos,
767 siblings.length,
768 )}
769 </div>
770 )}
771 </div>
772 );
773 },
774 },
775 {
776 key: "applies_to",
777 label: __("Applies to", "yatra"),
778 visible: true,
779 render: (template: UnifiedEmailTemplate) => (
780 <span className="text-sm text-purple-700 dark:text-purple-300">
781 {describeTargets(template.targets, tripTargetOptions) || "—"}
782 </span>
783 ),
784 },
785 ]
786 : []),
787 ...(overridesSupported && section === "global"
788 ? [
789 {
790 key: "overrides_count",
791 label: __("Overrides", "yatra"),
792 visible: true,
793 render: (template: UnifiedEmailTemplate) => {
794 const n = (overridesByParent.get(template.template_key) || [])
795 .length;
796 if (!n) {
797 return (
798 <span className="text-xs text-gray-400">
799 {template.overridable ? __("none", "yatra") : "—"}
800 </span>
801 );
802 }
803 return (
804 <button
805 type="button"
806 className="text-sm text-purple-700 dark:text-purple-300 hover:underline inline-flex items-center gap-1"
807 onClick={() => {
808 setSection("override");
809 setParentFilter(template.template_key);
810 }}
811 data-testid="overrides-count"
812 >
813 <Compass className="w-3.5 h-3.5" />
814 {n === 1
815 ? __("1 override →", "yatra")
816 : sprintf(__("%d overrides →", "yatra"), n)}
817 </button>
818 );
819 },
820 },
821 ]
822 : []),
823 {
824 key: "event",
825 label: __("Event"),
826 visible: visibleColumns.event,
827 render: (template: UnifiedEmailTemplate) => {
828 const eventKey = template.effective_event_key || template.event_key;
829 const eventInfo = events.find((e: any) => e.key === eventKey);
830 return (
831 <span
832 className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400 cursor-help"
833 title={eventInfo?.description || ""}
834 >
835 <Zap className="w-3 h-3" />
836 {formatEventKey(eventKey)}
837 </span>
838 );
839 },
840 },
841 {
842 key: "subject",
843 label: __("Subject"),
844 visible: visibleColumns.subject,
845 render: (template: UnifiedEmailTemplate) => (
846 <span
847 className="text-sm text-gray-800 dark:text-gray-200 font-mono line-clamp-2 max-w-xs"
848 title={template.subject || ""}
849 >
850 {(template.subject || "").trim() || "—"}
851 </span>
852 ),
853 },
854 {
855 key: "body_preview",
856 label: __("Body"),
857 // Hidden in the Override section: those rows are about targeting, so the
858 // Template / Overrides / Applies to columns get the room instead.
859 visible:
860 visibleColumns.body_preview &&
861 !(overridesSupported && section === "override"),
862 render: (template: UnifiedEmailTemplate) => {
863 const preview = plainTextEmailPreview(template.body || "");
864 return (
865 <span
866 className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 max-w-md"
867 title={preview}
868 >
869 {preview || "—"}
870 </span>
871 );
872 },
873 },
874 {
875 key: "description",
876 label: __("Description"),
877 // Hidden in the Override section: those rows are about targeting, so the
878 // Template / Overrides / Applies to columns get the room instead.
879 visible:
880 visibleColumns.description &&
881 !(overridesSupported && section === "override"),
882 render: (template: UnifiedEmailTemplate) => (
883 <span className="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">
884 {template.description || "-"}
885 </span>
886 ),
887 },
888 {
889 key: "category",
890 label: __("Category"),
891 // Hidden in the Override section: those rows are about targeting, so the
892 // Template / Overrides / Applies to columns get the room instead.
893 visible:
894 visibleColumns.category &&
895 !(overridesSupported && section === "override"),
896 render: (template: UnifiedEmailTemplate) => {
897 const CategoryIcon = categoryIcons[template.category] || Mail;
898 return (
899 <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
900 <CategoryIcon className="w-3 h-3" />
901 {template.category.charAt(0).toUpperCase() +
902 template.category.slice(1)}
903 </span>
904 );
905 },
906 },
907 {
908 key: "recipient_type",
909 label: __("Recipient"),
910 // Hidden in the Override section: those rows are about targeting, so the
911 // Template / Overrides / Applies to columns get the room instead.
912 visible:
913 visibleColumns.recipient_type &&
914 !(overridesSupported && section === "override"),
915 render: (template: UnifiedEmailTemplate) => {
916 const effectiveRecipient = getEffectiveRecipientType(template);
917 const toEmail = template.to_email || "";
918 return (
919 <div className="flex flex-col gap-1">
920 <span
921 className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium w-fit ${
922 effectiveRecipient === "admin"
923 ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
924 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
925 }`}
926 >
927 {effectiveRecipient === "admin" ? __("Admin") : __("Customer")}
928 </span>
929 {toEmail && (
930 <span
931 className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[150px]"
932 title={toEmail}
933 >
934 {toEmail}
935 </span>
936 )}
937 </div>
938 );
939 },
940 },
941 {
942 key: "is_active",
943 label: __("Status"),
944 visible: visibleColumns.is_active,
945 render: (template: UnifiedEmailTemplate) => (
946 <Switch
947 checked={template.is_active}
948 disabled={
949 (template.is_locked && !isModuleGatedLocked(template)) ||
950 toggleMutation.isPending
951 }
952 onCheckedChange={(checked) => {
953 if (template.is_locked && !isModuleGatedLocked(template)) return;
954 const proId = toNumericTemplateId(template.id);
955 if (automationModuleActive && proId !== null) {
956 toggleMutation.mutate({ id: proId, is_active: checked });
957 return;
958 }
959 const entry = EMAIL_TEMPLATES_CATALOG.find(
960 (e) => e.template_key === template.template_key,
961 );
962 if (entry?.settingsFlag) {
963 emitSettingsCheckbox(entry.settingsFlag, checked);
964 }
965 }}
966 />
967 ),
968 },
969 ];
970
971 const actions = [
972 ...(overridesSupported
973 ? [
974 {
975 key: "add_override",
976 label: __("Add override…", "yatra"),
977 icon: <Compass className="w-4 h-4" />,
978 onClick: (template: UnifiedEmailTemplate) =>
979 setOverrideModal({ open: true, parent: template }),
980 condition: (template: UnifiedEmailTemplate) =>
981 !template.overrides &&
982 !!template.overridable &&
983 isApiTemplate(template),
984 },
985 {
986 key: "override_up",
987 label: __("Higher priority", "yatra"),
988 icon: <ArrowUp className="w-4 h-4" />,
989 onClick: (template: UnifiedEmailTemplate) =>
990 moveOverride(template, -1),
991 condition: (template: UnifiedEmailTemplate) =>
992 !!template.overrides &&
993 (overridesByParent.get(template.overrides) || []).length > 1,
994 },
995 {
996 key: "override_down",
997 label: __("Lower priority", "yatra"),
998 icon: <ArrowDown className="w-4 h-4" />,
999 onClick: (template: UnifiedEmailTemplate) =>
1000 moveOverride(template, 1),
1001 condition: (template: UnifiedEmailTemplate) =>
1002 !!template.overrides &&
1003 (overridesByParent.get(template.overrides) || []).length > 1,
1004 },
1005 ]
1006 : []),
1007 {
1008 key: "preview",
1009 label: __("Preview", "yatra"),
1010 icon: <Eye className="w-4 h-4" />,
1011 onClick: handlePreview,
1012 condition: (template: UnifiedEmailTemplate) =>
1013 !template.is_locked || isModuleGatedLocked(template),
1014 },
1015 {
1016 key: "edit",
1017 label: __("Edit"),
1018 icon: <Edit className="w-4 h-4" />,
1019 onClick: handleEdit,
1020 },
1021 {
1022 key: "duplicate",
1023 label: __("Duplicate"),
1024 icon: <Copy className="w-4 h-4" />,
1025 onClick: handleDuplicate,
1026 condition: (template: UnifiedEmailTemplate) =>
1027 automationModuleActive &&
1028 isApiTemplate(template) &&
1029 !template.is_system,
1030 },
1031 {
1032 key: "delete",
1033 label: __("Delete"),
1034 icon: <Trash2 className="w-4 h-4" />,
1035 onClick: handleDelete,
1036 condition: (template: UnifiedEmailTemplate) =>
1037 automationModuleActive &&
1038 isApiTemplate(template) &&
1039 !template.is_system,
1040 variant: "destructive" as const,
1041 },
1042 ];
1043
1044 const columnOptions = [
1045 { key: "name", label: __("Template"), visible: visibleColumns.name },
1046 { key: "event", label: __("Event"), visible: visibleColumns.event },
1047 { key: "subject", label: __("Subject"), visible: visibleColumns.subject },
1048 {
1049 key: "body_preview",
1050 label: __("Body"),
1051 visible: visibleColumns.body_preview,
1052 },
1053 {
1054 key: "description",
1055 label: __("Description"),
1056 visible: visibleColumns.description,
1057 },
1058 {
1059 key: "category",
1060 label: __("Category"),
1061 visible: visibleColumns.category,
1062 },
1063 {
1064 key: "recipient_type",
1065 label: __("Recipient"),
1066 visible: visibleColumns.recipient_type,
1067 },
1068 {
1069 key: "is_active",
1070 label: __("Status"),
1071 visible: visibleColumns.is_active,
1072 },
1073 ];
1074
1075 return (
1076 <div className="space-y-4">
1077 {!automationModuleActive && (
1078 <p className="text-sm text-gray-600 dark:text-gray-400">
1079 {__(
1080 "Templates backed by site settings can be edited here. Rows for Yatra Pro Email Automation or optional modules stay visible with their on/off state; open a View only row to inspect content, or enable Pro or the module to edit HTML and unlock automation.",
1081 "yatra",
1082 )}
1083 </p>
1084 )}
1085
1086 {overridesSupported && (
1087 <div className="space-y-2" data-testid="template-sections">
1088 <div className="inline-flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden">
1089 <button
1090 type="button"
1091 onClick={() => {
1092 setSection("global");
1093 setParentFilter("");
1094 }}
1095 aria-pressed={section === "global"}
1096 className={`inline-flex items-center gap-2 px-4 h-10 text-sm font-medium ${section === "global" ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" : "bg-white text-gray-600 dark:bg-gray-800 dark:text-gray-300"}`}
1097 data-testid="section-global"
1098 >
1099 <Globe className="w-4 h-4" />
1100 {__("Global templates", "yatra")}
1101 <span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
1102 {globalTemplates.length}
1103 </span>
1104 </button>
1105 <button
1106 type="button"
1107 onClick={() => setSection("override")}
1108 aria-pressed={section === "override"}
1109 className={`inline-flex items-center gap-2 px-4 h-10 text-sm font-medium border-l border-gray-300 dark:border-gray-600 ${section === "override" ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" : "bg-white text-gray-600 dark:bg-gray-800 dark:text-gray-300"}`}
1110 data-testid="section-override"
1111 >
1112 <Compass className="w-4 h-4" />
1113 {__("Override templates", "yatra")}
1114 <span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
1115 {overrideTemplates.length}
1116 </span>
1117 </button>
1118 </div>
1119 <p className="text-sm text-gray-600 dark:text-gray-400">
1120 {section === "global"
1121 ? __(
1122 "One template per event, sent to every trip unless an override applies. Editing a global template never changes its overrides.",
1123 "yatra",
1124 )
1125 : __(
1126 "Trip-specific versions of a global template. Same event and merge tags as the template they override; used only for bookings on the trips they name. Everything else keeps the global template.",
1127 "yatra",
1128 )}
1129 </p>
1130 </div>
1131 )}
1132
1133 {automationModuleActive && (
1134 <div className="flex flex-wrap items-center justify-between gap-3">
1135 {overridesSupported ? (
1136 <div className="flex flex-wrap items-center gap-2 text-sm">
1137 <span className="text-gray-500">
1138 {__("View as a trip:", "yatra")}
1139 </span>
1140 <div className="w-72" data-testid="view-as-trip">
1141 <SearchableSelect
1142 value={asTrip ? String(asTrip) : ""}
1143 onChange={(value) => setAsTrip(value ? Number(value) : null)}
1144 options={tripOptions.map((t) => ({
1145 value: String(t.id),
1146 label: t.label,
1147 }))}
1148 placeholder={__("— none (show everything) —", "yatra")}
1149 searchPlaceholder={__("Search trips…", "yatra")}
1150 />
1151 </div>
1152 <span className="text-xs text-gray-400">
1153 {asTrip
1154 ? __(
1155 "Highlighted rows are exactly what this trip's customers receive.",
1156 "yatra",
1157 )
1158 : __(
1159 "Pick a trip to highlight exactly what that trip's customers receive.",
1160 "yatra",
1161 )}
1162 </span>
1163 </div>
1164 ) : (
1165 <span />
1166 )}
1167 <div className="flex items-center gap-2">
1168 {overridesSupported && section === "override" ? (
1169 <Button
1170 onClick={() => setOverrideModal({ open: true, parent: null })}
1171 className="bg-blue-600 hover:bg-blue-700 text-white"
1172 data-testid="add-override"
1173 >
1174 <Plus className="w-4 h-4 mr-2" />
1175 {__("Add override", "yatra")}
1176 </Button>
1177 ) : (
1178 <Button
1179 onClick={handleCreate}
1180 className="bg-blue-600 hover:bg-blue-700 text-white"
1181 >
1182 <Plus className="w-4 h-4 mr-2" />
1183 {__("Create Template")}
1184 </Button>
1185 )}
1186 </div>
1187 </div>
1188 )}
1189
1190 <Card>
1191 <CardContent className="p-4">
1192 <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-center">
1193 <div className="lg:col-span-6">
1194 <div className="relative">
1195 <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
1196 <Input
1197 value={searchTerm}
1198 onChange={(e) => setSearchTerm(e.target.value)}
1199 placeholder={__("Search templates...")}
1200 className="pl-10 w-full"
1201 />
1202 </div>
1203 </div>
1204 <div className="lg:col-span-2">
1205 {overridesSupported && section === "override" ? (
1206 <Select
1207 value={parentFilter}
1208 onChange={(e) => setParentFilter(e.target.value)}
1209 className="w-full"
1210 data-testid="parent-filter"
1211 >
1212 <option value="">{__("Overrides of: any", "yatra")}</option>
1213 {globalTemplates
1214 .filter(
1215 (g) =>
1216 (overridesByParent.get(g.template_key) || []).length >
1217 0,
1218 )
1219 .map((g) => (
1220 <option key={g.template_key} value={g.template_key}>
1221 {sprintf(__("Overrides of: %s", "yatra"), g.name)}
1222 </option>
1223 ))}
1224 </Select>
1225 ) : (
1226 <Select
1227 value={categoryFilter}
1228 onChange={(e) => setCategoryFilter(e.target.value)}
1229 className="w-full"
1230 >
1231 <option value="all">{__("All Categories")}</option>
1232 {categories.map((cat: string) => (
1233 <option key={cat} value={cat}>
1234 {cat.charAt(0).toUpperCase() + cat.slice(1)}
1235 </option>
1236 ))}
1237 </Select>
1238 )}
1239 </div>
1240 <div className="lg:col-span-2">
1241 <Select
1242 value={recipientFilter}
1243 onChange={(e) => setRecipientFilter(e.target.value)}
1244 className="w-full"
1245 >
1246 <option value="all">{__("All Recipients")}</option>
1247 <option value="customer">{__("Customer")}</option>
1248 <option value="admin">{__("Admin")}</option>
1249 </Select>
1250 </div>
1251 <div className="lg:col-span-2">
1252 <Select
1253 value={eventFilter}
1254 onChange={(e) => setEventFilter(e.target.value)}
1255 className="w-full"
1256 >
1257 <option value="all">{__("All Events")}</option>
1258 {events.map((event: any) => (
1259 <option key={event.key} value={event.key}>
1260 {event.name}
1261 </option>
1262 ))}
1263 </Select>
1264 </div>
1265 </div>
1266 </CardContent>
1267 </Card>
1268
1269 <BulkActionToolbar
1270 selectedIds={selectedIds}
1271 bulkAction={bulkAction}
1272 setBulkAction={setBulkAction}
1273 onApply={handleBulkAction}
1274 onClearSelection={() => setSelectedIds([])}
1275 statusFilter={statusFilter}
1276 setStatusFilter={setStatusFilter}
1277 statusOptions={[
1278 { key: "all", label: __("All"), count: templates.length },
1279 {
1280 key: "active",
1281 label: __("Active"),
1282 count: templates.filter((t) => t.is_active).length,
1283 },
1284 {
1285 key: "inactive",
1286 label: __("Inactive"),
1287 count: templates.filter((t) => !t.is_active).length,
1288 },
1289 ]}
1290 showColumnsDropdown={showColumnsDropdown}
1291 setShowColumnsDropdown={setShowColumnsDropdown}
1292 columnOptions={columnOptions}
1293 onToggleColumn={toggleColumn}
1294 bulkMutationPending={bulkDeleteMutation.isPending}
1295 totalItems={filteredTemplates.length}
1296 bulkActionOptions={
1297 automationModuleActive
1298 ? [{ value: "delete", label: __("Delete") }]
1299 : []
1300 }
1301 />
1302
1303 <Card>
1304 <CardContent className="p-0">
1305 <SharedTable
1306 data={paginatedTemplates}
1307 columns={columns}
1308 actions={actions}
1309 isLoading={isLoading}
1310 emptyText={
1311 overridesSupported && section === "override"
1312 ? __("No overrides yet", "yatra")
1313 : __("No templates found")
1314 }
1315 emptyDescription={
1316 overridesSupported && section === "override"
1317 ? __(
1318 "Every trip gets the global templates. Use “Add override” here or “Add override…” on a global template.",
1319 "yatra",
1320 )
1321 : __("Create your first email template to get started.")
1322 }
1323 onCreateClick={
1324 automationModuleActive
1325 ? overridesSupported && section === "override"
1326 ? () => setOverrideModal({ open: true, parent: null })
1327 : handleCreate
1328 : undefined
1329 }
1330 getItemId={(template: UnifiedEmailTemplate) => template.id}
1331 capability="yatra_manage_emails"
1332 skeletonRows={5}
1333 selectedItemIds={selectedIds}
1334 onSelectItem={handleSelectOne}
1335 onSelectAll={handleSelectAll}
1336 isAllSelected={
1337 selectedIds.length > 0 &&
1338 selectedIds.length === paginatedTemplates.length &&
1339 paginatedTemplates.length > 0
1340 }
1341 />
1342 </CardContent>
1343 </Card>
1344
1345 {totalFilteredItems > itemsPerPage && (
1346 <div className="mt-4">
1347 <Pagination
1348 currentPage={currentPage}
1349 totalPages={totalPages}
1350 totalItems={totalFilteredItems}
1351 itemsPerPage={itemsPerPage}
1352 onPageChange={setCurrentPage}
1353 itemName={__("templates")}
1354 />
1355 </div>
1356 )}
1357
1358 {overridesSupported && (
1359 <EmailOverrideCreateModal
1360 isOpen={overrideModal.open}
1361 onClose={() => setOverrideModal({ open: false, parent: null })}
1362 globals={globalTemplates.filter((g) => !!g.overridable)}
1363 parent={overrideModal.parent}
1364 />
1365 )}
1366
1367 <EmailPreviewModal
1368 open={previewOpen && Boolean(previewPayload)}
1369 onClose={() => {
1370 setPreviewOpen(false);
1371 setPreviewPayload(null);
1372 }}
1373 subject={previewPayload?.subject ?? ""}
1374 body={previewPayload?.body ?? ""}
1375 />
1376
1377 <ConfirmationDialog
1378 isOpen={deleteDialog.isOpen}
1379 onClose={cancelDelete}
1380 onConfirm={confirmDelete}
1381 title={__("Delete Template")}
1382 message={
1383 deleteDialog.template
1384 ? __(
1385 'Are you sure you want to delete "{name}"? This action cannot be undone.',
1386 ).replace("{name}", deleteDialog.template.name)
1387 : ""
1388 }
1389 confirmText={__("Delete")}
1390 cancelText={__("Cancel")}
1391 variant="danger"
1392 isLoading={deleteMutation.isPending}
1393 />
1394 </div>
1395 );
1396 };
1397
1398 export default EmailTemplatesList;
1399