PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
3.0.17 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 All 85 releases
yatra / resources / js / pages / AbandonedRecovery.tsx

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

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