mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #873 from Tria-plc/alpha
Passengers list report updates
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user