PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/components/email/EmailTemplatesList.tsx +477 -38 3.0.13 → 3.0.17 View file →
@@ -4,9 +4,9 @@
4 4 */
5 5
6 6 import React, { useState, useMemo, useCallback } from "react";
7 7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 -import { __ } from "../../lib/i18n";
8 +import { __, sprintf } from "../../lib/i18n";
9 9 import { useToast } from "../ui/toast";
10 10 import { Card, CardContent } from "../ui/card";
11 11 import { Button } from "../ui/button";
12 12 import { Input } from "../ui/input";
@@ -47,8 +47,16 @@
47 47 setEmailTemplateActive,
48 48 } from "../../api/email-automation-api";
49 49 import { previewCoreEmailTemplate } from "../../api/settings-api";
50 50 import { EmailPreviewModal } from "./EmailPreviewModal";
51 +import { EmailOverrideCreateModal } from "./EmailOverrideCreateModal";
52 +import { SearchableSelect } from "../ui/searchable-select";
53 +import {
54 + reorderEmailTemplateOverrides,
55 + resolveEmailTemplatesForTrip,
56 +} from "../../api/email-automation-api";
57 +import { describeTargets, useTripTargets } from "../../hooks/useTripTargets";
58 +import { ArrowDown, ArrowUp, Compass, Globe } from "lucide-react";
51 59
52 60 const EMAIL_TEMPLATE_VISIBLE_COLUMNS_DEFAULT: Record<string, boolean> = {
53 61 name: true,
54 62 event: true,
@@ -155,9 +163,12 @@
155 163 const [searchTerm, setSearchTerm] = useState("");
156 164 const [categoryFilter, setCategoryFilter] = useState("all");
157 165 const [recipientFilter, setRecipientFilter] = useState("all");
158 166 const [eventFilter, setEventFilter] = useState("all");
159 - const [statusFilter, setStatusFilter] = useState("all");
167 + // Default to the "Active" view so the list shows only in-use templates and
168 + // disabled/unused system templates stay decluttered out of sight. The "All"
169 + // and "Inactive" tabs remain one click away to review or re-enable them.
170 + const [statusFilter, setStatusFilter] = useState("active");
160 171 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
161 172 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
162 173 const [bulkAction, setBulkAction] = useState("");
163 174 const [currentPage, setCurrentPage] = useState(1);
@@ -186,8 +197,37 @@
186 197 JSON.stringify(newVisibleColumns),
187 198 );
188 199 };
189 200
201 + // Trip-specific overrides (Pro Email Automation ≥ the build that sets this
202 + // flag). Without it the tab is exactly the pre-override list.
203 + const overridesSupported =
204 + automationModuleActive &&
205 + !!(window as any).yatraAdmin?.emailTemplateOverridesEnabled;
206 + const [section, setSection] = useState<"global" | "override">("global");
207 + const [parentFilter, setParentFilter] = useState("");
208 + const [asTrip, setAsTrip] = useState<number | null>(null);
209 + const [overrideModal, setOverrideModal] = useState<{
210 + open: boolean;
211 + parent: UnifiedEmailTemplate | null;
212 + }>({ open: false, parent: null });
213 + const { options: tripTargetOptions } = useTripTargets(overridesSupported);
214 + const tripOptions = useMemo(
215 + () =>
216 + tripTargetOptions
217 + .filter((o) => String(o.value).startsWith("trip:"))
218 + .map((o) => ({
219 + id: Number(String(o.value).slice(5)),
220 + label: o.label.replace(/^[^:]+: /, ""),
221 + })),
222 + [tripTargetOptions],
223 + );
224 + const { data: resolution } = useQuery({
225 + queryKey: ["email-templates-resolve", asTrip],
226 + queryFn: () => resolveEmailTemplatesForTrip(asTrip as number),
227 + enabled: overridesSupported && !!asTrip,
228 + });
229 +
190 230 const { data: templatesData, isLoading: apiLoading } = useQuery({
191 231 queryKey: ["email-templates"],
192 232 queryFn: () => fetchEmailTemplates(),
193 233 enabled: automationModuleActive,
@@ -208,12 +248,82 @@
208 248 isModuleActive,
209 249 });
210 250 }, [automationModuleActive, settingsBridge]);
211 251
212 - const templates = automationModuleActive ? apiTemplates : localTemplates;
252 + const allTemplates = automationModuleActive ? apiTemplates : localTemplates;
253 + const overrideTemplates = useMemo(
254 + () => (overridesSupported ? allTemplates.filter((t) => !!t.overrides) : []),
255 + [allTemplates, overridesSupported],
256 + );
257 + const globalTemplates = useMemo(
258 + () =>
259 + overridesSupported
260 + ? allTemplates.filter((t) => !t.overrides)
261 + : allTemplates,
262 + [allTemplates, overridesSupported],
263 + );
264 + // The list the filters / table operate on: one section at a time.
265 + const templates =
266 + overridesSupported && section === "override"
267 + ? overrideTemplates
268 + : globalTemplates;
269 + const globalByKey = useMemo(() => {
270 + const m = new Map<string, UnifiedEmailTemplate>();
271 + globalTemplates.forEach((t) => m.set(t.template_key, t));
272 + return m;
273 + }, [globalTemplates]);
274 + const overridesByParent = useMemo(() => {
275 + const m = new Map<string, UnifiedEmailTemplate[]>();
276 + overrideTemplates.forEach((t) => {
277 + const k = t.overrides || "";
278 + m.set(k, [...(m.get(k) || []), t]);
279 + });
280 + m.forEach((arr) =>
281 + arr.sort(
282 + (a, b) =>
283 + (a.priority || 0) - (b.priority || 0) || Number(a.id) - Number(b.id),
284 + ),
285 + );
286 + return m;
287 + }, [overrideTemplates]);
213 288
214 289 const isLoading = automationModuleActive ? apiLoading : false;
215 290
291 + const reorderMutation = useMutation({
292 + mutationFn: ({ parentKey, ids }: { parentKey: string; ids: number[] }) =>
293 + reorderEmailTemplateOverrides(parentKey, ids),
294 + onSuccess: () => {
295 + queryClient.invalidateQueries({ queryKey: ["email-templates"] });
296 + queryClient.invalidateQueries({ queryKey: ["email-templates-resolve"] });
297 + },
298 + onError: (error: any) => {
299 + showToast(
300 + error?.message || __("Failed to reorder overrides", "yatra"),
301 + "error",
302 + );
303 + },
304 + });
305 + const moveOverride = (t: UnifiedEmailTemplate, delta: number) => {
306 + const siblings = overridesByParent.get(t.overrides || "") || [];
307 + const ids = siblings.map((x) => Number(x.id));
308 + const i = ids.indexOf(Number(t.id));
309 + const j = i + delta;
310 + if (i < 0 || j < 0 || j >= ids.length) return;
311 + [ids[i], ids[j]] = [ids[j], ids[i]];
312 + reorderMutation.mutate({ parentKey: t.overrides || "", ids });
313 + };
314 + /** "View as a trip": which row is sent for the chosen trip. */
315 + const sentForTrip = (t: UnifiedEmailTemplate): "hit" | "dim" | null => {
316 + if (!asTrip || !resolution) return null;
317 + if (t.overrides) {
318 + return resolution.resolved[t.overrides]?.id === Number(t.id)
319 + ? "hit"
320 + : "dim";
321 + }
322 + if (!resolution.overridable.includes(t.template_key)) return null; // not booking-bound: unaffected
323 + return resolution.resolved[t.template_key] ? "dim" : "hit";
324 + };
325 +
216 326 const toggleMutation = useMutation({
217 327 mutationFn: async ({
218 328 id,
219 329 is_active,
@@ -356,9 +466,13 @@
356 466 categoryFilter === "all" || t.category === categoryFilter;
357 467 const matchesRecipient =
358 468 recipientFilter === "all" ||
359 469 getEffectiveRecipientType(t) === recipientFilter;
360 - const matchesEvent = eventFilter === "all" || t.event_key === eventFilter;
470 + const matchesEvent =
471 + eventFilter === "all" ||
472 + (t.effective_event_key || t.event_key) === eventFilter;
473 + const matchesParent =
474 + !parentFilter || section !== "override" || t.overrides === parentFilter;
361 475 const matchesStatus =
362 476 statusFilter === "all" ||
363 477 (statusFilter === "active" && t.is_active) ||
364 478 (statusFilter === "inactive" && !t.is_active);
@@ -366,9 +480,10 @@
366 480 matchesSearch &&
367 481 matchesCategory &&
368 482 matchesRecipient &&
369 483 matchesEvent &&
370 - matchesStatus
484 + matchesStatus &&
485 + matchesParent
371 486 );
372 487 });
373 488 }, [
374 489 templates,
@@ -376,8 +491,10 @@
376 491 categoryFilter,
377 492 recipientFilter,
378 493 eventFilter,
379 494 statusFilter,
495 + parentFilter,
496 + section,
380 497 ]);
381 498
382 499 const totalFilteredItems = filteredTemplates.length;
383 500 const totalPages = Math.ceil(totalFilteredItems / itemsPerPage);
@@ -556,28 +673,161 @@
556 673 >
557 674 {template.name}
558 675 </button>
559 676 )}
560 - {template.is_system ? (
677 + {template.overrides ? (
561 678 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
562 - {__("System")}
679 + {__("Override", "yatra")}
563 680 </span>
681 + ) : template.is_system ? (
682 + <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
683 + {overridesSupported ? __("Global", "yatra") : __("System")}
684 + </span>
564 685 ) : (
565 686 <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
566 687 {__("Custom")}
567 688 </span>
568 689 )}
690 + {(() => {
691 + const mark = sentForTrip(template);
692 + const tripName =
693 + tripOptions.find((t) => t.id === asTrip)?.label || "";
694 + if (mark === "hit") {
695 + return (
696 + <span
697 + className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300"
698 + data-testid="sent-for-trip"
699 + >
700 + {sprintf(__("sent for %s", "yatra"), tripName)}
701 + {template.overrides &&
702 + resolution?.resolved[template.overrides]?.reason
703 + ? ` · ${resolution.resolved[template.overrides]?.reason}`
704 + : ""}
705 + </span>
706 + );
707 + }
708 + if (mark === "dim" && !template.overrides) {
709 + const ov = resolution?.resolved[template.template_key];
710 + return (
711 + <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
712 + {sprintf(
713 + __("overridden for %1$s → %2$s", "yatra"),
714 + tripName,
715 + ov?.name || "",
716 + )}
717 + </span>
718 + );
719 + }
720 + return null;
721 + })()}
722 + {template.overrides && !template.is_active && (
723 + <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
724 + {__("off — its trips get the global template", "yatra")}
725 + </span>
726 + )}
727 + {template.overrides &&
728 + template.is_active &&
729 + !String(template.body || "").trim() && (
730 + <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
731 + {__(
732 + "no body yet — its trips get the global template",
733 + "yatra",
734 + )}
735 + </span>
736 + )}
569 737 </div>
570 738 </div>
571 739 );
572 740 },
573 741 },
742 + ...(overridesSupported && section === "override"
743 + ? [
744 + {
745 + key: "overrides_of",
746 + label: __("Overrides", "yatra"),
747 + visible: true,
748 + render: (template: UnifiedEmailTemplate) => {
749 + const parent = globalByKey.get(template.overrides || "");
750 + const siblings =
751 + overridesByParent.get(template.overrides || "") || [];
752 + const pos = siblings.findIndex((x) => x.id === template.id) + 1;
753 + return (
754 + <div>
755 + <button
756 + type="button"
757 + className="text-blue-600 dark:text-blue-400 hover:underline font-medium text-left"
758 + onClick={() => parent && handleEdit(parent)}
759 + >
760 + {parent?.name || template.overrides}
761 + </button>
762 + {siblings.length > 1 && (
763 + <div className="text-xs text-gray-400">
764 + {sprintf(
765 + __("priority %1$d of %2$d", "yatra"),
766 + pos,
767 + siblings.length,
768 + )}
769 + </div>
770 + )}
771 + </div>
772 + );
773 + },
774 + },
775 + {
776 + key: "applies_to",
777 + label: __("Applies to", "yatra"),
778 + visible: true,
779 + render: (template: UnifiedEmailTemplate) => (
780 + <span className="text-sm text-purple-700 dark:text-purple-300">
781 + {describeTargets(template.targets, tripTargetOptions) || "—"}
782 + </span>
783 + ),
784 + },
785 + ]
786 + : []),
787 + ...(overridesSupported && section === "global"
788 + ? [
789 + {
790 + key: "overrides_count",
791 + label: __("Overrides", "yatra"),
792 + visible: true,
793 + render: (template: UnifiedEmailTemplate) => {
794 + const n = (overridesByParent.get(template.template_key) || [])
795 + .length;
796 + if (!n) {
797 + return (
798 + <span className="text-xs text-gray-400">
799 + {template.overridable ? __("none", "yatra") : "—"}
800 + </span>
801 + );
802 + }
803 + return (
804 + <button
805 + type="button"
806 + className="text-sm text-purple-700 dark:text-purple-300 hover:underline inline-flex items-center gap-1"
807 + onClick={() => {
808 + setSection("override");
809 + setParentFilter(template.template_key);
810 + }}
811 + data-testid="overrides-count"
812 + >
813 + <Compass className="w-3.5 h-3.5" />
814 + {n === 1
815 + ? __("1 override →", "yatra")
816 + : sprintf(__("%d overrides →", "yatra"), n)}
817 + </button>
818 + );
819 + },
820 + },
821 + ]
822 + : []),
574 823 {
575 824 key: "event",
576 825 label: __("Event"),
577 826 visible: visibleColumns.event,
578 827 render: (template: UnifiedEmailTemplate) => {
579 - const eventInfo = events.find((e: any) => e.key === template.event_key);
828 + const eventKey = template.effective_event_key || template.event_key;
829 + const eventInfo = events.find((e: any) => e.key === eventKey);
580 830 return (
581 831 <span
582 832 className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400 cursor-help"
583 833 title={eventInfo?.description || ""}
@@ -582,9 +832,9 @@
582 832 className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400 cursor-help"
583 833 title={eventInfo?.description || ""}
584 834 >
585 835 <Zap className="w-3 h-3" />
586 - {formatEventKey(template.event_key)}
836 + {formatEventKey(eventKey)}
587 837 </span>
588 838 );
589 839 },
590 840 },
@@ -603,9 +853,13 @@
603 853 },
604 854 {
605 855 key: "body_preview",
606 856 label: __("Body"),
607 - visible: visibleColumns.body_preview,
857 + // Hidden in the Override section: those rows are about targeting, so the
858 + // Template / Overrides / Applies to columns get the room instead.
859 + visible:
860 + visibleColumns.body_preview &&
861 + !(overridesSupported && section === "override"),
608 862 render: (template: UnifiedEmailTemplate) => {
609 863 const preview = plainTextEmailPreview(template.body || "");
610 864 return (
611 865 <span
@@ -619,9 +873,13 @@
619 873 },
620 874 {
621 875 key: "description",
622 876 label: __("Description"),
623 - visible: visibleColumns.description,
877 + // Hidden in the Override section: those rows are about targeting, so the
878 + // Template / Overrides / Applies to columns get the room instead.
879 + visible:
880 + visibleColumns.description &&
881 + !(overridesSupported && section === "override"),
624 882 render: (template: UnifiedEmailTemplate) => (
625 883 <span className="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">
626 884 {template.description || "-"}
627 885 </span>
@@ -629,9 +887,13 @@
629 887 },
630 888 {
631 889 key: "category",
632 890 label: __("Category"),
633 - visible: visibleColumns.category,
891 + // Hidden in the Override section: those rows are about targeting, so the
892 + // Template / Overrides / Applies to columns get the room instead.
893 + visible:
894 + visibleColumns.category &&
895 + !(overridesSupported && section === "override"),
634 896 render: (template: UnifiedEmailTemplate) => {
635 897 const CategoryIcon = categoryIcons[template.category] || Mail;
636 898 return (
637 899 <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
@@ -644,9 +906,13 @@
644 906 },
645 907 {
646 908 key: "recipient_type",
647 909 label: __("Recipient"),
648 - visible: visibleColumns.recipient_type,
910 + // Hidden in the Override section: those rows are about targeting, so the
911 + // Template / Overrides / Applies to columns get the room instead.
912 + visible:
913 + visibleColumns.recipient_type &&
914 + !(overridesSupported && section === "override"),
649 915 render: (template: UnifiedEmailTemplate) => {
650 916 const effectiveRecipient = getEffectiveRecipientType(template);
651 917 const toEmail = template.to_email || "";
652 918 return (
@@ -702,8 +968,43 @@
702 968 },
703 969 ];
704 970
705 971 const actions = [
972 + ...(overridesSupported
973 + ? [
974 + {
975 + key: "add_override",
976 + label: __("Add override…", "yatra"),
977 + icon: <Compass className="w-4 h-4" />,
978 + onClick: (template: UnifiedEmailTemplate) =>
979 + setOverrideModal({ open: true, parent: template }),
980 + condition: (template: UnifiedEmailTemplate) =>
981 + !template.overrides &&
982 + !!template.overridable &&
983 + isApiTemplate(template),
984 + },
985 + {
986 + key: "override_up",
987 + label: __("Higher priority", "yatra"),
988 + icon: <ArrowUp className="w-4 h-4" />,
989 + onClick: (template: UnifiedEmailTemplate) =>
990 + moveOverride(template, -1),
991 + condition: (template: UnifiedEmailTemplate) =>
992 + !!template.overrides &&
993 + (overridesByParent.get(template.overrides) || []).length > 1,
994 + },
995 + {
996 + key: "override_down",
997 + label: __("Lower priority", "yatra"),
998 + icon: <ArrowDown className="w-4 h-4" />,
999 + onClick: (template: UnifiedEmailTemplate) =>
1000 + moveOverride(template, 1),
1001 + condition: (template: UnifiedEmailTemplate) =>
1002 + !!template.overrides &&
1003 + (overridesByParent.get(template.overrides) || []).length > 1,
1004 + },
1005 + ]
1006 + : []),
706 1007 {
707 1008 key: "preview",
708 1009 label: __("Preview", "yatra"),
709 1010 icon: <Eye className="w-4 h-4" />,
@@ -781,17 +1082,109 @@
781 1082 )}
782 1083 </p>
783 1084 )}
784 1085
1086 + {overridesSupported && (
1087 + <div className="space-y-2" data-testid="template-sections">
1088 + <div className="inline-flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden">
1089 + <button
1090 + type="button"
1091 + onClick={() => {
1092 + setSection("global");
1093 + setParentFilter("");
1094 + }}
1095 + aria-pressed={section === "global"}
1096 + className={`inline-flex items-center gap-2 px-4 h-10 text-sm font-medium ${section === "global" ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" : "bg-white text-gray-600 dark:bg-gray-800 dark:text-gray-300"}`}
1097 + data-testid="section-global"
1098 + >
1099 + <Globe className="w-4 h-4" />
1100 + {__("Global templates", "yatra")}
1101 + <span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
1102 + {globalTemplates.length}
1103 + </span>
1104 + </button>
1105 + <button
1106 + type="button"
1107 + onClick={() => setSection("override")}
1108 + aria-pressed={section === "override"}
1109 + className={`inline-flex items-center gap-2 px-4 h-10 text-sm font-medium border-l border-gray-300 dark:border-gray-600 ${section === "override" ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" : "bg-white text-gray-600 dark:bg-gray-800 dark:text-gray-300"}`}
1110 + data-testid="section-override"
1111 + >
1112 + <Compass className="w-4 h-4" />
1113 + {__("Override templates", "yatra")}
1114 + <span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
1115 + {overrideTemplates.length}
1116 + </span>
1117 + </button>
1118 + </div>
1119 + <p className="text-sm text-gray-600 dark:text-gray-400">
1120 + {section === "global"
1121 + ? __(
1122 + "One template per event, sent to every trip unless an override applies. Editing a global template never changes its overrides.",
1123 + "yatra",
1124 + )
1125 + : __(
1126 + "Trip-specific versions of a global template. Same event and merge tags as the template they override; used only for bookings on the trips they name. Everything else keeps the global template.",
1127 + "yatra",
1128 + )}
1129 + </p>
1130 + </div>
1131 + )}
1132 +
785 1133 {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>
1134 + <div className="flex flex-wrap items-center justify-between gap-3">
1135 + {overridesSupported ? (
1136 + <div className="flex flex-wrap items-center gap-2 text-sm">
1137 + <span className="text-gray-500">
1138 + {__("View as a trip:", "yatra")}
1139 + </span>
1140 + <div className="w-72" data-testid="view-as-trip">
1141 + <SearchableSelect
1142 + value={asTrip ? String(asTrip) : ""}
1143 + onChange={(value) => setAsTrip(value ? Number(value) : null)}
1144 + options={tripOptions.map((t) => ({
1145 + value: String(t.id),
1146 + label: t.label,
1147 + }))}
1148 + placeholder={__("— none (show everything) —", "yatra")}
1149 + searchPlaceholder={__("Search trips…", "yatra")}
1150 + />
1151 + </div>
1152 + <span className="text-xs text-gray-400">
1153 + {asTrip
1154 + ? __(
1155 + "Highlighted rows are exactly what this trip's customers receive.",
1156 + "yatra",
1157 + )
1158 + : __(
1159 + "Pick a trip to highlight exactly what that trip's customers receive.",
1160 + "yatra",
1161 + )}
1162 + </span>
1163 + </div>
1164 + ) : (
1165 + <span />
1166 + )}
1167 + <div className="flex items-center gap-2">
1168 + {overridesSupported && section === "override" ? (
1169 + <Button
1170 + onClick={() => setOverrideModal({ open: true, parent: null })}
1171 + className="bg-blue-600 hover:bg-blue-700 text-white"
1172 + data-testid="add-override"
1173 + >
1174 + <Plus className="w-4 h-4 mr-2" />
1175 + {__("Add override", "yatra")}
1176 + </Button>
1177 + ) : (
1178 + <Button
1179 + onClick={handleCreate}
1180 + className="bg-blue-600 hover:bg-blue-700 text-white"
1181 + >
1182 + <Plus className="w-4 h-4 mr-2" />
1183 + {__("Create Template")}
1184 + </Button>
1185 + )}
1186 + </div>
794 1187 </div>
795 1188 )}
796 1189
797 1190 <Card>
@@ -808,20 +1201,42 @@
808 1201 />
809 1202 </div>
810 1203 </div>
811 1204 <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>
1205 + {overridesSupported && section === "override" ? (
1206 + <Select
1207 + value={parentFilter}
1208 + onChange={(e) => setParentFilter(e.target.value)}
1209 + className="w-full"
1210 + data-testid="parent-filter"
1211 + >
1212 + <option value="">{__("Overrides of: any", "yatra")}</option>
1213 + {globalTemplates
1214 + .filter(
1215 + (g) =>
1216 + (overridesByParent.get(g.template_key) || []).length >
1217 + 0,
1218 + )
1219 + .map((g) => (
1220 + <option key={g.template_key} value={g.template_key}>
1221 + {sprintf(__("Overrides of: %s", "yatra"), g.name)}
1222 + </option>
1223 + ))}
1224 + </Select>
1225 + ) : (
1226 + <Select
1227 + value={categoryFilter}
1228 + onChange={(e) => setCategoryFilter(e.target.value)}
1229 + className="w-full"
1230 + >
1231 + <option value="all">{__("All Categories")}</option>
1232 + {categories.map((cat: string) => (
1233 + <option key={cat} value={cat}>
1234 + {cat.charAt(0).toUpperCase() + cat.slice(1)}
1235 + </option>
1236 + ))}
1237 + </Select>
1238 + )}
824 1239 </div>
825 1240 <div className="lg:col-span-2">
826 1241 <Select
827 1242 value={recipientFilter}
@@ -891,13 +1306,28 @@
891 1306 data={paginatedTemplates}
892 1307 columns={columns}
893 1308 actions={actions}
894 1309 isLoading={isLoading}
895 - emptyText={__("No templates found")}
896 - emptyDescription={__(
897 - "Create your first email template to get started.",
898 - )}
899 - onCreateClick={automationModuleActive ? handleCreate : undefined}
1310 + emptyText={
1311 + overridesSupported && section === "override"
1312 + ? __("No overrides yet", "yatra")
1313 + : __("No templates found")
1314 + }
1315 + emptyDescription={
1316 + overridesSupported && section === "override"
1317 + ? __(
1318 + "Every trip gets the global templates. Use “Add override” here or “Add override…” on a global template.",
1319 + "yatra",
1320 + )
1321 + : __("Create your first email template to get started.")
1322 + }
1323 + onCreateClick={
1324 + automationModuleActive
1325 + ? overridesSupported && section === "override"
1326 + ? () => setOverrideModal({ open: true, parent: null })
1327 + : handleCreate
1328 + : undefined
1329 + }
900 1330 getItemId={(template: UnifiedEmailTemplate) => template.id}
901 1331 capability="yatra_manage_emails"
902 1332 skeletonRows={5}
903 1333 selectedItemIds={selectedIds}
@@ -922,8 +1352,17 @@
922 1352 onPageChange={setCurrentPage}
923 1353 itemName={__("templates")}
924 1354 />
925 1355 </div>
1356 + )}
1357 +
1358 + {overridesSupported && (
1359 + <EmailOverrideCreateModal
1360 + isOpen={overrideModal.open}
1361 + onClose={() => setOverrideModal({ open: false, parent: null })}
1362 + globals={globalTemplates.filter((g) => !!g.overridable)}
1363 + parent={overrideModal.parent}
1364 + />
926 1365 )}
927 1366
928 1367 <EmailPreviewModal
929 1368 open={previewOpen && Boolean(previewPayload)}