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

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

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