PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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 / pages / EmailTemplateForm.tsx

EmailTemplateForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.7, at resources/js/pages/EmailTemplateForm.tsx

1,118 lines 44.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect, useMemo } from "react";
2 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3 import { __ } from "../lib/i18n";
4 import { useToast } from "../components/ui/toast";
5 import {
6 Card,
7 CardContent,
8 CardHeader,
9 CardTitle,
10 } from "../components/ui/card";
11 import { Button } from "../components/ui/button";
12 import { Input } from "../components/ui/input";
13 import { Badge } from "../components/ui/badge";
14 import {
15 Mail,
16 ArrowLeft,
17 Save,
18 Eye,
19 Send,
20 Loader2,
21 Code,
22 Copy,
23 Check,
24 Info,
25 Zap,
26 ChevronDown,
27 } from "lucide-react";
28 import { Switch } from "../components/ui/switch";
29 import {
30 getCoreTemplateDefinition,
31 isCoreTemplateSlug,
32 } from "../lib/email-templates-catalog";
33 import { isEmailAutomationModuleEnabled } from "../lib/plugin-utils";
34 import {
35 fetchSettings,
36 previewCoreEmailTemplate,
37 saveSettings,
38 } from "../api/settings-api";
39 import { EmailPreviewModal } from "../components/email/EmailPreviewModal";
40 import {
41 createEmailTemplate,
42 fetchEmailTemplate,
43 fetchEmailTemplateVariables,
44 previewEmailTemplate,
45 sendEmailTemplateTest,
46 updateEmailTemplate,
47 } from "../api/email-automation-api";
48
49 interface PreviewData {
50 subject: string;
51 body: string;
52 }
53
54 const EmailTemplateForm: React.FC = () => {
55 const { id, isCreateMode, coreTemplateSlug } = useMemo(() => {
56 const params = new URLSearchParams(window.location.search);
57 const templateId = params.get("id");
58 const action = params.get("action");
59 const core = params.get("core_template") || "";
60 return {
61 id: templateId,
62 isCreateMode: action === "create",
63 coreTemplateSlug: core,
64 };
65 }, []);
66
67 const isCoreSettingsEdit = isCoreTemplateSlug(coreTemplateSlug);
68 const coreDef = isCoreSettingsEdit
69 ? getCoreTemplateDefinition(coreTemplateSlug)
70 : undefined;
71
72 const goBack = () => {
73 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=templates`;
74 };
75 const queryClient = useQueryClient();
76 const { showToast } = useToast();
77
78 const [formData, setFormData] = useState({
79 name: "",
80 description: "",
81 from_name: "",
82 from_email: "",
83 to_email: "",
84 reply_to: "",
85 subject: "",
86 body: "",
87 event_key: "",
88 is_active: true,
89 });
90 const [showPreview, setShowPreview] = useState(false);
91 const [previewData, setPreviewData] = useState<PreviewData | null>(null);
92 const [testEmail, setTestEmail] = useState("");
93 const [copiedVar, setCopiedVar] = useState<string | null>(null);
94 const [showEventDropdown, setShowEventDropdown] = useState(false);
95
96 useEffect(() => {
97 if (isCreateMode && !isEmailAutomationModuleEnabled()) {
98 window.location.href =
99 "admin.php?page=yatra&subpage=email-automation&tab=templates";
100 }
101 }, [isCreateMode]);
102
103 useEffect(() => {
104 const params = new URLSearchParams(window.location.search);
105 if (params.get("action") !== "edit") return;
106 const ct = params.get("core_template");
107 if (ct && !isCoreTemplateSlug(ct)) {
108 window.location.href =
109 "admin.php?page=yatra&subpage=email-automation&tab=templates";
110 }
111 }, []);
112
113 const { data: settings, isLoading: settingsLoading } = useQuery({
114 queryKey: ["settings"],
115 queryFn: () => fetchSettings(),
116 enabled: isCoreSettingsEdit,
117 });
118
119 const { data: templateData, isLoading } = useQuery({
120 queryKey: ["email-template", id],
121 queryFn: () => fetchEmailTemplate(id as string),
122 enabled: Boolean(id) && !isCoreSettingsEdit,
123 });
124
125 const { data: variablesData } = useQuery({
126 queryKey: ["email-variables"],
127 queryFn: () => fetchEmailTemplateVariables(),
128 enabled: !isCoreSettingsEdit && isEmailAutomationModuleEnabled(),
129 });
130
131 // Get events from window.yatraAdmin (passed from PHP)
132 const events = (window as any).yatraAdmin?.emailEvents || [];
133
134 useEffect(() => {
135 if (!templateData || typeof templateData !== "object") return;
136 const t = templateData as Record<string, unknown>;
137 setFormData({
138 name: String(t.name ?? ""),
139 description: String(t.description ?? ""),
140 from_name: String(t.from_name ?? ""),
141 from_email: String(t.from_email ?? ""),
142 to_email: String(t.to_email ?? ""),
143 reply_to: String(t.reply_to ?? ""),
144 subject: String(t.subject ?? ""),
145 body: String(t.body ?? ""),
146 event_key: String(t.event_key ?? ""),
147 is_active: Boolean(t.is_active ?? true),
148 });
149 }, [templateData]);
150
151 useEffect(() => {
152 if (!isCoreSettingsEdit || !settings || !coreDef) return;
153 const flag = coreDef.settingsFlag;
154 const subj = coreDef.settingsSubject;
155 const bodyKey = coreDef.settingsBody;
156 if (!flag || !subj || !bodyKey) return;
157 const s = settings as Record<string, unknown>;
158 setFormData({
159 name: coreDef.name,
160 description: coreDef.description,
161 from_name: "",
162 from_email: "",
163 to_email: coreDef.to_email || "",
164 reply_to: "",
165 subject: String(s[subj] ?? ""),
166 body: String(s[bodyKey] ?? ""),
167 event_key: coreDef.event_key,
168 is_active: Boolean(s[flag]),
169 });
170 }, [isCoreSettingsEdit, settings, coreDef]);
171
172 const saveMutation = useMutation({
173 mutationFn: async (data: typeof formData) => {
174 if (isCreateMode) {
175 const response = await createEmailTemplate(data);
176 const r = response as {
177 success?: boolean;
178 message?: string;
179 data?: { id?: string | number };
180 };
181 if (r && r.success === false) {
182 throw new Error(r.message || __("Failed to create template"));
183 }
184 return response;
185 }
186 return await updateEmailTemplate(id as string, data);
187 },
188 onSuccess: (response) => {
189 queryClient.invalidateQueries({ queryKey: ["email-templates"] });
190 if (isCreateMode) {
191 showToast(__("Template created successfully"), "success");
192 // Redirect to edit page after creation - response.data contains the template
193 const newId = (response as { data?: { id?: string | number } })?.data
194 ?.id;
195 if (newId) {
196 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=template&action=edit&id=${newId}`;
197 } else {
198 // Fallback to list page
199 window.location.href =
200 "admin.php?page=yatra&subpage=email-automation&tab=templates";
201 }
202 } else {
203 queryClient.invalidateQueries({ queryKey: ["email-template", id] });
204 showToast(__("Template saved successfully"), "success");
205 }
206 },
207 onError: (error: any) => {
208 const message =
209 error?.message ||
210 (isCreateMode
211 ? __("Failed to create template")
212 : __("Failed to save template"));
213 showToast(message, "error");
214 },
215 });
216
217 const saveCoreMutation = useMutation({
218 mutationFn: async () => {
219 if (
220 !coreDef?.settingsFlag ||
221 !coreDef.settingsSubject ||
222 !coreDef.settingsBody
223 ) {
224 throw new Error(__("Invalid template", "yatra"));
225 }
226 if (!settings) {
227 throw new Error(__("Settings not loaded", "yatra"));
228 }
229 const base = { ...(settings as Record<string, unknown>) };
230 base[coreDef.settingsSubject] = formData.subject;
231 base[coreDef.settingsBody] = formData.body;
232 base[coreDef.settingsFlag] = formData.is_active;
233 await saveSettings(base);
234 return base;
235 },
236 onSuccess: (saved) => {
237 queryClient.setQueryData(["settings"], saved);
238 showToast(__("Template saved", "yatra"), "success");
239 },
240 onError: (error: unknown) => {
241 const message =
242 error instanceof Error ? error.message : __("Failed to save", "yatra");
243 showToast(message, "error");
244 },
245 });
246
247 const previewMutation = useMutation({
248 mutationFn: async () => {
249 return previewEmailTemplate(id as string);
250 },
251 onSuccess: (data) => {
252 setPreviewData(data);
253 setShowPreview(true);
254 },
255 onError: () => {
256 showToast(__("Failed to load preview"), "error");
257 },
258 });
259
260 const corePreviewMutation = useMutation({
261 mutationFn: async () => {
262 if (!coreTemplateSlug) {
263 throw new Error(__("Invalid template", "yatra"));
264 }
265 return previewCoreEmailTemplate({
266 template_key: coreTemplateSlug,
267 subject: formData.subject,
268 body: formData.body,
269 });
270 },
271 onSuccess: (data) => {
272 setPreviewData(data);
273 setShowPreview(true);
274 },
275 onError: (error: unknown) => {
276 const message =
277 error instanceof Error ? error.message : __("Preview failed", "yatra");
278 showToast(message, "error");
279 },
280 });
281
282 const testMutation = useMutation({
283 mutationFn: async (email: string) => {
284 return sendEmailTemplateTest(id as string, email);
285 },
286 onSuccess: () => {
287 showToast(__("Test email sent successfully"), "success");
288 setTestEmail("");
289 },
290 onError: () => {
291 showToast(__("Failed to send test email"), "error");
292 },
293 });
294
295 const handleSubmit = (e?: React.FormEvent | React.MouseEvent) => {
296 if (e) e.preventDefault();
297
298 if (isCoreSettingsEdit) {
299 saveCoreMutation.mutate();
300 return;
301 }
302
303 if (!formData.name.trim()) {
304 showToast(__("Template name is required"), "error");
305 return;
306 }
307 if (!formData.subject.trim()) {
308 showToast(__("Subject line is required"), "error");
309 return;
310 }
311
312 saveMutation.mutate(formData);
313 };
314
315 const copyVariable = (variable: string) => {
316 navigator.clipboard.writeText(`{{${variable}}}`);
317 setCopiedVar(variable);
318 setTimeout(() => setCopiedVar(null), 2000);
319 };
320
321 const insertVariable = (variable: string) => {
322 const textarea = document.getElementById(
323 "email-body",
324 ) as HTMLTextAreaElement;
325 if (textarea) {
326 const start = textarea.selectionStart;
327 const end = textarea.selectionEnd;
328 const text = formData.body;
329 const newText =
330 text.substring(0, start) + `{{${variable}}}` + text.substring(end);
331 setFormData({ ...formData, body: newText });
332 }
333 };
334
335 const showEditSkeleton =
336 !isCreateMode && (isCoreSettingsEdit ? settingsLoading : isLoading);
337
338 if (showEditSkeleton) {
339 return (
340 <div className="space-y-6">
341 {/* Header Skeleton */}
342 <div className="flex items-center justify-between animate-pulse">
343 <div className="flex items-center gap-4">
344 <div className="h-10 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
345 <div>
346 <div className="h-8 w-56 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
347 <div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded"></div>
348 </div>
349 </div>
350 <div className="flex items-center gap-3">
351 <div className="h-10 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
352 <div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
353 </div>
354 </div>
355
356 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
357 {/* Main Form Skeleton */}
358 <div className="lg:col-span-2 space-y-6">
359 {/* Template Details Card */}
360 <Card>
361 <CardHeader>
362 <div className="flex items-center justify-between">
363 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
364 <div className="h-6 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
365 </div>
366 </CardHeader>
367 <CardContent className="space-y-4">
368 <div className="space-y-2">
369 <div className="h-4 w-28 bg-gray-200 dark:bg-gray-700 rounded"></div>
370 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
371 </div>
372 <div className="space-y-2">
373 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
374 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
375 </div>
376 <div className="space-y-2">
377 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
378 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
379 </div>
380 <div className="space-y-2">
381 <div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
382 <div className="h-64 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
383 </div>
384 </CardContent>
385 </Card>
386
387 {/* Test Email Card */}
388 <Card>
389 <CardHeader>
390 <div className="h-6 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
391 </CardHeader>
392 <CardContent>
393 <div className="flex gap-3">
394 <div className="h-10 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
395 <div className="h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
396 </div>
397 </CardContent>
398 </Card>
399 </div>
400
401 {/* Sidebar Skeleton */}
402 <div className="space-y-6">
403 {/* Variables Card */}
404 <Card>
405 <CardHeader>
406 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
407 </CardHeader>
408 <CardContent className="space-y-4">
409 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
410 {[1, 2, 3].map((group) => (
411 <div key={group} className="space-y-2">
412 <div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
413 {[1, 2, 3].map((item) => (
414 <div
415 key={item}
416 className="h-12 w-full bg-gray-200 dark:bg-gray-700 rounded"
417 ></div>
418 ))}
419 </div>
420 ))}
421 </CardContent>
422 </Card>
423
424 {/* Template Info Card */}
425 <Card>
426 <CardHeader>
427 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded"></div>
428 </CardHeader>
429 <CardContent className="space-y-3">
430 {[1, 2, 3].map((i) => (
431 <div key={i} className="flex justify-between">
432 <div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
433 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
434 </div>
435 ))}
436 </CardContent>
437 </Card>
438 </div>
439 </div>
440 </div>
441 );
442 }
443
444 return (
445 <div className="space-y-6">
446 {/* Header */}
447 <div className="flex items-center justify-between">
448 <div className="flex items-center gap-4">
449 <Button variant="ghost" onClick={() => goBack()}>
450 <ArrowLeft className="w-4 h-4 mr-2" />
451 {__("Back")}
452 </Button>
453 <div>
454 <h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
455 <Mail className="w-6 h-6 text-blue-500" />
456 {isCreateMode
457 ? __("Create Email Template")
458 : __("Edit Email Template")}
459 </h1>
460 <p className="text-gray-500 dark:text-gray-400 mt-1">
461 {isCreateMode
462 ? __("Create a new custom email template")
463 : isCoreSettingsEdit
464 ? __(
465 "Core customer email (saved with site settings). From address is set under Email → Delivery.",
466 "yatra",
467 )
468 : String(templateData?.name ?? "")}
469 </p>
470 </div>
471 </div>
472 <div className="flex items-center gap-3">
473 {!isCreateMode && (
474 <Button
475 variant="outline"
476 onClick={() =>
477 isCoreSettingsEdit
478 ? corePreviewMutation.mutate()
479 : previewMutation.mutate()
480 }
481 disabled={
482 previewMutation.isPending || corePreviewMutation.isPending
483 }
484 >
485 {previewMutation.isPending || corePreviewMutation.isPending ? (
486 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
487 ) : (
488 <Eye className="w-4 h-4 mr-2" />
489 )}
490 {__("Preview")}
491 </Button>
492 )}
493 <Button
494 onClick={handleSubmit}
495 disabled={saveMutation.isPending || saveCoreMutation.isPending}
496 className="bg-blue-600 hover:bg-blue-700 text-white"
497 >
498 {saveMutation.isPending || saveCoreMutation.isPending ? (
499 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
500 ) : (
501 <Save className="w-4 h-4 mr-2" />
502 )}
503 {isCreateMode ? __("Create Template") : __("Save Template")}
504 </Button>
505 </div>
506 </div>
507
508 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
509 {/* Main Form */}
510 <div className="lg:col-span-2 space-y-6">
511 <Card>
512 <CardHeader>
513 <CardTitle className="flex items-center justify-between">
514 <span>{__("Template Details")}</span>
515 <div className="flex items-center gap-2">
516 <span
517 className={`text-sm ${formData.is_active ? "text-green-600 dark:text-green-400" : "text-gray-500"}`}
518 >
519 {formData.is_active ? __("Active") : __("Inactive")}
520 </span>
521 <Switch
522 checked={formData.is_active}
523 onCheckedChange={(checked) =>
524 setFormData({ ...formData, is_active: checked })
525 }
526 />
527 </div>
528 </CardTitle>
529 </CardHeader>
530 <CardContent className="space-y-4">
531 <div>
532 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
533 {__("Template Name")}
534 </label>
535 <Input
536 value={formData.name}
537 onChange={(e) =>
538 setFormData({ ...formData, name: e.target.value })
539 }
540 placeholder={__("Enter template name")}
541 readOnly={isCoreSettingsEdit}
542 className={
543 isCoreSettingsEdit ? "bg-gray-50 dark:bg-gray-900/40" : ""
544 }
545 />
546 </div>
547
548 <div>
549 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
550 {__("Description")}
551 </label>
552 <Input
553 value={formData.description}
554 onChange={(e) =>
555 setFormData({ ...formData, description: e.target.value })
556 }
557 placeholder={__(
558 "Brief description of when this email is sent",
559 )}
560 readOnly={isCoreSettingsEdit}
561 className={
562 isCoreSettingsEdit ? "bg-gray-50 dark:bg-gray-900/40" : ""
563 }
564 />
565 </div>
566
567 {/* Event Selector */}
568 <div>
569 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
570 {__("Trigger Event")}
571 </label>
572 {Boolean(templateData?.is_system) || isCoreSettingsEdit ? (
573 <div className="space-y-2">
574 <div className="flex items-center gap-2">
575 <span className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800">
576 <Zap className="w-4 h-4" />
577 {formData.event_key || "-"}
578 </span>
579 <span className="text-xs text-gray-500 dark:text-gray-400">
580 {__("System templates cannot change events")}
581 </span>
582 </div>
583 {formData.event_key && (
584 <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
585 <p className="text-sm text-indigo-700 dark:text-indigo-300">
586 {events.find((e: any) => e.key === formData.event_key)
587 ?.description || ""}
588 </p>
589 </div>
590 )}
591 </div>
592 ) : (
593 <div className="relative">
594 {/* Dropdown Trigger Button */}
595 <button
596 type="button"
597 onClick={() => setShowEventDropdown(!showEventDropdown)}
598 className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-left flex items-center justify-between hover:border-gray-400 dark:hover:border-gray-500 transition-colors"
599 >
600 {formData.event_key ? (
601 <div className="flex items-center gap-2">
602 <Zap className="w-4 h-4 text-indigo-500" />
603 <span className="text-sm font-medium text-gray-900 dark:text-white">
604 {events.find(
605 (e: any) => e.key === formData.event_key,
606 )?.name || formData.event_key}
607 </span>
608 </div>
609 ) : (
610 <div className="flex items-center gap-2">
611 <Mail className="w-4 h-4 text-gray-400" />
612 <span className="text-sm text-gray-500 dark:text-gray-400">
613 {__("No Event (Sequence Only)")}
614 </span>
615 </div>
616 )}
617 <ChevronDown
618 className={`w-4 h-4 text-gray-400 transition-transform ${showEventDropdown ? "rotate-180" : ""}`}
619 />
620 </button>
621
622 {/* Dropdown List */}
623 {showEventDropdown && (
624 <div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-80 overflow-y-auto">
625 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
626 <p className="text-xs text-gray-500 dark:text-gray-400 px-2">
627 {__(
628 "Select an event to trigger this email, or leave empty for sequence use",
629 )}
630 </p>
631 </div>
632 <div className="py-1">
633 {/* None option - for templates used in sequences */}
634 <button
635 type="button"
636 onClick={() => {
637 setFormData({ ...formData, event_key: "" });
638 setShowEventDropdown(false);
639 }}
640 className={`w-full px-3 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-700 ${
641 !formData.event_key
642 ? "bg-indigo-50 dark:bg-indigo-900/20"
643 : ""
644 }`}
645 >
646 <div className="flex items-start gap-2">
647 <Mail
648 className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
649 !formData.event_key
650 ? "text-indigo-500"
651 : "text-gray-400"
652 }`}
653 />
654 <div className="flex-1 min-w-0">
655 <div className="flex items-center gap-2">
656 <span
657 className={`text-sm font-medium ${
658 !formData.event_key
659 ? "text-indigo-700 dark:text-indigo-400"
660 : "text-gray-900 dark:text-white"
661 }`}
662 >
663 {__("No Event (Sequence Only)")}
664 </span>
665 </div>
666 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
667 {__(
668 "This template will only be used in email sequences",
669 )}
670 </p>
671 </div>
672 {!formData.event_key && (
673 <Check className="w-4 h-4 text-indigo-500 flex-shrink-0" />
674 )}
675 </div>
676 </button>
677 {events.map((event: any) => (
678 <button
679 key={event.key}
680 type="button"
681 onClick={() => {
682 setFormData({
683 ...formData,
684 event_key: event.key,
685 });
686 setShowEventDropdown(false);
687 }}
688 className={`w-full px-3 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-700 last:border-b-0 ${
689 formData.event_key === event.key
690 ? "bg-indigo-50 dark:bg-indigo-900/20"
691 : ""
692 }`}
693 >
694 <div className="flex items-start gap-2">
695 <Zap
696 className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
697 formData.event_key === event.key
698 ? "text-indigo-500"
699 : "text-gray-400"
700 }`}
701 />
702 <div className="flex-1 min-w-0">
703 <div className="flex items-center gap-2">
704 <span
705 className={`text-sm font-medium ${
706 formData.event_key === event.key
707 ? "text-indigo-700 dark:text-indigo-400"
708 : "text-gray-900 dark:text-white"
709 }`}
710 >
711 {event.name}
712 </span>
713 <code className="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded">
714 {event.key}
715 </code>
716 </div>
717 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
718 {event.description}
719 </p>
720 </div>
721 {formData.event_key === event.key && (
722 <Check className="w-4 h-4 text-indigo-500 flex-shrink-0" />
723 )}
724 </div>
725 </button>
726 ))}
727 </div>
728 </div>
729 )}
730
731 {/* Click outside to close */}
732 {showEventDropdown && (
733 <div
734 className="fixed inset-0 z-40"
735 onClick={() => setShowEventDropdown(false)}
736 />
737 )}
738 </div>
739 )}
740 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
741 {__(
742 "This email will be triggered when the selected event occurs",
743 )}
744 </p>
745 </div>
746
747 {!isCoreSettingsEdit && (
748 <>
749 {/* From/Reply-To Settings */}
750 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
751 <div>
752 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
753 {__("From Name")}
754 </label>
755 <Input
756 value={formData.from_name}
757 onChange={(e) =>
758 setFormData({
759 ...formData,
760 from_name: e.target.value,
761 })
762 }
763 placeholder={__("e.g., Travel Agency Name")}
764 />
765 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
766 {__("Leave empty to use site default")}
767 </p>
768 </div>
769 <div>
770 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
771 {__("From Email")}
772 </label>
773 <Input
774 type="email"
775 value={formData.from_email}
776 onChange={(e) =>
777 setFormData({
778 ...formData,
779 from_email: e.target.value,
780 })
781 }
782 placeholder={__("e.g., [email protected]")}
783 />
784 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
785 {__("Leave empty to use site default")}
786 </p>
787 </div>
788 </div>
789
790 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
791 <div>
792 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
793 {__("To Email")}
794 </label>
795 <Input
796 type="email"
797 value={formData.to_email}
798 onChange={(e) =>
799 setFormData({ ...formData, to_email: e.target.value })
800 }
801 placeholder={__("e.g., {{customer_email}}")}
802 />
803 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
804 {__(
805 "Recipient email. Use variables or leave empty for default",
806 )}
807 </p>
808 </div>
809 <div>
810 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
811 {__("Reply-To Email")}
812 </label>
813 <Input
814 type="email"
815 value={formData.reply_to}
816 onChange={(e) =>
817 setFormData({ ...formData, reply_to: e.target.value })
818 }
819 placeholder={__("e.g., [email protected]")}
820 />
821 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
822 {__(
823 "Where replies will be sent. Leave empty to use From Email",
824 )}
825 </p>
826 </div>
827 </div>
828 </>
829 )}
830
831 {isCoreSettingsEdit && (
832 <p className="text-sm text-gray-600 dark:text-gray-400 rounded-lg border border-gray-200 dark:border-gray-600 p-3 bg-gray-50 dark:bg-gray-800/50">
833 {__(
834 "From / reply addresses are configured under Email → Delivery. Recipient uses the customer on the booking.",
835 "yatra",
836 )}
837 </p>
838 )}
839
840 <div>
841 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
842 {__("Subject Line")}
843 </label>
844 <Input
845 value={formData.subject}
846 onChange={(e) =>
847 setFormData({ ...formData, subject: e.target.value })
848 }
849 placeholder={__("Email subject with {{variables}}")}
850 className="font-mono text-sm"
851 />
852 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
853 {__("Use {{variable}} syntax to insert dynamic content")}
854 </p>
855 </div>
856
857 <div>
858 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
859 {__("Email Body (HTML)")}
860 </label>
861 <textarea
862 id="email-body"
863 value={formData.body}
864 onChange={(e) =>
865 setFormData({ ...formData, body: e.target.value })
866 }
867 rows={20}
868 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-mono text-sm"
869 placeholder={__("HTML email content...")}
870 />
871 </div>
872 </CardContent>
873 </Card>
874
875 {/* Test Email - Only show in edit mode */}
876 {!isCreateMode && !isCoreSettingsEdit && (
877 <Card>
878 <CardHeader>
879 <CardTitle className="flex items-center gap-2">
880 <Send className="w-5 h-5 text-blue-500" />
881 {__("Send Test Email")}
882 </CardTitle>
883 </CardHeader>
884 <CardContent>
885 <div className="flex gap-3">
886 <Input
887 type="email"
888 value={testEmail}
889 onChange={(e) => setTestEmail(e.target.value)}
890 placeholder={__("Enter email address")}
891 className="flex-1"
892 />
893 <Button
894 onClick={() => testMutation.mutate(testEmail)}
895 disabled={!testEmail || testMutation.isPending}
896 >
897 {testMutation.isPending ? (
898 <Loader2 className="w-4 h-4 animate-spin" />
899 ) : (
900 <Send className="w-4 h-4" />
901 )}
902 </Button>
903 </div>
904 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
905 {__(
906 "Send a test email with sample data to preview how it looks",
907 )}
908 </p>
909 </CardContent>
910 </Card>
911 )}
912 </div>
913
914 {/* Sidebar - Variables */}
915 <div className="space-y-6">
916 <Card>
917 <CardHeader>
918 <CardTitle className="flex items-center gap-2">
919 <Code className="w-5 h-5 text-purple-500" />
920 {__("Available Variables")}
921 </CardTitle>
922 </CardHeader>
923 <CardContent className="space-y-4">
924 {!isCoreSettingsEdit && (
925 <p className="text-sm text-gray-500 dark:text-gray-400">
926 {__("Click to copy, double-click to insert at cursor")}
927 </p>
928 )}
929
930 {isCoreSettingsEdit && coreDef?.mergeTags ? (
931 <div className="space-y-2">
932 <p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
933 {__("Merge tags", "yatra")}
934 </p>
935 <p className="text-xs text-gray-500 dark:text-gray-400">
936 {__(
937 "Click a tag to copy; paste into subject or HTML body.",
938 "yatra",
939 )}
940 </p>
941 <div className="flex flex-wrap gap-1">
942 {coreDef.mergeTags.split(",").map((raw) => {
943 const tag = raw.trim();
944 const inner = tag.replace(/\{\{|\}\}/g, "").trim();
945 if (!inner) return null;
946 return (
947 <button
948 key={tag}
949 type="button"
950 onClick={() => copyVariable(inner)}
951 className="rounded border border-gray-200 dark:border-gray-600 px-2 py-1 text-xs font-mono text-blue-600 dark:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800"
952 >
953 {tag}
954 </button>
955 );
956 })}
957 </div>
958 </div>
959 ) : (
960 variablesData &&
961 Object.entries(variablesData).map(
962 ([category, variables]: [string, any]) => (
963 <div key={category}>
964 <h4 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">
965 {category}
966 </h4>
967 <div className="space-y-1">
968 {variables.map((v: any) => (
969 <button
970 key={v.key}
971 type="button"
972 onClick={() => copyVariable(v.key)}
973 onDoubleClick={() => insertVariable(v.key)}
974 className="w-full text-left px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors group"
975 >
976 <div className="flex items-center justify-between">
977 <code className="text-xs text-blue-600 dark:text-blue-400">
978 {`{{${v.key}}}`}
979 </code>
980 {copiedVar === v.key ? (
981 <Check className="w-3 h-3 text-green-500" />
982 ) : (
983 <Copy className="w-3 h-3 text-gray-400 opacity-0 group-hover:opacity-100" />
984 )}
985 </div>
986 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
987 {v.description}
988 </p>
989 </button>
990 ))}
991 </div>
992 </div>
993 ),
994 )
995 )}
996 </CardContent>
997 </Card>
998
999 {/* Template Info - Different content for create vs edit */}
1000 <Card>
1001 <CardHeader>
1002 <CardTitle className="flex items-center gap-2">
1003 <Info className="w-5 h-5 text-gray-500" />
1004 {isCreateMode ? __("New Template") : __("Template Info")}
1005 </CardTitle>
1006 </CardHeader>
1007 <CardContent className="space-y-3 text-sm">
1008 <div className="space-y-2">
1009 <div className="flex justify-between items-center">
1010 <span className="text-gray-500 dark:text-gray-400">
1011 {__("Event")}
1012 </span>
1013 <span 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">
1014 <Zap className="w-3 h-3" />
1015 {formData.event_key || "-"}
1016 </span>
1017 </div>
1018 {formData.event_key && (
1019 <p className="text-xs text-gray-500 dark:text-gray-400 italic">
1020 {events.find((e: any) => e.key === formData.event_key)
1021 ?.description || ""}
1022 </p>
1023 )}
1024 </div>
1025 {isCreateMode ? (
1026 <>
1027 <div className="flex justify-between">
1028 <span className="text-gray-500 dark:text-gray-400">
1029 {__("Type")}
1030 </span>
1031 <span className="text-gray-900 dark:text-white">
1032 {__("Custom")}
1033 </span>
1034 </div>
1035 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
1036 <p className="text-xs text-blue-700 dark:text-blue-300">
1037 {__(
1038 "Custom templates can be edited and deleted. Select an event to trigger this email automatically.",
1039 )}
1040 </p>
1041 </div>
1042 </>
1043 ) : (
1044 <>
1045 <div className="flex justify-between">
1046 <span className="text-gray-500 dark:text-gray-400">
1047 {__("Key")}
1048 </span>
1049 <code className="text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
1050 {isCoreSettingsEdit
1051 ? coreDef?.template_key
1052 : String(templateData?.template_key ?? "")}
1053 </code>
1054 </div>
1055 <div className="flex justify-between">
1056 <span className="text-gray-500 dark:text-gray-400">
1057 {__("Category")}
1058 </span>
1059 <Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
1060 {isCoreSettingsEdit
1061 ? coreDef?.category
1062 : String(templateData?.category ?? "")}
1063 </Badge>
1064 </div>
1065 <div className="flex justify-between">
1066 <span className="text-gray-500 dark:text-gray-400">
1067 {__("Recipient")}
1068 </span>
1069 <span
1070 className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
1071 (isCoreSettingsEdit
1072 ? coreDef?.recipient_type
1073 : String(templateData?.recipient_type ?? "")) ===
1074 "admin"
1075 ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
1076 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
1077 }`}
1078 >
1079 {(isCoreSettingsEdit
1080 ? coreDef?.recipient_type
1081 : String(templateData?.recipient_type ?? "")) ===
1082 "admin"
1083 ? __("Admin")
1084 : __("Customer")}
1085 </span>
1086 </div>
1087 <div className="flex justify-between">
1088 <span className="text-gray-500 dark:text-gray-400">
1089 {__("Type")}
1090 </span>
1091 <span className="text-gray-900 dark:text-white">
1092 {isCoreSettingsEdit || Boolean(templateData?.is_system)
1093 ? __("System")
1094 : __("Custom")}
1095 </span>
1096 </div>
1097 </>
1098 )}
1099 </CardContent>
1100 </Card>
1101 </div>
1102 </div>
1103
1104 <EmailPreviewModal
1105 open={showPreview && Boolean(previewData)}
1106 onClose={() => {
1107 setShowPreview(false);
1108 setPreviewData(null);
1109 }}
1110 subject={previewData?.subject ?? ""}
1111 body={previewData?.body ?? ""}
1112 />
1113 </div>
1114 );
1115 };
1116
1117 export default EmailTemplateForm;
1118