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 7cee8e552..55f3e21bf 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: true } }, + 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.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) }; }); @@ -212,6 +217,7 @@ export class ReportsService { where: { status: { in: ['CONFIRMED', 'BOARDED'] } }, include: { seats: { + where: { leg: 1 }, include: { seat: { include: { coach: { include: { coachType: true } } } }, }, @@ -307,8 +313,8 @@ 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: { @@ -324,20 +330,25 @@ export class ReportsService { }, seat: { include: { coach: { select: { number: true, coachType: { select: { name: 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[] + seats.flatMap(bs => [bs.booking.originStationId, bs.booking.destinationStationId]).filter(Boolean) as string[], )]; - const stations = stationIds.length + const stations = stationIds.length > 0 ? 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])); + 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, @@ -347,8 +358,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, + origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? null) : null, + destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? null) : null, amountPaidMinor: bs.booking.totalMinor, currency: bs.booking.currency ?? 'ETB', isGroupBooking: (bs.booking._count?.seats ?? 0) > 1, 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..46bb34b76 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,29 @@ export class SeatsService { await this.prisma.journey.deleteMany({ where: { bookingId } as any }); } + async getBlockedSeats() { + const blocks = await this.prisma.seatBlock.findMany({ + where: { + NOT: { reason: { startsWith: 'MAINTENANCE:' } }, + }, + 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 bc657106d..8c2b38570 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 && ( @@ -161,7 +170,7 @@ function DashboardPageContent() { )} {/* Stat cards */} -
+
} iconBg="bg-blue-100 dark:bg-blue-900/30" @@ -174,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/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx index 2cbc07ca6..4e2647877 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,7 +20,6 @@ interface PassengersReport { interface PassengerRow { bookingRef: string; - bookingStatus: string; passengerName: string; passengerCategory: string; idDocumentType: string | null; @@ -139,9 +138,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.

} @@ -288,33 +284,19 @@ export default function PassengersReportPage() {
)} - {/* Passenger List tab */} {tab === 'list' && ( -
-
+
+
setListSearch(e.target.value)} /> - - + {passengerList.length > 0 && ( + Export CSV + )}
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..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 @@ -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,19 @@ export default function SeatStatusReportPage() {
+ +
+
+
+

Blocked Seats

+

+ {blockedSeats.length} +

+

Manually blocked

+
+ +
+
{/* 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),