Merge pull request #873 from Tria-plc/alpha

Passengers list report updates
This commit is contained in:
Stephanos A.
2026-07-21 14:09:11 +03:00
committed by GitHub
3 changed files with 47 additions and 109 deletions

View File

@@ -18,10 +18,10 @@ export class ReportsController {
return this.service.generateReport(dto);
}
@Get("schedules")
@ApiOperation({ summary: "List schedules for the passengers report picker" })
listSchedulesForPicker() {
return this.service.listSchedulesForPicker();
@Get('schedules')
@ApiOperation({ summary: 'List schedules for the passengers report picker' })
listSchedulesForPicker(@Query('all') all?: string) {
return this.service.listSchedulesForPicker(all === 'true');
}
@Get("passengers/list")

View File

@@ -411,10 +411,10 @@ export class ReportsService {
};
}
async listSchedulesForPicker() {
async listSchedulesForPicker(all = false) {
const now = new Date();
const schedules = await this.prisma.trainSchedule.findMany({
where: { departureAt: { gte: now } },
where: all ? undefined : { departureAt: { gte: now } },
select: {
id: true,
departureAt: true,
@@ -423,7 +423,7 @@ export class ReportsService {
originStation: { select: { name: true } },
destinationStation: { select: { name: true } },
},
orderBy: { departureAt: 'asc' },
orderBy: { departureAt: all ? 'desc' : 'asc' },
take: 200,
});
return schedules.map((s) => ({
@@ -439,8 +439,9 @@ export class ReportsService {
async getPassengerList(scheduleId: string) {
const seats = await this.prisma.bookingSeat.findMany({
where: {
scheduleId,
leg: 1,
booking: { scheduleId, status: { in: ["CONFIRMED", "BOARDED"] } },
booking: { status: { in: ["CONFIRMED", "BOARDED"] } },
},
include: {
booking: {
@@ -507,8 +508,9 @@ export class ReportsService {
// Booked seats — exclude dining coaches
const bookingSeats = await this.prisma.bookingSeat.findMany({
where: {
scheduleId,
leg: 1,
booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } },
booking: { status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } },
seat: { coach: { coachType: { type: { not: 'dining' } } } },
},
include: {

View File

@@ -56,11 +56,9 @@ interface PassengerRow {
seatClassName: string | null;
coachNumber: string | null;
coachType: string | null;
coachSeat: string | null;
nationality: string | null;
origin: string | null;
destination: string | null;
departureAt: string | null;
amountPaidMinor: number;
currency: string;
isGroupBooking: boolean;
@@ -73,7 +71,6 @@ 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 [filterSeatClass, setFilterSeatClass] = useState("");
const [filterCoachNumber, setFilterCoachNumber] = useState("");
@@ -81,8 +78,8 @@ export default function PassengersReportPage() {
const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery<
ScheduleOption[]
>({
queryKey: ["report-schedules"],
queryFn: () => apiClient.get("/reports/schedules"),
queryKey: ["report-schedules-all"],
queryFn: () => apiClient.get("/reports/schedules?all=true"),
});
const schedules = schedulesRaw ?? [];
@@ -102,7 +99,7 @@ export default function PassengersReportPage() {
enabled: !!scheduleId,
});
const coachOptions = [
const coachNumberOptions = [
...new Set(passengerList.map((p) => p.coachNumber).filter(Boolean)),
].sort() as string[];
const seatClassOptions = [
@@ -111,11 +108,9 @@ export default function PassengersReportPage() {
const originOptions = [
...new Set(passengerList.map((p) => p.origin).filter(Boolean)),
].sort() as string[];
const coachNumberOptions = coachOptions;
const filteredList = passengerList
.filter((p) => {
if (filterCoach && p.coachNumber !== filterCoach) return false;
if (filterCoachNumber && p.coachNumber !== filterCoachNumber) return false;
if (filterOrigin && p.origin !== filterOrigin) return false;
if (filterSeatClass && p.seatClassName !== filterSeatClass) return false;
@@ -161,29 +156,24 @@ export default function PassengersReportPage() {
};
const doExportList = () => {
if (!passengerList.length) return;
const headers = [
"#",
"Name",
"Coach·Seat",
"Origin",
"Destination",
"Date",
"Booking Ref",
];
const rows = passengerList.map((p, i) =>
if (!filteredList.length) return;
const headers = ['Name', 'Nationality', 'Passport Number', 'Seat Class', 'Coach', 'Seat', 'Origin', 'Destination', 'Amount Paid (ETB)', 'Booking Ref'];
const rows = filteredList.map((p) =>
[
String(i + 1),
p.passengerName,
p.coachSeat,
p.origin,
p.destination,
p.departureAt ? formatDateTime(p.departureAt) : "—",
p.nationality ?? '—',
p.passportNumber ?? '—',
p.seatClassName ?? '—',
p.coachNumber ?? '—',
p.seatNumber ?? '—',
p.origin ?? '—',
p.destination ?? '—',
(p.amountPaidMinor / 100).toFixed(2),
p.bookingRef,
].map((v) => `"${String(v).replace(/"/g, '""')}"`),
);
downloadCsv(
[headers.join(","), ...rows.map((r) => r.join(","))].join("\n"),
[headers.join(','), ...rows.map((r) => r.join(','))].join('\n'),
`passengers-${scheduleId}.csv`,
);
};
@@ -211,7 +201,6 @@ export default function PassengersReportPage() {
setScheduleId(e.target.value);
setTab("occupancy");
setListSearch("");
setFilterCoach("");
setFilterCoachNumber("");
setFilterOrigin("");
setFilterSeatClass("");
@@ -520,94 +509,41 @@ export default function PassengersReportPage() {
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left text-xs text-muted-foreground uppercase tracking-wider">
<th className="pb-2 pr-4">Name</th>
<th className="pb-2 pr-4">Nationality</th>
<th className="pb-2 pr-4">Passport</th>
<th className="pb-2 pr-4">Coach</th>
<th className="pb-2 pr-4">Seat</th>
<th className="pb-2 pr-4">Class</th>
<th className="pb-2 pr-4">Origin</th>
<th className="pb-2 pr-4">Destination</th>
<th className="pb-2 pr-4">Amount Paid</th>
<th className="pb-2">Booking Ref</th>
<th className="pb-2 pr-4 whitespace-nowrap">Name</th>
<th className="pb-2 pr-4 whitespace-nowrap">Nationality</th>
<th className="pb-2 pr-4 whitespace-nowrap">Seat Class · Coach · Seat</th>
<th className="pb-2 pr-4 whitespace-nowrap">Route</th>
<th className="pb-2 pr-4 whitespace-nowrap">Amount Paid</th>
<th className="pb-2 whitespace-nowrap">Booking Ref</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{filteredList.map((p, i) => (
<tr
key={`${p.bookingRef}-${i}`}
className="hover:bg-muted/30"
>
<td className="py-2 pr-4 font-medium">
{p.passengerName}
</td>
<tr key={`${p.bookingRef}-${i}`} className="hover:bg-muted/30">
<td className="py-2 pr-4 font-medium whitespace-nowrap">{p.passengerName}</td>
<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>
<span className="text-muted-foreground">{p.nationality ?? '—'}</span>
{p.passportNumber && (
<span className="ml-1.5 font-mono text-foreground">{p.passportNumber}</span>
)}
</td>
<td className="py-2 pr-4 font-mono text-xs">
{p.coachNumber && p.seatLabel ? (
<>
{p.coachNumber} · {p.seatLabel}
{p.coachType && (
<span className="font-sans text-muted-foreground ml-1">
({p.coachType})
</span>
)}
</>
) : (
(p.coachNumber ?? p.seatLabel ?? "—")
)}
</td>
<td className="py-2 pr-4 text-muted-foreground text-xs">
{p.origin && p.destination
? `${p.origin}${p.destination}`
: (p.origin ?? p.destination ?? "—")}
</td>
<td className="py-2 pr-4 text-xs">
<div className="flex items-center gap-1.5">
<span className="tabular-nums font-medium">
{(p.amountPaidMinor / 100).toLocaleString(
"en-US",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
)}{" "}
{p.currency}
</span>
{p.isGroupBooking && (
<span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-semibold bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
Group
</span>
)}
</div>
<span className="font-medium">{p.seatClassName ?? '—'}</span>
{p.coachNumber && <span className="text-muted-foreground"> · {p.coachNumber}</span>}
{p.seatNumber && <span className="text-muted-foreground"> · #{p.seatNumber}</span>}
</td>
<td className="py-2 font-mono text-xs">
{p.bookingRef}
<td className="py-2 pr-4 text-xs text-muted-foreground whitespace-nowrap">
{p.origin && p.destination ? `${p.origin}${p.destination}` : (p.origin ?? p.destination ?? '—')}
</td>
<td className="py-2 pr-4 tabular-nums text-xs">
{(p.amountPaidMinor / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })} {p.currency}
</td>
<td className="py-2 font-mono text-xs">{p.bookingRef}</td>
</tr>
))}
{filteredList.length === 0 && (
<tr>
<td
colSpan={6}
className="py-8 text-center text-sm text-muted-foreground"
>
No passengers found
</td>
<td colSpan={6} className="py-8 text-center text-sm text-muted-foreground">No passengers found</td>
</tr>
)}
</tbody>