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 / AbandonedRecovery.tsx

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

1,859 lines 66.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Abandoned Booking Recovery Page
3 *
4 * Premium module for tracking and recovering abandoned bookings.
5 * This page displays the UI shell in the free plugin with a premium gate.
6 * The actual functionality is provided by the Yatra Pro plugin.
7 *
8 * @package Yatra
9 * @since 3.0.0
10 */
11
12 import React, { useState, useEffect, useMemo } from "react";
13 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
14 import {
15 Card,
16 CardContent,
17 CardHeader,
18 CardTitle,
19 CardDescription,
20 } from "../components/ui/card";
21 import { Button } from "../components/ui/button";
22 import { Badge } from "../components/ui/badge";
23 import { Label } from "../components/ui/label";
24 import { Input } from "../components/ui/input";
25 import { Toggle } from "../components/ui/toggle";
26 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
27 import { Modal } from "../components/ui/modal";
28 import { Skeleton } from "../components/ui/skeleton";
29 import { PageHeader } from "../components/common/PageHeader";
30 import {
31 Pagination,
32 SearchFilterToolbar,
33 BulkActionToolbar,
34 Table as SharedTable,
35 } from "../components/shared";
36 import { useToast } from "../components/ui/toast";
37 import { __ } from "../lib/i18n";
38 import { apiService } from "../lib/api-client";
39 import { formatYatraMoney } from "../lib/currency-display";
40 import PremiumUpgradeCard from "./premium-pages/AbandonedRecovery";
41 import {
42 TrendingUp,
43 DollarSign,
44 CheckCircle,
45 BarChart,
46 Settings,
47 Send,
48 AlertCircle,
49 Eye,
50 Trash2,
51 RotateCcw,
52 } from "lucide-react";
53
54 // Skeleton components
55 const SkeletonStatCard = () => (
56 <Card>
57 <CardContent className="pt-6">
58 <div className="flex items-center justify-between">
59 <div>
60 <Skeleton className="h-4 w-32 mb-2" />
61 <Skeleton className="h-8 w-24" />
62 </div>
63 <Skeleton className="w-8 h-8 rounded-full" />
64 </div>
65 </CardContent>
66 </Card>
67 );
68
69 const SkeletonBookingCard = () => (
70 <div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
71 <div className="flex-1">
72 <Skeleton className="h-5 w-40 mb-2" />
73 <Skeleton className="h-4 w-48 mb-1" />
74 <Skeleton className="h-4 w-32" />
75 </div>
76 <div className="text-right mr-4">
77 <Skeleton className="h-5 w-20 mb-1" />
78 <Skeleton className="h-4 w-24" />
79 </div>
80 <Skeleton className="h-6 w-20 mr-4" />
81 <Skeleton className="h-9 w-28" />
82 </div>
83 );
84
85 // Check if module is available (Pro active and module enabled)
86 const isModuleAvailable = (): boolean => {
87 const yatraAdmin = (window as any)?.yatraAdmin;
88 return Boolean(yatraAdmin?.isPro);
89 };
90
91 // Currency formatting helper
92 const formatPrice = (price: number, currencyCode?: string): string => {
93 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
94 const currency = currencyCode || globalCurrency;
95 return formatYatraMoney(Number(price) || 0, currency, { zeroAsUnknown: false });
96 };
97
98 // Main Component
99 interface AbandonedRecoveryProps {
100 tab?: string;
101 }
102
103 const AbandonedRecoveryPage: React.FC<AbandonedRecoveryProps> = ({ tab }) => {
104 const { showToast } = useToast();
105 const queryClient = useQueryClient();
106
107 // Get URL parameters for actions
108 const urlParams = new URLSearchParams(window.location.search);
109 const action = urlParams.get("action");
110 const bookingId = urlParams.get("id");
111
112 // Modal state for viewing details
113 const [viewModalOpen, setViewModalOpen] = useState(false);
114 const [selectedBooking, setSelectedBooking] = useState<any>(null);
115
116 // Set active tab based on URL parameter, with dashboard as default
117 const [activeTab, setActiveTab] = useState<
118 "dashboard" | "bookings" | "settings"
119 >(
120 tab === "abandoned-booking"
121 ? "bookings"
122 : tab === "settings"
123 ? "settings"
124 : "dashboard",
125 );
126
127 // Update tab when URL changes
128 useEffect(() => {
129 const newTab =
130 tab === "abandoned-booking"
131 ? "bookings"
132 : tab === "settings"
133 ? "settings"
134 : "dashboard";
135 setActiveTab(newTab);
136 }, [tab]);
137 const [page, setPage] = useState(1);
138 const [perPage] = useState(20);
139 const [searchTerm, setSearchTerm] = useState("");
140 const [statusFilter, setStatusFilter] = useState<string>("all");
141 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
142 const [bulkAction, setBulkAction] = useState("");
143 const [settings, setSettings] = useState({
144 enabled: true,
145 tracking_enabled: true,
146 first_email_delay_hours: 1,
147 second_email_delay_hours: 24,
148 final_email_delay_hours: 72,
149 first_email_subject: "",
150 second_email_subject: "",
151 final_email_subject: "",
152 first_email_message: "",
153 second_email_message: "",
154 final_email_message: "",
155 });
156 const [isSavingSettings, setIsSavingSettings] = useState(false);
157
158 // Confirmation dialog state
159 const [confirmDialog, setConfirmDialog] = useState<{
160 isOpen: boolean;
161 title: string;
162 message: string;
163 onConfirm: () => void;
164 }>({ isOpen: false, title: "", message: "", onConfirm: () => {} });
165
166 // Email preview state
167 const [emailPreview, setEmailPreview] = useState<{
168 isOpen: boolean;
169 type: "first" | "second" | "final";
170 subject: string;
171 message: string;
172 }>({ isOpen: false, type: "first", subject: "", message: "" });
173
174 // Function to replace placeholders with sample data
175 const replaceEmailPlaceholders = (text: string): string => {
176 const placeholders: Record<string, string> = {
177 "{customer_name}": "John Doe",
178 "{trip_name}": "Everest Base Camp Trek",
179 "{trip_url}": "https://example.com/trips/everest-base-camp",
180 "{booking_amount}": "$2,500",
181 "{departure_date}": "March 15, 2025",
182 "{recovery_link}": "https://example.com/complete-booking?token=abc123",
183 "{site_name}": "Yatra Travel",
184 "{site_url}": "https://example.com",
185 };
186
187 let result = text;
188 Object.entries(placeholders).forEach(([placeholder, value]) => {
189 result = result.replace(new RegExp(placeholder, "g"), value);
190 });
191 return result;
192 };
193
194 // Function to show email preview
195 const showEmailPreview = (type: "first" | "second" | "final") => {
196 const subjectKey = `${type}_email_subject` as keyof typeof settings;
197 const messageKey = `${type}_email_message` as keyof typeof settings;
198
199 setEmailPreview({
200 isOpen: true,
201 type,
202 subject: replaceEmailPlaceholders(String(settings[subjectKey] || "")),
203 message: replaceEmailPlaceholders(String(settings[messageKey] || "")),
204 });
205 };
206
207 // Check if module is available
208 if (!isModuleAvailable()) {
209 return <PremiumUpgradeCard />;
210 }
211
212 // Build query params
213 const queryParams = useMemo(() => {
214 const params: Record<string, any> = {
215 page,
216 per_page: perPage,
217 };
218
219 if (searchTerm) {
220 params.search = searchTerm;
221 }
222
223 if (statusFilter !== "all") {
224 params.status = statusFilter;
225 }
226
227 return params;
228 }, [searchTerm, statusFilter, page, perPage]);
229
230 // Fetch abandoned bookings
231 const { data: bookingsData, isLoading } = useQuery({
232 queryKey: ["abandoned-bookings", queryParams],
233 queryFn: async () => {
234 const paramsObj: Record<string, any> = {};
235 Object.entries(queryParams).forEach(([key, value]) => {
236 if (value !== undefined) paramsObj[key] = value;
237 });
238
239 const response = await apiService.getAbandonedBookings(paramsObj);
240 return response;
241 },
242 });
243
244 // Fetch single booking details if viewing
245 const { data: bookingDetailsData, isLoading: isLoadingDetails } = useQuery({
246 queryKey: ["abandoned-booking-details", bookingId],
247 queryFn: async () => {
248 const response = await apiService.getAbandonedBooking(bookingId!);
249 return response.data;
250 },
251 enabled: !!bookingId,
252 });
253
254 // Fetch detailed booking data for modal
255 const { data: modalBookingData, isLoading: isModalLoading } = useQuery({
256 queryKey: ["abandoned-booking-modal", selectedBooking?.id],
257 queryFn: async () => {
258 const response = await apiService.getAbandonedBooking(
259 selectedBooking!.id,
260 );
261 return response.data;
262 },
263 enabled: !!selectedBooking?.id && viewModalOpen,
264 });
265
266 // Fetch settings
267 const { data: settingsData } = useQuery({
268 queryKey: ["abandoned-recovery-settings"],
269 queryFn: async () => {
270 const response = await apiService.getAbandonedBookingsSettings();
271 return response;
272 },
273 });
274
275 // Populate settings when data loads
276 useEffect(() => {
277 if (settingsData?.data) {
278 setSettings(settingsData.data);
279 }
280 }, [settingsData]);
281
282 // Fetch statistics
283 const { data: statsData } = useQuery({
284 queryKey: ["abandoned-statistics"],
285 queryFn: async () => {
286 const response = await apiService.getAbandonedBookingsStatistics();
287 return response;
288 },
289 });
290
291 // Send recovery email mutation
292 const sendEmailMutation = useMutation({
293 mutationFn: async (id: number) => {
294 return await apiService.sendAbandonedBookingEmail(id);
295 },
296 onSuccess: () => {
297 showToast(__("Recovery email sent successfully"), "success");
298 queryClient.invalidateQueries({ queryKey: ["abandoned-bookings"] });
299 },
300 onError: () => {
301 showToast(__("Failed to send recovery email"), "error");
302 },
303 });
304
305 const stats = statsData?.data || {};
306 const bookings = bookingsData?.data || [];
307 const pagination = bookingsData?.pagination || {
308 total: 0,
309 current_page: 1,
310 total_pages: 1,
311 per_page: perPage,
312 };
313
314 // Delete mutation
315 const deleteMutation = useMutation({
316 mutationFn: async (ids: (string | number)[]) => {
317 await Promise.all(ids.map((id) => apiService.deleteAbandonedBooking(id)));
318 },
319 onSuccess: () => {
320 queryClient.invalidateQueries({ queryKey: ["abandoned-bookings"] });
321 setSelectedIds([]);
322 showToast(__("Bookings deleted successfully"), "success");
323 },
324 onError: () => {
325 showToast(__("Failed to delete bookings"), "error");
326 },
327 });
328
329 // Helper functions
330 const handleBulkApply = () => {
331 if (!bulkAction) {
332 showToast(__("Select a bulk action first"), "warning");
333 return;
334 }
335
336 if (selectedIds.length === 0) {
337 showToast(__("Select at least one booking"), "warning");
338 return;
339 }
340
341 if (bulkAction === "delete") {
342 setConfirmDialog({
343 isOpen: true,
344 title: __("Delete Bookings"),
345 message: __(
346 "Are you sure you want to delete {count} booking(s)?",
347 ).replace("{count}", selectedIds.length.toString()),
348 onConfirm: () => {
349 deleteMutation.mutate(selectedIds);
350 setConfirmDialog({
351 isOpen: false,
352 title: "",
353 message: "",
354 onConfirm: () => {},
355 });
356 },
357 });
358 }
359 };
360
361 const handleResetFilters = () => {
362 setSearchTerm("");
363 setStatusFilter("all");
364 setPage(1);
365 setSelectedIds([]);
366 setBulkAction("");
367 };
368
369 const hasFilters = Boolean(searchTerm || statusFilter !== "all");
370
371 const viewFilters = [
372 { key: "all", label: __("All"), count: stats.total_abandoned || 0 },
373 {
374 key: "abandoned",
375 label: __("Abandoned"),
376 count:
377 stats.total_abandoned -
378 stats.total_recovered -
379 stats.total_expired -
380 stats.total_contacted || 0,
381 },
382 {
383 key: "contacted",
384 label: __("Contacted"),
385 count: stats.total_contacted || 0,
386 },
387 {
388 key: "recovered",
389 label: __("Recovered"),
390 count: stats.total_recovered || 0,
391 },
392 { key: "expired", label: __("Expired"), count: stats.total_expired || 0 },
393 ];
394
395 // Column definitions for SharedTable
396 const columns = [
397 {
398 key: "customer",
399 label: __("Customer"),
400 render: (booking: any) => (
401 <div>
402 <div className="font-medium text-gray-900 dark:text-white">
403 {booking.customer_name || __("Unknown")}
404 </div>
405 <div className="text-sm text-gray-500 dark:text-gray-400">
406 {booking.customer_email}
407 </div>
408 </div>
409 ),
410 },
411 {
412 key: "trip",
413 label: __("Trip"),
414 render: (booking: any) => {
415 const tripBase = (window as any)?.yatraAdmin?.tripBase || "trip";
416 const siteUrl = (window as any)?.yatraAdmin?.siteUrl || "";
417 const tripUrl = booking.trip_slug
418 ? `${siteUrl}/${tripBase}/${booking.trip_slug}`
419 : `${siteUrl}/?p=${booking.trip_id}`;
420
421 return (
422 <div className="text-sm">
423 <a
424 href={tripUrl}
425 target="_blank"
426 rel="noopener noreferrer"
427 className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline font-medium"
428 >
429 {booking.trip_name}
430 </a>
431 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
432 ID: {booking.trip_id}
433 </div>
434 </div>
435 );
436 },
437 },
438 {
439 key: "amount",
440 label: __("Amount"),
441 render: (booking: any) => (
442 <div className="text-sm font-medium text-gray-900 dark:text-white">
443 {formatPrice(booking.total_amount)}
444 </div>
445 ),
446 },
447 {
448 key: "emails_sent",
449 label: __("Emails Sent"),
450 render: (booking: any) => (
451 <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
452 {booking.recovery_emails_sent || 0}
453 </span>
454 ),
455 },
456 {
457 key: "status",
458 label: __("Status"),
459 render: (booking: any) => (
460 <Badge variant={booking.status === "recovered" ? "success" : "default"}>
461 {booking.status}
462 </Badge>
463 ),
464 },
465 {
466 key: "date",
467 label: __("Date"),
468 render: (booking: any) => (
469 <div className="text-sm text-gray-500 dark:text-gray-400">
470 {new Date(booking.created_at).toLocaleDateString()}
471 </div>
472 ),
473 },
474 ];
475
476 // Static actions array for SharedTable
477 const actions = [
478 {
479 key: "view",
480 label: __("View Details"),
481 icon: <Eye className="w-4 h-4" />,
482 onClick: (item: any) => {
483 // Open modal with booking details
484 setSelectedBooking(item);
485 setViewModalOpen(true);
486 },
487 },
488 {
489 key: "send-email",
490 label: __("Send Email"),
491 icon: <Send className="w-4 h-4" />,
492 onClick: (item: any) => {
493 setConfirmDialog({
494 isOpen: true,
495 title: __("Send Recovery Email"),
496 message: __(
497 "Are you sure you want to send a recovery email to {email}?",
498 ).replace("{email}", item.customer_email),
499 onConfirm: () => {
500 sendEmailMutation.mutate(item.id);
501 setConfirmDialog({
502 isOpen: false,
503 title: "",
504 message: "",
505 onConfirm: () => {},
506 });
507 },
508 });
509 },
510 condition: (item: any) => item.status !== "recovered",
511 },
512 {
513 key: "delete",
514 label: __("Delete"),
515 icon: <Trash2 className="w-4 h-4" />,
516 onClick: (item: any) => {
517 setConfirmDialog({
518 isOpen: true,
519 title: __("Delete Booking"),
520 message: __(
521 "Are you sure you want to delete this abandoned booking?",
522 ),
523 onConfirm: () => {
524 deleteMutation.mutate([item.id]);
525 setConfirmDialog({
526 isOpen: false,
527 title: "",
528 message: "",
529 onConfirm: () => {},
530 });
531 },
532 });
533 },
534 variant: "destructive" as const,
535 },
536 ];
537
538 // Save settings mutation
539 const saveSettingsMutation = useMutation({
540 mutationFn: async (data: typeof settings) => {
541 return await apiService.saveAbandonedBookingsSettings(data);
542 },
543 onSuccess: () => {
544 showToast(__("Settings saved successfully"), "success");
545 queryClient.invalidateQueries({
546 queryKey: ["abandoned-recovery-settings"],
547 });
548 setIsSavingSettings(false);
549 },
550 onError: () => {
551 showToast(__("Failed to save settings"), "error");
552 setIsSavingSettings(false);
553 },
554 });
555
556 const handleSaveSettings = () => {
557 setIsSavingSettings(true);
558 saveSettingsMutation.mutate(settings);
559 };
560
561 const handleSettingChange = (key: string, value: any) => {
562 setSettings((prev) => ({ ...prev, [key]: value }));
563 };
564
565 // Reset email template to default
566 const handleResetEmailTemplate = (type: "first" | "second" | "final") => {
567 const defaultTemplates = {
568 first: {
569 subject: __("Discover {trip_name} - {site_name}"),
570 message: `<p>Hi {customer_name},</p>
571 <p>We noticed you were interested in <strong>{trip_name}</strong>! This amazing adventure is waiting for you.</p>
572 <p>Come discover what makes this trip so special:</p>
573 <ul>
574 <li><strong>Destination:</strong> {trip_name}</li>
575 <li><strong>Departure:</strong> {departure_date}</li>
576 <li><strong>Travelers:</strong> {travelers_count}</li>
577 <li><strong>Starting from:</strong> {total_amount}</li>
578 </ul>
579 <p><a href="{trip_link}">View Trip Details</a></p>
580 <p>Discover more about this incredible journey!</p>`,
581 },
582 second: {
583 subject: __("Still Thinking About {trip_name}? - {site_name}"),
584 message: `<p>Hi {customer_name},</p>
585 <p>We noticed you were exploring <strong>{trip_name}</strong> recently. This incredible adventure might be exactly what you're looking for!</p>
586 <p>Come discover what makes this trip so special:</p>
587 <ul>
588 <li><strong>Destination:</strong> {trip_name}</li>
589 <li><strong>Departure:</strong> {departure_date}</li>
590 <li><strong>Travelers:</strong> {travelers_count}</li>
591 <li><strong>Starting from:</strong> {total_amount}</li>
592 </ul>
593 <p><a href="{trip_link}">Explore This Adventure</a></p>
594 <p>Find your next great adventure with us!</p>`,
595 },
596 final: {
597 subject: __("Don't Miss {trip_name}! - {site_name}"),
598 message: `<p>Hi {customer_name},</p>
599 <p><strong>⚠️ Spots Filling Up Fast!</strong> Your interest in <strong>{trip_name}</strong> won't last forever! This is your final reminder to discover this amazing adventure before it's too late.</p>
600 <ul>
601 <li><strong>Destination:</strong> {trip_name}</li>
602 <li><strong>Departure:</strong> {departure_date}</li>
603 <li><strong>Travelers:</strong> {travelers_count}</li>
604 <li><strong>Starting from:</strong> {total_amount}</li>
605 </ul>
606 <p><a href="{trip_link}">View Trip Before It's Gone</a></p>
607 <p><strong>Important:</strong> Popular trips like this fill up quickly. Don't miss your chance!</p>
608 <p>This is your final opportunity to explore this incredible journey.</p>`,
609 },
610 };
611
612 const template = defaultTemplates[type];
613 setSettings((prev) => ({
614 ...prev,
615 [`${type}_email_subject`]: template.subject,
616 [`${type}_email_message`]: template.message,
617 }));
618 };
619
620 // If viewing a specific booking, show details page
621 if (action === "view" && bookingId) {
622 const booking = bookingDetailsData?.data;
623
624 if (isLoadingDetails) {
625 return (
626 <div className="space-y-6">
627 <PageHeader title={__("Loading Booking Details...")} description="" />
628 <Card>
629 <CardContent className="p-6">
630 <div className="space-y-4">
631 <Skeleton className="h-8 w-64" />
632 <Skeleton className="h-4 w-full" />
633 <Skeleton className="h-4 w-full" />
634 <Skeleton className="h-4 w-3/4" />
635 </div>
636 </CardContent>
637 </Card>
638 </div>
639 );
640 }
641
642 if (!booking) {
643 return (
644 <div className="space-y-6">
645 <PageHeader title={__("Booking Not Found")} description="" />
646 <Card>
647 <CardContent className="p-6">
648 <p className="text-gray-500">
649 {__("The booking you are looking for does not exist.")}
650 </p>
651 <Button
652 className="mt-4"
653 onClick={() =>
654 (window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=abandoned-recovery&tab=abandoned-booking`)
655 }
656 >
657 {__("Back to Abandoned Bookings")}
658 </Button>
659 </CardContent>
660 </Card>
661 </div>
662 );
663 }
664
665 return (
666 <div className="space-y-6">
667 <PageHeader
668 title={__("Abandoned Booking Details")}
669 description={`Booking ID: ${booking.id}`}
670 actions={
671 <Button
672 variant="outline"
673 onClick={() =>
674 (window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=abandoned-recovery&tab=abandoned-booking`)
675 }
676 >
677 {__("Back to List")}
678 </Button>
679 }
680 />
681
682 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
683 {/* Main Details */}
684 <div className="lg:col-span-2 space-y-6">
685 {/* Customer Information */}
686 <Card>
687 <CardHeader>
688 <CardTitle>{__("Customer Information")}</CardTitle>
689 </CardHeader>
690 <CardContent>
691 <div className="grid grid-cols-2 gap-4">
692 <div>
693 <Label className="text-sm text-gray-500 dark:text-gray-400">
694 {__("Name")}
695 </Label>
696 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
697 {booking.customer_name || __("Unknown")}
698 </p>
699 </div>
700 <div>
701 <Label className="text-sm text-gray-500 dark:text-gray-400">
702 {__("Email")}
703 </Label>
704 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
705 {booking.customer_email}
706 </p>
707 </div>
708 {booking.customer_phone && (
709 <div>
710 <Label className="text-sm text-gray-500 dark:text-gray-400">
711 {__("Phone")}
712 </Label>
713 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
714 {booking.customer_phone}
715 </p>
716 </div>
717 )}
718 </div>
719 </CardContent>
720 </Card>
721
722 {/* Trip Information */}
723 <Card>
724 <CardHeader>
725 <CardTitle>{__("Trip Information")}</CardTitle>
726 </CardHeader>
727 <CardContent>
728 <div className="grid grid-cols-2 gap-4">
729 <div>
730 <Label className="text-sm text-gray-500 dark:text-gray-400">
731 {__("Trip Name")}
732 </Label>
733 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
734 {booking.trip_name}
735 </p>
736 </div>
737 {booking.departure_date && (
738 <div>
739 <Label className="text-sm text-gray-500 dark:text-gray-400">
740 {__("Departure Date")}
741 </Label>
742 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
743 {new Date(booking.departure_date).toLocaleDateString()}
744 </p>
745 </div>
746 )}
747 <div>
748 <Label className="text-sm text-gray-500 dark:text-gray-400">
749 {__("Travelers")}
750 </Label>
751 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
752 {booking.travelers_count || 1}
753 </p>
754 </div>
755 <div>
756 <Label className="text-sm text-gray-500 dark:text-gray-400">
757 {__("Total Amount")}
758 </Label>
759 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
760 {formatPrice(booking.total_amount)}
761 </p>
762 </div>
763 </div>
764 </CardContent>
765 </Card>
766
767 {/* Booking Data */}
768 {booking.booking_data && (
769 <Card>
770 <CardHeader>
771 <CardTitle>{__("Additional Details")}</CardTitle>
772 </CardHeader>
773 <CardContent>
774 <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
775 <pre className="text-xs text-gray-700 dark:text-gray-300 overflow-x-auto">
776 {JSON.stringify(booking.booking_data, null, 2)}
777 </pre>
778 </div>
779 </CardContent>
780 </Card>
781 )}
782 </div>
783
784 {/* Sidebar */}
785 <div className="space-y-6">
786 {/* Status Card */}
787 <Card>
788 <CardHeader>
789 <CardTitle>{__("Booking Status")}</CardTitle>
790 </CardHeader>
791 <CardContent className="space-y-4">
792 <div>
793 <Label className="text-sm text-gray-500 dark:text-gray-400">
794 {__("Status")}
795 </Label>
796 <div className="mt-1">
797 <Badge
798 variant={
799 booking.status === "recovered" ? "success" : "default"
800 }
801 >
802 {booking.status}
803 </Badge>
804 </div>
805 </div>
806 <div>
807 <Label className="text-sm text-gray-500 dark:text-gray-400">
808 {__("Recovery Emails Sent")}
809 </Label>
810 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
811 {booking.recovery_emails_sent || 0}
812 </p>
813 </div>
814 <div>
815 <Label className="text-sm text-gray-500 dark:text-gray-400">
816 {__("Created At")}
817 </Label>
818 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
819 {new Date(booking.created_at).toLocaleString()}
820 </p>
821 </div>
822 {booking.last_email_sent_at && (
823 <div>
824 <Label className="text-sm text-gray-500 dark:text-gray-400">
825 {__("Last Email Sent")}
826 </Label>
827 <p className="text-sm font-medium text-gray-900 dark:text-white mt-1">
828 {new Date(booking.last_email_sent_at).toLocaleString()}
829 </p>
830 </div>
831 )}
832 </CardContent>
833 </Card>
834
835 {/* Actions Card */}
836 <Card>
837 <CardHeader>
838 <CardTitle>{__("Actions")}</CardTitle>
839 </CardHeader>
840 <CardContent className="space-y-2">
841 <Button
842 className="w-full"
843 onClick={() => {
844 setConfirmDialog({
845 isOpen: true,
846 title: __("Send Recovery Email"),
847 message: __(
848 "Are you sure you want to send a recovery email to {email}?",
849 ).replace("{email}", booking.customer_email),
850 onConfirm: () => {
851 sendEmailMutation.mutate(booking.id);
852 setConfirmDialog({
853 isOpen: false,
854 title: "",
855 message: "",
856 onConfirm: () => {},
857 });
858 },
859 });
860 }}
861 disabled={
862 booking.status === "recovered" ||
863 sendEmailMutation.isPending
864 }
865 >
866 <Send className="w-4 h-4 mr-2" />
867 {__("Send Recovery Email")}
868 </Button>
869 <Button
870 variant="destructive"
871 className="w-full"
872 onClick={() => {
873 setConfirmDialog({
874 isOpen: true,
875 title: __("Delete Booking"),
876 message: __(
877 "Are you sure you want to delete this abandoned booking?",
878 ),
879 onConfirm: () => {
880 deleteMutation.mutate([booking.id]);
881 setConfirmDialog({
882 isOpen: false,
883 title: "",
884 message: "",
885 onConfirm: () => {},
886 });
887 // Redirect after delete
888 setTimeout(() => {
889 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=abandoned-recovery&tab=abandoned-booking`;
890 }, 1000);
891 },
892 });
893 }}
894 disabled={deleteMutation.isPending}
895 >
896 <Trash2 className="w-4 h-4 mr-2" />
897 {__("Delete Booking")}
898 </Button>
899 </CardContent>
900 </Card>
901 </div>
902 </div>
903 </div>
904 );
905 }
906
907 return (
908 <div className="space-y-6">
909 <PageHeader
910 title={__("Abandoned Booking Recovery")}
911 description={__(
912 "Track and recover abandoned bookings with automated email campaigns",
913 )}
914 />
915
916 {/* Tabs with Action Button */}
917 <div className="border-b border-gray-200 dark:border-gray-700">
918 <div className="flex items-center justify-between">
919 <nav className="-mb-px flex space-x-8">
920 <button
921 onClick={() => setActiveTab("dashboard")}
922 className={`py-4 px-1 border-b-2 font-medium text-sm ${
923 activeTab === "dashboard"
924 ? "border-blue-500 text-blue-600 dark:text-blue-400"
925 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
926 }`}
927 >
928 <BarChart className="w-5 h-5 inline-block mr-2" />
929 {__("Dashboard")}
930 </button>
931 <button
932 onClick={() => {
933 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=abandoned-recovery&tab=abandoned-booking`;
934 }}
935 className={`py-4 px-1 border-b-2 font-medium text-sm ${
936 activeTab === "bookings"
937 ? "border-blue-500 text-blue-600 dark:text-blue-400"
938 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
939 }`}
940 >
941 <AlertCircle className="w-5 h-5 inline-block mr-2" />
942 {__("Abandoned Bookings")}
943 </button>
944 <button
945 onClick={() => {
946 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=abandoned-recovery&tab=settings`;
947 }}
948 className={`py-4 px-1 border-b-2 font-medium text-sm ${
949 activeTab === "settings"
950 ? "border-blue-500 text-blue-600 dark:text-blue-400"
951 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
952 }`}
953 >
954 <Settings className="w-5 h-5 inline-block mr-2" />
955 {__("Settings")}
956 </button>
957 </nav>
958 </div>
959 </div>
960
961 {/* Dashboard Tab */}
962 {activeTab === "dashboard" && (
963 <div className="space-y-6">
964 {/* Stats Cards */}
965 {isLoading ? (
966 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
967 <SkeletonStatCard />
968 <SkeletonStatCard />
969 <SkeletonStatCard />
970 <SkeletonStatCard />
971 </div>
972 ) : (
973 <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
974 <Card>
975 <CardContent className="pt-6">
976 <div className="flex items-center justify-between">
977 <div>
978 <p className="text-sm text-gray-600 dark:text-gray-400">
979 {__("Total Abandoned")}
980 </p>
981 <p className="text-3xl font-bold">
982 {stats.total_abandoned || 0}
983 </p>
984 </div>
985 <AlertCircle className="w-8 h-8 text-orange-600 dark:text-orange-400" />
986 </div>
987 </CardContent>
988 </Card>
989
990 <Card>
991 <CardContent className="pt-6">
992 <div className="flex items-center justify-between">
993 <div>
994 <p className="text-sm text-gray-600 dark:text-gray-400">
995 {__("Recovered")}
996 </p>
997 <p className="text-3xl font-bold text-green-600 dark:text-green-400">
998 {stats.total_recovered || 0}
999 </p>
1000 </div>
1001 <CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
1002 </div>
1003 </CardContent>
1004 </Card>
1005
1006 <Card>
1007 <CardContent className="pt-6">
1008 <div className="flex items-center justify-between">
1009 <div>
1010 <p className="text-sm text-gray-600 dark:text-gray-400">
1011 {__("Recovery Rate")}
1012 </p>
1013 <p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
1014 {stats.recovery_rate || 0}%
1015 </p>
1016 </div>
1017 <TrendingUp className="w-8 h-8 text-blue-600 dark:text-blue-400" />
1018 </div>
1019 </CardContent>
1020 </Card>
1021
1022 <Card>
1023 <CardContent className="pt-6">
1024 <div className="flex items-center justify-between">
1025 <div>
1026 <p className="text-sm text-gray-600 dark:text-gray-400">
1027 {__("Revenue Recovered")}
1028 </p>
1029 <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
1030 ${(stats.total_recovered_value || 0).toLocaleString()}
1031 </p>
1032 </div>
1033 <DollarSign className="w-8 h-8 text-purple-600 dark:text-purple-400" />
1034 </div>
1035 </CardContent>
1036 </Card>
1037 </div>
1038 )}
1039
1040 {/* Recent Abandoned Bookings */}
1041 <Card>
1042 <CardHeader>
1043 <CardTitle>{__("Recent Abandoned Bookings")}</CardTitle>
1044 <CardDescription>
1045 {__("Latest bookings that were started but not completed")}
1046 </CardDescription>
1047 </CardHeader>
1048 <CardContent>
1049 {isLoading ? (
1050 <div className="space-y-4">
1051 <SkeletonBookingCard />
1052 <SkeletonBookingCard />
1053 <SkeletonBookingCard />
1054 <SkeletonBookingCard />
1055 <SkeletonBookingCard />
1056 </div>
1057 ) : bookings.length === 0 ? (
1058 <div className="text-center py-8 text-gray-500">
1059 {__("No abandoned bookings found")}
1060 </div>
1061 ) : (
1062 <div className="space-y-4">
1063 {bookings.slice(0, 5).map((booking: any) => (
1064 <div
1065 key={booking.id}
1066 className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
1067 >
1068 <div className="flex-1">
1069 <p className="font-medium">
1070 {booking.customer_name || __("Unknown Customer")}
1071 </p>
1072 <p className="text-sm text-gray-600 dark:text-gray-400">
1073 {booking.customer_email}
1074 </p>
1075 <p className="text-sm text-gray-500">
1076 {booking.trip_name}
1077 </p>
1078 </div>
1079 <div className="text-right mr-4">
1080 <p className="font-semibold">
1081 {formatPrice(booking.total_amount)}
1082 </p>
1083 <p className="text-sm text-gray-500">
1084 {booking.travelers_count} {__("travelers")}
1085 </p>
1086 </div>
1087 <Badge
1088 variant={
1089 booking.status === "recovered" ? "success" : "default"
1090 }
1091 >
1092 {booking.status}
1093 </Badge>
1094 <Button
1095 size="sm"
1096 variant="outline"
1097 className="ml-4"
1098 onClick={() => {
1099 setConfirmDialog({
1100 isOpen: true,
1101 title: __("Send Recovery Email"),
1102 message: __(
1103 "Are you sure you want to send a recovery email to {email}?",
1104 ).replace("{email}", booking.customer_email),
1105 onConfirm: () => {
1106 sendEmailMutation.mutate(booking.id);
1107 setConfirmDialog({
1108 isOpen: false,
1109 title: "",
1110 message: "",
1111 onConfirm: () => {},
1112 });
1113 },
1114 });
1115 }}
1116 disabled={sendEmailMutation.isPending}
1117 >
1118 <Send className="w-4 h-4 mr-2" />
1119 {__("Send Email")}
1120 </Button>
1121 </div>
1122 ))}
1123 </div>
1124 )}
1125 </CardContent>
1126 </Card>
1127 </div>
1128 )}
1129
1130 {/* Bookings Tab */}
1131 {activeTab === "bookings" && (
1132 <div className="space-y-3">
1133 {/* Search and Filters */}
1134 <Card>
1135 <CardContent className="p-3">
1136 <SearchFilterToolbar
1137 searchTerm={searchTerm}
1138 onSearchChange={setSearchTerm}
1139 statusFilter={statusFilter}
1140 onStatusChange={(value) => {
1141 setStatusFilter(value);
1142 setPage(1);
1143 setSelectedIds([]);
1144 setBulkAction("");
1145 }}
1146 statusOptions={[
1147 { value: "all", label: __("All Status") },
1148 { value: "abandoned", label: __("Abandoned") },
1149 { value: "contacted", label: __("Contacted") },
1150 { value: "recovered", label: __("Recovered") },
1151 { value: "expired", label: __("Expired") },
1152 ]}
1153 sortBy="created_at"
1154 onSortByChange={() => {}}
1155 sortOrder="desc"
1156 onSortOrderChange={() => {}}
1157 sortOptions={[
1158 { value: "created_at", label: __("Date") },
1159 { value: "customer_email", label: __("Email") },
1160 ]}
1161 onResetFilters={handleResetFilters}
1162 hasFilters={hasFilters}
1163 placeholder={__("Search by customer email...")}
1164 />
1165 </CardContent>
1166 </Card>
1167
1168 {/* Bulk Actions Toolbar */}
1169 <BulkActionToolbar
1170 selectedIds={selectedIds}
1171 bulkAction={bulkAction}
1172 setBulkAction={setBulkAction}
1173 onApply={handleBulkApply}
1174 onClearSelection={() => setSelectedIds([])}
1175 statusFilter={statusFilter}
1176 setStatusFilter={(value) => {
1177 setStatusFilter(value);
1178 setPage(1);
1179 setSelectedIds([]);
1180 setBulkAction("");
1181 }}
1182 statusOptions={viewFilters}
1183 bulkMutationPending={deleteMutation.isPending}
1184 totalItems={bookings.length}
1185 bulkActionOptions={[
1186 { value: "", label: __("Bulk Actions") },
1187 { value: "delete", label: __("Delete") },
1188 ]}
1189 showColumnsDropdown={false}
1190 setShowColumnsDropdown={() => {}}
1191 columnOptions={[]}
1192 onToggleColumn={() => {}}
1193 />
1194
1195 {/* Table */}
1196 <Card className="overflow-visible">
1197 <CardContent className="p-0 overflow-visible">
1198 <SharedTable
1199 data={bookings}
1200 columns={columns}
1201 actions={actions}
1202 isLoading={isLoading}
1203 selectedItemIds={selectedIds}
1204 onSelectItem={(id, checked) => {
1205 if (checked) {
1206 setSelectedIds([...selectedIds, id]);
1207 } else {
1208 setSelectedIds(
1209 selectedIds.filter((selectedId) => selectedId !== id),
1210 );
1211 }
1212 }}
1213 onSelectAll={(checked) => {
1214 if (checked) {
1215 setSelectedIds(bookings.map((b: any) => b.id));
1216 } else {
1217 setSelectedIds([]);
1218 }
1219 }}
1220 isAllSelected={
1221 selectedIds.length === bookings.length && bookings.length > 0
1222 }
1223 emptyText={__("No abandoned bookings found")}
1224 emptyDescription={__(
1225 "Abandoned bookings will appear here when customers start but don't complete the booking process",
1226 )}
1227 />
1228 </CardContent>
1229 </Card>
1230
1231 {/* Pagination */}
1232 {pagination.total_pages > 1 && (
1233 <Pagination
1234 currentPage={pagination.current_page}
1235 totalPages={pagination.total_pages}
1236 onPageChange={setPage}
1237 totalItems={pagination.total}
1238 itemsPerPage={perPage}
1239 />
1240 )}
1241 </div>
1242 )}
1243
1244 {/* Settings Tab */}
1245 {activeTab === "settings" && (
1246 <div className="space-y-6">
1247 {/* General Settings */}
1248 <Card>
1249 <CardHeader>
1250 <CardTitle>{__("General Settings")}</CardTitle>
1251 <CardDescription>
1252 {__("Configure abandoned booking tracking")}
1253 </CardDescription>
1254 </CardHeader>
1255 <CardContent>
1256 <div className="flex items-center justify-between">
1257 <div className="flex-1">
1258 <Label
1259 htmlFor="tracking_enabled"
1260 className="text-base font-medium"
1261 >
1262 {__("Enable Tracking")}
1263 </Label>
1264 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1265 {__("Track when customers abandon the booking process")}
1266 </p>
1267 </div>
1268 <Toggle
1269 checked={settings.tracking_enabled}
1270 onChange={(checked) =>
1271 handleSettingChange("tracking_enabled", checked)
1272 }
1273 />
1274 </div>
1275 </CardContent>
1276 </Card>
1277
1278 {/* Email Timing Settings */}
1279 <Card>
1280 <CardHeader>
1281 <CardTitle>{__("Email Timing")}</CardTitle>
1282 <CardDescription>
1283 {__("Configure when recovery emails are sent")}
1284 </CardDescription>
1285 </CardHeader>
1286 <CardContent className="space-y-4">
1287 <div>
1288 <Label htmlFor="first_email_delay">
1289 {__("First Email Delay (hours)")}
1290 </Label>
1291 <Input
1292 id="first_email_delay"
1293 type="number"
1294 min="0"
1295 value={settings.first_email_delay_hours}
1296 onChange={(e) =>
1297 handleSettingChange(
1298 "first_email_delay_hours",
1299 parseInt(e.target.value),
1300 )
1301 }
1302 placeholder="1"
1303 />
1304 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1305 {__("Send first recovery email after this many hours")}
1306 </p>
1307 </div>
1308
1309 <div>
1310 <Label htmlFor="second_email_delay">
1311 {__("Second Email Delay (hours)")}
1312 </Label>
1313 <Input
1314 id="second_email_delay"
1315 type="number"
1316 min="0"
1317 value={settings.second_email_delay_hours}
1318 onChange={(e) =>
1319 handleSettingChange(
1320 "second_email_delay_hours",
1321 parseInt(e.target.value),
1322 )
1323 }
1324 placeholder="24"
1325 />
1326 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1327 {__("Send second recovery email after this many hours")}
1328 </p>
1329 </div>
1330
1331 <div>
1332 <Label htmlFor="final_email_delay">
1333 {__("Final Email Delay (hours)")}
1334 </Label>
1335 <Input
1336 id="final_email_delay"
1337 type="number"
1338 min="0"
1339 value={settings.final_email_delay_hours}
1340 onChange={(e) =>
1341 handleSettingChange(
1342 "final_email_delay_hours",
1343 parseInt(e.target.value),
1344 )
1345 }
1346 placeholder="72"
1347 />
1348 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1349 {__("Send final recovery email after this many hours")}
1350 </p>
1351 </div>
1352 </CardContent>
1353 </Card>
1354
1355 {/* Email Templates */}
1356 <Card>
1357 <CardHeader>
1358 <CardTitle>{__("Email Templates")}</CardTitle>
1359 <CardDescription>
1360 {__("Customize recovery email subjects and messages")}
1361 </CardDescription>
1362 </CardHeader>
1363 <CardContent className="space-y-6">
1364 {/* First Email */}
1365 <div className="space-y-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1366 <h4 className="font-medium">{__("First Recovery Email")}</h4>
1367 <div>
1368 <div className="flex items-center justify-between">
1369 <Label htmlFor="first_email_subject">{__("Subject")}</Label>
1370 <Button
1371 type="button"
1372 variant="outline"
1373 size="sm"
1374 onClick={() => showEmailPreview("first")}
1375 className="h-8 mb-2"
1376 >
1377 <Eye className="w-4 h-4 mr-2" />
1378 {__("Preview")}
1379 </Button>
1380 </div>
1381 <Input
1382 id="first_email_subject"
1383 value={settings.first_email_subject}
1384 onChange={(e) =>
1385 handleSettingChange("first_email_subject", e.target.value)
1386 }
1387 placeholder={__("Complete Your Booking - {trip_name}")}
1388 />
1389 </div>
1390 <div>
1391 <div className="flex items-center justify-between">
1392 <Label htmlFor="first_email_message">{__("Message")}</Label>
1393 <Button
1394 type="button"
1395 variant="outline"
1396 size="sm"
1397 onClick={() => handleResetEmailTemplate("first")}
1398 className="h-8 mb-2"
1399 title={__("Reset to default template")}
1400 >
1401 <RotateCcw className="w-4 h-4 mr-2" />
1402 {__("Reset")}
1403 </Button>
1404 </div>
1405 <textarea
1406 id="first_email_message"
1407 value={settings.first_email_message}
1408 onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
1409 handleSettingChange("first_email_message", e.target.value)
1410 }
1411 rows={4}
1412 placeholder={__(
1413 "Hi {customer_name}, we noticed you started booking {trip_name}...",
1414 )}
1415 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
1416 />
1417 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
1418 {__(
1419 "Available placeholders: {customer_name}, {trip_name}, {trip_link}",
1420 )}
1421 </p>
1422 </div>
1423 </div>
1424
1425 {/* Second Email */}
1426 <div className="space-y-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1427 <h4 className="font-medium">{__("Second Recovery Email")}</h4>
1428 <div>
1429 <div className="flex items-center justify-between">
1430 <Label htmlFor="second_email_subject">
1431 {__("Subject")}
1432 </Label>
1433 <Button
1434 type="button"
1435 variant="outline"
1436 size="sm"
1437 onClick={() => showEmailPreview("second")}
1438 className="h-8 mb-2"
1439 >
1440 <Eye className="w-4 h-4 mr-2" />
1441 {__("Preview")}
1442 </Button>
1443 </div>
1444 <Input
1445 id="second_email_subject"
1446 value={settings.second_email_subject}
1447 onChange={(e) =>
1448 handleSettingChange(
1449 "second_email_subject",
1450 e.target.value,
1451 )
1452 }
1453 placeholder={__("Still Interested? Complete Your Booking")}
1454 />
1455 </div>
1456 <div>
1457 <div className="flex items-center justify-between">
1458 <Label htmlFor="second_email_message">
1459 {__("Message")}
1460 </Label>
1461 <Button
1462 type="button"
1463 variant="outline"
1464 size="sm"
1465 onClick={() => handleResetEmailTemplate("second")}
1466 className="h-8 mb-2"
1467 title={__("Reset to default template")}
1468 >
1469 <RotateCcw className="w-4 h-4 mr-2" />
1470 {__("Reset")}
1471 </Button>
1472 </div>
1473 <textarea
1474 id="second_email_message"
1475 value={settings.second_email_message}
1476 onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
1477 handleSettingChange(
1478 "second_email_message",
1479 e.target.value,
1480 )
1481 }
1482 rows={4}
1483 placeholder={__("Still interested in {trip_name}?")}
1484 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
1485 />
1486 </div>
1487 </div>
1488
1489 {/* Final Email */}
1490 <div className="space-y-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
1491 <h4 className="font-medium">{__("Final Recovery Email")}</h4>
1492 <div>
1493 <div className="flex items-center justify-between">
1494 <Label htmlFor="final_email_subject">{__("Subject")}</Label>
1495 <Button
1496 type="button"
1497 variant="outline"
1498 size="sm"
1499 onClick={() => showEmailPreview("final")}
1500 className="h-8 mb-2"
1501 >
1502 <Eye className="w-4 h-4 mr-2" />
1503 {__("Preview")}
1504 </Button>
1505 </div>
1506 <Input
1507 id="final_email_subject"
1508 value={settings.final_email_subject}
1509 onChange={(e) =>
1510 handleSettingChange("final_email_subject", e.target.value)
1511 }
1512 placeholder={__("Last Chance - Your Booking Expires Soon")}
1513 />
1514 </div>
1515 <div>
1516 <div className="flex items-center justify-between">
1517 <Label htmlFor="final_email_message">{__("Message")}</Label>
1518 <Button
1519 type="button"
1520 variant="outline"
1521 size="sm"
1522 onClick={() => handleResetEmailTemplate("final")}
1523 className="h-8 mb-2"
1524 title={__("Reset to default template")}
1525 >
1526 <RotateCcw className="w-4 h-4 mr-2" />
1527 {__("Reset")}
1528 </Button>
1529 </div>
1530 <textarea
1531 id="final_email_message"
1532 value={settings.final_email_message}
1533 onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
1534 handleSettingChange("final_email_message", e.target.value)
1535 }
1536 rows={4}
1537 placeholder={__(
1538 "This is your last chance to complete your booking for {trip_name}",
1539 )}
1540 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
1541 />
1542 </div>
1543 </div>
1544 </CardContent>
1545 </Card>
1546
1547 {/* Save Button */}
1548 <div className="flex justify-end">
1549 <Button
1550 onClick={handleSaveSettings}
1551 disabled={isSavingSettings}
1552 size="lg"
1553 >
1554 {isSavingSettings ? __("Saving...") : __("Save Settings")}
1555 </Button>
1556 </div>
1557 </div>
1558 )}
1559
1560 {/* Confirmation Dialog */}
1561 <ConfirmationDialog
1562 isOpen={confirmDialog.isOpen}
1563 onClose={() =>
1564 setConfirmDialog({
1565 isOpen: false,
1566 title: "",
1567 message: "",
1568 onConfirm: () => {},
1569 })
1570 }
1571 onConfirm={confirmDialog.onConfirm}
1572 title={confirmDialog.title}
1573 message={confirmDialog.message}
1574 />
1575
1576 {/* Email Preview Modal */}
1577 <Modal
1578 isOpen={emailPreview.isOpen}
1579 onClose={() => setEmailPreview({ ...emailPreview, isOpen: false })}
1580 title={
1581 (emailPreview.type === "first" &&
1582 __("First Recovery Email Preview")) ||
1583 (emailPreview.type === "second" &&
1584 __("Second Recovery Email Preview")) ||
1585 (emailPreview.type === "final" && __("Final Recovery Email Preview"))
1586 }
1587 description={__(
1588 "This is how your email will look to customers. Placeholders are replaced with sample data.",
1589 )}
1590 size="xl"
1591 showCloseButton={true}
1592 footer={
1593 <div className="flex justify-end">
1594 <Button
1595 variant="outline"
1596 onClick={() =>
1597 setEmailPreview({ ...emailPreview, isOpen: false })
1598 }
1599 >
1600 {__("Close")}
1601 </Button>
1602 </div>
1603 }
1604 >
1605 <div className="space-y-4 max-h-[60vh] overflow-y-auto">
1606 {/* Email Subject */}
1607 <div>
1608 <Label className="text-sm font-medium text-gray-700 dark:text-gray-300">
1609 {__("Subject:")}
1610 </Label>
1611 <div className="mt-1 p-3 bg-gray-50 dark:bg-gray-900 rounded-md border border-gray-200 dark:border-gray-700">
1612 <p className="text-gray-900 dark:text-white">
1613 {emailPreview.subject}
1614 </p>
1615 </div>
1616 </div>
1617
1618 {/* Email Message */}
1619 <div>
1620 <Label className="text-sm font-medium text-gray-700 dark:text-gray-300">
1621 {__("Message:")}
1622 </Label>
1623 <div className="mt-1 p-4 bg-gray-50 dark:bg-gray-900 rounded-md border border-gray-200 dark:border-gray-700">
1624 <div className="prose prose-sm max-w-none dark:prose-invert">
1625 <div
1626 dangerouslySetInnerHTML={{
1627 __html: emailPreview.message.replace(/\n/g, "<br>"),
1628 }}
1629 />
1630 </div>
1631 </div>
1632 </div>
1633
1634 {/* Available Placeholders */}
1635 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
1636 <Label className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">
1637 {__("Available Placeholders:")}
1638 </Label>
1639 <div className="grid grid-cols-2 gap-2 text-xs">
1640 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1641 {"{customer_name}"}
1642 </code>
1643 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1644 {"{customer_email}"}
1645 </code>
1646 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1647 {"{trip_name}"}
1648 </code>
1649 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1650 {"{trip_id}"}
1651 </code>
1652 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1653 {"{departure_date}"}
1654 </code>
1655 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1656 {"{travelers_count}"}
1657 </code>
1658 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1659 {"{total_amount}"}
1660 </code>
1661 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1662 {"{trip_link}"}
1663 </code>
1664 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1665 {"{site_name}"}
1666 </code>
1667 <code className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-gray-100">
1668 {"{site_url}"}
1669 </code>
1670 </div>
1671 </div>
1672
1673 {/* Email Footer */}
1674 <div className="border-t border-gray-200 dark:border-gray-700 pt-4">
1675 <p className="text-xs text-gray-500 dark:text-gray-400 italic">
1676 {__(
1677 "Note: This preview uses sample data. Actual emails will contain customer-specific information.",
1678 )}
1679 </p>
1680 </div>
1681 </div>
1682 </Modal>
1683
1684 {/* View Details Modal */}
1685 <Modal
1686 isOpen={viewModalOpen}
1687 onClose={() => {
1688 setViewModalOpen(false);
1689 setSelectedBooking(null);
1690 }}
1691 title={__("Booking Details")}
1692 size="lg"
1693 footer={
1694 <div className="flex justify-end space-x-2">
1695 <Button
1696 variant="outline"
1697 onClick={() => {
1698 setViewModalOpen(false);
1699 setSelectedBooking(null);
1700 }}
1701 >
1702 {__("Close")}
1703 </Button>
1704 </div>
1705 }
1706 >
1707 {isModalLoading ? (
1708 <div className="space-y-4">
1709 <Skeleton className="h-6 w-32" />
1710 <Skeleton className="h-4 w-full" />
1711 <Skeleton className="h-4 w-3/4" />
1712 </div>
1713 ) : modalBookingData ? (
1714 <div className="space-y-6">
1715 {/* Customer Information */}
1716 <div>
1717 <h3 className="text-lg font-semibold mb-3">
1718 {__("Customer Information")}
1719 </h3>
1720 <div className="grid grid-cols-2 gap-4">
1721 <div>
1722 <Label className="text-sm text-gray-500">{__("Name")}</Label>
1723 <p className="font-medium">
1724 {modalBookingData.customer_name || __("Unknown")}
1725 </p>
1726 </div>
1727 <div>
1728 <Label className="text-sm text-gray-500">{__("Email")}</Label>
1729 <p className="font-medium">
1730 {modalBookingData.customer_email || __("Not provided")}
1731 </p>
1732 </div>
1733 <div>
1734 <Label className="text-sm text-gray-500">{__("Phone")}</Label>
1735 <p className="font-medium">
1736 {modalBookingData.customer_phone || __("Not provided")}
1737 </p>
1738 </div>
1739 <div>
1740 <Label className="text-sm text-gray-500">
1741 {__("Session ID")}
1742 </Label>
1743 <p className="font-mono text-sm">
1744 {modalBookingData.session_id}
1745 </p>
1746 </div>
1747 </div>
1748 </div>
1749
1750 {/* Trip Information */}
1751 <div>
1752 <h3 className="text-lg font-semibold mb-3">
1753 {__("Trip Information")}
1754 </h3>
1755 <div className="grid grid-cols-2 gap-4">
1756 <div>
1757 <Label className="text-sm text-gray-500">{__("Trip")}</Label>
1758 <p className="font-medium">
1759 {modalBookingData.trip_name || __("Unknown Trip")}
1760 </p>
1761 </div>
1762 <div>
1763 <Label className="text-sm text-gray-500">
1764 {__("Departure Date")}
1765 </Label>
1766 <p className="font-medium">
1767 {modalBookingData.departure_date || __("Not set")}
1768 </p>
1769 </div>
1770 <div>
1771 <Label className="text-sm text-gray-500">
1772 {__("Travelers")}
1773 </Label>
1774 <p className="font-medium">
1775 {modalBookingData.travelers_count || 1}
1776 </p>
1777 </div>
1778 <div>
1779 <Label className="text-sm text-gray-500">
1780 {__("Total Amount")}
1781 </Label>
1782 <p className="font-medium">
1783 {formatPrice(modalBookingData.total_amount || 0)}
1784 </p>
1785 </div>
1786 </div>
1787 </div>
1788
1789 {/* Status and Dates */}
1790 <div>
1791 <h3 className="text-lg font-semibold mb-3">
1792 {__("Status & Dates")}
1793 </h3>
1794 <div className="grid grid-cols-2 gap-4">
1795 <div>
1796 <Label className="text-sm text-gray-500">
1797 {__("Status")}
1798 </Label>
1799 <Badge
1800 variant={
1801 modalBookingData.status === "recovered"
1802 ? "success"
1803 : "warning"
1804 }
1805 >
1806 {modalBookingData.status}
1807 </Badge>
1808 </div>
1809 <div>
1810 <Label className="text-sm text-gray-500">
1811 {__("Created")}
1812 </Label>
1813 <p className="font-medium">
1814 {new Date(modalBookingData.created_at).toLocaleString()}
1815 </p>
1816 </div>
1817 <div>
1818 <Label className="text-sm text-gray-500">
1819 {__("Expires")}
1820 </Label>
1821 <p className="font-medium">
1822 {new Date(modalBookingData.expires_at).toLocaleString()}
1823 </p>
1824 </div>
1825 <div>
1826 <Label className="text-sm text-gray-500">
1827 {__("Emails Sent")}
1828 </Label>
1829 <p className="font-medium">
1830 {modalBookingData.recovery_emails_sent || 0}
1831 </p>
1832 </div>
1833 </div>
1834 </div>
1835
1836 {/* Form Data (Debug) */}
1837 {modalBookingData.booking_data && (
1838 <div>
1839 <h3 className="text-lg font-semibold mb-3">
1840 {__("Form Data")}
1841 </h3>
1842 <div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-md max-h-40 overflow-y-auto">
1843 <pre className="text-xs">
1844 {JSON.stringify(modalBookingData.booking_data, null, 2)}
1845 </pre>
1846 </div>
1847 </div>
1848 )}
1849 </div>
1850 ) : (
1851 <p className="text-gray-500">{__("No booking data available.")}</p>
1852 )}
1853 </Modal>
1854 </div>
1855 );
1856 };
1857
1858 export default AbandonedRecoveryPage;
1859