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

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

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