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

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