mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Passenger reports filter updates
This commit is contained in:
@@ -311,11 +311,20 @@ export class ReportsService {
|
||||
booking: { status: { in: ['CONFIRMED', 'BOARDED'] } },
|
||||
},
|
||||
include: {
|
||||
booking: { select: { bookingRef: true, status: true } },
|
||||
booking: { select: { bookingRef: true, status: true, originStationId: true, destinationStationId: true } },
|
||||
seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } },
|
||||
},
|
||||
orderBy: [{ seat: { coach: { number: 'asc' } } }],
|
||||
});
|
||||
|
||||
const stationIds = [...new Set(
|
||||
seats.flatMap(bs => [bs.booking.originStationId, bs.booking.destinationStationId]).filter(Boolean) as string[]
|
||||
)];
|
||||
const stations = stationIds.length
|
||||
? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } })
|
||||
: [];
|
||||
const stationMap = new Map(stations.map(s => [s.id, s.name]));
|
||||
|
||||
return seats.map(bs => ({
|
||||
bookingRef: bs.booking.bookingRef,
|
||||
bookingStatus: bs.booking.status,
|
||||
@@ -328,6 +337,8 @@ export class ReportsService {
|
||||
seatLabel: bs.seatLabelSnapshot,
|
||||
coachNumber: bs.seat?.coach?.number ?? null,
|
||||
coachType: (bs.seat?.coach as any)?.coachType?.name ?? null,
|
||||
origin: bs.booking.originStationId ? (stationMap.get(bs.booking.originStationId) ?? null) : null,
|
||||
destination: bs.booking.destinationStationId ? (stationMap.get(bs.booking.destinationStationId) ?? null) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ import { formatDateTime } from '@/lib/utils';
|
||||
|
||||
interface ScheduleOption { id: string; label: string; }
|
||||
|
||||
interface ScheduleOption { id: string; label: string; }
|
||||
|
||||
interface PassengersReport {
|
||||
schedule: { id: string; trainName: string; origin: string; destination: string; departureAt: string; arrivalAt: string; };
|
||||
schedule: { id: string; trainName: string; origin: string; destination: string; departureAt: string; arrivalAt: string; };
|
||||
summary: { totalSeats: number; totalPassengers: number; occupancyRate: number };
|
||||
byCoach: { coachNumber: string; coachType: string; totalSeats: number; booked: number; occupancyRate: number }[];
|
||||
@@ -30,9 +27,12 @@ interface PassengerRow {
|
||||
idDocumentNumber: string | null;
|
||||
passportNumber: string | null;
|
||||
passportCountry: string | null;
|
||||
nationality: string | null;
|
||||
seatLabel: string | null;
|
||||
coachNumber: string | null;
|
||||
coachType: string | null;
|
||||
origin: string | null;
|
||||
destination: string | null;
|
||||
}
|
||||
|
||||
type Tab = 'occupancy' | 'list';
|
||||
@@ -41,6 +41,8 @@ export default function PassengersReportPage() {
|
||||
const [scheduleId, setScheduleId] = useState('');
|
||||
const [tab, setTab] = useState<Tab>('occupancy');
|
||||
const [listSearch, setListSearch] = useState('');
|
||||
const [filterCoach, setFilterCoach] = useState('');
|
||||
const [filterOrigin, setFilterOrigin] = useState('');
|
||||
|
||||
const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery<ScheduleOption[]>({
|
||||
queryKey: ['report-schedules'],
|
||||
@@ -60,14 +62,23 @@ export default function PassengersReportPage() {
|
||||
enabled: !!scheduleId,
|
||||
});
|
||||
|
||||
const filteredList = listSearch.trim()
|
||||
? passengerList.filter(p =>
|
||||
p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) ||
|
||||
p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()) ||
|
||||
(p.idDocumentNumber ?? '').toLowerCase().includes(listSearch.toLowerCase()) ||
|
||||
(p.passportNumber ?? '').toLowerCase().includes(listSearch.toLowerCase()),
|
||||
)
|
||||
: passengerList;
|
||||
const coachOptions = [...new Set(passengerList.map(p => p.coachNumber).filter(Boolean))].sort() as string[];
|
||||
const originOptions = [...new Set(passengerList.map(p => p.origin).filter(Boolean))].sort() as string[];
|
||||
|
||||
const filteredList = passengerList.filter(p => {
|
||||
if (filterCoach && p.coachNumber !== filterCoach) return false;
|
||||
if (filterOrigin && p.origin !== filterOrigin) return false;
|
||||
if (listSearch.trim()) {
|
||||
const q = listSearch.toLowerCase();
|
||||
return (
|
||||
p.passengerName.toLowerCase().includes(q) ||
|
||||
p.bookingRef.toLowerCase().includes(q) ||
|
||||
(p.idDocumentNumber ?? '').toLowerCase().includes(q) ||
|
||||
(p.passportNumber ?? '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const downloadCsv = (csv: string, filename: string) => {
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
@@ -85,11 +96,12 @@ export default function PassengersReportPage() {
|
||||
|
||||
const doExportList = () => {
|
||||
if (!passengerList.length) return;
|
||||
const headers = ['Booking Ref', 'Status', 'Name', 'Category', 'ID Type', 'ID Number', 'Passport', 'Country', 'Seat', 'Coach', 'Class'];
|
||||
const headers = ['Booking Ref', 'Status', 'Name', 'Category', 'Nationality / Passport', 'Coach · Seat', 'Origin', 'Destination'];
|
||||
const rows = passengerList.map(p => [
|
||||
p.bookingRef, p.bookingStatus, p.passengerName, p.passengerCategory,
|
||||
p.idDocumentType ?? '', p.idDocumentNumber ?? '', p.passportNumber ?? '',
|
||||
p.passportCountry ?? '', p.seatLabel ?? '', p.coachNumber ?? '', p.coachType ?? '',
|
||||
p.passportNumber ? `${p.passportCountry ?? ''} · ${p.passportNumber}` : (p.idDocumentNumber ?? ''),
|
||||
p.coachNumber && p.seatLabel ? `${p.coachNumber} · ${p.seatLabel}` : (p.coachNumber ?? p.seatLabel ?? ''),
|
||||
p.origin ?? '', p.destination ?? '',
|
||||
].map(v => `"${String(v).replace(/"/g, '""')}"`));
|
||||
downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`);
|
||||
};
|
||||
@@ -109,7 +121,7 @@ export default function PassengersReportPage() {
|
||||
<select
|
||||
className="input"
|
||||
value={scheduleId}
|
||||
onChange={e => { setScheduleId(e.target.value); setTab('occupancy'); setListSearch(''); }}
|
||||
onChange={e => { setScheduleId(e.target.value); setTab('occupancy'); setListSearch(''); setFilterCoach(''); setFilterOrigin(''); }}
|
||||
disabled={loadingSchedules}
|
||||
>
|
||||
<option value="">{loadingSchedules ? 'Loading schedules…' : 'Select a schedule…'}</option>
|
||||
@@ -273,13 +285,31 @@ export default function PassengersReportPage() {
|
||||
{/* Passenger List tab */}
|
||||
{tab === 'list' && (
|
||||
<div className="card space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
className="input max-w-sm"
|
||||
placeholder="Search by name, booking ref or ID…"
|
||||
value={listSearch}
|
||||
onChange={e => setListSearch(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<input
|
||||
type="text"
|
||||
className="input min-w-52 flex-1"
|
||||
placeholder="Search by name, booking ref or ID…"
|
||||
value={listSearch}
|
||||
onChange={e => setListSearch(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="input w-44"
|
||||
value={filterCoach}
|
||||
onChange={e => setFilterCoach(e.target.value)}
|
||||
>
|
||||
<option value="">All coaches</option>
|
||||
{coachOptions.map(c => <option key={c} value={c}>Coach {c}</option>)}
|
||||
</select>
|
||||
<select
|
||||
className="input w-52"
|
||||
value={filterOrigin}
|
||||
onChange={e => setFilterOrigin(e.target.value)}
|
||||
>
|
||||
<option value="">All origins</option>
|
||||
{originOptions.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
@@ -287,9 +317,10 @@ export default function PassengersReportPage() {
|
||||
<th className="pb-2 pr-4">#</th>
|
||||
<th className="pb-2 pr-4">Name</th>
|
||||
<th className="pb-2 pr-4">Category</th>
|
||||
<th className="pb-2 pr-4">ID / Passport</th>
|
||||
<th className="pb-2 pr-4">Seat</th>
|
||||
<th className="pb-2 pr-4">Coach</th>
|
||||
<th className="pb-2 pr-4">Nationality / Passport</th>
|
||||
<th className="pb-2 pr-4">Coach · Seat</th>
|
||||
<th className="pb-2 pr-4">Origin</th>
|
||||
<th className="pb-2 pr-4">Destination</th>
|
||||
<th className="pb-2 pr-4">Booking Ref</th>
|
||||
<th className="pb-2">Status</th>
|
||||
</tr>
|
||||
@@ -304,15 +335,23 @@ export default function PassengersReportPage() {
|
||||
{p.passengerCategory}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-muted-foreground text-xs">
|
||||
{p.idDocumentNumber ?? p.passportNumber ?? '—'}
|
||||
{p.passportCountry && <span className="ml-1 text-muted-foreground/60">({p.passportCountry})</span>}
|
||||
<td className="py-2 pr-4 text-xs">
|
||||
{p.passportNumber ? (
|
||||
<>
|
||||
<span className="text-muted-foreground">{p.passportCountry ?? 'Intl'}</span>
|
||||
<span className="ml-1 font-mono">{p.passportNumber}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">{p.idDocumentNumber ?? '—'}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 pr-4 font-mono text-xs">{p.seatLabel ?? '—'}</td>
|
||||
<td className="py-2 pr-4 text-muted-foreground">
|
||||
{p.coachNumber ?? '—'}
|
||||
{p.coachType && <span className="ml-1 text-xs text-muted-foreground/60">({p.coachType})</span>}
|
||||
<td className="py-2 pr-4 font-mono text-xs">
|
||||
{p.coachNumber && p.seatLabel
|
||||
? `${p.coachNumber} · ${p.seatLabel}`
|
||||
: (p.coachNumber ?? p.seatLabel ?? '—')}
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-muted-foreground text-xs">{p.origin ?? '—'}</td>
|
||||
<td className="py-2 pr-4 text-muted-foreground text-xs">{p.destination ?? '—'}</td>
|
||||
<td className="py-2 pr-4 font-mono text-xs">{p.bookingRef}</td>
|
||||
<td className="py-2">
|
||||
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded ${p.bookingStatus === 'BOARDED' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'}`}>
|
||||
@@ -322,7 +361,7 @@ export default function PassengersReportPage() {
|
||||
</tr>
|
||||
))}
|
||||
{filteredList.length === 0 && (
|
||||
<tr><td colSpan={8} className="py-8 text-center text-sm text-muted-foreground">No passengers found</td></tr>
|
||||
<tr><td colSpan={9} className="py-8 text-center text-sm text-muted-foreground">No passengers found</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user