mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
344 lines
13 KiB
TypeScript
344 lines
13 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 ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { passengersApi, apiClient } from '@/lib/api';
|
|
import { formatDate, formatDateTime } from '@/lib/utils';
|
|
import { PassengerFilters } from '@/types';
|
|
|
|
export default function PassengersPage() {
|
|
const [filters, setFilters] = useState<PassengerFilters>({
|
|
page: 1,
|
|
pageSize: 20,
|
|
search: '',
|
|
role: 'PASSENGER',
|
|
});
|
|
const [selectedPassenger, setSelectedPassenger] = useState<any>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; passenger: any | null }>({ isOpen: false, passenger: null });
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/passengers/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['passengers'] });
|
|
},
|
|
});
|
|
|
|
const handleDelete = (passenger: any) => {
|
|
setDeleteConfirm({ isOpen: true, passenger });
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (deleteConfirm.passenger) {
|
|
await deleteMutation.mutateAsync(deleteConfirm.passenger.id);
|
|
setDeleteConfirm({ isOpen: false, passenger: null });
|
|
}
|
|
};
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['passengers', filters],
|
|
queryFn: () => passengersApi.getAll(filters),
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Passengers API Error:', error);
|
|
}
|
|
|
|
const columns = [
|
|
{
|
|
key: 'fullName',
|
|
label: 'Name',
|
|
sortable: true,
|
|
render: (passenger: any) => (
|
|
<div>
|
|
<div className="font-medium">{passenger.fullName}</div>
|
|
<div className="text-sm text-muted-foreground">{passenger.email}</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'phone',
|
|
label: 'Phone',
|
|
render: (passenger: any) => passenger.phone,
|
|
},
|
|
{
|
|
key: 'nationalId',
|
|
label: 'National ID',
|
|
render: (passenger: any) => passenger.nationalId || 'N/A',
|
|
},
|
|
{
|
|
key: 'dateOfBirth',
|
|
label: 'Date of Birth',
|
|
render: (passenger: any) => passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : 'N/A',
|
|
},
|
|
{
|
|
key: 'verified',
|
|
label: 'Status',
|
|
render: (passenger: any) => (
|
|
<Badge variant="status" status={passenger.nationalId ? 'CONFIRMED' : 'PENDING'}>
|
|
{passenger.nationalId ? 'Verified' : 'Unverified'}
|
|
</Badge>
|
|
),
|
|
},
|
|
];
|
|
|
|
const actions = [
|
|
{
|
|
label: 'View Details',
|
|
onClick: (passenger: any) => setSelectedPassenger(passenger),
|
|
variant: 'secondary' as const,
|
|
icon: Eye,
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: handleDelete,
|
|
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>
|
|
<div className="flex gap-2">
|
|
<ActionButton variant="export" icon={Download}>Export</ActionButton>
|
|
</div>
|
|
</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 flex flex-wrap gap-4">
|
|
<div className="flex-1">
|
|
<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-48"
|
|
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>
|
|
</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>
|
|
|
|
{/* Delete Confirmation */}
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.isOpen}
|
|
onClose={() => setDeleteConfirm({ isOpen: false, passenger: null })}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Passenger"
|
|
message={`Are you sure you want to delete ${deleteConfirm.passenger?.fullName}?`}
|
|
confirmText="Delete"
|
|
isDanger={true}
|
|
warning="This passenger may have active bookings, loyalty points, and wallet balance. Deleting will impact these systems and records."
|
|
/>
|
|
|
|
{/* Passenger Details Modal */}
|
|
<Modal
|
|
isOpen={!!selectedPassenger}
|
|
onClose={() => setSelectedPassenger(null)}
|
|
title="Passenger Details"
|
|
size="xl"
|
|
>
|
|
{selectedPassenger && (
|
|
<div className="space-y-6">
|
|
{/* Personal Information */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Personal Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Full Name</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.fullName}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Date of Birth</label>
|
|
<p className="text-lg font-semibold">
|
|
{selectedPassenger.dateOfBirth ? formatDate(selectedPassenger.dateOfBirth) : 'N/A'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Gender</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.gender || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Nationality</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.nationality || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr className="border-muted" />
|
|
|
|
{/* Contact Information */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Contact Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Email</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.email || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Phone</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.phone || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr className="border-muted" />
|
|
|
|
{/* Identification */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Identification</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">National ID</label>
|
|
<p className="text-lg font-mono font-semibold">{selectedPassenger.nationalId || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Passport Number</label>
|
|
<p className="text-lg font-mono font-semibold">{selectedPassenger.passportNumber || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Passport Country</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.passportCountry || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Verification Status</label>
|
|
<div className="mt-1">
|
|
<Badge
|
|
variant="status"
|
|
status={selectedPassenger.nationalId ? 'CONFIRMED' : 'PENDING'}
|
|
>
|
|
{selectedPassenger.nationalId ? 'Verified' : 'Unverified'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr className="border-muted" />
|
|
|
|
{/* Account Information */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Account Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Passenger ID</label>
|
|
<p className="text-sm font-mono">{selectedPassenger.id}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">User ID</label>
|
|
<p className="text-sm font-mono">{selectedPassenger.userId || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Loyalty & Wallet (if available) */}
|
|
{(selectedPassenger.loyalty || selectedPassenger.wallet) && (
|
|
<>
|
|
<hr className="border-muted" />
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{selectedPassenger.loyalty && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">Loyalty Account</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Tier</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.loyalty.tier || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Points Balance</label>
|
|
<p className="text-lg font-semibold">{selectedPassenger.loyalty.pointsBalance || 0}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedPassenger.wallet && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">Wallet</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Balance</label>
|
|
<p className="text-lg font-semibold">
|
|
{(selectedPassenger.wallet.balanceMinor / 100).toFixed(2)} {selectedPassenger.wallet.currency}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<hr className="border-muted" />
|
|
|
|
{/* Timestamps */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Timestamps</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Created</label>
|
|
<p className="text-sm">{selectedPassenger.createdAt ? formatDateTime(selectedPassenger.createdAt) : 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Last Updated</label>
|
|
<p className="text-sm">{selectedPassenger.updatedAt ? formatDateTime(selectedPassenger.updatedAt) : 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<ActionButton
|
|
variant="secondary"
|
|
onClick={() => setSelectedPassenger(null)}
|
|
>
|
|
Close
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|