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 +489 -41 3.0.8 → 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);
@@ -387,9 +504,9 @@
387 504 }, [filteredTemplates, currentPage, itemsPerPage]);
388 505
389 506 useMemo(() => {
390 507 setCurrentPage(1);
391 - // eslint-disable-next-line react-hooks/exhaustive-deps
508 + // eslint-disable-next-line react-hooks/exhaustive-deps
392 509 }, [searchTerm, categoryFilter, recipientFilter, eventFilter, statusFilter]);
393 510
394 511 const handleLockedNavigate = useCallback(() => {
395 512 if (isProPluginActive()) {
@@ -470,12 +587,21 @@
470 587 "admin.php?page=yatra&subpage=email-automation&tab=template&action=create";
471 588 };
472 589
473 590 const handleBulkAction = () => {
474 - if (!bulkAction || selectedIds.length === 0) {
475 - showToast(__("Please select templates and an action"), "error");
591 + // Report the input that is actually missing. The Apply button is disabled
592 + // whenever nothing is selected, so this can only be reached with a non-empty
593 + // selection — the combined message blamed the selection and left operators
594 + // re-selecting rows that were already ticked.
595 + if (selectedIds.length === 0) {
596 + showToast(__("Please select at least one template.", "yatra"), "error");
476 597 return;
477 598 }
599 +
600 + if (!bulkAction) {
601 + showToast(__("Please choose an action to apply.", "yatra"), "error");
602 + return;
603 + }
478 604 if (bulkAction === "delete") {
479 605 bulkDeleteMutation.mutate(selectedIds);
480 606 }
481 607 };
@@ -547,28 +673,161 @@
547 673 >
548 674 {template.name}
549 675 </button>
550 676 )}
551 - {template.is_system ? (
677 + {template.overrides ? (
552 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">
553 - {__("System")}
679 + {__("Override", "yatra")}
554 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>
555 685 ) : (
556 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">
557 687 {__("Custom")}
558 688 </span>
559 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 + )}
560 737 </div>
561 738 </div>
562 739 );
563 740 },
564 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 + : []),
565 823 {
566 824 key: "event",
567 825 label: __("Event"),
568 826 visible: visibleColumns.event,
569 827 render: (template: UnifiedEmailTemplate) => {
570 - 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);
571 830 return (
572 831 <span
573 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"
574 833 title={eventInfo?.description || ""}
@@ -573,9 +832,9 @@
573 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"
574 833 title={eventInfo?.description || ""}
575 834 >
576 835 <Zap className="w-3 h-3" />
577 - {formatEventKey(template.event_key)}
836 + {formatEventKey(eventKey)}
578 837 </span>
579 838 );
580 839 },
581 840 },
@@ -594,9 +853,13 @@
594 853 },
595 854 {
596 855 key: "body_preview",
597 856 label: __("Body"),
598 - 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"),
599 862 render: (template: UnifiedEmailTemplate) => {
600 863 const preview = plainTextEmailPreview(template.body || "");
601 864 return (
602 865 <span
@@ -610,9 +873,13 @@
610 873 },
611 874 {
612 875 key: "description",
613 876 label: __("Description"),
614 - 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"),
615 882 render: (template: UnifiedEmailTemplate) => (
616 883 <span className="text-sm text-gray-600 dark:text-gray-400 line-clamp-1">
617 884 {template.description || "-"}
618 885 </span>
@@ -620,9 +887,13 @@
620 887 },
621 888 {
622 889 key: "category",
623 890 label: __("Category"),
624 - 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"),
625 896 render: (template: UnifiedEmailTemplate) => {
626 897 const CategoryIcon = categoryIcons[template.category] || Mail;
627 898 return (
628 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">
@@ -635,9 +906,13 @@
635 906 },
636 907 {
637 908 key: "recipient_type",
638 909 label: __("Recipient"),
639 - 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"),
640 915 render: (template: UnifiedEmailTemplate) => {
641 916 const effectiveRecipient = getEffectiveRecipientType(template);
642 917 const toEmail = template.to_email || "";
643 918 return (
@@ -693,8 +968,43 @@
693 968 },
694 969 ];
695 970
696 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 + : []),
697 1007 {
698 1008 key: "preview",
699 1009 label: __("Preview", "yatra"),
700 1010 icon: <Eye className="w-4 h-4" />,
@@ -772,17 +1082,109 @@
772 1082 )}
773 1083 </p>
774 1084 )}
775 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 +
776 1133 {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>
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>
785 1187 </div>
786 1188 )}
787 1189
788 1190 <Card>
@@ -799,20 +1201,42 @@
799 1201 />
800 1202 </div>
801 1203 </div>
802 1204 <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>
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 + )}
815 1239 </div>
816 1240 <div className="lg:col-span-2">
817 1241 <Select
818 1242 value={recipientFilter}
@@ -882,13 +1306,28 @@
882 1306 data={paginatedTemplates}
883 1307 columns={columns}
884 1308 actions={actions}
885 1309 isLoading={isLoading}
886 - emptyText={__("No templates found")}
887 - emptyDescription={__(
888 - "Create your first email template to get started.",
889 - )}
890 - 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 + }
891 1330 getItemId={(template: UnifiedEmailTemplate) => template.id}
892 1331 capability="yatra_manage_emails"
893 1332 skeletonRows={5}
894 1333 selectedItemIds={selectedIds}
@@ -913,8 +1352,17 @@
913 1352 onPageChange={setCurrentPage}
914 1353 itemName={__("templates")}
915 1354 />
916 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 + />
917 1365 )}
918 1366
919 1367 <EmailPreviewModal
920 1368 open={previewOpen && Boolean(previewPayload)}