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

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