PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / resources / js / components / email / EmailOverrideCreateModal.tsx

EmailOverrideCreateModal.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.15, at resources/js/components/email/EmailOverrideCreateModal.tsx

264 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useMemo, useState } from "react";
2 import { useMutation, useQueryClient } from "@tanstack/react-query";
3 import { __, sprintf } from "../../lib/i18n";
4 import { Modal } from "../ui/modal";
5 import { Button } from "../ui/button";
6 import { Input } from "../ui/input";
7 import { Label } from "../ui/label";
8 import { SearchableSelect } from "../ui/searchable-select";
9 import { MultiSelect } from "../ui/multi-select";
10 import { useToast } from "../ui/toast";
11 import {
12 decodeTargets,
13 describeTargets,
14 encodeTargets,
15 hasTargets,
16 useTripTargets,
17 type TripTargets,
18 } from "../../hooks/useTripTargets";
19 import { createEmailTemplateOverride } from "../../api/email-automation-api";
20 import type { UnifiedEmailTemplate } from "../../lib/email-templates-catalog";
21
22 /**
23 * "Add an override" — the first step of creating a trip-specific version of a
24 * global email template (Pro Email Automation). Asks WHICH trips first so an
25 * override can never exist without a target, suggests a name from the
26 * selection, then opens the new override in the editor.
27 */
28 interface EmailOverrideCreateModalProps {
29 isOpen: boolean;
30 onClose: () => void;
31 /** Global templates the operator may override (overridable, not overrides). */
32 globals: UnifiedEmailTemplate[];
33 /** Preselected global template; when omitted the modal asks which one. */
34 parent?: UnifiedEmailTemplate | null;
35 }
36
37 export const EmailOverrideCreateModal: React.FC<
38 EmailOverrideCreateModalProps
39 > = ({ isOpen, onClose, globals, parent }) => {
40 const { showToast } = useToast();
41 const queryClient = useQueryClient();
42 const [parentId, setParentId] = useState<string>("");
43 const [targets, setTargets] = useState<TripTargets>({});
44 const [name, setName] = useState("");
45 const [nameTouched, setNameTouched] = useState(false);
46 const [copy, setCopy] = useState(true);
47 const { options } = useTripTargets(isOpen);
48
49 useEffect(() => {
50 if (!isOpen) return;
51 setParentId(parent ? String(parent.id) : String(globals[0]?.id ?? ""));
52 setTargets({});
53 setName("");
54 setNameTouched(false);
55 setCopy(true);
56 }, [isOpen, parent, globals]);
57
58 const selectedParent = useMemo(
59 () => globals.find((g) => String(g.id) === parentId) || parent || null,
60 [globals, parentId, parent],
61 );
62
63 const suggestedName = useMemo(() => {
64 if (!selectedParent) return "";
65 const where = describeTargets(targets, options);
66 return where ? `${selectedParent.name}${where}` : "";
67 }, [selectedParent, targets, options]);
68
69 const createMutation = useMutation({
70 mutationFn: async () => {
71 if (!selectedParent)
72 throw new Error(__("Pick a global template.", "yatra"));
73 const response: any = await createEmailTemplateOverride(
74 selectedParent.id,
75 {
76 targets,
77 name: (nameTouched ? name : suggestedName).trim(),
78 copy,
79 },
80 );
81 if (response && response.success === false) {
82 throw new Error(
83 response.message || __("Failed to create override.", "yatra"),
84 );
85 }
86 return response;
87 },
88 onSuccess: (response: any) => {
89 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
90 const newId = response?.data?.id;
91 showToast(
92 __("Override created — now edit the wording.", "yatra"),
93 "success",
94 );
95 onClose();
96 if (newId) {
97 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${newId}`;
98 }
99 },
100 onError: (error: any) => {
101 showToast(
102 error?.message || __("Failed to create override.", "yatra"),
103 "error",
104 );
105 },
106 });
107
108 const canSubmit =
109 !!selectedParent && hasTargets(targets) && !createMutation.isPending;
110
111 return (
112 <Modal
113 isOpen={isOpen}
114 onClose={onClose}
115 size="md"
116 title={
117 parent
118 ? sprintf(__("Add an override of “%s”", "yatra"), parent.name)
119 : __("Add an override", "yatra")
120 }
121 description={__(
122 "Same event, same merge tags, your own wording. Bookings on the trips you pick get this override; every other booking keeps the global template.",
123 "yatra",
124 )}
125 footer={
126 <div className="flex justify-end gap-2 w-full">
127 <Button type="button" variant="outline" onClick={onClose}>
128 {__("Cancel", "yatra")}
129 </Button>
130 <Button
131 type="button"
132 disabled={!canSubmit}
133 onClick={() => createMutation.mutate()}
134 data-testid="override-create-submit"
135 >
136 {createMutation.isPending
137 ? __("Creating…", "yatra")
138 : __("Create & edit →", "yatra")}
139 </Button>
140 </div>
141 }
142 >
143 <div className="space-y-4" data-testid="override-create-modal">
144 {!parent && (
145 <div>
146 <Label className="text-sm font-medium">
147 {__("Override which global template?", "yatra")}
148 </Label>
149 <div className="mt-1" data-testid="override-parent">
150 <SearchableSelect
151 value={parentId}
152 onChange={(value) => setParentId(value)}
153 options={globals.map((g) => ({
154 value: String(g.id),
155 label: `${g.name} · ${g.event_key}`,
156 }))}
157 placeholder={__("Pick a global template…", "yatra")}
158 searchPlaceholder={__("Search templates…", "yatra")}
159 />
160 </div>
161 </div>
162 )}
163
164 <div>
165 <Label className="text-sm font-medium">
166 {__("Use this override for", "yatra")}
167 </Label>
168 <MultiSelect
169 value={encodeTargets(targets)}
170 onChange={(vals) => setTargets(decodeTargets(vals))}
171 options={options}
172 placeholder={__("Search trips, categories, trip types…", "yatra")}
173 className="mt-1"
174 />
175 <p className="text-xs text-gray-500 mt-1">
176 {__(
177 "Trips, whole categories (sub-categories included) or a trip type. You can change this later.",
178 "yatra",
179 )}
180 </p>
181 </div>
182
183 <div>
184 <Label className="text-sm font-medium">{__("Name", "yatra")}</Label>
185 <Input
186 value={nameTouched ? name : suggestedName}
187 onChange={(e) => {
188 setNameTouched(true);
189 setName(e.target.value);
190 }}
191 placeholder={
192 selectedParent
193 ? `${selectedParent.name} — …`
194 : __("Name", "yatra")
195 }
196 className="mt-1"
197 data-testid="override-name"
198 />
199 <p className="text-xs text-gray-500 mt-1">
200 {__(
201 "Suggested from what you picked; shown in the list and in Email logs.",
202 "yatra",
203 )}
204 </p>
205 </div>
206
207 <div>
208 <Label className="text-sm font-medium">
209 {__("Start from", "yatra")}
210 </Label>
211 <div className="mt-1 space-y-2">
212 <label
213 className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer ${copy ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700"}`}
214 >
215 <input
216 type="radio"
217 name="override-start"
218 checked={copy}
219 onChange={() => setCopy(true)}
220 className="mt-1"
221 />
222 <span>
223 <span className="block text-sm font-medium">
224 {__("A copy of the global template", "yatra")}
225 </span>
226 <span className="block text-xs text-gray-500">
227 {__(
228 "Recommended — edit only the parts that differ.",
229 "yatra",
230 )}
231 </span>
232 </span>
233 </label>
234 <label
235 className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer ${!copy ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700"}`}
236 >
237 <input
238 type="radio"
239 name="override-start"
240 checked={!copy}
241 onChange={() => setCopy(false)}
242 className="mt-1"
243 />
244 <span>
245 <span className="block text-sm font-medium">
246 {__("A blank template", "yatra")}
247 </span>
248 <span className="block text-xs text-gray-500">
249 {__(
250 "Start from scratch with the same merge tags. It stays unused until it has a body.",
251 "yatra",
252 )}
253 </span>
254 </span>
255 </label>
256 </div>
257 </div>
258 </div>
259 </Modal>
260 );
261 };
262
263 export default EmailOverrideCreateModal;
264