PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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.13, at resources/js/components/email/EmailTemplatesList.tsx

960 lines 30.5 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 { __ } 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
52 const EMAIL_TEMPLATE_VISIBLE_COLUMNS_DEFAULT: Record<string, boolean> = {
53 name: true,
54 event: true,
55 subject: true,
56 body_preview: true,
57 description: true,
58 category: true,
59 recipient_type: true,
60 is_active: true,
61 };
62
63 function plainTextEmailPreview(html: string, maxLen = 100): string {
64 if (!html) return "";
65 const plain = html
66 .replace(/<script[\s\S]*?<\/script>/gi, " ")
67 .replace(/<style[\s\S]*?<\/style>/gi, " ")
68 .replace(/<[^>]+>/g, " ")
69 .replace(/\s+/g, " ")
70 .trim();
71 if (plain.length <= maxLen) return plain;
72 return `${plain.slice(0, maxLen)}…`;
73 }
74
75 export type EmailTemplatesListProps = {
76 automationModuleActive: boolean;
77 settingsBridge?: {
78 values: EmailSettingsValues;
79 onFieldChange: EmailFieldChangeHandler;
80 };
81 };
82
83 /** Module required by catalog but not active — template stays visible; content is view-only in free UI. */
84 function isModuleGatedLocked(template: UnifiedEmailTemplate): boolean {
85 const entry = getCatalogEntryByTemplateKey(template.template_key);
86 if (!entry?.requiresModule) {
87 return false;
88 }
89 return !isProPluginActive() || !isModuleActive(entry.requiresModule);
90 }
91
92 const getEffectiveRecipientType = (
93 template: UnifiedEmailTemplate,
94 ): "customer" | "admin" => {
95 const catalog = getCatalogEntryByTemplateKey(template.template_key);
96 if (catalog?.recipient_type) {
97 return catalog.recipient_type;
98 }
99 const fromApi = template.recipient_type;
100 if (fromApi === "admin" || fromApi === "customer") {
101 return fromApi;
102 }
103 const toEmail = template.to_email || "";
104 if (toEmail.includes("{{admin_email}}")) {
105 return "admin";
106 }
107 if (
108 toEmail.includes("{customer_email}") ||
109 toEmail.includes("{{customer_email}}")
110 ) {
111 return "customer";
112 }
113 return "customer";
114 };
115
116 const categoryIcons: Record<string, React.ElementType> = {
117 booking: FileText,
118 payment: CreditCard,
119 enquiry: MessageSquare,
120 reminder: Bell,
121 marketing: Megaphone,
122 };
123
124 const isApiTemplate = (t: UnifiedEmailTemplate): boolean => {
125 if (t.is_locked) {
126 return false;
127 }
128 if (typeof t.id === "number") {
129 return true;
130 }
131 return typeof t.id === "string" && /^[0-9]+$/.test(String(t.id).trim());
132 };
133
134 /** Pro DB templates use numeric id; the API may return id as a string (e.g. "42"). */
135 function toNumericTemplateId(id: string | number | undefined): number | null {
136 if (id === undefined) {
137 return null;
138 }
139 if (typeof id === "number" && Number.isFinite(id)) {
140 return id;
141 }
142 if (typeof id === "string" && /^[0-9]+$/.test(id.trim())) {
143 return Number(id);
144 }
145 return null;
146 }
147
148 export const EmailTemplatesList: React.FC<EmailTemplatesListProps> = ({
149 automationModuleActive,
150 settingsBridge,
151 }) => {
152 const queryClient = useQueryClient();
153 const { showToast } = useToast();
154
155 const [searchTerm, setSearchTerm] = useState("");
156 const [categoryFilter, setCategoryFilter] = useState("all");
157 const [recipientFilter, setRecipientFilter] = useState("all");
158 const [eventFilter, setEventFilter] = useState("all");
159 const [statusFilter, setStatusFilter] = useState("all");
160 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
161 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
162 const [bulkAction, setBulkAction] = useState("");
163 const [currentPage, setCurrentPage] = useState(1);
164 const itemsPerPage = 20;
165
166 const [previewOpen, setPreviewOpen] = useState(false);
167 const [previewPayload, setPreviewPayload] = useState<{
168 subject: string;
169 body: string;
170 } | null>(null);
171
172 const [visibleColumns, setVisibleColumns] = useState(() => {
173 const saved = localStorage.getItem("yatra-email-templates-columns");
174 const parsed = saved ? JSON.parse(saved) : {};
175 return { ...EMAIL_TEMPLATE_VISIBLE_COLUMNS_DEFAULT, ...parsed };
176 });
177
178 const toggleColumn = (columnKey: string) => {
179 const newVisibleColumns = {
180 ...visibleColumns,
181 [columnKey]: !visibleColumns[columnKey],
182 };
183 setVisibleColumns(newVisibleColumns);
184 localStorage.setItem(
185 "yatra-email-templates-columns",
186 JSON.stringify(newVisibleColumns),
187 );
188 };
189
190 const { data: templatesData, isLoading: apiLoading } = useQuery({
191 queryKey: ["email-templates"],
192 queryFn: () => fetchEmailTemplates(),
193 enabled: automationModuleActive,
194 });
195
196 const apiTemplates: UnifiedEmailTemplate[] = useMemo(() => {
197 const raw = Array.isArray(templatesData) ? templatesData : [];
198 return (raw as UnifiedEmailTemplate[]).map((t) => ({
199 ...t,
200 is_locked: false,
201 }));
202 }, [templatesData]);
203
204 const localTemplates = useMemo(() => {
205 if (automationModuleActive || !settingsBridge) return [];
206 return buildLocalTemplateRows(settingsBridge.values, {
207 isProPluginActive: isProPluginActive(),
208 isModuleActive,
209 });
210 }, [automationModuleActive, settingsBridge]);
211
212 const templates = automationModuleActive ? apiTemplates : localTemplates;
213
214 const isLoading = automationModuleActive ? apiLoading : false;
215
216 const toggleMutation = useMutation({
217 mutationFn: async ({
218 id,
219 is_active,
220 }: {
221 id: number;
222 is_active: boolean;
223 }) => {
224 return await setEmailTemplateActive(id, is_active);
225 },
226 onSuccess: () => {
227 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
228 showToast(__("Template updated"), "success");
229 },
230 });
231
232 const deleteMutation = useMutation({
233 mutationFn: async (id: number) => {
234 return await deleteEmailTemplate(id);
235 },
236 onSuccess: () => {
237 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
238 showToast(__("Template deleted"), "success");
239 },
240 onError: (error: any) => {
241 showToast(error?.message || __("Failed to delete template"), "error");
242 },
243 });
244
245 const duplicateMutation = useMutation({
246 mutationFn: async (id: number) => {
247 return await duplicateEmailTemplate(id);
248 },
249 onSuccess: () => {
250 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
251 showToast(__("Template duplicated successfully"), "success");
252 },
253 onError: (error: any) => {
254 showToast(error?.message || __("Failed to duplicate template"), "error");
255 },
256 });
257
258 const proPreviewMutation = useMutation({
259 mutationFn: (templateId: number) => previewEmailTemplate(templateId),
260 onSuccess: (data) => {
261 setPreviewPayload(data);
262 setPreviewOpen(true);
263 },
264 onError: () => {
265 showToast(__("Failed to load preview", "yatra"), "error");
266 },
267 });
268
269 const corePreviewMutation = useMutation({
270 mutationFn: (t: UnifiedEmailTemplate) =>
271 previewCoreEmailTemplate({
272 template_key: t.template_key,
273 subject: t.subject || "",
274 body: t.body || "",
275 }),
276 onSuccess: (data) => {
277 setPreviewPayload(data);
278 setPreviewOpen(true);
279 },
280 onError: () => {
281 showToast(__("Failed to load preview", "yatra"), "error");
282 },
283 });
284
285 const bulkDeleteMutation = useMutation({
286 mutationFn: async (ids: (string | number)[]) => {
287 const customTemplateIds = ids
288 .map((id) => toNumericTemplateId(id))
289 .filter((id): id is number => id !== null)
290 .filter((id) => {
291 const template = templates.find(
292 (t) => toNumericTemplateId(t.id) === id,
293 );
294 return template && !template.is_system && isApiTemplate(template);
295 });
296
297 if (customTemplateIds.length === 0) {
298 throw new Error(
299 __(
300 "No custom templates selected. System templates cannot be deleted.",
301 ),
302 );
303 }
304
305 await Promise.all(customTemplateIds.map((id) => deleteEmailTemplate(id)));
306 return {
307 deleted: customTemplateIds.length,
308 skipped: ids.length - customTemplateIds.length,
309 };
310 },
311 onSuccess: (data) => {
312 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
313 setSelectedIds([]);
314 setBulkAction("");
315 if (data.skipped > 0) {
316 showToast(
317 __(
318 `Deleted ${data.deleted} template(s). ${data.skipped} system template(s) were skipped.`,
319 ),
320 "success",
321 );
322 } else {
323 showToast(__(`Deleted ${data.deleted} template(s)`), "success");
324 }
325 },
326 onError: (error: any) => {
327 showToast(error?.message || __("Failed to delete templates"), "error");
328 },
329 });
330
331 const categories: string[] = useMemo(
332 () => [...new Set(templates.map((t) => t.category))] as string[],
333 [templates],
334 );
335
336 const events = useMemo(() => {
337 if (automationModuleActive) {
338 return (window as any).yatraAdmin?.emailEvents || [];
339 }
340 return [...new Set(EMAIL_TEMPLATES_CATALOG.map((e) => e.event_key))].map(
341 (key) => ({ key, name: key }),
342 );
343 }, [automationModuleActive]);
344
345 const filteredTemplates = useMemo(() => {
346 return templates.filter((t) => {
347 const q = searchTerm.toLowerCase();
348 const bodyPlain = plainTextEmailPreview(t.body || "", 500).toLowerCase();
349 const matchesSearch =
350 !searchTerm ||
351 t.name.toLowerCase().includes(q) ||
352 (t.description || "").toLowerCase().includes(q) ||
353 (t.subject || "").toLowerCase().includes(q) ||
354 bodyPlain.includes(q);
355 const matchesCategory =
356 categoryFilter === "all" || t.category === categoryFilter;
357 const matchesRecipient =
358 recipientFilter === "all" ||
359 getEffectiveRecipientType(t) === recipientFilter;
360 const matchesEvent = eventFilter === "all" || t.event_key === eventFilter;
361 const matchesStatus =
362 statusFilter === "all" ||
363 (statusFilter === "active" && t.is_active) ||
364 (statusFilter === "inactive" && !t.is_active);
365 return (
366 matchesSearch &&
367 matchesCategory &&
368 matchesRecipient &&
369 matchesEvent &&
370 matchesStatus
371 );
372 });
373 }, [
374 templates,
375 searchTerm,
376 categoryFilter,
377 recipientFilter,
378 eventFilter,
379 statusFilter,
380 ]);
381
382 const totalFilteredItems = filteredTemplates.length;
383 const totalPages = Math.ceil(totalFilteredItems / itemsPerPage);
384 const paginatedTemplates = useMemo(() => {
385 const startIndex = (currentPage - 1) * itemsPerPage;
386 return filteredTemplates.slice(startIndex, startIndex + itemsPerPage);
387 }, [filteredTemplates, currentPage, itemsPerPage]);
388
389 useMemo(() => {
390 setCurrentPage(1);
391 // eslint-disable-next-line react-hooks/exhaustive-deps
392 }, [searchTerm, categoryFilter, recipientFilter, eventFilter, statusFilter]);
393
394 const handleLockedNavigate = useCallback(() => {
395 if (isProPluginActive()) {
396 window.location.href = "admin.php?page=yatra&subpage=modules";
397 } else {
398 window.open(
399 "https://wpyatra.com/pricing?module=email-automation",
400 "_blank",
401 );
402 }
403 }, []);
404
405 const handleEdit = (template: UnifiedEmailTemplate) => {
406 if (!template.is_locked) {
407 const proId = toNumericTemplateId(template.id);
408 if (automationModuleActive && proId !== null) {
409 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${proId}`;
410 return;
411 }
412 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&core_template=${encodeURIComponent(template.template_key)}`;
413 return;
414 }
415 if (isModuleGatedLocked(template)) {
416 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&core_template=${encodeURIComponent(template.template_key)}`;
417 return;
418 }
419 handleLockedNavigate();
420 };
421
422 const handlePreview = (template: UnifiedEmailTemplate) => {
423 if (template.is_locked && !isModuleGatedLocked(template)) {
424 return;
425 }
426 const numericId = toNumericTemplateId(template.id);
427
428 if (automationModuleActive && numericId !== null) {
429 proPreviewMutation.mutate(numericId);
430 } else {
431 corePreviewMutation.mutate(template);
432 }
433 };
434
435 const [deleteDialog, setDeleteDialog] = useState<{
436 isOpen: boolean;
437 template: UnifiedEmailTemplate | null;
438 }>({ isOpen: false, template: null });
439
440 const handleDelete = useCallback((template: UnifiedEmailTemplate) => {
441 setDeleteDialog({ isOpen: true, template });
442 }, []);
443
444 const confirmDelete = useCallback(() => {
445 const t = deleteDialog.template;
446 if (!t) {
447 setDeleteDialog({ isOpen: false, template: null });
448 return;
449 }
450 const id = toNumericTemplateId(t.id);
451 if (id !== null) {
452 deleteMutation.mutate(id);
453 }
454 setDeleteDialog({ isOpen: false, template: null });
455 }, [deleteDialog.template, deleteMutation]);
456
457 const cancelDelete = useCallback(() => {
458 setDeleteDialog({ isOpen: false, template: null });
459 }, []);
460
461 const handleDuplicate = (template: UnifiedEmailTemplate) => {
462 const id = toNumericTemplateId(template.id);
463 if (id !== null) {
464 duplicateMutation.mutate(id);
465 }
466 };
467
468 const handleCreate = () => {
469 window.location.href =
470 "admin.php?page=yatra&subpage=email-automation&tab=template&action=create";
471 };
472
473 const handleBulkAction = () => {
474 // Report the input that is actually missing. The Apply button is disabled
475 // whenever nothing is selected, so this can only be reached with a non-empty
476 // selection — the combined message blamed the selection and left operators
477 // re-selecting rows that were already ticked.
478 if (selectedIds.length === 0) {
479 showToast(__("Please select at least one template.", "yatra"), "error");
480 return;
481 }
482
483 if (!bulkAction) {
484 showToast(__("Please choose an action to apply.", "yatra"), "error");
485 return;
486 }
487 if (bulkAction === "delete") {
488 bulkDeleteMutation.mutate(selectedIds);
489 }
490 };
491
492 const handleSelectAll = (checked: boolean) => {
493 if (checked) {
494 setSelectedIds(filteredTemplates.map((t) => t.id));
495 } else {
496 setSelectedIds([]);
497 }
498 };
499
500 const handleSelectOne = (id: string | number, checked: boolean) => {
501 if (checked) {
502 setSelectedIds([...selectedIds, id]);
503 } else {
504 setSelectedIds(selectedIds.filter((i) => i !== id));
505 }
506 };
507
508 const formatEventKey = (eventKey: string) => eventKey || "-";
509
510 const emitSettingsCheckbox = (
511 flagKey: keyof EmailSettingsValues,
512 checked: boolean,
513 ) => {
514 if (!settingsBridge) return;
515 settingsBridge.onFieldChange({
516 target: {
517 name: flagKey,
518 type: "checkbox",
519 checked,
520 },
521 } as React.ChangeEvent<HTMLInputElement>);
522 };
523
524 const columns = [
525 {
526 key: "name",
527 label: __("Template"),
528 visible: visibleColumns.name,
529 render: (template: UnifiedEmailTemplate) => {
530 const CategoryIcon = categoryIcons[template.category] || Mail;
531 return (
532 <div className="flex items-center gap-3">
533 <div className="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0">
534 <CategoryIcon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
535 </div>
536 <div>
537 {template.is_locked ? (
538 <button
539 type="button"
540 onClick={() => handleEdit(template)}
541 className="text-left font-medium text-gray-600 dark:text-gray-300 hover:text-amber-700 dark:hover:text-amber-400"
542 >
543 {template.name}
544 <Lock className="inline-block w-3.5 h-3.5 ml-1.5 -mt-0.5 opacity-70" />
545 <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">
546 {isModuleGatedLocked(template)
547 ? __("View only", "yatra")
548 : __("Pro", "yatra")}
549 </span>
550 </button>
551 ) : (
552 <button
553 type="button"
554 onClick={() => handleEdit(template)}
555 className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline text-left"
556 >
557 {template.name}
558 </button>
559 )}
560 {template.is_system ? (
561 <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">
562 {__("System")}
563 </span>
564 ) : (
565 <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">
566 {__("Custom")}
567 </span>
568 )}
569 </div>
570 </div>
571 );
572 },
573 },
574 {
575 key: "event",
576 label: __("Event"),
577 visible: visibleColumns.event,
578 render: (template: UnifiedEmailTemplate) => {
579 const eventInfo = events.find((e: any) => e.key === template.event_key);
580 return (
581 <span
582 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"
583 title={eventInfo?.description || ""}
584 >
585 <Zap className="w-3 h-3" />
586 {formatEventKey(template.event_key)}
587 </span>
588 );
589 },
590 },
591 {
592 key: "subject",
593 label: __("Subject"),
594 visible: visibleColumns.subject,
595 render: (template: UnifiedEmailTemplate) => (
596 <span
597 className="text-sm text-gray-800 dark:text-gray-200 font-mono line-clamp-2 max-w-xs"
598 title={template.subject || ""}
599 >
600 {(template.subject || "").trim() || "—"}
601 </span>
602 ),
603 },
604 {
605 key: "body_preview",
606 label: __("Body"),
607 visible: visibleColumns.body_preview,
608 render: (template: UnifiedEmailTemplate) => {
609 const preview = plainTextEmailPreview(template.body || "");
610 return (
611 <span
612 className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 max-w-md"
613 title={preview}
614 >
615 {preview || "—"}
616 </span>
617 );
618 },
619 },
620 {
621 key: "description",
622 label: __("Description"),
623 visible: visibleColumns.description,
624 render: (template: UnifiedEmailTemplate) => (
625 <span className="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">
626 {template.description || "-"}
627 </span>
628 ),
629 },
630 {
631 key: "category",
632 label: __("Category"),
633 visible: visibleColumns.category,
634 render: (template: UnifiedEmailTemplate) => {
635 const CategoryIcon = categoryIcons[template.category] || Mail;
636 return (
637 <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">
638 <CategoryIcon className="w-3 h-3" />
639 {template.category.charAt(0).toUpperCase() +
640 template.category.slice(1)}
641 </span>
642 );
643 },
644 },
645 {
646 key: "recipient_type",
647 label: __("Recipient"),
648 visible: visibleColumns.recipient_type,
649 render: (template: UnifiedEmailTemplate) => {
650 const effectiveRecipient = getEffectiveRecipientType(template);
651 const toEmail = template.to_email || "";
652 return (
653 <div className="flex flex-col gap-1">
654 <span
655 className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium w-fit ${
656 effectiveRecipient === "admin"
657 ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
658 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
659 }`}
660 >
661 {effectiveRecipient === "admin" ? __("Admin") : __("Customer")}
662 </span>
663 {toEmail && (
664 <span
665 className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[150px]"
666 title={toEmail}
667 >
668 {toEmail}
669 </span>
670 )}
671 </div>
672 );
673 },
674 },
675 {
676 key: "is_active",
677 label: __("Status"),
678 visible: visibleColumns.is_active,
679 render: (template: UnifiedEmailTemplate) => (
680 <Switch
681 checked={template.is_active}
682 disabled={
683 (template.is_locked && !isModuleGatedLocked(template)) ||
684 toggleMutation.isPending
685 }
686 onCheckedChange={(checked) => {
687 if (template.is_locked && !isModuleGatedLocked(template)) return;
688 const proId = toNumericTemplateId(template.id);
689 if (automationModuleActive && proId !== null) {
690 toggleMutation.mutate({ id: proId, is_active: checked });
691 return;
692 }
693 const entry = EMAIL_TEMPLATES_CATALOG.find(
694 (e) => e.template_key === template.template_key,
695 );
696 if (entry?.settingsFlag) {
697 emitSettingsCheckbox(entry.settingsFlag, checked);
698 }
699 }}
700 />
701 ),
702 },
703 ];
704
705 const actions = [
706 {
707 key: "preview",
708 label: __("Preview", "yatra"),
709 icon: <Eye className="w-4 h-4" />,
710 onClick: handlePreview,
711 condition: (template: UnifiedEmailTemplate) =>
712 !template.is_locked || isModuleGatedLocked(template),
713 },
714 {
715 key: "edit",
716 label: __("Edit"),
717 icon: <Edit className="w-4 h-4" />,
718 onClick: handleEdit,
719 },
720 {
721 key: "duplicate",
722 label: __("Duplicate"),
723 icon: <Copy className="w-4 h-4" />,
724 onClick: handleDuplicate,
725 condition: (template: UnifiedEmailTemplate) =>
726 automationModuleActive &&
727 isApiTemplate(template) &&
728 !template.is_system,
729 },
730 {
731 key: "delete",
732 label: __("Delete"),
733 icon: <Trash2 className="w-4 h-4" />,
734 onClick: handleDelete,
735 condition: (template: UnifiedEmailTemplate) =>
736 automationModuleActive &&
737 isApiTemplate(template) &&
738 !template.is_system,
739 variant: "destructive" as const,
740 },
741 ];
742
743 const columnOptions = [
744 { key: "name", label: __("Template"), visible: visibleColumns.name },
745 { key: "event", label: __("Event"), visible: visibleColumns.event },
746 { key: "subject", label: __("Subject"), visible: visibleColumns.subject },
747 {
748 key: "body_preview",
749 label: __("Body"),
750 visible: visibleColumns.body_preview,
751 },
752 {
753 key: "description",
754 label: __("Description"),
755 visible: visibleColumns.description,
756 },
757 {
758 key: "category",
759 label: __("Category"),
760 visible: visibleColumns.category,
761 },
762 {
763 key: "recipient_type",
764 label: __("Recipient"),
765 visible: visibleColumns.recipient_type,
766 },
767 {
768 key: "is_active",
769 label: __("Status"),
770 visible: visibleColumns.is_active,
771 },
772 ];
773
774 return (
775 <div className="space-y-4">
776 {!automationModuleActive && (
777 <p className="text-sm text-gray-600 dark:text-gray-400">
778 {__(
779 "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.",
780 "yatra",
781 )}
782 </p>
783 )}
784
785 {automationModuleActive && (
786 <div className="flex justify-end">
787 <Button
788 onClick={handleCreate}
789 className="bg-blue-600 hover:bg-blue-700 text-white"
790 >
791 <Plus className="w-4 h-4 mr-2" />
792 {__("Create Template")}
793 </Button>
794 </div>
795 )}
796
797 <Card>
798 <CardContent className="p-4">
799 <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-center">
800 <div className="lg:col-span-6">
801 <div className="relative">
802 <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
803 <Input
804 value={searchTerm}
805 onChange={(e) => setSearchTerm(e.target.value)}
806 placeholder={__("Search templates...")}
807 className="pl-10 w-full"
808 />
809 </div>
810 </div>
811 <div className="lg:col-span-2">
812 <Select
813 value={categoryFilter}
814 onChange={(e) => setCategoryFilter(e.target.value)}
815 className="w-full"
816 >
817 <option value="all">{__("All Categories")}</option>
818 {categories.map((cat: string) => (
819 <option key={cat} value={cat}>
820 {cat.charAt(0).toUpperCase() + cat.slice(1)}
821 </option>
822 ))}
823 </Select>
824 </div>
825 <div className="lg:col-span-2">
826 <Select
827 value={recipientFilter}
828 onChange={(e) => setRecipientFilter(e.target.value)}
829 className="w-full"
830 >
831 <option value="all">{__("All Recipients")}</option>
832 <option value="customer">{__("Customer")}</option>
833 <option value="admin">{__("Admin")}</option>
834 </Select>
835 </div>
836 <div className="lg:col-span-2">
837 <Select
838 value={eventFilter}
839 onChange={(e) => setEventFilter(e.target.value)}
840 className="w-full"
841 >
842 <option value="all">{__("All Events")}</option>
843 {events.map((event: any) => (
844 <option key={event.key} value={event.key}>
845 {event.name}
846 </option>
847 ))}
848 </Select>
849 </div>
850 </div>
851 </CardContent>
852 </Card>
853
854 <BulkActionToolbar
855 selectedIds={selectedIds}
856 bulkAction={bulkAction}
857 setBulkAction={setBulkAction}
858 onApply={handleBulkAction}
859 onClearSelection={() => setSelectedIds([])}
860 statusFilter={statusFilter}
861 setStatusFilter={setStatusFilter}
862 statusOptions={[
863 { key: "all", label: __("All"), count: templates.length },
864 {
865 key: "active",
866 label: __("Active"),
867 count: templates.filter((t) => t.is_active).length,
868 },
869 {
870 key: "inactive",
871 label: __("Inactive"),
872 count: templates.filter((t) => !t.is_active).length,
873 },
874 ]}
875 showColumnsDropdown={showColumnsDropdown}
876 setShowColumnsDropdown={setShowColumnsDropdown}
877 columnOptions={columnOptions}
878 onToggleColumn={toggleColumn}
879 bulkMutationPending={bulkDeleteMutation.isPending}
880 totalItems={filteredTemplates.length}
881 bulkActionOptions={
882 automationModuleActive
883 ? [{ value: "delete", label: __("Delete") }]
884 : []
885 }
886 />
887
888 <Card>
889 <CardContent className="p-0">
890 <SharedTable
891 data={paginatedTemplates}
892 columns={columns}
893 actions={actions}
894 isLoading={isLoading}
895 emptyText={__("No templates found")}
896 emptyDescription={__(
897 "Create your first email template to get started.",
898 )}
899 onCreateClick={automationModuleActive ? handleCreate : undefined}
900 getItemId={(template: UnifiedEmailTemplate) => template.id}
901 capability="yatra_manage_emails"
902 skeletonRows={5}
903 selectedItemIds={selectedIds}
904 onSelectItem={handleSelectOne}
905 onSelectAll={handleSelectAll}
906 isAllSelected={
907 selectedIds.length > 0 &&
908 selectedIds.length === paginatedTemplates.length &&
909 paginatedTemplates.length > 0
910 }
911 />
912 </CardContent>
913 </Card>
914
915 {totalFilteredItems > itemsPerPage && (
916 <div className="mt-4">
917 <Pagination
918 currentPage={currentPage}
919 totalPages={totalPages}
920 totalItems={totalFilteredItems}
921 itemsPerPage={itemsPerPage}
922 onPageChange={setCurrentPage}
923 itemName={__("templates")}
924 />
925 </div>
926 )}
927
928 <EmailPreviewModal
929 open={previewOpen && Boolean(previewPayload)}
930 onClose={() => {
931 setPreviewOpen(false);
932 setPreviewPayload(null);
933 }}
934 subject={previewPayload?.subject ?? ""}
935 body={previewPayload?.body ?? ""}
936 />
937
938 <ConfirmationDialog
939 isOpen={deleteDialog.isOpen}
940 onClose={cancelDelete}
941 onConfirm={confirmDelete}
942 title={__("Delete Template")}
943 message={
944 deleteDialog.template
945 ? __(
946 'Are you sure you want to delete "{name}"? This action cannot be undone.',
947 ).replace("{name}", deleteDialog.template.name)
948 : ""
949 }
950 confirmText={__("Delete")}
951 cancelText={__("Cancel")}
952 variant="danger"
953 isLoading={deleteMutation.isPending}
954 />
955 </div>
956 );
957 };
958
959 export default EmailTemplatesList;
960