Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx

531 lines
28 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Download, Eye, Trash2 } from 'lucide-react';
import DataTable from '@/components/ui/DataTable';
import Badge from '@/components/ui/Badge';
import Pagination from '@/components/ui/Pagination';
import ActionButton from '@/components/ui/ActionButton';
import Modal from '@/components/ui/Modal';
import { PermissionGuard } from '@/components/layout/PermissionGuard';
import { usePermission } from '@/lib/use-permission';
import { PERMS } from '@/lib/permissions';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import { bookingsApi, apiClient } from '@/lib/api';
import { formatCurrency, formatDateTime } from '@/lib/utils';
import { BookingFilters } from '@/types';
const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => (
<div className="bg-muted/40 rounded-lg p-3">
<p className="text-xs text-muted-foreground mb-1">{label}</p>
<p className={`text-sm font-semibold text-foreground${mono ? ' font-mono' : ''}${truncate ? ' truncate' : ''}`} title={value}>{value || '—'}</p>
</div>
);
const SectionHeader = ({ title }: { title: string }) => (
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-3 flex items-center gap-2">
<span className="w-4 h-px bg-muted-foreground/40 inline-block" />{title}
</h3>
);
function BookingsPageContent() {
const canManage = usePermission(PERMS.bookings.manage);
const [filters, setFilters] = useState<BookingFilters>({ page: 1, pageSize: 20, search: '', status: '' });
const [extraFilters, setExtraFilters] = useState({ bookingType: '', dateFrom: '', dateTo: '', paymentStatus: '' });
const [showExtraFilters, setShowExtraFilters] = useState(false);
const [selectedBooking, setSelectedBooking] = useState<any>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [bookingToDelete, setBookingToDelete] = useState<any>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState('');
const [exportModalOpen, setExportModalOpen] = useState(false);
const [exportFormat, setExportFormat] = useState<'csv' | 'excel' | 'pdf'>('csv');
const [exportDateFrom, setExportDateFrom] = useState('');
const [exportDateTo, setExportDateTo] = useState('');
const [exportColumns, setExportColumns] = useState<Record<string, boolean>>({
bookingRef: true, bookingType: true, passengerNames: true, contactPhone: true,
contactEmail: true, passengerCount: false, paymentStatus: true, totalMinor: true, status: true, createdAt: true,
});
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['bookings', filters, extraFilters],
queryFn: () => bookingsApi.getAll({
...filters,
...(extraFilters.bookingType && { bookingType: extraFilters.bookingType }),
...(extraFilters.paymentStatus && { paymentStatus: extraFilters.paymentStatus }),
...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }),
...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }),
}),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiClient.delete(`/bookings/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bookings'] });
setDeleteConfirmOpen(false);
setBookingToDelete(null);
setDeleteError(null);
setSuccessMessage('Booking deleted successfully');
setTimeout(() => setSuccessMessage(''), 3000);
},
onError: (error: any) => {
setDeleteError(error?.response?.data?.message || error?.message || 'Failed to delete booking');
},
});
const BOOKING_COLS = [
{ key: 'bookingRef', label: 'Booking Reference' }, { key: 'journeyType', label: 'Journey Type' },
{ key: 'passengerNames', label: 'Passenger Names' }, { key: 'contactPhone', label: 'Contact Phone' },
{ key: 'contactEmail', label: 'Contact Email' }, { key: 'passengerCount', label: 'Passenger Count' },
{ key: 'paymentStatus', label: 'Payment Status' }, { key: 'totalMinor', label: 'Amount' },
{ key: 'status', label: 'Status' }, { key: 'createdAt', label: 'Created At' },
];
const confirmExport = async () => {
const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k);
if (!cols.length) { alert('Please select at least one column'); return; }
// Fetch all records (not just current page)
const allData = await bookingsApi.getAll({ ...filters, page: 1, pageSize: 9999 });
const exportItems = (allData?.items || []).filter((b: any) => {
if (!exportDateFrom && !exportDateTo) return true;
const d = b.createdAt ? new Date(b.createdAt).toISOString().split('T')[0] : null;
if (exportDateFrom && (!d || d < exportDateFrom)) return false;
if (exportDateTo && (!d || d > exportDateTo)) return false;
return true;
});
const rows = exportItems.map((booking: any) =>
BOOKING_COLS.filter(c => cols.includes(c.key)).map(({ key }) => {
switch (key) {
case 'bookingRef': return booking.bookingRef;
case 'journeyType': return booking.bookingType || 'N/A';
case 'passengerNames': return booking.passengerNames?.join(', ') || 'N/A';
case 'contactPhone': return booking.contactPhone || 'N/A';
case 'contactEmail': return booking.contactEmail || 'N/A';
case 'passengerCount': return String((booking.adultCount ?? 0) + (booking.childCount ?? 0));
case 'paymentStatus': return booking.paymentIntent?.status || 'PENDING';
case 'totalMinor': return formatCurrency(booking.totalMinor, booking.currency);
case 'status': return booking.status;
case 'createdAt': return booking.createdAt ? formatDateTime(booking.createdAt) : '';
default: return '';
}
})
);
const headers = BOOKING_COLS.filter(c => cols.includes(c.key)).map(c => c.label);
const dateStr = new Date().toISOString().split('T')[0];
if (exportFormat === 'pdf') {
const w = window.open('', '_blank')!;
w.document.write(`<!DOCTYPE html><html><head><title>Bookings Export</title><style>body{font-family:sans-serif;font-size:11px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccc;padding:4px 8px}th{background:#10b981;color:#fff}</style></head><body>`);
w.document.write(`<h2>Bookings Export — ${dateStr}</h2><table><thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead><tbody>`);
rows.forEach((r: string[]) => { w.document.write(`<tr>${r.map((v: string) => `<td>${v}</td>`).join('')}</tr>`); });
w.document.write('</tbody></table></body></html>');
w.document.close();
w.print();
} else if (exportFormat === 'excel') {
const tsv = [headers.join('\t'), ...rows.map((r: string[]) => r.join('\t'))].join('\n');
const blob = new Blob([tsv], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `bookings-${dateStr}.xls`; a.click();
} else {
const csv = [headers.map(h => `"${h}"`).join(','), ...rows.map((r: string[]) => r.map((v: string) => `"${v}"`).join(','))].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `bookings-${dateStr}.csv`; a.click();
}
setExportModalOpen(false);
};
const columns = [
{
key: 'bookingRef', label: 'Reference', sortable: true,
render: (booking: any) => (
<div>
<div className="font-mono font-semibold flex items-center gap-1.5">
{booking.bookingRef}
{booking.isPackageBooking && (
<span className="inline-flex items-center gap-1 bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400 text-xs font-semibold px-1.5 py-0.5 rounded" title="Package booking">
PKG
</span>
)}
</div>
<div className="text-xs text-muted-foreground">{booking.bookingType || 'ONE_WAY'}</div>
</div>
),
},
{
key: 'passengerNames', label: 'Names',
render: (booking: any) => {
const passengers = booking.passengers || [];
if (!passengers.length) {
// Fallback to old logic if passengers array not available
const names: string[] = booking.passengerNames || [];
const adultCount = booking.adultCount || 0;
if (!names.length) return <span className="text-muted-foreground"></span>;
return (
<div className="flex flex-col gap-0.5">
{names.map((name, i) => {
const isAdult = i < adultCount;
const passengerType = isAdult ? 'A' : 'C';
return (
<span key={i} className="text-sm">
{name} ({passengerType})
</span>
);
})}
</div>
);
}
return (
<div className="flex flex-col gap-0.5">
{passengers.map((p: any, i: number) => {
const passengerType = p.category === 'ADULT' ? 'A' : 'C';
return (
<span key={i} className="text-sm">
{p.name} ({passengerType})
</span>
);
})}
</div>
);
},
},
{
key: 'contact', label: 'Primary contact',
render: (booking: any) => (
<div>
<div className="font-medium">{booking.contactPhone || booking.passenger?.phone}</div>
<div className="text-sm text-muted-foreground">{booking.contactEmail || booking.passenger?.email}</div>
</div>
),
},
{
key: 'passengerCount', label: 'Passengers',
render: (booking: any) => {
const adults = booking.adultCount || 0, children = booking.childCount || 0;
if (!adults && !children) return '—';
return <><div>Adult: {adults}</div><div className="text-sm text-muted-foreground">Child: {children}</div></>;
},
},
{
key: 'paymentStatus', label: 'Payment',
render: (booking: any) => (
<div>
<Badge variant="status" status={booking.paymentIntent?.status || 'PENDING'}>{booking.paymentIntent?.status || 'PENDING'}</Badge>
<div className="text-sm text-muted-foreground">{formatCurrency(booking.totalMinor, booking.currency)}</div>
</div>
),
},
{
key: 'status', label: 'Status',
render: (booking: any) => <Badge variant="status" status={booking.status}>{booking.status}</Badge>,
},
];
const actions = [
{ label: 'View Details', onClick: (b: any) => setSelectedBooking(b), variant: 'secondary' as const, icon: Eye },
{ label: 'Delete', onClick: (b: any) => { setDeleteError(null); setBookingToDelete(b); setDeleteConfirmOpen(true); }, variant: 'danger' as const, icon: Trash2 },
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Bookings</h1>
<p className="text-muted-foreground">Manage all passenger bookings</p>
</div>
<ActionButton variant="export" icon={Download} onClick={() => setExportModalOpen(true)}>Export</ActionButton>
</div>
<div className="card">
{successMessage && (
<div className="mb-4 rounded-lg bg-green-50 dark:bg-green-900/20 p-4 text-sm text-green-800 dark:text-green-200"> {successMessage}</div>
)}
{error && (
<div className="mb-4 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-sm text-red-800 dark:text-red-200">
Error loading bookings: {error instanceof Error ? error.message : 'Unknown error'}
</div>
)}
<div className="mb-4 space-y-3">
<div className="flex flex-wrap gap-3">
<div className="flex-1 min-w-48">
<input type="text" placeholder="Search by reference, email, or phone..." className="input"
value={filters.search} onChange={(e) => setFilters({ ...filters, search: e.target.value, page: 1 })} />
</div>
<select className="input w-44" value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value || undefined, page: 1 })}>
<option value="">All Status</option>
<option value="PENDING_PAYMENT">Pending Payment</option>
<option value="CONFIRMED">Confirmed</option>
<option value="CANCELLED">Cancelled</option>
<option value="BOARDED">Boarded</option>
</select>
<select className="input w-44" value={extraFilters.bookingType}
onChange={(e) => setExtraFilters({ ...extraFilters, bookingType: e.target.value })}>
<option value="">All Types</option>
<option value="ONE_WAY">One Way</option>
<option value="ROUND_TRIP">Round Trip</option>
<option value="ROUND_TRIP_TRANSIT">Round Trip Transit</option>
<option value="PACKAGE">Package</option>
</select>
<button type="button" className="input w-auto px-4 text-sm font-medium text-primary border-primary/40"
onClick={() => setShowExtraFilters(v => !v)}>
{showExtraFilters ? 'Hide Filters ▲' : 'More Filters ▼'}
</button>
</div>
{showExtraFilters && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 pt-1">
<div>
<label className="label">Payment Status</label>
<select className="input" value={extraFilters.paymentStatus}
onChange={(e) => setExtraFilters({ ...extraFilters, paymentStatus: e.target.value })}>
<option value="">All Payments</option>
<option value="PAID">Paid</option>
<option value="PENDING">Pending</option>
<option value="FAILED">Failed</option>
<option value="REFUNDED">Refunded</option>
</select>
</div>
<div>
<label className="label">Created From</label>
<input type="date" className="input" value={extraFilters.dateFrom}
onChange={(e) => setExtraFilters({ ...extraFilters, dateFrom: e.target.value })} />
</div>
<div>
<label className="label">Created To</label>
<input type="date" className="input" value={extraFilters.dateTo}
onChange={(e) => setExtraFilters({ ...extraFilters, dateTo: e.target.value })} />
</div>
</div>
)}
</div>
<DataTable data={data?.items || []} columns={columns} actions={actions} loading={isLoading} emptyMessage="No bookings found" />
{data?.meta && (
<Pagination currentPage={data.meta.page} totalPages={data.meta.totalPages}
onPageChange={(page) => setFilters({ ...filters, page })} />
)}
</div>
{/* Booking Details Modal */}
<Modal isOpen={!!selectedBooking} onClose={() => setSelectedBooking(null)} title="Booking Details" size="xl">
{selectedBooking && (() => {
const b = selectedBooking;
const isRoundTrip = b.bookingType === 'ROUND_TRIP' || b.bookingType === 'ROUND_TRIP_TRANSIT';
return (
<div>
{/* Gradient header */}
<div className="-mx-6 -mt-4 mb-6 px-6 py-5 bg-gradient-to-r from-emerald-600 to-emerald-700 rounded-t-lg">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-emerald-100 text-xs font-semibold uppercase tracking-widest mb-1">Booking Reference</p>
<p className="text-white text-3xl font-mono font-bold tracking-wider">{b.bookingRef}</p>
</div>
<div className="text-right shrink-0">
<Badge variant="status" status={b.status}>{b.status}</Badge>
<p className="text-emerald-200 text-xs mt-2">{formatDateTime(b.createdAt)}</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{[
(b.bookingType || 'ONE_WAY').replace(/_/g, ' '),
b.isPackageBooking && b.packageName ? `PKG: ${b.packageName}` : null,
`${b.adultCount ?? 0} Adult${(b.adultCount ?? 0) !== 1 ? 's' : ''}${(b.childCount ?? 0) > 0 ? ` · ${b.childCount} Child${b.childCount !== 1 ? 'ren' : ''}` : ''}`,
b.displayCurrency || b.currency || 'ETB',
].filter(Boolean).map((tag) => (
<span key={tag} className="inline-flex items-center gap-1.5 bg-white/20 text-white text-xs font-medium px-3 py-1 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-200" />{tag}
</span>
))}
</div>
</div>
<div className="space-y-6">
{/* Passenger */}
<section>
<SectionHeader title="Passenger" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Full Name" value={b.passenger?.fullName || b.contactEmail} />
<Field label="Email" value={b.contactEmail || b.passenger?.email} />
<Field label="Phone" value={b.contactPhone || b.passenger?.phone} />
<Field label="Passenger ID" value={b.passengerId} mono truncate />
</div>
</section>
{/* Package info */}
{b.isPackageBooking && (
<section>
<SectionHeader title="Package" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Package Name" value={b.packageName || '—'} />
<Field label="Package Code" value={b.packageCode || '—'} mono />
<Field label="Tier" value={b.tierLabel || '—'} />
<Field label="Package ID" value={b.packageId || '—'} mono truncate />
</div>
</section>
)}
{/* Journey */}
<section>
<SectionHeader title="Journey" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Origin" value={b.schedule?.originStation?.name || b.schedule?.origin?.name} />
<Field label="Destination" value={b.schedule?.destinationStation?.name || b.schedule?.destination?.name} />
<Field label="Departure" value={b.schedule?.departureAt ? formatDateTime(b.schedule.departureAt) : ''} />
<Field label="Arrival" value={b.schedule?.arrivalAt ? formatDateTime(b.schedule.arrivalAt) : ''} />
<Field label="Adults" value={String(b.adultCount ?? 0)} />
<Field label="Children" value={String(b.childCount ?? 0)} />
<Field label="Promo Code" value={b.promoCode || 'None'} />
{!b.isPackageBooking && <Field label="Schedule ID" value={b.scheduleId} mono truncate />}
</div>
</section>
{/* Return leg */}
{isRoundTrip && (
<section>
<SectionHeader title="Return Leg" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Leg Status" value={(b.returnLegStatus || '—').replace(/_/g, ' ')} />
<Field label="Outbound Boarded" value={b.outboundBoardedAt ? formatDateTime(b.outboundBoardedAt) : 'Not yet'} />
<Field label="Return Boarded" value={b.returnBoardedAt ? formatDateTime(b.returnBoardedAt) : 'Not yet'} />
<Field label="Return Schedule ID" value={b.returnScheduleId} mono truncate />
</div>
</section>
)}
{/* Payment */}
<section>
<SectionHeader title="Payment" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-800 rounded-lg p-3 col-span-2">
<p className="text-xs text-emerald-700 dark:text-emerald-400 mb-1">Total Amount</p>
<p className="text-xl font-bold text-emerald-800 dark:text-emerald-300">{formatCurrency(b.totalMinor, b.currency || 'ETB')}</p>
{b.displayCurrency && b.displayCurrency !== (b.currency || 'ETB') && (
<p className="text-xs text-emerald-600 dark:text-emerald-500 mt-0.5">
{formatCurrency(b.displayTotalMinor ?? b.totalMinor, b.displayCurrency)}
</p>
)}
</div>
<div className="bg-muted/40 rounded-lg p-3">
<p className="text-xs text-muted-foreground mb-2">Payment Status</p>
<Badge variant="status" status={b.paymentIntent?.status || 'PENDING'}>{b.paymentIntent?.status || 'PENDING'}</Badge>
</div>
<Field label="Method" value={b.paymentIntent?.method || '—'} />
<Field label="Paid At" value={b.paidAt ? formatDateTime(b.paidAt) : 'Not paid'} />
<Field label="Display Currency" value={b.displayCurrency || b.currency || 'ETB'} />
<Field label="Payment ID" value={b.paymentIntent?.id || '—'} mono truncate />
</div>
</section>
{/* Seats / Passengers */}
{(() => {
const items: any[] = b.seats?.length ? b.seats : (b.passengers?.length ? b.passengers : []);
if (!items.length) return null;
const isSeats = !!b.seats?.length;
return (
<section>
<SectionHeader title={`${isSeats ? 'Seats' : 'Passengers'} (${items.length})`} />
<div className="divide-y divide-muted rounded-lg border border-muted overflow-hidden">
{items.map((p: any, i: number) => (
<div key={i} className="flex items-center justify-between px-4 py-3 bg-muted/20 hover:bg-muted/40 transition-colors">
<div className="flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400 text-xs font-bold flex items-center justify-center shrink-0">{i + 1}</span>
<div>
<p className="text-sm font-semibold">{p.passengerName || p.fullName || p.name || '—'}</p>
<p className="text-xs text-muted-foreground">
{p.passengerCategory || p.category || '—'}{p.leg ? ` · Leg ${p.leg}` : ''}{p.idDocumentType ? ` · ${p.idDocumentType}` : ''}
{p.verifaydaVerified ? ' · ✓ Verified' : ''}
</p>
</div>
</div>
{isSeats && (
<div className="text-right">
<p className="text-sm font-mono font-semibold">{p.seat?.seatNumber || p.seatId || '—'}</p>
<p className="text-xs text-muted-foreground">{formatCurrency(p.fareMinor ?? 0, b.currency || 'ETB')}</p>
</div>
)}
</div>
))}
</div>
</section>
);
})()}
{/* Timestamps */}
<section>
<SectionHeader title="Timestamps & Meta" />
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<Field label="Created" value={formatDateTime(b.createdAt)} />
<Field label="Last Updated" value={formatDateTime(b.updatedAt)} />
<Field label="Source / Device" value={b.source || b.userAgent || '—'} truncate />
</div>
</section>
</div>
<div className="flex justify-end gap-2 pt-6 mt-2 border-t border-muted">
<ActionButton variant="secondary" onClick={() => setSelectedBooking(null)}>Close</ActionButton>
</div>
</div>
);
})()}
</Modal>
<ConfirmDialog
isOpen={deleteConfirmOpen}
onClose={() => { setDeleteConfirmOpen(false); setBookingToDelete(null); setDeleteError(null); }}
onConfirm={async () => { if (bookingToDelete) await deleteMutation.mutateAsync(bookingToDelete.id); }}
title="Delete Booking"
message={`Permanently delete booking ${bookingToDelete?.bookingRef}? This cannot be undone and will release all associated seats.`}
confirmText="Delete" cancelText="Cancel" isLoading={deleteMutation.isPending} isDanger
error={deleteError ?? undefined}
/>
<Modal isOpen={exportModalOpen} onClose={() => setExportModalOpen(false)} title="Export Bookings" size="md">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><label className="label">Date From (Created)</label><input type="date" className="input" value={exportDateFrom} onChange={(e) => setExportDateFrom(e.target.value)} /></div>
<div><label className="label">Date To (Created)</label><input type="date" className="input" value={exportDateTo} onChange={(e) => setExportDateTo(e.target.value)} /></div>
</div>
<div>
<p className="text-sm font-medium mb-2">Select Columns</p>
<div className="space-y-2 max-h-56 overflow-y-auto">
{BOOKING_COLS.map((col) => (
<label key={col.key} className="flex items-center gap-3 p-2 hover:bg-gray-50 dark:hover:bg-gray-900/50 rounded cursor-pointer">
<input type="checkbox" checked={exportColumns[col.key] || false}
onChange={(e) => setExportColumns({ ...exportColumns, [col.key]: e.target.checked })}
className="w-4 h-4 rounded border-gray-300" />
<span className="text-sm font-medium">{col.label}</span>
</label>
))}
</div>
</div>
<div>
<p className="text-sm font-medium mb-2">Export Format</p>
<div className="flex gap-3">
{(['csv', 'excel', 'pdf'] as const).map(fmt => (
<label key={fmt} className="flex items-center gap-2 cursor-pointer">
<input type="radio" name="exportFormat" value={fmt} checked={exportFormat === fmt}
onChange={() => setExportFormat(fmt)} className="w-4 h-4" />
<span className="text-sm font-medium capitalize">{fmt === 'excel' ? 'Excel (.xls)' : fmt === 'pdf' ? 'PDF (Print)' : 'CSV'}</span>
</label>
))}
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<ActionButton variant="secondary" onClick={() => setExportModalOpen(false)}>Cancel</ActionButton>
<ActionButton onClick={confirmExport}>Export</ActionButton>
</div>
</div>
</Modal>
</div>
);
}
export default function BookingsPage() {
return (
<PermissionGuard permission={PERMS.bookings.view}>
<BookingsPageContent />
</PermissionGuard>
);
}