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

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