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

951 lines 30.1 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 if (!bulkAction || selectedIds.length === 0) {
475 showToast(__("Please select templates and an action"), "error");
476 return;
477 }
478 if (bulkAction === "delete") {
479 bulkDeleteMutation.mutate(selectedIds);
480 }
481 };
482
483 const handleSelectAll = (checked: boolean) => {
484 if (checked) {
485 setSelectedIds(filteredTemplates.map((t) => t.id));
486 } else {
487 setSelectedIds([]);
488 }
489 };
490
491 const handleSelectOne = (id: string | number, checked: boolean) => {
492 if (checked) {
493 setSelectedIds([...selectedIds, id]);
494 } else {
495 setSelectedIds(selectedIds.filter((i) => i !== id));
496 }
497 };
498
499 const formatEventKey = (eventKey: string) => eventKey || "-";
500
501 const emitSettingsCheckbox = (
502 flagKey: keyof EmailSettingsValues,
503 checked: boolean,
504 ) => {
505 if (!settingsBridge) return;
506 settingsBridge.onFieldChange({
507 target: {
508 name: flagKey,
509 type: "checkbox",
510 checked,
511 },
512 } as React.ChangeEvent<HTMLInputElement>);
513 };
514
515 const columns = [
516 {
517 key: "name",
518 label: __("Template"),
519 visible: visibleColumns.name,
520 render: (template: UnifiedEmailTemplate) => {
521 const CategoryIcon = categoryIcons[template.category] || Mail;
522 return (
523 <div className="flex items-center gap-3">
524 <div className="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0">
525 <CategoryIcon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
526 </div>
527 <div>
528 {template.is_locked ? (
529 <button
530 type="button"
531 onClick={() => handleEdit(template)}
532 className="text-left font-medium text-gray-600 dark:text-gray-300 hover:text-amber-700 dark:hover:text-amber-400"
533 >
534 {template.name}
535 <Lock className="inline-block w-3.5 h-3.5 ml-1.5 -mt-0.5 opacity-70" />
536 <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">
537 {isModuleGatedLocked(template)
538 ? __("View only", "yatra")
539 : __("Pro", "yatra")}
540 </span>
541 </button>
542 ) : (
543 <button
544 type="button"
545 onClick={() => handleEdit(template)}
546 className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline text-left"
547 >
548 {template.name}
549 </button>
550 )}
551 {template.is_system ? (
552 <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">
553 {__("System")}
554 </span>
555 ) : (
556 <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">
557 {__("Custom")}
558 </span>
559 )}
560 </div>
561 </div>
562 );
563 },
564 },
565 {
566 key: "event",
567 label: __("Event"),
568 visible: visibleColumns.event,
569 render: (template: UnifiedEmailTemplate) => {
570 const eventInfo = events.find((e: any) => e.key === template.event_key);
571 return (
572 <span
573 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"
574 title={eventInfo?.description || ""}
575 >
576 <Zap className="w-3 h-3" />
577 {formatEventKey(template.event_key)}
578 </span>
579 );
580 },
581 },
582 {
583 key: "subject",
584 label: __("Subject"),
585 visible: visibleColumns.subject,
586 render: (template: UnifiedEmailTemplate) => (
587 <span
588 className="text-sm text-gray-800 dark:text-gray-200 font-mono line-clamp-2 max-w-xs"
589 title={template.subject || ""}
590 >
591 {(template.subject || "").trim() || "—"}
592 </span>
593 ),
594 },
595 {
596 key: "body_preview",
597 label: __("Body"),
598 visible: visibleColumns.body_preview,
599 render: (template: UnifiedEmailTemplate) => {
600 const preview = plainTextEmailPreview(template.body || "");
601 return (
602 <span
603 className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 max-w-md"
604 title={preview}
605 >
606 {preview || "—"}
607 </span>
608 );
609 },
610 },
611 {
612 key: "description",
613 label: __("Description"),
614 visible: visibleColumns.description,
615 render: (template: UnifiedEmailTemplate) => (
616 <span className="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">
617 {template.description || "-"}
618 </span>
619 ),
620 },
621 {
622 key: "category",
623 label: __("Category"),
624 visible: visibleColumns.category,
625 render: (template: UnifiedEmailTemplate) => {
626 const CategoryIcon = categoryIcons[template.category] || Mail;
627 return (
628 <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">
629 <CategoryIcon className="w-3 h-3" />
630 {template.category.charAt(0).toUpperCase() +
631 template.category.slice(1)}
632 </span>
633 );
634 },
635 },
636 {
637 key: "recipient_type",
638 label: __("Recipient"),
639 visible: visibleColumns.recipient_type,
640 render: (template: UnifiedEmailTemplate) => {
641 const effectiveRecipient = getEffectiveRecipientType(template);
642 const toEmail = template.to_email || "";
643 return (
644 <div className="flex flex-col gap-1">
645 <span
646 className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium w-fit ${
647 effectiveRecipient === "admin"
648 ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
649 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
650 }`}
651 >
652 {effectiveRecipient === "admin" ? __("Admin") : __("Customer")}
653 </span>
654 {toEmail && (
655 <span
656 className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[150px]"
657 title={toEmail}
658 >
659 {toEmail}
660 </span>
661 )}
662 </div>
663 );
664 },
665 },
666 {
667 key: "is_active",
668 label: __("Status"),
669 visible: visibleColumns.is_active,
670 render: (template: UnifiedEmailTemplate) => (
671 <Switch
672 checked={template.is_active}
673 disabled={
674 (template.is_locked && !isModuleGatedLocked(template)) ||
675 toggleMutation.isPending
676 }
677 onCheckedChange={(checked) => {
678 if (template.is_locked && !isModuleGatedLocked(template)) return;
679 const proId = toNumericTemplateId(template.id);
680 if (automationModuleActive && proId !== null) {
681 toggleMutation.mutate({ id: proId, is_active: checked });
682 return;
683 }
684 const entry = EMAIL_TEMPLATES_CATALOG.find(
685 (e) => e.template_key === template.template_key,
686 );
687 if (entry?.settingsFlag) {
688 emitSettingsCheckbox(entry.settingsFlag, checked);
689 }
690 }}
691 />
692 ),
693 },
694 ];
695
696 const actions = [
697 {
698 key: "preview",
699 label: __("Preview", "yatra"),
700 icon: <Eye className="w-4 h-4" />,
701 onClick: handlePreview,
702 condition: (template: UnifiedEmailTemplate) =>
703 !template.is_locked || isModuleGatedLocked(template),
704 },
705 {
706 key: "edit",
707 label: __("Edit"),
708 icon: <Edit className="w-4 h-4" />,
709 onClick: handleEdit,
710 },
711 {
712 key: "duplicate",
713 label: __("Duplicate"),
714 icon: <Copy className="w-4 h-4" />,
715 onClick: handleDuplicate,
716 condition: (template: UnifiedEmailTemplate) =>
717 automationModuleActive &&
718 isApiTemplate(template) &&
719 !template.is_system,
720 },
721 {
722 key: "delete",
723 label: __("Delete"),
724 icon: <Trash2 className="w-4 h-4" />,
725 onClick: handleDelete,
726 condition: (template: UnifiedEmailTemplate) =>
727 automationModuleActive &&
728 isApiTemplate(template) &&
729 !template.is_system,
730 variant: "destructive" as const,
731 },
732 ];
733
734 const columnOptions = [
735 { key: "name", label: __("Template"), visible: visibleColumns.name },
736 { key: "event", label: __("Event"), visible: visibleColumns.event },
737 { key: "subject", label: __("Subject"), visible: visibleColumns.subject },
738 {
739 key: "body_preview",
740 label: __("Body"),
741 visible: visibleColumns.body_preview,
742 },
743 {
744 key: "description",
745 label: __("Description"),
746 visible: visibleColumns.description,
747 },
748 {
749 key: "category",
750 label: __("Category"),
751 visible: visibleColumns.category,
752 },
753 {
754 key: "recipient_type",
755 label: __("Recipient"),
756 visible: visibleColumns.recipient_type,
757 },
758 {
759 key: "is_active",
760 label: __("Status"),
761 visible: visibleColumns.is_active,
762 },
763 ];
764
765 return (
766 <div className="space-y-4">
767 {!automationModuleActive && (
768 <p className="text-sm text-gray-600 dark:text-gray-400">
769 {__(
770 "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.",
771 "yatra",
772 )}
773 </p>
774 )}
775
776 {automationModuleActive && (
777 <div className="flex justify-end">
778 <Button
779 onClick={handleCreate}
780 className="bg-blue-600 hover:bg-blue-700 text-white"
781 >
782 <Plus className="w-4 h-4 mr-2" />
783 {__("Create Template")}
784 </Button>
785 </div>
786 )}
787
788 <Card>
789 <CardContent className="p-4">
790 <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-center">
791 <div className="lg:col-span-6">
792 <div className="relative">
793 <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
794 <Input
795 value={searchTerm}
796 onChange={(e) => setSearchTerm(e.target.value)}
797 placeholder={__("Search templates...")}
798 className="pl-10 w-full"
799 />
800 </div>
801 </div>
802 <div className="lg:col-span-2">
803 <Select
804 value={categoryFilter}
805 onChange={(e) => setCategoryFilter(e.target.value)}
806 className="w-full"
807 >
808 <option value="all">{__("All Categories")}</option>
809 {categories.map((cat: string) => (
810 <option key={cat} value={cat}>
811 {cat.charAt(0).toUpperCase() + cat.slice(1)}
812 </option>
813 ))}
814 </Select>
815 </div>
816 <div className="lg:col-span-2">
817 <Select
818 value={recipientFilter}
819 onChange={(e) => setRecipientFilter(e.target.value)}
820 className="w-full"
821 >
822 <option value="all">{__("All Recipients")}</option>
823 <option value="customer">{__("Customer")}</option>
824 <option value="admin">{__("Admin")}</option>
825 </Select>
826 </div>
827 <div className="lg:col-span-2">
828 <Select
829 value={eventFilter}
830 onChange={(e) => setEventFilter(e.target.value)}
831 className="w-full"
832 >
833 <option value="all">{__("All Events")}</option>
834 {events.map((event: any) => (
835 <option key={event.key} value={event.key}>
836 {event.name}
837 </option>
838 ))}
839 </Select>
840 </div>
841 </div>
842 </CardContent>
843 </Card>
844
845 <BulkActionToolbar
846 selectedIds={selectedIds}
847 bulkAction={bulkAction}
848 setBulkAction={setBulkAction}
849 onApply={handleBulkAction}
850 onClearSelection={() => setSelectedIds([])}
851 statusFilter={statusFilter}
852 setStatusFilter={setStatusFilter}
853 statusOptions={[
854 { key: "all", label: __("All"), count: templates.length },
855 {
856 key: "active",
857 label: __("Active"),
858 count: templates.filter((t) => t.is_active).length,
859 },
860 {
861 key: "inactive",
862 label: __("Inactive"),
863 count: templates.filter((t) => !t.is_active).length,
864 },
865 ]}
866 showColumnsDropdown={showColumnsDropdown}
867 setShowColumnsDropdown={setShowColumnsDropdown}
868 columnOptions={columnOptions}
869 onToggleColumn={toggleColumn}
870 bulkMutationPending={bulkDeleteMutation.isPending}
871 totalItems={filteredTemplates.length}
872 bulkActionOptions={
873 automationModuleActive
874 ? [{ value: "delete", label: __("Delete") }]
875 : []
876 }
877 />
878
879 <Card>
880 <CardContent className="p-0">
881 <SharedTable
882 data={paginatedTemplates}
883 columns={columns}
884 actions={actions}
885 isLoading={isLoading}
886 emptyText={__("No templates found")}
887 emptyDescription={__(
888 "Create your first email template to get started.",
889 )}
890 onCreateClick={automationModuleActive ? handleCreate : undefined}
891 getItemId={(template: UnifiedEmailTemplate) => template.id}
892 capability="yatra_manage_emails"
893 skeletonRows={5}
894 selectedItemIds={selectedIds}
895 onSelectItem={handleSelectOne}
896 onSelectAll={handleSelectAll}
897 isAllSelected={
898 selectedIds.length > 0 &&
899 selectedIds.length === paginatedTemplates.length &&
900 paginatedTemplates.length > 0
901 }
902 />
903 </CardContent>
904 </Card>
905
906 {totalFilteredItems > itemsPerPage && (
907 <div className="mt-4">
908 <Pagination
909 currentPage={currentPage}
910 totalPages={totalPages}
911 totalItems={totalFilteredItems}
912 itemsPerPage={itemsPerPage}
913 onPageChange={setCurrentPage}
914 itemName={__("templates")}
915 />
916 </div>
917 )}
918
919 <EmailPreviewModal
920 open={previewOpen && Boolean(previewPayload)}
921 onClose={() => {
922 setPreviewOpen(false);
923 setPreviewPayload(null);
924 }}
925 subject={previewPayload?.subject ?? ""}
926 body={previewPayload?.body ?? ""}
927 />
928
929 <ConfirmationDialog
930 isOpen={deleteDialog.isOpen}
931 onClose={cancelDelete}
932 onConfirm={confirmDelete}
933 title={__("Delete Template")}
934 message={
935 deleteDialog.template
936 ? __(
937 'Are you sure you want to delete "{name}"? This action cannot be undone.',
938 ).replace("{name}", deleteDialog.template.name)
939 : ""
940 }
941 confirmText={__("Delete")}
942 cancelText={__("Cancel")}
943 variant="danger"
944 isLoading={deleteMutation.isPending}
945 />
946 </div>
947 );
948 };
949
950 export default EmailTemplatesList;
951