From e84ab9e2b5cfbd469d871e04d3f09d431ac76899 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sat, 18 Jul 2026 18:59:02 +0300 Subject: [PATCH 1/5] Passenger list report --- .../src/modules/reports/reports.controller.ts | 12 + .../src/modules/reports/reports.service.ts | 45 ++ .../src/app/reports/passengers/page.tsx | 397 ++++++++++++------ 3 files changed, 315 insertions(+), 139 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/reports/reports.controller.ts b/apps/edr-passenger-api/src/modules/reports/reports.controller.ts index a4da5b208..74bad3514 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.controller.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.controller.ts @@ -18,6 +18,18 @@ 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('passengers/list') + @ApiOperation({ summary: 'Flat passenger list for a specific schedule' }) + getPassengerList(@Query('scheduleId') scheduleId: string) { + return this.service.getPassengerList(scheduleId); + } + @Get('passengers') @ApiOperation({ summary: 'Passengers report for a specific schedule' }) getOccupancyReport(@Query('scheduleId') scheduleId: string) { diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index f4141ecb6..785bf3c17 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -286,6 +286,51 @@ export class ReportsService { }; } + async listSchedulesForPicker() { + const schedules = await this.prisma.trainSchedule.findMany({ + select: { + id: true, + departureAt: true, + train: { select: { number: true } }, + originStation: { select: { name: true } }, + destinationStation: { select: { name: true } }, + }, + orderBy: { departureAt: 'desc' }, + take: 200, + }); + return schedules.map(s => ({ + id: s.id, + label: `${s.train.number} · ${s.originStation.name} → ${s.destinationStation.name} · ${new Date(s.departureAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}`, + })); + } + + async getPassengerList(scheduleId: string) { + const seats = await this.prisma.bookingSeat.findMany({ + where: { + scheduleId, + booking: { status: { in: ['CONFIRMED', 'BOARDED'] } }, + }, + include: { + booking: { select: { bookingRef: true, status: true } }, + seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } }, + }, + orderBy: [{ seat: { coach: { number: 'asc' } } }], + }); + return seats.map(bs => ({ + bookingRef: bs.booking.bookingRef, + bookingStatus: bs.booking.status, + passengerName: bs.passengerName, + passengerCategory: bs.passengerCategory, + idDocumentType: bs.idDocumentType, + idDocumentNumber: bs.idDocumentNumber, + passportNumber: bs.passportNumber, + passportCountry: bs.passportCountry, + seatLabel: bs.seatLabelSnapshot, + coachNumber: bs.seat?.coach?.number ?? null, + coachType: (bs.seat?.coach as any)?.coachType?.name ?? null, + })); + } + async getReport(reportId: string) { return this.prisma.operationalReport.findUnique({ where: { id: reportId } }); } diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx index 8b7ef240f..8c6958565 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx @@ -7,15 +7,10 @@ import { apiClient } from '@/lib/api-client'; import ActionButton from '@/components/ui/ActionButton'; import { formatDateTime } from '@/lib/utils'; +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 }[]; byClass: { className: string; totalSeats: number; booked: number; occupancyRate: number }[]; @@ -23,30 +18,79 @@ interface PassengersReport { byDestination: { stationName: string; passengers: number }[]; } +interface PassengerRow { + bookingRef: string; + bookingStatus: string; + passengerName: string; + passengerCategory: string; + idDocumentType: string | null; + idDocumentNumber: string | null; + passportNumber: string | null; + passportCountry: string | null; + seatLabel: string | null; + coachNumber: string | null; + coachType: string | null; +} + +type Tab = 'occupancy' | 'list'; + export default function PassengersReportPage() { const [scheduleId, setScheduleId] = useState(''); - const [submittedId, setSubmittedId] = useState(''); + const [tab, setTab] = useState('occupancy'); + const [listSearch, setListSearch] = useState(''); + + const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery({ + queryKey: ['report-schedules'], + queryFn: () => apiClient.get('/reports/schedules'), + }); + const schedules = schedulesRaw ?? []; const { data, isLoading, isError } = useQuery({ - queryKey: ['passengers-report', submittedId], - queryFn: () => apiClient.get(`/reports/passengers?scheduleId=${submittedId}`), - enabled: !!submittedId, + queryKey: ['passengers-report', scheduleId], + queryFn: () => apiClient.get(`/reports/passengers?scheduleId=${scheduleId}`), + enabled: !!scheduleId, }); - const doExport = () => { - if (!data) return; - const rows = data.byCoach.map((c) => [c.coachNumber, c.coachType, String(c.totalSeats), String(c.booked), `${c.occupancyRate}%`]); - const headers = ['Coach', 'Type', 'Total Seats', 'Booked', 'Occupancy']; - const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); + const { data: passengerList = [], isLoading: listLoading } = useQuery({ + queryKey: ['passengers-list', scheduleId], + queryFn: () => apiClient.get(`/reports/passengers/list?scheduleId=${scheduleId}`), + 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 downloadCsv = (csv: string, filename: string) => { const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; - a.download = `passengers-report-${submittedId}.csv`; - a.click(); + a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; + const doExportOccupancy = () => { + if (!data) return; + const rows = data.byCoach.map(c => [c.coachNumber, c.coachType, String(c.totalSeats), String(c.booked), `${c.occupancyRate}%`]); + downloadCsv([['Coach', 'Type', 'Total Seats', 'Booked', 'Occupancy'].join(','), ...rows.map(r => r.join(','))].join('\n'), `occupancy-${scheduleId}.csv`); + }; + + const doExportList = () => { + if (!passengerList.length) return; + const headers = ['Booking Ref', 'Status', 'Name', 'Category', 'ID Type', 'ID Number', 'Passport', 'Country', 'Seat', 'Coach', 'Class']; + 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 ?? '', + ].map(v => `"${String(v).replace(/"/g, '""')}"`)); + downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`); + }; + return (
@@ -54,30 +98,32 @@ export default function PassengersReportPage() {

Occupancy and passenger breakdown for a schedule

- {/* Schedule ID input */} + {/* Schedule selector */}
-
- - + +
- setSubmittedId(scheduleId)} disabled={!scheduleId.trim() || isLoading}> - Load Report - - {data && ( - - Export CSV - + {data && tab === 'occupancy' && ( + Export CSV + )} + {passengerList.length > 0 && tab === 'list' && ( + Export CSV )}
- {isLoading &&

Loading…

} - {isError &&

Failed to load report. Check the schedule ID.

} + {(isLoading || listLoading) &&

Loading…

} + {isError &&

Failed to load report.

}
{data && ( @@ -92,121 +138,194 @@ export default function PassengersReportPage() {
- {/* Summary cards */} -
-
-
-

Total Seats

-
-
-

{data.summary.totalSeats}

-
-
-
-

Passengers

-
-
-

{data.summary.totalPassengers}

-
-
-
-

Occupancy Rate

-
-
-

{data.summary.occupancyRate}%

-
-
-
-
+ {/* Tabs */} +
+ +
- {/* By Coach */} -
-

By Coach

-
- - - - - - - - - - - - {data.byCoach.map((c) => ( - - - - - - - - ))} - -
CoachTypeSeatsBookedOccupancy
{c.coachNumber}{c.coachType}{c.totalSeats}{c.booked} + {/* Occupancy tab */} + {tab === 'occupancy' && ( +
+
+
+
+

Total Seats

+
+
+

{data.summary.totalSeats}

+
+
+
+

Passengers

+
+
+

{data.summary.totalPassengers}

+
+
+
+

Occupancy Rate

+
+
+

{data.summary.occupancyRate}%

+
+
+
+
+
+ +
+

By Coach

+
+ + + + + + + + + + + + {data.byCoach.map(c => ( + + + + + + + + ))} + +
CoachTypeSeatsBookedOccupancy
{c.coachNumber}{c.coachType}{c.totalSeats}{c.booked} +
+
+
+
+ {c.occupancyRate}% +
+
+
+
+ +
+
+

By Class

+
+ {data.byClass.map(c => ( +
+
+ {c.className} + {c.booked}/{c.totalSeats} +
-
+
- {c.occupancyRate}% + {c.occupancyRate}%
-
-
-
- - {/* By Class + By Origin/Destination */} -
- {/* By Class */} -
-

By Class

-
- {data.byClass.map((c) => ( -
-
- {c.className} - {c.booked}/{c.totalSeats} -
-
-
-
- {c.occupancyRate}% -
+ ))}
- ))} +
+
+

By Boarding Station

+
+ {data.byOrigin.map(o => ( +
+ {o.stationName} + {o.passengers} +
+ ))} + {data.byOrigin.length === 0 &&

No data

} +
+
+
+

By Alighting Station

+
+ {data.byDestination.map(d => ( +
+ {d.stationName} + {d.passengers} +
+ ))} + {data.byDestination.length === 0 &&

No data

} +
+
+ )} - {/* By Origin */} -
-

By Boarding Station

-
- {data.byOrigin.map((o) => ( -
- {o.stationName} - {o.passengers} -
- ))} - {data.byOrigin.length === 0 &&

No data

} + {/* Passenger List tab */} + {tab === 'list' && ( +
+ setListSearch(e.target.value)} + /> +
+ + + + + + + + + + + + + + + {filteredList.map((p, i) => ( + + + + + + + + + + + ))} + {filteredList.length === 0 && ( + + )} + +
#NameCategoryID / PassportSeatCoachBooking RefStatus
{i + 1}{p.passengerName} + + {p.passengerCategory} + + + {p.idDocumentNumber ?? p.passportNumber ?? '—'} + {p.passportCountry && ({p.passportCountry})} + {p.seatLabel ?? '—'} + {p.coachNumber ?? '—'} + {p.coachType && ({p.coachType})} + {p.bookingRef} + + {p.bookingStatus} + +
No passengers found
- - {/* By Destination */} -
-

By Alighting Station

-
- {data.byDestination.map((d) => ( -
- {d.stationName} - {d.passengers} -
- ))} - {data.byDestination.length === 0 &&

No data

} -
-
-
+ )} )}
From 6ddcc738473b91033723030b50f3fad1a7ed4886 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sat, 18 Jul 2026 19:12:02 +0300 Subject: [PATCH 2/5] Boarding icon added to dashboard --- .../backoffice/src/app/dashboard/page.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx index bc657106d..6d1e19649 100644 --- a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { PermissionGuard } from '@/components/layout/PermissionGuard'; import { PERMS } from '@/lib/permissions'; -import { Ticket, AlertCircle, BookOpen, Banknote, ArrowRight } from 'lucide-react'; +import { Ticket, AlertCircle, BookOpen, Banknote, ArrowRight, ScanLine } from 'lucide-react'; import { dashboardApi } from '@/lib/api/dashboard'; import { apiClient } from '@/lib/api-client'; import { formatCurrency } from '@/lib/utils'; @@ -143,9 +143,18 @@ function DashboardPageContent() { return (
-
-

Dashboard

-

Welcome back! Here's your operational summary.

+
+
+

Dashboard

+

Welcome back! Here's your operational summary.

+
+ + + Boarding +
{statsError && ( From 554baf2595b11a82d023ac21b893bc43f7116dbd Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sat, 18 Jul 2026 19:51:27 +0300 Subject: [PATCH 3/5] Build issue resolution --- apps/edr-passenger-api/src/modules/reports/reports.service.ts | 3 ++- .../backoffice/src/app/reports/passengers/page.tsx | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 785bf3c17..57c5db895 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -95,7 +95,7 @@ export class ReportsService { where: { departureAt: { gte: dateFrom, lte: dateTo } }, include: { coachAssignments: { include: { coach: { include: { seats: true } } } }, - bookings: { include: { seats: true } }, + bookings: { include: { seats: { where: { scheduleId: schedule.id } } } }, }, }); @@ -212,6 +212,7 @@ export class ReportsService { where: { status: { in: ['CONFIRMED', 'BOARDED'] } }, include: { seats: { + where: { scheduleId }, include: { seat: { include: { coach: { include: { coachType: true } } } }, }, diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx index d80110aa7..8c6958565 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx @@ -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 }[]; From 4805a54bf7bc1c1c861120781922f6f39de58fdf Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sat, 18 Jul 2026 20:16:24 +0300 Subject: [PATCH 4/5] Report figures correction updates --- .../modules/dashboard/dashboard.service.ts | 4 +- .../src/modules/reports/reports.service.ts | 9 ++++- .../src/modules/seats/seats.controller.ts | 10 +++++ .../src/modules/seats/seats.service.ts | 20 ++++++++++ .../backoffice/src/app/dashboard/page.tsx | 15 +------- .../backoffice/src/app/reports/seats/page.tsx | 38 +++++++++++++++++-- .../backoffice/src/lib/api/index.ts | 1 + 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts index aed950b73..155391814 100644 --- a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts +++ b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts @@ -13,8 +13,8 @@ export class DashboardService { async getBackofficeStats() { const [totalBookings, totalPackageBookings, totalTickets, totalPassengers, revenueRows, packageRevenueRows] = await Promise.all([ - this.prisma.booking.count(), - this.prisma.booking.count({ where: { packageId: { not: null } } }), + this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] } } }), + this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] }, packageId: { not: null } } }), this.prisma.ticket.count(), this.prisma.passenger.count(), this.prisma.$queryRaw<{ currency: string; total: bigint }[]>` diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 57c5db895..0da670ec7 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -95,13 +95,18 @@ export class ReportsService { where: { departureAt: { gte: dateFrom, lte: dateTo } }, include: { coachAssignments: { include: { coach: { include: { seats: true } } } }, - bookings: { include: { seats: { where: { scheduleId: schedule.id } } } }, + bookings: { + where: { status: { in: ['CONFIRMED', 'BOARDED'] } }, + include: { seats: true }, + }, }, }); const tripData = schedules.map(schedule => { const totalSeats = schedule.coachAssignments.reduce((sum, a) => sum + a.coach.seats.length, 0); - const bookedSeats = schedule.bookings.reduce((sum, b) => sum + b.seats.length, 0); + const bookedSeats = schedule.bookings.reduce( + (sum, b) => sum + b.seats.filter((s: any) => s.scheduleId === schedule.id).length, 0, + ); const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0; return { scheduleId: schedule.id, departureAt: schedule.departureAt, totalSeats, bookedSeats, occupancyRate: +occupancyRate.toFixed(2) }; }); diff --git a/apps/edr-passenger-api/src/modules/seats/seats.controller.ts b/apps/edr-passenger-api/src/modules/seats/seats.controller.ts index 596c88bb3..a302a01ba 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.controller.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.controller.ts @@ -29,6 +29,16 @@ import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry"; export class SeatsController { constructor(private service: SeatsService) {} + // ── Blocked Seats ───────────────────────────────────────────────────────── + @Get('blocks') + @PassengerStaff([PASSENGER_PERMS.seats.manage, PASSENGER_PERMS.admin]) + @ApiBearerAuth('IAM-auth') + @ApiOperation({ summary: 'List all blocked seats with reason and coach info' }) + @ApiResponse({ status: 200, description: 'Blocked seat records' }) + getBlockedSeats() { + return this.service.getBlockedSeats(); + } + // ── Coach Availability ──────────────────────────────────────────────────── @Get('coaches/:scheduleId') @SetMetadata('isPublic', true) diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 10368f380..6d5301cfb 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -598,6 +598,26 @@ export class SeatsService { await this.prisma.journey.deleteMany({ where: { bookingId } as any }); } + async getBlockedSeats() { + const blocks = await this.prisma.seatBlock.findMany({ + include: { + seat: { include: { coach: { select: { number: true } } } }, + }, + orderBy: { blockedAt: 'desc' }, + }); + return blocks.map(b => ({ + id: b.id, + seatId: b.seatId, + seatNumber: b.seat.seatNumber, + coachNumber: b.seat.coach.number, + scheduleId: b.scheduleId, + reason: b.reason, + blockedBy: b.blockedBy, + blockedAt: b.blockedAt, + unblockAt: b.unblockAt, + })); + } + async getCoachesWithAvailability(scheduleId: string, originStationId?: string, destinationStationId?: string) { const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: scheduleId }, diff --git a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx index 6d1e19649..8c2b38570 100644 --- a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx @@ -170,7 +170,7 @@ function DashboardPageContent() { )} {/* Stat cards */} -
+
} iconBg="bg-blue-100 dark:bg-blue-900/30" @@ -183,18 +183,7 @@ function DashboardPageContent() { { label: 'Package', value: stats?.totalPackageBookings ?? 0, href: '/package-bookings' }, ]} /> - } - iconBg="bg-emerald-100 dark:bg-emerald-900/30" - label="Tickets" - total={stats?.totalTickets ?? 0} - loading={statsLoading} - href="/tickets" - rows={[ - { label: 'Regular', value: stats?.totalNormalTickets ?? 0, href: '/tickets' }, - { label: 'Package', value: stats?.totalPackageTickets ?? 0, href: '/tickets' }, - ]} - /> + {/* Tickets card hidden temporarily */} {/* Revenue card */}
diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx index ea2d65cf0..196e83c46 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx @@ -2,8 +2,8 @@ import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Download, Armchair, CheckCircle, Clock, AlertCircle } from 'lucide-react'; -import { bookingsApi } from '@/lib/api'; +import { Download, Armchair, CheckCircle, Clock, AlertCircle, Ban } from 'lucide-react'; +import { bookingsApi, seatsApi } from '@/lib/api'; import Badge from '@/components/ui/Badge'; import ActionButton from '@/components/ui/ActionButton'; import { formatDateTime, formatCurrency } from '@/lib/utils'; @@ -46,6 +46,11 @@ export default function SeatStatusReportPage() { const [statusFilter, setStatusFilter] = useState<'ALL' | 'PAID' | 'UNPAID'>('ALL'); const [search, setSearch] = useState(''); + const { data: blockedSeats = [] } = useQuery({ + queryKey: ['blocked-seats'], + queryFn: () => seatsApi.getBlocked().then((r: any) => Array.isArray(r) ? r : r?.data ?? []), + }); + const { data: bookingsData, isLoading } = useQuery({ queryKey: ['seat-report-bookings'], queryFn: () => bookingsApi.getAll({ pageSize: 1000 }), @@ -150,7 +155,7 @@ export default function SeatStatusReportPage() {
{/* Summary Cards */} -
+
@@ -189,6 +194,33 @@ export default function SeatStatusReportPage() {
+ +
+
+
+

Blocked Seats

+

+ {blockedSeats.length} +

+

Manually blocked

+
+ +
+ {blockedSeats.length > 0 && ( +
+ {blockedSeats.map((b: any) => ( +
+ + Seat {b.seatNumber} · Coach {b.coachNumber} + + + {b.reason} + +
+ ))} +
+ )} +
{/* Filters */} diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts index e8ed56195..07ebefa56 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts @@ -151,6 +151,7 @@ export const seatsApi = { return apiClient.get(`/seats/seatmap/${scheduleId}${params}`); }, getBySchedule: (scheduleId: string) => apiClient.get(`/seats/schedule/${scheduleId}`), + getBlocked: () => apiClient.get('/seats/blocks'), hold: (data: any) => apiClient.post('/seats/hold', data), release: (holdId: string) => apiClient.delete(`/seats/hold/${holdId}`), block: (seatId: string, data: any) => apiClient.post(`/seats/${seatId}/block`, data), From 93c583edf643c9419f07a24f4cef7c1d17030199 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sun, 19 Jul 2026 08:07:59 +0300 Subject: [PATCH 5/5] Report number updates --- .../src/modules/reports/reports.service.ts | 51 ++++--- .../src/modules/seats/seats.service.ts | 3 + .../src/app/reports/passengers/page.tsx | 127 ++++++++---------- .../backoffice/src/app/reports/seats/page.tsx | 16 +-- 4 files changed, 92 insertions(+), 105 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 0da670ec7..3d6345af7 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -105,7 +105,7 @@ export class ReportsService { const tripData = schedules.map(schedule => { const totalSeats = schedule.coachAssignments.reduce((sum, a) => sum + a.coach.seats.length, 0); const bookedSeats = schedule.bookings.reduce( - (sum, b) => sum + b.seats.filter((s: any) => s.scheduleId === schedule.id).length, 0, + (sum, b) => sum + b.seats.filter((s: any) => s.leg === 1).length, 0, ); const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0; return { scheduleId: schedule.id, departureAt: schedule.departureAt, totalSeats, bookedSeats, occupancyRate: +occupancyRate.toFixed(2) }; @@ -217,7 +217,7 @@ export class ReportsService { where: { status: { in: ['CONFIRMED', 'BOARDED'] } }, include: { seats: { - where: { scheduleId }, + where: { leg: 1 }, include: { seat: { include: { coach: { include: { coachType: true } } } }, }, @@ -313,27 +313,46 @@ export class ReportsService { async getPassengerList(scheduleId: string) { const seats = await this.prisma.bookingSeat.findMany({ where: { - scheduleId, - booking: { status: { in: ['CONFIRMED', 'BOARDED'] } }, + leg: 1, + booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } }, }, include: { - booking: { select: { bookingRef: true, status: true } }, - seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } }, + booking: { + select: { + bookingRef: true, + status: true, + originStationId: true, + destinationStationId: true, + }, + }, + seat: { include: { coach: { select: { number: true } } } }, }, - orderBy: [{ seat: { coach: { number: 'asc' } } }], + orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }], }); + + // Resolve station names in one query + const stationIds = [...new Set( + seats.flatMap(bs => [bs.booking.originStationId, bs.booking.destinationStationId]).filter(Boolean) as string[], + )]; + const stations = stationIds.length > 0 + ? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } }) + : []; + const stationName = new Map(stations.map(s => [s.id, s.name])); + + const schedule = await this.prisma.trainSchedule.findUnique({ + where: { id: scheduleId }, + select: { departureAt: true }, + }); + return seats.map(bs => ({ bookingRef: bs.booking.bookingRef, - bookingStatus: bs.booking.status, passengerName: bs.passengerName, - passengerCategory: bs.passengerCategory, - idDocumentType: bs.idDocumentType, - idDocumentNumber: bs.idDocumentNumber, - passportNumber: bs.passportNumber, - passportCountry: bs.passportCountry, - seatLabel: bs.seatLabelSnapshot, - coachNumber: bs.seat?.coach?.number ?? null, - coachType: (bs.seat?.coach as any)?.coachType?.name ?? null, + coachSeat: bs.seat?.coach?.number && bs.seatLabelSnapshot + ? `${bs.seat.coach.number}·${bs.seatLabelSnapshot}` + : (bs.seatLabelSnapshot ?? '—'), + origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? '—') : '—', + destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? '—') : '—', + departureAt: schedule?.departureAt ?? null, })); } diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 6d5301cfb..46bb34b76 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -600,6 +600,9 @@ export class SeatsService { async getBlockedSeats() { const blocks = await this.prisma.seatBlock.findMany({ + where: { + NOT: { reason: { startsWith: 'MAINTENANCE:' } }, + }, include: { seat: { include: { coach: { select: { number: true } } } }, }, diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx index 8c6958565..fa164dbca 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx @@ -20,16 +20,11 @@ interface PassengersReport { interface PassengerRow { bookingRef: string; - bookingStatus: string; passengerName: string; - passengerCategory: string; - idDocumentType: string | null; - idDocumentNumber: string | null; - passportNumber: string | null; - passportCountry: string | null; - seatLabel: string | null; - coachNumber: string | null; - coachType: string | null; + coachSeat: string; + origin: string; + destination: string; + departureAt: string | null; } type Tab = 'occupancy' | 'list'; @@ -60,9 +55,7 @@ export default function PassengersReportPage() { 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()), + p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()), ) : passengerList; @@ -82,11 +75,10 @@ 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 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 ?? '', + const headers = ['#', 'Name', 'Coach·Seat', 'Origin', 'Destination', 'Date', 'Booking Ref']; + const rows = passengerList.map((p, i) => [ + String(i + 1), p.passengerName, p.coachSeat, p.origin, p.destination, + p.departureAt ? formatDateTime(p.departureAt) : '—', p.bookingRef, ].map(v => `"${String(v).replace(/"/g, '""')}"`)); downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`); }; @@ -118,9 +110,6 @@ export default function PassengersReportPage() { {data && tab === 'occupancy' && ( Export CSV )} - {passengerList.length > 0 && tab === 'list' && ( - Export CSV - )}
{(isLoading || listLoading) &&

Loading…

} {isError &&

Failed to load report.

} @@ -267,62 +256,52 @@ export default function PassengersReportPage() {
)} - {/* Passenger List tab */} {tab === 'list' && ( -
- setListSearch(e.target.value)} - /> -
- - - - - - - - - - - - - - - {filteredList.map((p, i) => ( - - - - - - - - - +
+
+ setListSearch(e.target.value)} + /> + {passengerList.length > 0 && ( + Export CSV + )} +
+
+
+
#NameCategoryID / PassportSeatCoachBooking RefStatus
{i + 1}{p.passengerName} - - {p.passengerCategory} - - - {p.idDocumentNumber ?? p.passportNumber ?? '—'} - {p.passportCountry && ({p.passportCountry})} - {p.seatLabel ?? '—'} - {p.coachNumber ?? '—'} - {p.coachType && ({p.coachType})} - {p.bookingRef} - - {p.bookingStatus} - -
+ + + + + + + + + - ))} - {filteredList.length === 0 && ( - - )} - -
#NameCoach · SeatOriginDestinationDateBooking Ref
No passengers found
+ + + {filteredList.map((p, i) => ( + + {i + 1} + {p.passengerName} + {p.coachSeat} + {p.origin} + {p.destination} + {p.departureAt ? formatDateTime(p.departureAt) : '—'} + {p.bookingRef} + + ))} + {filteredList.length === 0 && ( + No passengers found + )} + + +
)} diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx index 196e83c46..f56f26e8e 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/seats/page.tsx @@ -155,7 +155,7 @@ export default function SeatStatusReportPage() {
{/* Summary Cards */} -
+
@@ -206,20 +206,6 @@ export default function SeatStatusReportPage() {
- {blockedSeats.length > 0 && ( -
- {blockedSeats.map((b: any) => ( -
- - Seat {b.seatNumber} · Coach {b.coachNumber} - - - {b.reason} - -
- ))} -
- )}