| @@ -1,7 +1,22 @@ | ||
| 1 | 1 | import React, { useState, useEffect, useMemo } from "react"; |
| 2 | 2 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
| 3 | -import { __ } from "../lib/i18n"; | |
| 3 | +import { __, sprintf } from "../lib/i18n"; | |
| 4 | +import { MultiSelect } from "../components/ui/multi-select"; | |
| 5 | +import { SearchableSelect } from "../components/ui/searchable-select"; | |
| 6 | +import { | |
| 7 | + decodeTargets, | |
| 8 | + describeTargets, | |
| 9 | + encodeTargets, | |
| 10 | + hasTargets, | |
| 11 | + useTripTargets, | |
| 12 | + type TripTargets, | |
| 13 | +} from "../hooks/useTripTargets"; | |
| 14 | +import { EmailOverrideCreateModal } from "../components/email/EmailOverrideCreateModal"; | |
| 15 | +import { | |
| 16 | + fetchEmailTemplates, | |
| 17 | + resolveEmailTemplatesForTrip, | |
| 18 | +} from "../api/email-automation-api"; | |
| 4 | 19 | import { useToast } from "../components/ui/toast"; |
| 5 | 20 | import { |
| 6 | 21 | Card, |
| 7 | 22 | CardContent, |
| @@ -133,13 +148,19 @@ | ||
| 133 | 148 | to_email: "", |
| 134 | 149 | reply_to: "", |
| 135 | 150 | bcc: "", |
| 136 | 151 | cc: "", |
| 152 | + category: "booking", | |
| 137 | 153 | subject: "", |
| 138 | 154 | body: "", |
| 139 | 155 | event_key: "", |
| 140 | 156 | is_active: true, |
| 141 | 157 | }); |
| 158 | + // Trip-specific overrides (Pro Email Automation). `targets` is only sent | |
| 159 | + // for override rows; `previewTrip` feeds preview / test with a real trip. | |
| 160 | + const [targets, setTargets] = useState<TripTargets>({}); | |
| 161 | + const [previewTrip, setPreviewTrip] = useState<number | null>(null); | |
| 162 | + const [overrideModalOpen, setOverrideModalOpen] = useState(false); | |
| 142 | 163 | const [showPreview, setShowPreview] = useState(false); |
| 143 | 164 | const [previewData, setPreviewData] = useState<PreviewData | null>(null); |
| 144 | 165 | const [testEmail, setTestEmail] = useState(""); |
| 145 | 166 | const [copiedVar, setCopiedVar] = useState<string | null>(null); |
| @@ -176,8 +197,64 @@ | ||
| 176 | 197 | }); |
| 177 | 198 | |
| 178 | 199 | // System templates own their trigger event; everything else may choose one. |
| 179 | 200 | const isSystemTemplate = Boolean((templateData as any)?.is_system); |
| 201 | + const overridesSupported = | |
| 202 | + !isCoreSettingsEdit && | |
| 203 | + !!(window as any).yatraAdmin?.emailTemplateOverridesEnabled; | |
| 204 | + const overrideOf = String((templateData as any)?.overrides ?? ""); | |
| 205 | + const isOverride = overridesSupported && overrideOf !== ""; | |
| 206 | + const isOverridableGlobal = | |
| 207 | + overridesSupported && | |
| 208 | + !isOverride && | |
| 209 | + Boolean((templateData as any)?.overridable); | |
| 210 | + // Global list (for the override's parent name, the global's override strip | |
| 211 | + // and the picker labels). Cached with the Templates tab. | |
| 212 | + const { data: allTemplatesData } = useQuery({ | |
| 213 | + queryKey: ["email-templates"], | |
| 214 | + queryFn: () => fetchEmailTemplates(), | |
| 215 | + enabled: overridesSupported && !isCreateMode, | |
| 216 | + }); | |
| 217 | + const allTemplates = useMemo( | |
| 218 | + () => (Array.isArray(allTemplatesData) ? (allTemplatesData as any[]) : []), | |
| 219 | + [allTemplatesData], | |
| 220 | + ); | |
| 221 | + const parentTemplate = useMemo( | |
| 222 | + () => allTemplates.find((t) => t.template_key === overrideOf) || null, | |
| 223 | + [allTemplates, overrideOf], | |
| 224 | + ); | |
| 225 | + const myOverrides = useMemo( | |
| 226 | + () => | |
| 227 | + allTemplates | |
| 228 | + .filter( | |
| 229 | + (t) => | |
| 230 | + t.overrides && t.overrides === (templateData as any)?.template_key, | |
| 231 | + ) | |
| 232 | + .sort((a, b) => (a.priority || 0) - (b.priority || 0)), | |
| 233 | + [allTemplates, templateData], | |
| 234 | + ); | |
| 235 | + const { options: tripTargetOptions } = useTripTargets( | |
| 236 | + overridesSupported && (isOverride || isOverridableGlobal), | |
| 237 | + ); | |
| 238 | + const tripOptions = useMemo( | |
| 239 | + () => | |
| 240 | + tripTargetOptions | |
| 241 | + .filter((o) => String(o.value).startsWith("trip:")) | |
| 242 | + .map((o) => ({ | |
| 243 | + id: Number(String(o.value).slice(5)), | |
| 244 | + label: o.label.replace(/^[^:]+: /, ""), | |
| 245 | + })), | |
| 246 | + [tripTargetOptions], | |
| 247 | + ); | |
| 248 | + // "Check a trip": which template the chosen trip would actually get. | |
| 249 | + const { data: tripResolution } = useQuery({ | |
| 250 | + queryKey: ["email-templates-resolve", previewTrip], | |
| 251 | + queryFn: () => resolveEmailTemplatesForTrip(previewTrip as number), | |
| 252 | + enabled: | |
| 253 | + overridesSupported && | |
| 254 | + !!previewTrip && | |
| 255 | + (isOverride || isOverridableGlobal), | |
| 256 | + }); | |
| 180 | 257 | |
| 181 | 258 | // Event-scoped variables: the sidebar re-fetches whenever the |
| 182 | 259 | // operator switches the trigger event so the "Available |
| 183 | 260 | // Variables" panel reflects only tags that will actually |
| @@ -240,13 +317,21 @@ | ||
| 240 | 317 | to_email: String(t.to_email ?? ""), |
| 241 | 318 | reply_to: String(t.reply_to ?? ""), |
| 242 | 319 | bcc: String(t.bcc ?? ""), |
| 243 | 320 | cc: String(t.cc ?? ""), |
| 321 | + category: String(t.category ?? "booking"), | |
| 244 | 322 | subject: String(t.subject ?? ""), |
| 245 | 323 | body: String(t.body ?? ""), |
| 246 | - event_key: String(t.event_key ?? ""), | |
| 324 | + // An override's own event_key is stored empty; show (and fetch | |
| 325 | + // variables for) the global template's event instead. | |
| 326 | + event_key: String(t.effective_event_key || t.event_key || ""), | |
| 247 | 327 | is_active: Boolean(t.is_active ?? true), |
| 248 | 328 | }); |
| 329 | + setTargets( | |
| 330 | + t.overrides && t.targets && typeof t.targets === "object" | |
| 331 | + ? (t.targets as TripTargets) | |
| 332 | + : {}, | |
| 333 | + ); | |
| 249 | 334 | }, [templateData]); |
| 250 | 335 | |
| 251 | 336 | useEffect(() => { |
| 252 | 337 | if (!isCoreSettingsEdit || !settings || !coreDef) return; |
| @@ -263,8 +348,9 @@ | ||
| 263 | 348 | to_email: coreDef.to_email || "", |
| 264 | 349 | reply_to: "", |
| 265 | 350 | bcc: String(s[coreAddressKey(subj, "bcc") ?? ""] ?? ""), |
| 266 | 351 | cc: String(s[coreAddressKey(subj, "cc") ?? ""] ?? ""), |
| 352 | + category: String(coreDef.category ?? "booking"), | |
| 267 | 353 | subject: String(s[subj] ?? ""), |
| 268 | 354 | body: String(s[bodyKey] ?? ""), |
| 269 | 355 | event_key: coreDef.event_key, |
| 270 | 356 | is_active: Boolean(s[flag]), |
| @@ -360,9 +446,9 @@ | ||
| 360 | 446 | }); |
| 361 | 447 | |
| 362 | 448 | const previewMutation = useMutation({ |
| 363 | 449 | mutationFn: async () => { |
| 364 | - return previewEmailTemplate(id as string); | |
| 450 | + return previewEmailTemplate(id as string, previewTrip); | |
| 365 | 451 | }, |
| 366 | 452 | onSuccess: (data) => { |
| 367 | 453 | setPreviewData(data); |
| 368 | 454 | setShowPreview(true); |
| @@ -395,9 +481,9 @@ | ||
| 395 | 481 | }); |
| 396 | 482 | |
| 397 | 483 | const testMutation = useMutation({ |
| 398 | 484 | mutationFn: async (email: string) => { |
| 399 | - return sendEmailTemplateTest(id as string, email); | |
| 485 | + return sendEmailTemplateTest(id as string, email, previewTrip); | |
| 400 | 486 | }, |
| 401 | 487 | onSuccess: () => { |
| 402 | 488 | showToast(__("Test email sent successfully"), "success"); |
| 403 | 489 | setTestEmail(""); |
| @@ -425,10 +511,29 @@ | ||
| 425 | 511 | if (!formData.subject.trim()) { |
| 426 | 512 | showToast(__("Subject line is required"), "error"); |
| 427 | 513 | return; |
| 428 | 514 | } |
| 515 | + if (isOverride && !hasTargets(targets)) { | |
| 516 | + showToast( | |
| 517 | + __( | |
| 518 | + "An override needs at least one trip, category or trip type — or delete it to use the global template.", | |
| 519 | + "yatra", | |
| 520 | + ), | |
| 521 | + "error", | |
| 522 | + ); | |
| 523 | + return; | |
| 524 | + } | |
| 429 | 525 | |
| 430 | - saveMutation.mutate(formData); | |
| 526 | + saveMutation.mutate( | |
| 527 | + isOverride | |
| 528 | + ? // event_key is not sent: an override always fires on its global's event. | |
| 529 | + ({ | |
| 530 | + ...formData, | |
| 531 | + event_key: undefined, | |
| 532 | + targets, | |
| 533 | + } as unknown as typeof formData) | |
| 534 | + : formData, | |
| 535 | + ); | |
| 431 | 536 | }; |
| 432 | 537 | |
| 433 | 538 | const copyVariable = (variable: string) => { |
| 434 | 539 | const text = `{{${variable}}}`; |
| @@ -627,8 +732,33 @@ | ||
| 627 | 732 | "Core customer email (saved with site settings). From address is set under Email → Delivery.", |
| 628 | 733 | "yatra", |
| 629 | 734 | ) |
| 630 | 735 | : String(templateData?.name ?? "")} |
| 736 | + {isOverride && ( | |
| 737 | + <span className="ml-2 inline-flex items-center gap-1 text-xs align-middle"> | |
| 738 | + <span className="px-1.5 py-0.5 rounded font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300"> | |
| 739 | + {__("Override", "yatra")} | |
| 740 | + </span> | |
| 741 | + <span className="text-gray-400"> | |
| 742 | + {__("of global template", "yatra")} | |
| 743 | + </span> | |
| 744 | + {parentTemplate ? ( | |
| 745 | + <a | |
| 746 | + href={`admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${parentTemplate.id}`} | |
| 747 | + className="text-blue-600 dark:text-blue-400 hover:underline font-medium" | |
| 748 | + > | |
| 749 | + {parentTemplate.name} | |
| 750 | + </a> | |
| 751 | + ) : ( | |
| 752 | + <span>{overrideOf}</span> | |
| 753 | + )} | |
| 754 | + </span> | |
| 755 | + )} | |
| 756 | + {isOverridableGlobal && ( | |
| 757 | + <span className="ml-2 px-1.5 py-0.5 rounded text-xs font-medium align-middle bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300"> | |
| 758 | + {__("Global", "yatra")} | |
| 759 | + </span> | |
| 760 | + )} | |
| 631 | 761 | </p> |
| 632 | 762 | </div> |
| 633 | 763 | </div> |
| 634 | 764 | <div className="flex items-center gap-3"> |
| @@ -690,8 +820,118 @@ | ||
| 690 | 820 | |
| 691 | 821 | <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| 692 | 822 | {/* Main Form */} |
| 693 | 823 | <div className="lg:col-span-2 space-y-6"> |
| 824 | + {isOverride && ( | |
| 825 | + <div | |
| 826 | + className="rounded-lg border border-purple-300 dark:border-purple-800 overflow-hidden" | |
| 827 | + data-testid="override-applies-to" | |
| 828 | + > | |
| 829 | + <div className="px-4 py-2.5 bg-purple-50 dark:bg-purple-900/20 text-sm font-semibold text-purple-800 dark:text-purple-200"> | |
| 830 | + {__("Applies to", "yatra")}{" "} | |
| 831 | + <span className="font-normal text-purple-600 dark:text-purple-300"> | |
| 832 | + {__( | |
| 833 | + "— bookings on these trips get this override instead of the global template", | |
| 834 | + "yatra", | |
| 835 | + )} | |
| 836 | + </span> | |
| 837 | + </div> | |
| 838 | + <div className="p-4 space-y-3"> | |
| 839 | + <MultiSelect | |
| 840 | + value={encodeTargets(targets)} | |
| 841 | + onChange={(vals) => setTargets(decodeTargets(vals))} | |
| 842 | + options={tripTargetOptions} | |
| 843 | + placeholder={__( | |
| 844 | + "Search trips, categories, trip types…", | |
| 845 | + "yatra", | |
| 846 | + )} | |
| 847 | + /> | |
| 848 | + {!hasTargets(targets) && ( | |
| 849 | + <p className="text-xs text-red-600"> | |
| 850 | + {__( | |
| 851 | + "Pick at least one trip, category or trip type.", | |
| 852 | + "yatra", | |
| 853 | + )} | |
| 854 | + </p> | |
| 855 | + )} | |
| 856 | + <ul className="text-xs text-gray-500 dark:text-gray-400 list-disc pl-5 space-y-0.5"> | |
| 857 | + <li> | |
| 858 | + {__( | |
| 859 | + "Any match applies. A trip in a sub-category also matches its parent category.", | |
| 860 | + "yatra", | |
| 861 | + )} | |
| 862 | + </li> | |
| 863 | + <li> | |
| 864 | + {__( | |
| 865 | + "When several overrides match one trip, the most specific wins — trip › category › trip type — then the priority order in the Override templates list.", | |
| 866 | + "yatra", | |
| 867 | + )} | |
| 868 | + </li> | |
| 869 | + <li> | |
| 870 | + {__( | |
| 871 | + "Trips that match no override keep the global template. Switching this override off does the same for its trips.", | |
| 872 | + "yatra", | |
| 873 | + )} | |
| 874 | + </li> | |
| 875 | + </ul> | |
| 876 | + </div> | |
| 877 | + </div> | |
| 878 | + )} | |
| 879 | + {isOverridableGlobal && ( | |
| 880 | + <div | |
| 881 | + className="flex flex-wrap items-start gap-3 rounded-lg border border-purple-300 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 px-4 py-3 text-sm text-purple-900 dark:text-purple-200" | |
| 882 | + data-testid="global-overrides-strip" | |
| 883 | + > | |
| 884 | + <div className="flex-1"> | |
| 885 | + {myOverrides.length === 0 | |
| 886 | + ? __( | |
| 887 | + "No overrides yet. Every trip gets this template. Add an override to give some trips different wording.", | |
| 888 | + "yatra", | |
| 889 | + ) | |
| 890 | + : sprintf( | |
| 891 | + __( | |
| 892 | + "This global template has %d override(s) with their own wording. Changes you make here don't change them; every other trip gets this template.", | |
| 893 | + "yatra", | |
| 894 | + ), | |
| 895 | + myOverrides.length, | |
| 896 | + )} | |
| 897 | + {myOverrides.length > 0 && ( | |
| 898 | + <ul className="mt-1 space-y-0.5"> | |
| 899 | + {myOverrides.map((o) => ( | |
| 900 | + <li | |
| 901 | + key={o.id} | |
| 902 | + className="flex flex-wrap items-center gap-2" | |
| 903 | + > | |
| 904 | + <a | |
| 905 | + href={`admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${o.id}`} | |
| 906 | + className="font-medium text-blue-700 dark:text-blue-300 hover:underline" | |
| 907 | + > | |
| 908 | + {o.name} | |
| 909 | + </a> | |
| 910 | + <span className="text-xs text-purple-700 dark:text-purple-300"> | |
| 911 | + {describeTargets(o.targets, tripTargetOptions)} | |
| 912 | + </span> | |
| 913 | + {!o.is_active && ( | |
| 914 | + <span className="text-xs text-amber-700 dark:text-amber-300"> | |
| 915 | + {__("(off)", "yatra")} | |
| 916 | + </span> | |
| 917 | + )} | |
| 918 | + </li> | |
| 919 | + ))} | |
| 920 | + </ul> | |
| 921 | + )} | |
| 922 | + </div> | |
| 923 | + <Button | |
| 924 | + type="button" | |
| 925 | + variant="outline" | |
| 926 | + size="sm" | |
| 927 | + onClick={() => setOverrideModalOpen(true)} | |
| 928 | + data-testid="add-override-from-global" | |
| 929 | + > | |
| 930 | + {__("+ Add override", "yatra")} | |
| 931 | + </Button> | |
| 932 | + </div> | |
| 933 | + )} | |
| 694 | 934 | <Card> |
| 695 | 935 | <CardHeader> |
| 696 | 936 | <CardTitle className="flex items-center justify-between"> |
| 697 | 937 | <span>{__("Template Details")}</span> |
| @@ -752,9 +992,11 @@ | ||
| 752 | 992 | <div> |
| 753 | 993 | <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> |
| 754 | 994 | {__("Trigger Event")} |
| 755 | 995 | </label> |
| 756 | - {Boolean(templateData?.is_system) || isCoreSettingsEdit ? ( | |
| 996 | + {Boolean(templateData?.is_system) || | |
| 997 | + isCoreSettingsEdit || | |
| 998 | + isOverride ? ( | |
| 757 | 999 | <div className="space-y-2"> |
| 758 | 1000 | <div className="flex items-center gap-2"> |
| 759 | 1001 | <span className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800"> |
| 760 | 1002 | <Zap className="w-4 h-4" /> |
| @@ -760,9 +1002,14 @@ | ||
| 760 | 1002 | <Zap className="w-4 h-4" /> |
| 761 | 1003 | {formData.event_key || "-"} |
| 762 | 1004 | </span> |
| 763 | 1005 | <span className="text-xs text-gray-500 dark:text-gray-400"> |
| 764 | - {__("System templates cannot change events")} | |
| 1006 | + {isOverride | |
| 1007 | + ? __( | |
| 1008 | + "Same as the global template — an override can't change when it is sent, only what it says", | |
| 1009 | + "yatra", | |
| 1010 | + ) | |
| 1011 | + : __("System templates cannot change events")} | |
| 765 | 1012 | </span> |
| 766 | 1013 | </div> |
| 767 | 1014 | {formData.event_key && ( |
| 768 | 1015 | <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800"> |
| @@ -977,8 +1224,42 @@ | ||
| 977 | 1224 | </p> |
| 978 | 1225 | </div> |
| 979 | 1226 | </div> |
| 980 | 1227 | |
| 1228 | + {/* Category — lets the operator classify the template (e.g. | |
| 1229 | + Payment) instead of it always defaulting to Booking. The | |
| 1230 | + recipient (Customer/Admin) is derived server-side from the | |
| 1231 | + To address ({{admin_email}} => admin). Locked for core/ | |
| 1232 | + system templates whose category is fixed. */} | |
| 1233 | + {!isCoreSettingsEdit && ( | |
| 1234 | + <div className="mb-4"> | |
| 1235 | + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
| 1236 | + {__("Category")} | |
| 1237 | + </label> | |
| 1238 | + <select | |
| 1239 | + aria-label={__("Category")} | |
| 1240 | + value={formData.category || "booking"} | |
| 1241 | + onChange={(e) => | |
| 1242 | + setFormData({ ...formData, category: e.target.value }) | |
| 1243 | + } | |
| 1244 | + disabled={isCoreViewMode || isSystemTemplate} | |
| 1245 | + className="w-full px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 dark:text-white disabled:opacity-60" | |
| 1246 | + > | |
| 1247 | + <option value="booking">{__("Booking")}</option> | |
| 1248 | + <option value="payment">{__("Payment")}</option> | |
| 1249 | + <option value="reminder">{__("Reminder")}</option> | |
| 1250 | + <option value="enquiry">{__("Enquiry")}</option> | |
| 1251 | + <option value="account">{__("Account")}</option> | |
| 1252 | + <option value="marketing">{__("Marketing")}</option> | |
| 1253 | + </select> | |
| 1254 | + <p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> | |
| 1255 | + {__( | |
| 1256 | + "How this template is grouped in the list. Recipient (Customer/Admin) follows the To address.", | |
| 1257 | + )} | |
| 1258 | + </p> | |
| 1259 | + </div> | |
| 1260 | + )} | |
| 1261 | + | |
| 981 | 1262 | <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 982 | 1263 | <div> |
| 983 | 1264 | <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> |
| 984 | 1265 | {__("To Email")} |
| @@ -1131,8 +1412,117 @@ | ||
| 1131 | 1412 | </div> |
| 1132 | 1413 | </CardContent> |
| 1133 | 1414 | </Card> |
| 1134 | 1415 | |
| 1416 | + {/* One trip dropdown for the editor: preview, test AND "which | |
| 1417 | + template does this trip get". It is NEVER saved — the saved | |
| 1418 | + targeting is the Applies-to block. */} | |
| 1419 | + {!isCreateMode && | |
| 1420 | + !isCoreSettingsEdit && | |
| 1421 | + overridesSupported && | |
| 1422 | + (isOverride || isOverridableGlobal) && ( | |
| 1423 | + <Card> | |
| 1424 | + <CardHeader> | |
| 1425 | + <CardTitle className="text-base"> | |
| 1426 | + {__("Try it with a real trip", "yatra")} | |
| 1427 | + </CardTitle> | |
| 1428 | + </CardHeader> | |
| 1429 | + <CardContent className="space-y-2"> | |
| 1430 | + <div data-testid="preview-trip"> | |
| 1431 | + <SearchableSelect | |
| 1432 | + value={previewTrip ? String(previewTrip) : ""} | |
| 1433 | + onChange={(value) => | |
| 1434 | + setPreviewTrip(value ? Number(value) : null) | |
| 1435 | + } | |
| 1436 | + options={tripOptions.map((t) => ({ | |
| 1437 | + value: String(t.id), | |
| 1438 | + label: t.label, | |
| 1439 | + }))} | |
| 1440 | + placeholder={__("Sample data (no trip)", "yatra")} | |
| 1441 | + searchPlaceholder={__("Search trips…", "yatra")} | |
| 1442 | + /> | |
| 1443 | + </div> | |
| 1444 | + <p className="text-xs text-gray-500"> | |
| 1445 | + {__( | |
| 1446 | + "Not saved. Preview and Send Test Email use this trip's real name, link and sample booking data. Which trips actually get this template is set in “Applies to”.", | |
| 1447 | + "yatra", | |
| 1448 | + )} | |
| 1449 | + </p> | |
| 1450 | + {previewTrip && tripResolution && ( | |
| 1451 | + <div | |
| 1452 | + className="text-sm rounded-md border border-dashed border-gray-300 dark:border-gray-600 px-3 py-2" | |
| 1453 | + data-testid="check-trip-result" | |
| 1454 | + > | |
| 1455 | + <span className="text-gray-500"> | |
| 1456 | + {__("This trip's customers get:", "yatra")} | |
| 1457 | + </span>{" "} | |
| 1458 | + {(() => { | |
| 1459 | + const parentKey = isOverride | |
| 1460 | + ? overrideOf | |
| 1461 | + : String((templateData as any)?.template_key || ""); | |
| 1462 | + const hit = tripResolution.resolved[parentKey]; | |
| 1463 | + if (isOverride) { | |
| 1464 | + if (hit?.id === Number(id)) { | |
| 1465 | + return ( | |
| 1466 | + <span className="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"> | |
| 1467 | + {__("this override", "yatra")} | |
| 1468 | + {hit?.reason ? ` · ${hit.reason}` : ""} | |
| 1469 | + </span> | |
| 1470 | + ); | |
| 1471 | + } | |
| 1472 | + const noBodyYet = !String( | |
| 1473 | + (templateData as any)?.body || "", | |
| 1474 | + ).trim() | |
| 1475 | + ? " — " + | |
| 1476 | + __( | |
| 1477 | + "this override has no body yet, so it is skipped", | |
| 1478 | + "yatra", | |
| 1479 | + ) | |
| 1480 | + : ""; | |
| 1481 | + if (hit) { | |
| 1482 | + return ( | |
| 1483 | + <span className="text-amber-700 dark:text-amber-300"> | |
| 1484 | + {sprintf( | |
| 1485 | + __("“%1$s” (%2$s)", "yatra"), | |
| 1486 | + hit.name, | |
| 1487 | + hit.reason, | |
| 1488 | + )} | |
| 1489 | + {noBodyYet} | |
| 1490 | + </span> | |
| 1491 | + ); | |
| 1492 | + } | |
| 1493 | + return ( | |
| 1494 | + <span className="text-gray-700 dark:text-gray-300"> | |
| 1495 | + {sprintf( | |
| 1496 | + __("the global template (%s)", "yatra"), | |
| 1497 | + parentTemplate?.name || overrideOf, | |
| 1498 | + )} | |
| 1499 | + {noBodyYet} | |
| 1500 | + </span> | |
| 1501 | + ); | |
| 1502 | + } | |
| 1503 | + return hit ? ( | |
| 1504 | + <span className="text-amber-700 dark:text-amber-300"> | |
| 1505 | + {sprintf( | |
| 1506 | + __( | |
| 1507 | + "the override “%1$s” (%2$s), not this template", | |
| 1508 | + "yatra", | |
| 1509 | + ), | |
| 1510 | + hit.name, | |
| 1511 | + hit.reason, | |
| 1512 | + )} | |
| 1513 | + </span> | |
| 1514 | + ) : ( | |
| 1515 | + <span className="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"> | |
| 1516 | + {__("this template", "yatra")} | |
| 1517 | + </span> | |
| 1518 | + ); | |
| 1519 | + })()} | |
| 1520 | + </div> | |
| 1521 | + )} | |
| 1522 | + </CardContent> | |
| 1523 | + </Card> | |
| 1524 | + )} | |
| 1135 | 1525 | {/* Test Email - Only show in edit mode */} |
| 1136 | 1526 | {!isCreateMode && !isCoreSettingsEdit && ( |
| 1137 | 1527 | <Card> |
| 1138 | 1528 | <CardHeader> |
| @@ -1488,8 +1878,16 @@ | ||
| 1488 | 1878 | onAccept={({ subject, body }) => |
| 1489 | 1879 | setFormData({ ...formData, subject, body }) |
| 1490 | 1880 | } |
| 1491 | 1881 | /> |
| 1882 | + {isOverridableGlobal && templateData && ( | |
| 1883 | + <EmailOverrideCreateModal | |
| 1884 | + isOpen={overrideModalOpen} | |
| 1885 | + onClose={() => setOverrideModalOpen(false)} | |
| 1886 | + globals={[templateData as any]} | |
| 1887 | + parent={templateData as any} | |
| 1888 | + /> | |
| 1889 | + )} | |
| 1492 | 1890 | </div> |
| 1493 | 1891 | ); |
| 1494 | 1892 | }; |
| 1495 | 1893 | |