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

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