Passenger reports filter updates

This commit is contained in:
Stephanos A
2026-07-19 17:21:44 +03:00
parent 3e76069e9f
commit 80915f94ec
2 changed files with 84 additions and 34 deletions

View File

@@ -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,
}));
}

View File

@@ -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>