mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 07:08:18 +00:00
Passengers list report updates
This commit is contained in:
@@ -18,10 +18,10 @@ export class ReportsController {
|
|||||||
return this.service.generateReport(dto);
|
return this.service.generateReport(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("schedules")
|
@Get('schedules')
|
||||||
@ApiOperation({ summary: "List schedules for the passengers report picker" })
|
@ApiOperation({ summary: 'List schedules for the passengers report picker' })
|
||||||
listSchedulesForPicker() {
|
listSchedulesForPicker(@Query('all') all?: string) {
|
||||||
return this.service.listSchedulesForPicker();
|
return this.service.listSchedulesForPicker(all === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("passengers/list")
|
@Get("passengers/list")
|
||||||
|
|||||||
@@ -411,10 +411,10 @@ export class ReportsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async listSchedulesForPicker() {
|
async listSchedulesForPicker(all = false) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const schedules = await this.prisma.trainSchedule.findMany({
|
const schedules = await this.prisma.trainSchedule.findMany({
|
||||||
where: { departureAt: { gte: now } },
|
where: all ? undefined : { departureAt: { gte: now } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
departureAt: true,
|
departureAt: true,
|
||||||
@@ -423,7 +423,7 @@ export class ReportsService {
|
|||||||
originStation: { select: { name: true } },
|
originStation: { select: { name: true } },
|
||||||
destinationStation: { select: { name: true } },
|
destinationStation: { select: { name: true } },
|
||||||
},
|
},
|
||||||
orderBy: { departureAt: 'asc' },
|
orderBy: { departureAt: all ? 'desc' : 'asc' },
|
||||||
take: 200,
|
take: 200,
|
||||||
});
|
});
|
||||||
return schedules.map((s) => ({
|
return schedules.map((s) => ({
|
||||||
@@ -439,8 +439,9 @@ export class ReportsService {
|
|||||||
async getPassengerList(scheduleId: string) {
|
async getPassengerList(scheduleId: string) {
|
||||||
const seats = await this.prisma.bookingSeat.findMany({
|
const seats = await this.prisma.bookingSeat.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
scheduleId,
|
||||||
leg: 1,
|
leg: 1,
|
||||||
booking: { scheduleId, status: { in: ["CONFIRMED", "BOARDED"] } },
|
booking: { status: { in: ["CONFIRMED", "BOARDED"] } },
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -507,8 +508,9 @@ export class ReportsService {
|
|||||||
// Booked seats — exclude dining coaches
|
// Booked seats — exclude dining coaches
|
||||||
const bookingSeats = await this.prisma.bookingSeat.findMany({
|
const bookingSeats = await this.prisma.bookingSeat.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
scheduleId,
|
||||||
leg: 1,
|
leg: 1,
|
||||||
booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } },
|
booking: { status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } },
|
||||||
seat: { coach: { coachType: { type: { not: 'dining' } } } },
|
seat: { coach: { coachType: { type: { not: 'dining' } } } },
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@@ -56,11 +56,9 @@ interface PassengerRow {
|
|||||||
seatClassName: string | null;
|
seatClassName: string | null;
|
||||||
coachNumber: string | null;
|
coachNumber: string | null;
|
||||||
coachType: string | null;
|
coachType: string | null;
|
||||||
coachSeat: string | null;
|
|
||||||
nationality: string | null;
|
nationality: string | null;
|
||||||
origin: string | null;
|
origin: string | null;
|
||||||
destination: string | null;
|
destination: string | null;
|
||||||
departureAt: string | null;
|
|
||||||
amountPaidMinor: number;
|
amountPaidMinor: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
isGroupBooking: boolean;
|
isGroupBooking: boolean;
|
||||||
@@ -73,7 +71,6 @@ export default function PassengersReportPage() {
|
|||||||
const [scheduleId, setScheduleId] = useState("");
|
const [scheduleId, setScheduleId] = useState("");
|
||||||
const [tab, setTab] = useState<Tab>("occupancy");
|
const [tab, setTab] = useState<Tab>("occupancy");
|
||||||
const [listSearch, setListSearch] = useState("");
|
const [listSearch, setListSearch] = useState("");
|
||||||
const [filterCoach, setFilterCoach] = useState("");
|
|
||||||
const [filterOrigin, setFilterOrigin] = useState("");
|
const [filterOrigin, setFilterOrigin] = useState("");
|
||||||
const [filterSeatClass, setFilterSeatClass] = useState("");
|
const [filterSeatClass, setFilterSeatClass] = useState("");
|
||||||
const [filterCoachNumber, setFilterCoachNumber] = useState("");
|
const [filterCoachNumber, setFilterCoachNumber] = useState("");
|
||||||
@@ -81,8 +78,8 @@ export default function PassengersReportPage() {
|
|||||||
const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery<
|
const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery<
|
||||||
ScheduleOption[]
|
ScheduleOption[]
|
||||||
>({
|
>({
|
||||||
queryKey: ["report-schedules"],
|
queryKey: ["report-schedules-all"],
|
||||||
queryFn: () => apiClient.get("/reports/schedules"),
|
queryFn: () => apiClient.get("/reports/schedules?all=true"),
|
||||||
});
|
});
|
||||||
const schedules = schedulesRaw ?? [];
|
const schedules = schedulesRaw ?? [];
|
||||||
|
|
||||||
@@ -102,7 +99,7 @@ export default function PassengersReportPage() {
|
|||||||
enabled: !!scheduleId,
|
enabled: !!scheduleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const coachOptions = [
|
const coachNumberOptions = [
|
||||||
...new Set(passengerList.map((p) => p.coachNumber).filter(Boolean)),
|
...new Set(passengerList.map((p) => p.coachNumber).filter(Boolean)),
|
||||||
].sort() as string[];
|
].sort() as string[];
|
||||||
const seatClassOptions = [
|
const seatClassOptions = [
|
||||||
@@ -111,11 +108,9 @@ export default function PassengersReportPage() {
|
|||||||
const originOptions = [
|
const originOptions = [
|
||||||
...new Set(passengerList.map((p) => p.origin).filter(Boolean)),
|
...new Set(passengerList.map((p) => p.origin).filter(Boolean)),
|
||||||
].sort() as string[];
|
].sort() as string[];
|
||||||
const coachNumberOptions = coachOptions;
|
|
||||||
|
|
||||||
const filteredList = passengerList
|
const filteredList = passengerList
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
if (filterCoach && p.coachNumber !== filterCoach) return false;
|
|
||||||
if (filterCoachNumber && p.coachNumber !== filterCoachNumber) return false;
|
if (filterCoachNumber && p.coachNumber !== filterCoachNumber) return false;
|
||||||
if (filterOrigin && p.origin !== filterOrigin) return false;
|
if (filterOrigin && p.origin !== filterOrigin) return false;
|
||||||
if (filterSeatClass && p.seatClassName !== filterSeatClass) return false;
|
if (filterSeatClass && p.seatClassName !== filterSeatClass) return false;
|
||||||
@@ -161,29 +156,24 @@ export default function PassengersReportPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doExportList = () => {
|
const doExportList = () => {
|
||||||
if (!passengerList.length) return;
|
if (!filteredList.length) return;
|
||||||
const headers = [
|
const headers = ['Name', 'Nationality', 'Passport Number', 'Seat Class', 'Coach', 'Seat', 'Origin', 'Destination', 'Amount Paid (ETB)', 'Booking Ref'];
|
||||||
"#",
|
const rows = filteredList.map((p) =>
|
||||||
"Name",
|
|
||||||
"Coach·Seat",
|
|
||||||
"Origin",
|
|
||||||
"Destination",
|
|
||||||
"Date",
|
|
||||||
"Booking Ref",
|
|
||||||
];
|
|
||||||
const rows = passengerList.map((p, i) =>
|
|
||||||
[
|
[
|
||||||
String(i + 1),
|
|
||||||
p.passengerName,
|
p.passengerName,
|
||||||
p.coachSeat,
|
p.nationality ?? '—',
|
||||||
p.origin,
|
p.passportNumber ?? '—',
|
||||||
p.destination,
|
p.seatClassName ?? '—',
|
||||||
p.departureAt ? formatDateTime(p.departureAt) : "—",
|
p.coachNumber ?? '—',
|
||||||
|
p.seatNumber ?? '—',
|
||||||
|
p.origin ?? '—',
|
||||||
|
p.destination ?? '—',
|
||||||
|
(p.amountPaidMinor / 100).toFixed(2),
|
||||||
p.bookingRef,
|
p.bookingRef,
|
||||||
].map((v) => `"${String(v).replace(/"/g, '""')}"`),
|
].map((v) => `"${String(v).replace(/"/g, '""')}"`),
|
||||||
);
|
);
|
||||||
downloadCsv(
|
downloadCsv(
|
||||||
[headers.join(","), ...rows.map((r) => r.join(","))].join("\n"),
|
[headers.join(','), ...rows.map((r) => r.join(','))].join('\n'),
|
||||||
`passengers-${scheduleId}.csv`,
|
`passengers-${scheduleId}.csv`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -211,7 +201,6 @@ export default function PassengersReportPage() {
|
|||||||
setScheduleId(e.target.value);
|
setScheduleId(e.target.value);
|
||||||
setTab("occupancy");
|
setTab("occupancy");
|
||||||
setListSearch("");
|
setListSearch("");
|
||||||
setFilterCoach("");
|
|
||||||
setFilterCoachNumber("");
|
setFilterCoachNumber("");
|
||||||
setFilterOrigin("");
|
setFilterOrigin("");
|
||||||
setFilterSeatClass("");
|
setFilterSeatClass("");
|
||||||
@@ -520,94 +509,41 @@ export default function PassengersReportPage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border text-left text-xs text-muted-foreground uppercase tracking-wider">
|
<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 whitespace-nowrap">Name</th>
|
||||||
<th className="pb-2 pr-4">Nationality</th>
|
<th className="pb-2 pr-4 whitespace-nowrap">Nationality</th>
|
||||||
<th className="pb-2 pr-4">Passport</th>
|
<th className="pb-2 pr-4 whitespace-nowrap">Seat Class · Coach · Seat</th>
|
||||||
<th className="pb-2 pr-4">Coach</th>
|
<th className="pb-2 pr-4 whitespace-nowrap">Route</th>
|
||||||
<th className="pb-2 pr-4">Seat</th>
|
<th className="pb-2 pr-4 whitespace-nowrap">Amount Paid</th>
|
||||||
<th className="pb-2 pr-4">Class</th>
|
<th className="pb-2 whitespace-nowrap">Booking Ref</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border">
|
<tbody className="divide-y divide-border">
|
||||||
{filteredList.map((p, i) => (
|
{filteredList.map((p, i) => (
|
||||||
<tr
|
<tr key={`${p.bookingRef}-${i}`} className="hover:bg-muted/30">
|
||||||
key={`${p.bookingRef}-${i}`}
|
<td className="py-2 pr-4 font-medium whitespace-nowrap">{p.passengerName}</td>
|
||||||
className="hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<td className="py-2 pr-4 font-medium">
|
|
||||||
{p.passengerName}
|
|
||||||
</td>
|
|
||||||
<td className="py-2 pr-4 text-xs">
|
<td className="py-2 pr-4 text-xs">
|
||||||
{p.passportNumber ? (
|
<span className="text-muted-foreground">{p.nationality ?? '—'}</span>
|
||||||
<>
|
{p.passportNumber && (
|
||||||
<span className="text-muted-foreground">
|
<span className="ml-1.5 font-mono text-foreground">{p.passportNumber}</span>
|
||||||
{p.passportCountry ?? "Intl"}
|
|
||||||
</span>
|
|
||||||
<span className="ml-1 font-mono">
|
|
||||||
{p.passportNumber}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{p.idDocumentNumber ?? "—"}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</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">
|
<td className="py-2 pr-4 text-xs">
|
||||||
<div className="flex items-center gap-1.5">
|
<span className="font-medium">{p.seatClassName ?? '—'}</span>
|
||||||
<span className="tabular-nums font-medium">
|
{p.coachNumber && <span className="text-muted-foreground"> · {p.coachNumber}</span>}
|
||||||
{(p.amountPaidMinor / 100).toLocaleString(
|
{p.seatNumber && <span className="text-muted-foreground"> · #{p.seatNumber}</span>}
|
||||||
"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>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 font-mono text-xs">
|
<td className="py-2 pr-4 text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{p.bookingRef}
|
{p.origin && p.destination ? `${p.origin} → ${p.destination}` : (p.origin ?? p.destination ?? '—')}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{filteredList.length === 0 && (
|
{filteredList.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td colSpan={6} className="py-8 text-center text-sm text-muted-foreground">No passengers found</td>
|
||||||
colSpan={6}
|
|
||||||
className="py-8 text-center text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
No passengers found
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user