mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
441 lines
24 KiB
TypeScript
441 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Download, Eye, Trash2, ShieldCheck, ShieldOff, Star, Wallet } 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 ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { passengersApi, apiClient } from '@/lib/api';
|
|
import { formatDate, formatDateTime } from '@/lib/utils';
|
|
import { PassengerFilters } 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>
|
|
);
|
|
|
|
const TIER_COLORS: Record<string, string> = {
|
|
BRONZE: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800',
|
|
SILVER: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600',
|
|
GOLD: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800',
|
|
PLATINUM: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800',
|
|
};
|
|
|
|
export default function PassengersPage() {
|
|
const [filters, setFilters] = useState<PassengerFilters>({ page: 1, pageSize: 20, search: '', role: 'PASSENGER' });
|
|
const [showExtraFilters, setShowExtraFilters] = useState(false);
|
|
const [extraFilters, setExtraFilters] = useState({ gender: '', nationality: '', dateFrom: '', dateTo: '' });
|
|
const [selectedPassenger, setSelectedPassenger] = useState<any>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; passenger: any | null }>({ isOpen: false, passenger: null });
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
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>>({
|
|
fullName: true, email: true, phone: true, gender: true, nationality: true, verified: true,
|
|
});
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/passengers/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['passengers'] });
|
|
setDeleteConfirm({ isOpen: false, passenger: null });
|
|
setDeleteError(null);
|
|
},
|
|
onError: (error: any) => {
|
|
setDeleteError(error?.response?.data?.message || error?.message || 'Failed to delete passenger');
|
|
},
|
|
});
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['passengers', filters],
|
|
queryFn: () => passengersApi.getAll(filters),
|
|
});
|
|
|
|
const PASSENGER_COLS = [
|
|
{ key: 'fullName', label: 'Full Name' }, { key: 'email', label: 'Email' }, { key: 'phone', label: 'Phone' },
|
|
{ key: 'dateOfBirth', label: 'Date of Birth' }, { key: 'gender', label: 'Gender' },
|
|
{ key: 'nationality', label: 'Nationality' }, { key: 'verified', label: 'Verified' },
|
|
];
|
|
|
|
const confirmExportPassengers = 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
|
|
const allData = await passengersApi.getAll({ ...filters, page: 1, pageSize: 9999 });
|
|
const exportItems = (allData?.items || []).filter((p: any) => {
|
|
if (!exportDateFrom && !exportDateTo) return true;
|
|
const d = p.createdAt ? new Date(p.createdAt).toISOString().split('T')[0] : null;
|
|
if (exportDateFrom && (!d || d < exportDateFrom)) return false;
|
|
if (exportDateTo && (!d || d > exportDateTo)) return false;
|
|
return true;
|
|
});
|
|
const headers = PASSENGER_COLS.filter(c => cols.includes(c.key)).map(c => c.label);
|
|
const rows = exportItems.map((p: any) =>
|
|
PASSENGER_COLS.filter(c => cols.includes(c.key)).map(({ key }) => {
|
|
switch (key) {
|
|
case 'fullName': return p.fullName || '';
|
|
case 'email': return p.email || '';
|
|
case 'phone': return p.phone || '';
|
|
case 'dateOfBirth': return p.dateOfBirth ? formatDate(p.dateOfBirth) : '';
|
|
case 'gender': return p.gender || '';
|
|
case 'nationality': return p.nationality || '';
|
|
case 'verified': return p.faydaVerified ? 'Yes' : 'No';
|
|
default: return '';
|
|
}
|
|
})
|
|
);
|
|
const dateStr = new Date().toISOString().split('T')[0];
|
|
if (exportFormat === 'pdf') {
|
|
const w = window.open('', '_blank')!;
|
|
w.document.write(`<!DOCTYPE html><html><head><title>Passengers 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>Passengers 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 = `passengers-${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 = `passengers-${dateStr}.csv`; a.click();
|
|
}
|
|
setExportModalOpen(false);
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
key: 'fullName', label: 'Name', sortable: true,
|
|
render: (p: any) => (
|
|
<div>
|
|
<div className="font-medium">{p.fullName}</div>
|
|
<div className="text-sm text-muted-foreground">{p.email}</div>
|
|
</div>
|
|
),
|
|
},
|
|
{ key: 'phone', label: 'Phone', sortable: true, render: (p: any) => p.phone || 'N/A' },
|
|
{ key: 'nationality', label: 'Nationality', sortable: true, render: (p: any) => p.nationality || 'N/A' },
|
|
{ key: 'gender', label: 'Gender', sortable: true, render: (p: any) => p.gender || 'N/A' },
|
|
{ key: 'dateOfBirth', label: 'Date of Birth', sortable: true, render: (p: any) => p.dateOfBirth ? formatDate(p.dateOfBirth) : 'N/A' },
|
|
{
|
|
key: 'verified', label: 'Status',
|
|
render: (p: any) => (
|
|
<Badge variant="status" status={p.faydaVerified ? 'CONFIRMED' : 'PENDING'}>
|
|
{p.faydaVerified ? 'Verified' : 'Unverified'}
|
|
</Badge>
|
|
),
|
|
},
|
|
];
|
|
|
|
const actions = [
|
|
{ label: 'View Details', onClick: (p: any) => setSelectedPassenger(p), variant: 'secondary' as const, icon: Eye },
|
|
{ label: 'Delete', onClick: (p: any) => { setDeleteError(null); setDeleteConfirm({ isOpen: true, passenger: p }); }, 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">Passengers</h1>
|
|
<p className="text-muted-foreground">Manage passenger profiles and verification</p>
|
|
</div>
|
|
<ActionButton variant="export" icon={Download} onClick={() => setExportModalOpen(true)}>Export</ActionButton>
|
|
</div>
|
|
|
|
<div className="card">
|
|
{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 passengers: {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 name, 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.verified?.toString() || ''}
|
|
onChange={(e) => setFilters({ ...filters, verified: e.target.value ? e.target.value === 'true' : undefined, page: 1 })}>
|
|
<option value="">All Passengers</option>
|
|
<option value="true">Verified</option>
|
|
<option value="false">Unverified</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">Gender</label>
|
|
<select className="input" value={extraFilters.gender}
|
|
onChange={(e) => setExtraFilters({ ...extraFilters, gender: e.target.value })}>
|
|
<option value="">All Genders</option>
|
|
<option value="Male">Male</option>
|
|
<option value="Female">Female</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Nationality</label>
|
|
<input type="text" className="input" placeholder="e.g. Ethiopian"
|
|
value={extraFilters.nationality}
|
|
onChange={(e) => setExtraFilters({ ...extraFilters, nationality: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Registered From</label>
|
|
<input type="date" className="input" value={extraFilters.dateFrom}
|
|
onChange={(e) => setExtraFilters({ ...extraFilters, dateFrom: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Registered 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 passengers found" />
|
|
{data?.meta && (
|
|
<Pagination currentPage={data.meta.page} totalPages={data.meta.totalPages}
|
|
onPageChange={(page) => setFilters({ ...filters, page })} />
|
|
)}
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.isOpen}
|
|
onClose={() => { setDeleteConfirm({ isOpen: false, passenger: null }); setDeleteError(null); }}
|
|
onConfirm={async () => {
|
|
if (deleteConfirm.passenger) {
|
|
await deleteMutation.mutateAsync(deleteConfirm.passenger.id);
|
|
}
|
|
}}
|
|
title="Delete Passenger"
|
|
message={`Are you sure you want to delete ${deleteConfirm.passenger?.fullName}?`}
|
|
confirmText="Delete" isDanger
|
|
isLoading={deleteMutation.isPending}
|
|
warning="This passenger may have active bookings, loyalty points, and wallet balance. Deleting will impact these systems and records."
|
|
error={deleteError ?? undefined}
|
|
/>
|
|
|
|
{/* Passenger Details Modal */}
|
|
<Modal isOpen={!!selectedPassenger} onClose={() => setSelectedPassenger(null)} title="Passenger Details" size="xl">
|
|
{selectedPassenger && (() => {
|
|
const p = selectedPassenger;
|
|
const isVerified = !!p.faydaVerified || !!p.nationalId;
|
|
const tier = p.loyalty?.tier || p.loyaltyTier;
|
|
const tierColor = TIER_COLORS[tier] || TIER_COLORS.BRONZE;
|
|
|
|
return (
|
|
<div>
|
|
{/* Gradient header with avatar */}
|
|
<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-center gap-4">
|
|
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center shrink-0">
|
|
<span className="text-white text-2xl font-bold">{(p.fullName || p.email || '?')[0].toUpperCase()}</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white text-xl font-bold truncate">{p.fullName}</p>
|
|
<p className="text-emerald-200 text-sm truncate">{p.email}</p>
|
|
</div>
|
|
<div className="text-right shrink-0 space-y-1">
|
|
<div>
|
|
<Badge variant="status" status={isVerified ? 'CONFIRMED' : 'PENDING'}>
|
|
{isVerified ? '✓ Verified' : 'Unverified'}
|
|
</Badge>
|
|
</div>
|
|
{tier && (
|
|
<span className={`inline-flex items-center gap-1 text-xs font-bold px-2.5 py-0.5 rounded-full border ${tierColor}`}>
|
|
<Star className="w-3 h-3" />{tier}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick stats */}
|
|
<div className="mt-4 grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: 'Loyalty Points', value: (p.passenger?.loyalty?.pointsBalance ?? p.loyalty?.pointsBalance ?? 0).toLocaleString() },
|
|
{ label: 'Wallet Balance', value: p.passenger?.wallet || p.wallet ? `ETB ${((p.passenger?.wallet?.balanceMinor ?? p.wallet?.balanceMinor ?? 0) / 100).toFixed(2)}` : '—' },
|
|
{ label: 'Nationality', value: p.nationality || '—' },
|
|
].map(({ label, value }) => (
|
|
<div key={label} className="bg-white/10 rounded-lg px-3 py-2">
|
|
<p className="text-emerald-200 text-xs">{label}</p>
|
|
<p className="text-white text-sm font-bold truncate">{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{/* Personal */}
|
|
<section>
|
|
<SectionHeader title="Personal Information" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<Field label="Full Name" value={p.fullName} />
|
|
<Field label="Date of Birth" value={p.dateOfBirth ? formatDate(p.dateOfBirth) : ''} />
|
|
<Field label="Gender" value={p.gender} />
|
|
<Field label="Nationality" value={p.nationality} />
|
|
<Field label="Nationality Code" value={p.nationalityCode} />
|
|
<Field label="Preferred Language" value={p.passenger?.preferredLanguage || p.preferredLanguage} />
|
|
<Field label="Last Login" value={p.lastLoginAt ? formatDateTime(p.lastLoginAt) : 'Never'} />
|
|
<Field label="Role" value={p.role} />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Contact */}
|
|
<section>
|
|
<SectionHeader title="Contact Information" />
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
<Field label="Email" value={p.email} />
|
|
<Field label="Phone" value={p.phone} />
|
|
<Field label="Address" value={p.address} />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Identification */}
|
|
<section>
|
|
<SectionHeader title="Identification & Verification" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div className="bg-muted/40 rounded-lg p-3 col-span-2 md:col-span-1">
|
|
<p className="text-xs text-muted-foreground mb-2">Fayda (National ID)</p>
|
|
<div className="flex items-center gap-2">
|
|
{isVerified
|
|
? <ShieldCheck className="w-4 h-4 text-emerald-600 dark:text-emerald-400 shrink-0" />
|
|
: <ShieldOff className="w-4 h-4 text-muted-foreground shrink-0" />}
|
|
<span className={`text-sm font-semibold ${isVerified ? 'text-emerald-700 dark:text-emerald-400' : 'text-muted-foreground'}`}>
|
|
{isVerified ? 'Verified' : 'Not verified'}
|
|
</span>
|
|
</div>
|
|
{p.faydaVerifiedAt && <p className="text-xs text-muted-foreground mt-1">{formatDateTime(p.faydaVerifiedAt)}</p>}
|
|
</div>
|
|
<Field label="Passport Number" value={p.passportNumber} mono />
|
|
<Field label="Passport Country" value={p.passportCountry} />
|
|
<Field label="Passport Expiry" value={p.passportExpiryDate ? formatDate(p.passportExpiryDate) : ''} />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Loyalty & Wallet */}
|
|
{(p.passenger?.loyalty || p.loyalty || p.passenger?.wallet || p.wallet) && (
|
|
<section>
|
|
<SectionHeader title="Loyalty & Wallet" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{(p.passenger?.loyalty || p.loyalty) && (() => {
|
|
const loyalty = p.passenger?.loyalty || p.loyalty;
|
|
return (
|
|
<>
|
|
<div className={`rounded-lg p-3 border ${tierColor}`}>
|
|
<p className="text-xs font-medium mb-1 opacity-70">Tier</p>
|
|
<div className="flex items-center gap-1.5">
|
|
<Star className="w-4 h-4" />
|
|
<span className="text-sm font-bold">{loyalty.tier}</span>
|
|
</div>
|
|
</div>
|
|
<Field label="Points Balance" value={(loyalty.pointsBalance ?? 0).toLocaleString()} />
|
|
<Field label="Lifetime Points" value={(loyalty.lifetimePoints ?? 0).toLocaleString()} />
|
|
</>
|
|
);
|
|
})()}
|
|
{(p.passenger?.wallet || p.wallet) && (() => {
|
|
const wallet = p.passenger?.wallet || p.wallet;
|
|
return (
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-3">
|
|
<p className="text-xs text-blue-600 dark:text-blue-400 mb-1 flex items-center gap-1"><Wallet className="w-3 h-3" />Wallet Balance</p>
|
|
<p className="text-base font-bold text-blue-800 dark:text-blue-300">
|
|
ETB {((wallet.balanceMinor ?? 0) / 100).toFixed(2)}
|
|
</p>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Account */}
|
|
<section>
|
|
<SectionHeader title="Account IDs" />
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<Field label="User ID" value={p.id || p.userId} mono truncate />
|
|
<Field label="Passenger ID" value={p.passenger?.id || p.passengerId} mono truncate />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Timestamps */}
|
|
<section>
|
|
<SectionHeader title="Timestamps" />
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
<Field label="Registered" value={p.createdAt ? formatDateTime(p.createdAt) : ''} />
|
|
<Field label="Last Updated" value={p.updatedAt ? formatDateTime(p.updatedAt) : ''} />
|
|
<Field label="Fayda Verified At" value={p.faydaVerifiedAt ? formatDateTime(p.faydaVerifiedAt) : 'N/A'} />
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-6 mt-2 border-t border-muted">
|
|
<ActionButton variant="secondary" onClick={() => setSelectedPassenger(null)}>Close</ActionButton>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</Modal>
|
|
|
|
<Modal isOpen={exportModalOpen} onClose={() => setExportModalOpen(false)} title="Export Passengers" size="md">
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div><label className="label">Date From (Registered)</label><input type="date" className="input" value={exportDateFrom} onChange={(e) => setExportDateFrom(e.target.value)} /></div>
|
|
<div><label className="label">Date To (Registered)</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">
|
|
{PASSENGER_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="exportFormatP" 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={confirmExportPassengers}>Export</ActionButton>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|