PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / pages / EmailSequenceForm.tsx

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

927 lines 35.8 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 {
5 createEmailSequence,
6 fetchEmailSequence,
7 fetchEmailTemplates,
8 updateEmailSequence,
9 } from "../api/email-automation-api";
10 import { useToast } from "../components/ui/toast";
11 import {
12 Card,
13 CardContent,
14 CardHeader,
15 CardTitle,
16 } from "../components/ui/card";
17 import { Button } from "../components/ui/button";
18 import { Input } from "../components/ui/input";
19 import { Badge } from "../components/ui/badge";
20 import { Select } from "../components/ui/select";
21 import { Switch } from "../components/ui/switch";
22 import {
23 GitBranch,
24 ArrowLeft,
25 Save,
26 Loader2,
27 Plus,
28 Trash2,
29 Zap,
30 GripVertical,
31 Info,
32 ChevronDown,
33 Mail,
34 Check,
35 } from "lucide-react";
36
37 interface EmailTemplate {
38 id: number;
39 name: string;
40 template_key: string;
41 category: string;
42 description?: string;
43 event_key?: string;
44 recipient_type?: string;
45 }
46
47 interface SequenceStep {
48 id?: number;
49 step_order: number;
50 delay_value: number;
51 delay_unit: "minutes" | "hours" | "days" | "weeks";
52 template_id: number | null;
53 template_name?: string;
54 custom_subject: string;
55 custom_body: string;
56 conditions: any[];
57 is_active: boolean;
58 }
59
60 interface SequenceData {
61 id?: number;
62 name: string;
63 description: string;
64 trigger_type: string;
65 trigger_config: any;
66 status: "active" | "paused" | "draft";
67 applicable_to: string;
68 trip_ids: number[];
69 steps?: SequenceStep[];
70 }
71
72 const delayUnits = [
73 { value: "minutes", label: __("Minutes") },
74 { value: "hours", label: __("Hours") },
75 { value: "days", label: __("Days") },
76 { value: "weeks", label: __("Weeks") },
77 ];
78
79 const EmailSequenceForm: React.FC = () => {
80 const id = useMemo(() => {
81 const params = new URLSearchParams(window.location.search);
82 return params.get("id");
83 }, []);
84
85 const isEditing = !!id;
86
87 const goBack = () => {
88 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=sequences`;
89 };
90
91 const queryClient = useQueryClient();
92 const { showToast } = useToast();
93
94 const [formData, setFormData] = useState<SequenceData>({
95 name: "",
96 description: "",
97 trigger_type: "booking.created",
98 trigger_config: {},
99 status: "draft",
100 applicable_to: "all",
101 trip_ids: [],
102 });
103
104 const [steps, setSteps] = useState<SequenceStep[]>([
105 {
106 step_order: 0,
107 delay_value: 0,
108 delay_unit: "minutes",
109 template_id: null,
110 custom_subject: "",
111 custom_body: "",
112 conditions: [],
113 is_active: true,
114 },
115 ]);
116
117 // State for template dropdown per step
118 const [openTemplateDropdown, setOpenTemplateDropdown] = useState<
119 number | null
120 >(null);
121
122 // Get events from window.yatraAdmin (passed from PHP)
123 // eslint-disable-next-line react-hooks/exhaustive-deps
124 const events = (window as any).yatraAdmin?.emailEvents || [];
125
126 // Fetch sequence data if editing
127 const { data: sequenceData, isLoading: isLoadingSequence } = useQuery({
128 queryKey: ["email-sequence", id],
129 queryFn: () => fetchEmailSequence(id as string),
130 enabled: isEditing,
131 });
132
133 // Fetch templates for step selection
134 const { data: templatesData } = useQuery({
135 queryKey: ["email-templates"],
136 queryFn: () => fetchEmailTemplates(),
137 });
138
139 // eslint-disable-next-line react-hooks/exhaustive-deps
140 const templates: EmailTemplate[] = Array.isArray(templatesData)
141 ? (templatesData as EmailTemplate[])
142 : [];
143
144 // Get all trigger events from Schema (passed via window.yatraAdmin.emailEvents)
145 const allTriggerEvents = useMemo(() => {
146 return events.map((e: any) => ({
147 key: e.key,
148 name: e.name,
149 description: e.description,
150 category: e.category,
151 }));
152 }, [events]);
153
154 // State for trigger dropdown
155 const [openTriggerDropdown, setOpenTriggerDropdown] = useState(false);
156
157 // Sequences send to the customer; exclude admin-only templates. Event-bound templates may still be reused as step content.
158 const sequenceTemplates = useMemo(() => {
159 return templates.filter((t) => t.recipient_type !== "admin");
160 }, [templates]);
161
162 // Populate form when editing
163 useEffect(() => {
164 if (!sequenceData || typeof sequenceData !== "object") {
165 return;
166 }
167 const loaded = sequenceData as SequenceData;
168 setFormData({
169 name: loaded.name || "",
170 description: loaded.description || "",
171 trigger_type: loaded.trigger_type || "booking.created",
172 trigger_config: loaded.trigger_config || {},
173 status: loaded.status || "draft",
174 applicable_to: loaded.applicable_to || "all",
175 trip_ids: loaded.trip_ids || [],
176 });
177
178 if (loaded.steps && loaded.steps.length > 0) {
179 setSteps(loaded.steps);
180 }
181 }, [sequenceData]);
182
183 // Create mutation
184 const createMutation = useMutation({
185 mutationFn: async (data: any) => {
186 return await createEmailSequence(data);
187 },
188 onSuccess: () => {
189 queryClient.invalidateQueries({ queryKey: ["email-sequences"] });
190 showToast(__("Sequence created successfully"), "success");
191 goBack();
192 },
193 onError: () => {
194 showToast(__("Failed to create sequence"), "error");
195 },
196 });
197
198 // Update mutation
199 const updateMutation = useMutation({
200 mutationFn: async (data: any) => {
201 return await updateEmailSequence(id as string, data);
202 },
203 onSuccess: () => {
204 queryClient.invalidateQueries({ queryKey: ["email-sequences"] });
205 queryClient.invalidateQueries({ queryKey: ["email-sequence", id] });
206 showToast(__("Sequence updated successfully"), "success");
207 },
208 onError: () => {
209 showToast(__("Failed to update sequence"), "error");
210 },
211 });
212
213 const handleSubmit = (e: React.FormEvent) => {
214 e.preventDefault();
215
216 const payload = {
217 ...formData,
218 steps: steps.map((step, index) => ({
219 ...step,
220 step_order: index,
221 })),
222 };
223
224 if (isEditing) {
225 updateMutation.mutate(payload);
226 } else {
227 createMutation.mutate(payload);
228 }
229 };
230
231 const addStep = () => {
232 setSteps([
233 ...steps,
234 {
235 step_order: steps.length,
236 delay_value: 1,
237 delay_unit: "days",
238 template_id: null,
239 custom_subject: "",
240 custom_body: "",
241 conditions: [],
242 is_active: true,
243 },
244 ]);
245 };
246
247 const removeStep = (index: number) => {
248 if (steps.length > 1) {
249 setSteps(steps.filter((_, i) => i !== index));
250 }
251 };
252
253 const updateStep = (index: number, field: string, value: any) => {
254 const newSteps = [...steps];
255 (newSteps[index] as any)[field] = value;
256 setSteps(newSteps);
257 };
258
259 const selectedTrigger = allTriggerEvents.find(
260 (t: any) => t.key === formData.trigger_type,
261 );
262 const isPending = createMutation.isPending || updateMutation.isPending;
263
264 if (isEditing && isLoadingSequence) {
265 return (
266 <div className="space-y-6">
267 {/* Header Skeleton */}
268 <div className="flex items-center justify-between animate-pulse">
269 <div className="flex items-center gap-4">
270 <div className="h-10 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
271 <div>
272 <div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
273 <div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
274 </div>
275 </div>
276 <div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
277 </div>
278
279 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
280 {/* Main Form Skeleton */}
281 <div className="lg:col-span-2 space-y-6">
282 {/* Basic Info Card */}
283 <Card>
284 <CardHeader>
285 <div className="h-6 w-40 bg-gray-200 dark:bg-gray-700 rounded"></div>
286 </CardHeader>
287 <CardContent className="space-y-4">
288 <div className="space-y-2">
289 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
290 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
291 </div>
292 <div className="space-y-2">
293 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
294 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
295 </div>
296 </CardContent>
297 </Card>
298
299 {/* Trigger Card */}
300 <Card>
301 <CardHeader>
302 <div className="h-6 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
303 </CardHeader>
304 <CardContent>
305 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
306 {[1, 2, 3, 4, 5, 6].map((i) => (
307 <div
308 key={i}
309 className="h-24 bg-gray-200 dark:bg-gray-700 rounded-lg"
310 ></div>
311 ))}
312 </div>
313 </CardContent>
314 </Card>
315
316 {/* Steps Card */}
317 <Card>
318 <CardHeader>
319 <div className="flex items-center justify-between">
320 <div className="h-6 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
321 <div className="h-8 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
322 </div>
323 </CardHeader>
324 <CardContent>
325 <div className="space-y-4">
326 {[1, 2].map((i) => (
327 <div
328 key={i}
329 className="border border-gray-200 dark:border-gray-700 rounded-lg p-4"
330 >
331 <div className="h-6 w-20 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
332 <div className="grid grid-cols-2 gap-4">
333 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
334 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
335 </div>
336 </div>
337 ))}
338 </div>
339 </CardContent>
340 </Card>
341 </div>
342
343 {/* Sidebar Skeleton */}
344 <div className="space-y-6">
345 <Card>
346 <CardHeader>
347 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
348 </CardHeader>
349 <CardContent className="space-y-3">
350 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
351 <div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-700 rounded"></div>
352 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded"></div>
353 </CardContent>
354 </Card>
355
356 <Card>
357 <CardHeader>
358 <div className="h-6 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
359 </CardHeader>
360 <CardContent className="space-y-3">
361 {[1, 2, 3].map((i) => (
362 <div key={i} className="flex justify-between">
363 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
364 <div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
365 </div>
366 ))}
367 </CardContent>
368 </Card>
369 </div>
370 </div>
371 </div>
372 );
373 }
374
375 return (
376 <div className="space-y-6">
377 {/* Header */}
378 <div className="flex items-center justify-between">
379 <div className="flex items-center gap-4">
380 <Button variant="ghost" onClick={goBack}>
381 <ArrowLeft className="w-4 h-4 mr-2" />
382 {__("Back")}
383 </Button>
384 <div>
385 <h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
386 <GitBranch className="w-6 h-6 text-blue-500" />
387 {isEditing ? __("Edit Sequence") : __("Create Sequence")}
388 </h1>
389 {isEditing && formData.name && (
390 <p className="text-gray-500 dark:text-gray-400 mt-1">
391 {formData.name}
392 </p>
393 )}
394 </div>
395 </div>
396 <div className="flex items-center gap-3">
397 <Button
398 onClick={handleSubmit}
399 disabled={isPending || !formData.name}
400 className="bg-blue-600 hover:bg-blue-700 text-white"
401 >
402 {isPending ? (
403 <Loader2 className="w-4 h-4 mr-2 animate-spin" />
404 ) : (
405 <Save className="w-4 h-4 mr-2" />
406 )}
407 {isEditing ? __("Save Changes") : __("Create Sequence")}
408 </Button>
409 </div>
410 </div>
411
412 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
413 {/* Main Form */}
414 <div className="lg:col-span-2 space-y-6">
415 {/* Basic Info */}
416 <Card>
417 <CardHeader>
418 <CardTitle className="flex items-center justify-between">
419 <span>{__("Sequence Details")}</span>
420 <div className="flex items-center gap-3">
421 <Select
422 value={formData.status}
423 onChange={(e) =>
424 setFormData({
425 ...formData,
426 status: e.target.value as any,
427 })
428 }
429 className="w-32"
430 >
431 <option value="draft">{__("Draft")}</option>
432 <option value="active">{__("Active")}</option>
433 <option value="paused">{__("Paused")}</option>
434 </Select>
435 </div>
436 </CardTitle>
437 </CardHeader>
438 <CardContent className="space-y-4">
439 <div>
440 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
441 {__("Sequence Name")} <span className="text-red-500">*</span>
442 </label>
443 <Input
444 value={formData.name}
445 onChange={(e) =>
446 setFormData({ ...formData, name: e.target.value })
447 }
448 placeholder={__("e.g., Post-Booking Welcome Series")}
449 required
450 />
451 </div>
452
453 <div>
454 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
455 {__("Description")}
456 </label>
457 <Input
458 value={formData.description}
459 onChange={(e) =>
460 setFormData({ ...formData, description: e.target.value })
461 }
462 placeholder={__(
463 "Brief description of what this sequence does",
464 )}
465 />
466 </div>
467 </CardContent>
468 </Card>
469
470 {/* Trigger */}
471 <Card>
472 <CardHeader>
473 <CardTitle className="flex items-center gap-2">
474 <Zap className="w-5 h-5 text-yellow-500" />
475 {__("Trigger")}
476 </CardTitle>
477 </CardHeader>
478 <CardContent className="space-y-4">
479 <div className="relative">
480 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
481 {__("When should this sequence start?")}
482 </label>
483
484 {/* Dropdown Trigger Button */}
485 <button
486 type="button"
487 onClick={() => setOpenTriggerDropdown(!openTriggerDropdown)}
488 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"
489 >
490 {selectedTrigger ? (
491 <div className="flex items-center gap-2">
492 <Zap className="w-4 h-4 text-blue-500" />
493 <span className="text-sm font-medium text-gray-900 dark:text-white">
494 {selectedTrigger.name}
495 </span>
496 </div>
497 ) : (
498 <span className="text-sm text-gray-500 dark:text-gray-400">
499 {__("Select a trigger event...")}
500 </span>
501 )}
502 <ChevronDown
503 className={`w-4 h-4 text-gray-400 transition-transform ${openTriggerDropdown ? "rotate-180" : ""}`}
504 />
505 </button>
506
507 {/* Dropdown List - Same style as Email Template form */}
508 {openTriggerDropdown && (
509 <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">
510 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
511 <p className="text-xs text-gray-500 dark:text-gray-400 px-2">
512 {__("Select an event to trigger this sequence")}
513 </p>
514 </div>
515 <div className="py-1">
516 {allTriggerEvents.map((trigger: any) => {
517 const isSelected =
518 formData.trigger_type === trigger.key;
519 return (
520 <button
521 key={trigger.key}
522 type="button"
523 onClick={() => {
524 setFormData({
525 ...formData,
526 trigger_type: trigger.key,
527 });
528 setOpenTriggerDropdown(false);
529 }}
530 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 ${
531 isSelected ? "bg-blue-50 dark:bg-blue-900/20" : ""
532 }`}
533 >
534 <div className="flex items-start gap-2">
535 <Zap
536 className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
537 isSelected ? "text-blue-500" : "text-gray-400"
538 }`}
539 />
540 <div className="flex-1 min-w-0">
541 <div className="flex items-center gap-2">
542 <span
543 className={`text-sm font-medium ${
544 isSelected
545 ? "text-blue-700 dark:text-blue-400"
546 : "text-gray-900 dark:text-white"
547 }`}
548 >
549 {trigger.name}
550 </span>
551 <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">
552 {trigger.key}
553 </code>
554 {isSelected && (
555 <Check className="w-4 h-4 text-blue-500" />
556 )}
557 </div>
558 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
559 {trigger.description}
560 </p>
561 </div>
562 </div>
563 </button>
564 );
565 })}
566 </div>
567 </div>
568 )}
569
570 {/* Click outside to close */}
571 {openTriggerDropdown && (
572 <div
573 className="fixed inset-0 z-40"
574 onClick={() => setOpenTriggerDropdown(false)}
575 />
576 )}
577 </div>
578
579 {/* Days input for schedule events */}
580 {(formData.trigger_type === "schedule.days_before_trip" ||
581 formData.trigger_type === "schedule.days_after_trip") && (
582 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
583 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
584 {__("Number of Days")}
585 </label>
586 <Input
587 type="number"
588 min="1"
589 value={formData.trigger_config.days || 7}
590 onChange={(e) =>
591 setFormData({
592 ...formData,
593 trigger_config: {
594 ...formData.trigger_config,
595 days: parseInt(e.target.value),
596 },
597 })
598 }
599 className="w-32"
600 />
601 </div>
602 )}
603 </CardContent>
604 </Card>
605
606 {/* Steps */}
607 <Card>
608 <CardHeader>
609 <CardTitle className="flex items-center justify-between">
610 <div className="flex items-center gap-2">
611 <GitBranch className="w-5 h-5 text-purple-500" />
612 {__("Sequence Steps")}
613 </div>
614 <Button
615 type="button"
616 variant="outline"
617 size="sm"
618 onClick={addStep}
619 >
620 <Plus className="w-4 h-4 mr-1" />
621 {__("Add Step")}
622 </Button>
623 </CardTitle>
624 </CardHeader>
625 <CardContent>
626 <div className="space-y-4">
627 {steps.map((step, index) => (
628 <div
629 key={index}
630 className="border border-gray-200 dark:border-gray-700 rounded-lg"
631 >
632 {/* Step Header */}
633 <div className="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
634 <div className="flex items-center gap-3">
635 <GripVertical className="w-4 h-4 text-gray-400" />
636 <span className="font-medium text-gray-900 dark:text-white">
637 {__("Step")} {index + 1}
638 </span>
639 {index === 0 && (
640 <Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
641 {__("First Email")}
642 </Badge>
643 )}
644 </div>
645 <div className="flex items-center gap-2">
646 <Switch
647 checked={step.is_active}
648 onCheckedChange={(checked) =>
649 updateStep(index, "is_active", checked)
650 }
651 />
652 {steps.length > 1 && (
653 <Button
654 type="button"
655 variant="ghost"
656 size="sm"
657 onClick={() => removeStep(index)}
658 className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
659 >
660 <Trash2 className="w-4 h-4" />
661 </Button>
662 )}
663 </div>
664 </div>
665
666 {/* Step Content */}
667 <div className="p-4 space-y-4">
668 {/* Delay */}
669 <div>
670 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
671 {index === 0
672 ? __("Send after trigger")
673 : __("Wait before sending")}
674 </label>
675 <div className="flex items-center gap-2">
676 <Input
677 type="number"
678 min="0"
679 value={step.delay_value}
680 onChange={(e) =>
681 updateStep(
682 index,
683 "delay_value",
684 parseInt(e.target.value) || 0,
685 )
686 }
687 className="w-24"
688 />
689 <Select
690 value={step.delay_unit}
691 onChange={(e) =>
692 updateStep(index, "delay_unit", e.target.value)
693 }
694 className="w-32"
695 >
696 {delayUnits.map((unit) => (
697 <option key={unit.value} value={unit.value}>
698 {unit.label}
699 </option>
700 ))}
701 </Select>
702 {index === 0 && step.delay_value === 0 && (
703 <span className="text-sm text-gray-500 dark:text-gray-400">
704 ({__("Immediately")})
705 </span>
706 )}
707 </div>
708 </div>
709
710 {/* Template Selection - Card Style Dropdown */}
711 <div className="relative">
712 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
713 {__("Email Template")}
714 </label>
715
716 {/* Dropdown Trigger */}
717 <button
718 type="button"
719 onClick={() =>
720 setOpenTemplateDropdown(
721 openTemplateDropdown === index ? null : index,
722 )
723 }
724 className="w-full flex items-center justify-between px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-left hover:border-blue-400 dark:hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
725 >
726 {step.template_id ? (
727 <div className="flex items-center gap-2 min-w-0">
728 <Mail className="w-4 h-4 text-blue-500 flex-shrink-0" />
729 <span className="text-gray-900 dark:text-white truncate">
730 {sequenceTemplates.find(
731 (t) => t.id === step.template_id,
732 )?.name ||
733 templates.find(
734 (t) => t.id === step.template_id,
735 )?.name ||
736 __("Unknown Template")}
737 </span>
738 </div>
739 ) : (
740 <span className="text-gray-500 dark:text-gray-400">
741 {__("Select a template...")}
742 </span>
743 )}
744 <ChevronDown
745 className={`w-4 h-4 text-gray-400 flex-shrink-0 transition-transform ${openTemplateDropdown === index ? "rotate-180" : ""}`}
746 />
747 </button>
748
749 {/* Dropdown List */}
750 {openTemplateDropdown === index && (
751 <div className="absolute z-50 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-80 overflow-y-auto">
752 {sequenceTemplates.length === 0 ? (
753 <div className="p-4 text-center text-gray-500 dark:text-gray-400">
754 <p className="font-medium">
755 {__("No templates available")}
756 </p>
757 <p className="text-xs mt-1">
758 {__(
759 "Add customer email templates under Email → Templates first.",
760 )}
761 </p>
762 </div>
763 ) : (
764 sequenceTemplates.map((template) => {
765 const isSelected =
766 step.template_id === template.id;
767
768 return (
769 <button
770 key={template.id}
771 type="button"
772 onClick={() => {
773 updateStep(
774 index,
775 "template_id",
776 template.id,
777 );
778 setOpenTemplateDropdown(null);
779 }}
780 className={`w-full text-left p-3 border-b border-gray-100 dark:border-gray-700 last:border-b-0 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${isSelected ? "bg-blue-50 dark:bg-blue-900/20" : ""}`}
781 >
782 <div className="flex items-start justify-between gap-2">
783 <div className="min-w-0 flex-1">
784 <div className="flex items-center gap-2">
785 <span className="font-medium text-gray-900 dark:text-white">
786 {template.name}
787 </span>
788 {isSelected && (
789 <Check className="w-4 h-4 text-blue-500 flex-shrink-0" />
790 )}
791 </div>
792 {template.description && (
793 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
794 {template.description}
795 </p>
796 )}
797 </div>
798 <Badge
799 variant="outline"
800 className="text-xs flex-shrink-0"
801 >
802 {template.category}
803 </Badge>
804 </div>
805 </button>
806 );
807 })
808 )}
809 </div>
810 )}
811
812 {/* Click outside to close */}
813 {openTemplateDropdown === index && (
814 <div
815 className="fixed inset-0 z-40"
816 onClick={() => setOpenTemplateDropdown(null)}
817 />
818 )}
819
820 {!step.template_id && (
821 <p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
822 {__("Please select a template for this step")}
823 </p>
824 )}
825 </div>
826 </div>
827 </div>
828 ))}
829 </div>
830
831 {steps.length === 0 && (
832 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
833 <GitBranch className="w-12 h-12 mx-auto mb-3 opacity-50" />
834 <p>{__("No steps added yet")}</p>
835 <Button
836 type="button"
837 variant="outline"
838 size="sm"
839 onClick={addStep}
840 className="mt-3"
841 >
842 <Plus className="w-4 h-4 mr-1" />
843 {__("Add First Step")}
844 </Button>
845 </div>
846 )}
847 </CardContent>
848 </Card>
849 </div>
850
851 {/* Sidebar */}
852 <div className="space-y-6">
853 {/* Info Card */}
854 <Card>
855 <CardHeader>
856 <CardTitle className="flex items-center gap-2">
857 <Info className="w-5 h-5 text-gray-500" />
858 {__("How Sequences Work")}
859 </CardTitle>
860 </CardHeader>
861 <CardContent className="space-y-3 text-sm text-gray-600 dark:text-gray-400">
862 <p>
863 {__(
864 "Email sequences are automated workflows that send emails based on triggers and timing.",
865 )}
866 </p>
867 <ol className="list-decimal list-inside space-y-2">
868 <li>{__("Choose a trigger event (e.g., booking created)")}</li>
869 <li>{__("Add steps with delays and email templates")}</li>
870 <li>{__("Set the sequence to Active")}</li>
871 <li>{__("Emails will be sent automatically!")}</li>
872 </ol>
873 </CardContent>
874 </Card>
875
876 {/* Status Info */}
877 <Card>
878 <CardHeader>
879 <CardTitle>{__("Status")}</CardTitle>
880 </CardHeader>
881 <CardContent className="space-y-3">
882 <div className="flex items-center justify-between">
883 <span className="text-sm text-gray-500 dark:text-gray-400">
884 {__("Current Status")}
885 </span>
886 <Badge
887 className={
888 formData.status === "active"
889 ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
890 : formData.status === "paused"
891 ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
892 : "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
893 }
894 >
895 {formData.status === "active"
896 ? __("Active")
897 : formData.status === "paused"
898 ? __("Paused")
899 : __("Draft")}
900 </Badge>
901 </div>
902 <div className="flex items-center justify-between">
903 <span className="text-sm text-gray-500 dark:text-gray-400">
904 {__("Steps")}
905 </span>
906 <span className="text-sm font-medium text-gray-900 dark:text-white">
907 {steps.length}
908 </span>
909 </div>
910 <div className="flex items-center justify-between">
911 <span className="text-sm text-gray-500 dark:text-gray-400">
912 {__("Trigger")}
913 </span>
914 <span className="text-sm font-medium text-gray-900 dark:text-white">
915 {selectedTrigger?.label}
916 </span>
917 </div>
918 </CardContent>
919 </Card>
920 </div>
921 </div>
922 </div>
923 );
924 };
925
926 export default EmailSequenceForm;
927