'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 }) => (
);
const SectionHeader = ({ title }: { title: string }) => (
{title}
);
const TIER_COLORS: Record = {
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({ page: 1, pageSize: 20, search: '', role: 'PASSENGER' });
const [showExtraFilters, setShowExtraFilters] = useState(false);
const [extraFilters, setExtraFilters] = useState({ gender: '', nationality: '', dateFrom: '', dateTo: '' });
const [selectedPassenger, setSelectedPassenger] = useState(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; passenger: any | null }>({ isOpen: false, passenger: null });
const [deleteError, setDeleteError] = useState(null);
const [deleteCascade, setDeleteCascade] = useState(false);
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>({
fullName: true, email: true, phone: true, gender: true, nationality: true, verified: true,
});
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: ({ id, cascade }: { id: string; cascade: boolean }) => passengersApi.delete(id, cascade),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['passengers'] });
setDeleteConfirm({ isOpen: false, passenger: null });
setDeleteError(null);
setDeleteCascade(false);
},
onError: (error: any) => {
setDeleteError(error?.response?.data?.message || error?.message || 'Failed to delete passenger');
},
});
const { data, isLoading, error } = useQuery({
queryKey: ['passengers', filters, extraFilters],
queryFn: () => passengersApi.getAll({
...filters,
...(extraFilters.gender && { gender: extraFilters.gender }),
...(extraFilters.nationality && { nationality: extraFilters.nationality }),
...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }),
...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }),
}),
});
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(`Passengers Export`);
w.document.write(`Passengers Export — ${dateStr}
${headers.map(h => `| ${h} | `).join('')}
`);
rows.forEach((r: string[]) => { w.document.write(`${r.map((v: string) => `| ${v} | `).join('')}
`); });
w.document.write('
');
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) => (
),
},
{ 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) => (
{p.faydaVerified ? 'Verified' : 'Unverified'}
),
},
];
const actions = [
{ label: 'View Details', onClick: (p: any) => setSelectedPassenger(p), variant: 'secondary' as const, icon: Eye },
{ label: 'Delete', onClick: (p: any) => { setDeleteError(null); setDeleteCascade(false); setDeleteConfirm({ isOpen: true, passenger: p }); }, variant: 'danger' as const, icon: Trash2 },
];
return (
Passengers
Manage passenger profiles and verification
setExportModalOpen(true)}>Export
{error && (
Error loading passengers: {error instanceof Error ? error.message : 'Unknown error'}
)}
{showExtraFilters && (
)}
{data?.meta && (
setFilters({ ...filters, page })} />
)}
{ setDeleteConfirm({ isOpen: false, passenger: null }); setDeleteError(null); setDeleteCascade(false); }}
onConfirm={async () => {
if (deleteConfirm.passenger) {
await deleteMutation.mutateAsync({ id: deleteConfirm.passenger.id, cascade: deleteCascade });
}
}}
title="Delete Passenger"
message={`Are you sure you want to delete ${deleteConfirm.passenger?.fullName}?`}
confirmText="Delete" isDanger
isLoading={deleteMutation.isPending}
error={deleteError ?? undefined}
cascadeWarning={deleteError ? 'This passenger has related records (bookings, loyalty, wallet). Check the box below to force delete everything.' : undefined}
cascadeChecked={deleteCascade}
onCascadeChange={setDeleteCascade}
/>
{/* Passenger Details Modal */}
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 (
{/* Gradient header with avatar */}
{(p.fullName || p.email || '?')[0].toUpperCase()}
{isVerified ? '✓ Verified' : 'Unverified'}
{tier && (
{tier}
)}
{/* Quick stats */}
{[
{ 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 }) => (
))}
{/* Personal */}
{/* Contact */}
{/* Identification */}
Fayda (National ID)
{isVerified
?
: }
{isVerified ? 'Verified' : 'Not verified'}
{p.faydaVerifiedAt &&
{formatDateTime(p.faydaVerifiedAt)}
}
{/* Loyalty & Wallet */}
{(p.passenger?.loyalty || p.loyalty || p.passenger?.wallet || p.wallet) && (
{(p.passenger?.loyalty || p.loyalty) && (() => {
const loyalty = p.passenger?.loyalty || p.loyalty;
return (
<>
>
);
})()}
{(p.passenger?.wallet || p.wallet) && (() => {
const wallet = p.passenger?.wallet || p.wallet;
return (
Wallet Balance
ETB {((wallet.balanceMinor ?? 0) / 100).toFixed(2)}
);
})()}
)}
{/* Account */}
{/* Timestamps */}
setSelectedPassenger(null)}>Close
);
})()}
setExportModalOpen(false)} title="Export Passengers" size="md">
setExportModalOpen(false)}>Cancel
Export
);
}