PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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.4, at resources/js/pages/EmailTemplateForm.tsx

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