PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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.4, at resources/js/pages/AbandonedRecovery.tsx

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