PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 / EmailAutomation.tsx

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

675 lines 23.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useMemo, useCallback } from "react";
2 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3 import { __ } from "../lib/i18n";
4 import {
5 deleteEmailSequence,
6 fetchEmailLogs,
7 fetchEmailSequences,
8 updateEmailSequenceStatus,
9 } from "../api/email-automation-api";
10 import { useToast } from "../components/ui/toast";
11 import {
12 Card,
13 CardContent,
14 CardFooter,
15 CardHeader,
16 CardTitle,
17 } from "../components/ui/card";
18 import { Button } from "../components/ui/button";
19 import { Badge } from "../components/ui/badge";
20 import { PageHeader } from "../components/common/PageHeader";
21 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
22 import PremiumUpgradeCard from "./premium-pages/EmailAutomation";
23 import {
24 Mail,
25 Edit,
26 Clock,
27 FileText,
28 Bell,
29 Plus,
30 Trash2,
31 Play,
32 Pause,
33 Zap,
34 GitBranch,
35 Loader2,
36 Save,
37 Settings2,
38 Send,
39 } from "lucide-react";
40 import {
41 isProPluginActive,
42 isEmailAutomationModuleEnabled,
43 } from "../lib/plugin-utils";
44 import { useEmailSettingsManager } from "../hooks/useEmailSettingsManager";
45 import { EmailDeliverySection } from "../components/settings/EmailDeliverySection";
46 import { EmailTemplatesList } from "../components/email/EmailTemplatesList";
47 import { Pagination } from "../components/shared";
48
49 interface EmailLog {
50 id: number;
51 template_key: string;
52 recipient_email: string;
53 recipient_name: string;
54 subject: string;
55 status: "sent" | "failed" | "opened" | "clicked";
56 sent_at: string;
57 }
58
59 interface EmailSequence {
60 id: number;
61 name: string;
62 description: string;
63 trigger_type: string;
64 trigger_config: any;
65 status: "active" | "paused" | "draft";
66 applicable_to: string;
67 trip_ids: number[];
68 steps_count?: number;
69 }
70
71 const EmailSequencesList: React.FC = () => {
72 const queryClient = useQueryClient();
73 const { showToast } = useToast();
74
75 const { data: sequencesData, isLoading } = useQuery({
76 queryKey: ["email-sequences"],
77 queryFn: () => fetchEmailSequences(),
78 enabled: isEmailAutomationModuleEnabled(),
79 });
80
81 const deleteMutation = useMutation({
82 mutationFn: async (id: number) => {
83 return await deleteEmailSequence(id);
84 },
85 onSuccess: () => {
86 queryClient.invalidateQueries({ queryKey: ["email-sequences"] });
87 showToast(__("Sequence deleted"), "success");
88 },
89 });
90
91 const toggleStatusMutation = useMutation({
92 mutationFn: async ({ id, status }: { id: number; status: string }) => {
93 return await updateEmailSequenceStatus(id, status);
94 },
95 onSuccess: () => {
96 queryClient.invalidateQueries({ queryKey: ["email-sequences"] });
97 showToast(__("Sequence status updated"), "success");
98 },
99 });
100
101 const sequences = (
102 Array.isArray(sequencesData) ? sequencesData : []
103 ) as EmailSequence[];
104
105 const navigateToCreate = () => {
106 window.location.href =
107 "admin.php?page=yatra&subpage=email-automation&tab=sequence&action=create";
108 };
109
110 const navigateToEdit = (id: number) => {
111 window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=sequence&action=edit&id=${id}`;
112 };
113
114 if (isLoading) {
115 return (
116 <div className="space-y-6 animate-pulse">
117 {/* Header Skeleton */}
118 <div className="flex items-center justify-between">
119 <div>
120 <div className="h-6 w-36 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
121 <div className="h-4 w-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
122 </div>
123 <div className="h-10 w-36 bg-gray-200 dark:bg-gray-700 rounded"></div>
124 </div>
125
126 {/* Sequences List Skeleton */}
127 <div className="space-y-4">
128 {[1, 2, 3].map((i) => (
129 <Card key={i}>
130 <CardContent className="p-4">
131 <div className="flex items-center justify-between">
132 <div className="flex-1">
133 <div className="flex items-center gap-3 mb-2">
134 <div className="h-5 w-48 bg-gray-200 dark:bg-gray-700 rounded"></div>
135 <div className="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
136 </div>
137 <div className="h-4 w-72 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
138 <div className="flex items-center gap-4">
139 <div className="h-3 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
140 <div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
141 </div>
142 </div>
143 <div className="flex items-center gap-2">
144 <div className="h-8 w-8 bg-gray-200 dark:bg-gray-700 rounded"></div>
145 <div className="h-8 w-16 bg-gray-200 dark:bg-gray-700 rounded"></div>
146 <div className="h-8 w-8 bg-gray-200 dark:bg-gray-700 rounded"></div>
147 </div>
148 </div>
149 </CardContent>
150 </Card>
151 ))}
152 </div>
153 </div>
154 );
155 }
156
157 const getStatusBadge = (status: string) => {
158 switch (status) {
159 case "active":
160 return (
161 <Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
162 {__("Active")}
163 </Badge>
164 );
165 case "paused":
166 return (
167 <Badge className="bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400">
168 {__("Paused")}
169 </Badge>
170 );
171 default:
172 return (
173 <Badge className="bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400">
174 {__("Draft")}
175 </Badge>
176 );
177 }
178 };
179
180 const emailEvents = (window as any).yatraAdmin?.emailEvents || [];
181
182 const getTriggerLabel = (triggerType: string) => {
183 const ev = emailEvents.find((e: { key?: string }) => e.key === triggerType);
184 return ev?.name || triggerType;
185 };
186
187 return (
188 <div className="space-y-6">
189 {/* Header */}
190 <div className="flex items-center justify-between">
191 <div>
192 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
193 {__("Email Sequences")}
194 </h2>
195 <p className="text-sm text-gray-500 dark:text-gray-400">
196 {__("Create automated email workflows triggered by events")}
197 </p>
198 </div>
199 <Button
200 onClick={navigateToCreate}
201 className="bg-blue-600 hover:bg-blue-700 text-white"
202 >
203 <Plus className="w-4 h-4 mr-2" />
204 {__("Create Sequence")}
205 </Button>
206 </div>
207
208 {/* Sequences List */}
209 {sequences.length === 0 ? (
210 <Card>
211 <CardContent className="p-8 text-center">
212 <GitBranch className="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
213 <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
214 {__("No Sequences Yet")}
215 </h3>
216 <p className="text-gray-500 dark:text-gray-400 mb-4">
217 {__(
218 "Create your first automated email sequence to engage customers at the right time.",
219 )}
220 </p>
221 <Button
222 onClick={navigateToCreate}
223 className="bg-blue-600 hover:bg-blue-700 text-white"
224 >
225 <Plus className="w-4 h-4 mr-2" />
226 {__("Create Your First Sequence")}
227 </Button>
228 </CardContent>
229 </Card>
230 ) : (
231 <div className="space-y-4">
232 {sequences.map((sequence) => (
233 <Card key={sequence.id}>
234 <CardContent className="p-4">
235 <div className="flex items-center justify-between">
236 <div className="flex-1">
237 <div className="flex items-center gap-3">
238 <h3 className="font-medium text-gray-900 dark:text-white">
239 {sequence.name}
240 </h3>
241 {getStatusBadge(sequence.status)}
242 </div>
243 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
244 {sequence.description || __("No description")}
245 </p>
246 <div className="flex items-center gap-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
247 <span className="flex items-center gap-1">
248 <Zap className="w-3 h-3" />
249 {getTriggerLabel(sequence.trigger_type)}
250 </span>
251 <span className="flex items-center gap-1">
252 <GitBranch className="w-3 h-3" />
253 {sequence.steps_count || 0} {__("steps")}
254 </span>
255 </div>
256 </div>
257
258 <div className="flex items-center gap-2">
259 {sequence.status === "active" ? (
260 <Button
261 variant="ghost"
262 size="sm"
263 onClick={() =>
264 toggleStatusMutation.mutate({
265 id: sequence.id,
266 status: "paused",
267 })
268 }
269 >
270 <Pause className="w-4 h-4 text-yellow-500" />
271 </Button>
272 ) : (
273 <Button
274 variant="ghost"
275 size="sm"
276 onClick={() =>
277 toggleStatusMutation.mutate({
278 id: sequence.id,
279 status: "active",
280 })
281 }
282 >
283 <Play className="w-4 h-4 text-green-500" />
284 </Button>
285 )}
286 <Button
287 variant="outline"
288 size="sm"
289 onClick={() => navigateToEdit(sequence.id)}
290 >
291 <Edit className="w-4 h-4 mr-1" />
292 {__("Edit")}
293 </Button>
294 <Button
295 variant="ghost"
296 size="sm"
297 onClick={() => {
298 if (
299 confirm(
300 __(
301 "Are you sure you want to delete this sequence?",
302 ),
303 )
304 ) {
305 deleteMutation.mutate(sequence.id);
306 }
307 }}
308 >
309 <Trash2 className="w-4 h-4 text-red-500" />
310 </Button>
311 </div>
312 </div>
313 </CardContent>
314 </Card>
315 ))}
316 </div>
317 )}
318 </div>
319 );
320 };
321
322 const EmailLogsList: React.FC = () => {
323 const [page, setPage] = useState(1);
324 const perPage = 20;
325
326 const { data: logsData, isLoading } = useQuery({
327 queryKey: ["email-logs", page, perPage],
328 queryFn: () => fetchEmailLogs({ page, per_page: perPage }),
329 enabled: isEmailAutomationModuleEnabled(),
330 });
331
332 const logs = (logsData?.items ?? []) as EmailLog[];
333 const totalItems = logsData?.total ?? 0;
334 const totalPages = Math.max(1, Math.ceil(totalItems / perPage));
335
336 if (isLoading) {
337 return (
338 <Card>
339 <CardHeader>
340 <div className="h-6 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
341 </CardHeader>
342 <CardContent className="p-0">
343 <div className="overflow-x-auto">
344 <table className="w-full">
345 <thead className="bg-gray-50 dark:bg-gray-800">
346 <tr>
347 {[1, 2, 3, 4, 5].map((i) => (
348 <th key={i} className="px-4 py-3">
349 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
350 </th>
351 ))}
352 </tr>
353 </thead>
354 <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
355 {[1, 2, 3, 4, 5].map((row) => (
356 <tr key={row}>
357 {[1, 2, 3, 4, 5].map((col) => (
358 <td key={col} className="px-4 py-3">
359 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
360 </td>
361 ))}
362 </tr>
363 ))}
364 </tbody>
365 </table>
366 </div>
367 </CardContent>
368 </Card>
369 );
370 }
371
372 return (
373 <Card>
374 <CardHeader>
375 <CardTitle className="flex items-center gap-2">
376 <Clock className="w-5 h-5 text-blue-500" />
377 {__("Email Logs")}
378 </CardTitle>
379 </CardHeader>
380 <CardContent className="p-0">
381 {logs.length === 0 ? (
382 <div className="p-8 text-center text-gray-500 dark:text-gray-400">
383 {__("No emails sent yet")}
384 </div>
385 ) : (
386 <div className="overflow-x-auto">
387 <table className="w-full">
388 <thead className="bg-gray-50 dark:bg-gray-800">
389 <tr>
390 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
391 {__("Recipient")}
392 </th>
393 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
394 {__("Subject")}
395 </th>
396 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
397 {__("Template")}
398 </th>
399 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
400 {__("Status")}
401 </th>
402 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
403 {__("Sent")}
404 </th>
405 </tr>
406 </thead>
407 <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
408 {logs.map((log) => (
409 <tr
410 key={log.id}
411 className="hover:bg-gray-50 dark:hover:bg-gray-800/50"
412 >
413 <td className="px-4 py-3">
414 <div className="text-sm font-medium text-gray-900 dark:text-white">
415 {log.recipient_name || "-"}
416 </div>
417 <div className="text-sm text-gray-500 dark:text-gray-400">
418 {log.recipient_email}
419 </div>
420 </td>
421 <td className="px-4 py-3 text-sm text-gray-900 dark:text-white max-w-xs truncate">
422 {log.subject}
423 </td>
424 <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
425 {log.template_key}
426 </td>
427 <td className="px-4 py-3">
428 <Badge
429 className={
430 log.status === "sent"
431 ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
432 : log.status === "failed"
433 ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
434 : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
435 }
436 >
437 {log.status}
438 </Badge>
439 </td>
440 <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
441 {log.sent_at
442 ? new Date(log.sent_at).toLocaleString()
443 : "—"}
444 </td>
445 </tr>
446 ))}
447 </tbody>
448 </table>
449 </div>
450 )}
451 {logs.length > 0 && totalPages > 1 && (
452 <div className="border-t border-gray-200 px-4 py-3 dark:border-gray-700">
453 <Pagination
454 currentPage={page}
455 totalPages={totalPages}
456 totalItems={totalItems}
457 itemsPerPage={perPage}
458 onPageChange={setPage}
459 itemName={__("entries", "yatra")}
460 />
461 </div>
462 )}
463 </CardContent>
464 </Card>
465 );
466 };
467
468 type EmailHubTab = "delivery" | "templates" | "sequences" | "logs";
469
470 function getInitialEmailHubTab(): EmailHubTab {
471 if (typeof window === "undefined") return "delivery";
472 const tab = new URLSearchParams(window.location.search).get("tab");
473 if (tab === "sequences" || tab === "sequence") return "sequences";
474 if (tab === "template" || tab === "templates") return "templates";
475 if (tab === "logs") return "logs";
476 if (tab === "delivery" || tab === "core") return "delivery";
477 return "delivery";
478 }
479
480 const EmailAutomationModulePrompt: React.FC = () => (
481 <div className="rounded-xl border border-amber-200 bg-amber-50 p-6 dark:border-amber-800 dark:bg-amber-950/30">
482 <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
483 <div className="flex gap-3">
484 <Settings2 className="mt-0.5 h-6 w-6 shrink-0 text-amber-600 dark:text-amber-400" />
485 <div>
486 <h3 className="text-base font-semibold text-amber-900 dark:text-amber-100">
487 {__("Enable the Email Automation module", "yatra")}
488 </h3>
489 <p className="mt-1 text-sm text-amber-800/90 dark:text-amber-200/90">
490 {__(
491 "Yatra Pro is active. Turn on Email Automation under Modules to use custom templates, sequences, and send logs.",
492 "yatra",
493 )}
494 </p>
495 </div>
496 </div>
497 <Button
498 type="button"
499 variant="outline"
500 className="shrink-0 border-amber-300 bg-white hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950 dark:hover:bg-amber-900"
501 onClick={() => {
502 window.location.href = "admin.php?page=yatra&subpage=modules";
503 }}
504 >
505 {__("Open Modules", "yatra")}
506 </Button>
507 </div>
508 </div>
509 );
510
511 const EmailAutomation: React.FC = () => {
512 const emailMgr = useEmailSettingsManager();
513 const [activeTab, setActiveTab] = useState<EmailHubTab>(() => {
514 const t = getInitialEmailHubTab();
515 if (!isProPluginActive() && (t === "sequences" || t === "logs")) {
516 return "delivery";
517 }
518 return t;
519 });
520
521 const showProAutomationExtras = isProPluginActive();
522 const automationReady = isEmailAutomationModuleEnabled();
523
524 const tabs = [
525 {
526 key: "delivery" as const,
527 label: __("Delivery", "yatra"),
528 icon: Send,
529 },
530 {
531 key: "templates" as const,
532 label: __("Templates", "yatra"),
533 icon: FileText,
534 },
535 ...(showProAutomationExtras
536 ? [
537 {
538 key: "sequences" as const,
539 label: __("Sequences", "yatra"),
540 icon: GitBranch,
541 },
542 {
543 key: "logs" as const,
544 label: __("Email logs", "yatra"),
545 icon: Mail,
546 },
547 ]
548 : []),
549 ];
550
551 return (
552 <div className="space-y-6">
553 <PageHeader
554 description={__(
555 "Delivery (addresses & SMTP), core templates for everyone, and optional Pro sequences and logs.",
556 "yatra",
557 )}
558 />
559
560 <Card>
561 <div className="border-b border-gray-200 dark:border-gray-700">
562 <nav className="flex flex-wrap gap-1 px-4">
563 {tabs.map((tab) => (
564 <button
565 key={tab.key}
566 type="button"
567 onClick={() => setActiveTab(tab.key)}
568 className={`flex items-center gap-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${
569 activeTab === tab.key
570 ? "border-blue-500 text-blue-600 dark:text-blue-400"
571 : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
572 }`}
573 >
574 <tab.icon className="w-4 h-4" />
575 {tab.label}
576 </button>
577 ))}
578 </nav>
579 </div>
580
581 <CardContent className="p-6">
582 {activeTab === "delivery" && (
583 <>
584 {!emailMgr.ready ? (
585 <div className="flex justify-center py-16">
586 <Loader2 className="h-10 w-10 animate-spin text-gray-400" />
587 </div>
588 ) : (
589 <EmailDeliverySection
590 values={emailMgr.values}
591 onFieldChange={emailMgr.handleFieldChange}
592 />
593 )}
594 </>
595 )}
596
597 {activeTab === "templates" && (
598 <>
599 {automationReady && (
600 <p className="mb-6 text-sm text-gray-600 dark:text-gray-400">
601 {__(
602 "Customer booking, payment, cancellation, and trip-reminder emails use the matching template when its body is filled; otherwise the free plugin defaults or settings HTML apply. Admin: New Booking sends when that template has a body (plain-text admin notices from checkout and notifications are skipped to avoid duplicates). Other booking.created templates send in addition to the customer email. Booking Confirmed uses the “Booking Confirmed” row when a booking moves to confirmed; Trip Completed uses its template instead of the generic completed email when the body is filled.",
603 "yatra",
604 )}
605 </p>
606 )}
607 {!emailMgr.ready && !automationReady ? (
608 <div className="flex justify-center py-16">
609 <Loader2 className="h-10 w-10 animate-spin text-gray-400" />
610 </div>
611 ) : (
612 <EmailTemplatesList
613 automationModuleActive={automationReady}
614 settingsBridge={
615 automationReady
616 ? undefined
617 : {
618 values: emailMgr.values,
619 onFieldChange: emailMgr.handleFieldChange,
620 }
621 }
622 />
623 )}
624 </>
625 )}
626
627 {activeTab === "sequences" &&
628 (automationReady ? (
629 <EmailSequencesList />
630 ) : isProPluginActive() ? (
631 <EmailAutomationModulePrompt />
632 ) : (
633 <PremiumUpgradeCard />
634 ))}
635
636 {activeTab === "logs" &&
637 (automationReady ? (
638 <EmailLogsList />
639 ) : isProPluginActive() ? (
640 <EmailAutomationModulePrompt />
641 ) : (
642 <PremiumUpgradeCard />
643 ))}
644 </CardContent>
645
646 {(activeTab === "delivery" || activeTab === "templates") &&
647 emailMgr.ready && (
648 <CardFooter className="flex justify-end border-t border-gray-100 pt-4 dark:border-gray-700/50">
649 <Button
650 type="button"
651 onClick={emailMgr.save}
652 disabled={!emailMgr.canSave}
653 className="flex items-center gap-2"
654 >
655 {emailMgr.isSaving ? (
656 <>
657 <Loader2 className="h-4 w-4 animate-spin" />
658 {__("Saving…", "yatra")}
659 </>
660 ) : (
661 <>
662 <Save className="h-4 w-4" />
663 {__("Save email settings", "yatra")}
664 </>
665 )}
666 </Button>
667 </CardFooter>
668 )}
669 </Card>
670 </div>
671 );
672 };
673
674 export default EmailAutomation;
675