diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 29be84adc..89dfe0ca0 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -390,8 +390,8 @@ export class SearchService { // Exclude schedules with no seats at all if (allValidSeatIds.length === 0) return null; - // Run availability batch and fare calculation in parallel - const [freeSeats, faresByClass] = await Promise.all([ + // Run availability batch, fare calculation, and schedule-scoped seat blocks in parallel + const [freeSeatsRaw, faresByClass, scheduleBlocks] = await Promise.all([ this.segmentsService.getFreeSeatIds( schedule.id, allValidSeatIds, @@ -400,7 +400,17 @@ export class SearchService { destStop.sequence, ), this.calculateFaresForSegment(schedule, originStationId, destinationStationId, nationality), + this.prisma.seatBlock.findMany({ + where: { + scheduleId: schedule.id, + seatId: { in: allValidSeatIds }, + }, + select: { seatId: true }, + }), ]); + const scheduleBlockedIds = new Set(scheduleBlocks.map((b: any) => b.seatId)); + // Seats that are free from holds/bookings AND not schedule-blocked + const freeSeats = new Set([...freeSeatsRaw].filter(id => !scheduleBlockedIds.has(id))); // Compute per-class availability using the pre-computed free seat set. A coach type // has separate seat classes per nationality tier (e.g. "VIP Bed Upper (Local)" AND 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 91af5e11d..abb747710 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -325,6 +325,18 @@ export class SeatsService { throw new ConflictException(`Seat(s) ${blocked.map(s => s.seatNumber).join(', ')} are already taken`); const seatLabelById = Object.fromEntries(seats.map(s => [s.id, s.seatNumber])); + + // Schedule-scoped blocks prevent holding a seat on this specific schedule + // even if its global Seat.status is AVAILABLE. + const scheduleBlockRecords = await tx.seatBlock.findMany({ + where: { scheduleId: dto.scheduleId, seatId: { in: seatIds } }, + select: { seatId: true }, + }); + if (scheduleBlockRecords.length > 0) { + const blockedNums = scheduleBlockRecords.map(b => seatLabelById[b.seatId]).join(', '); + throw new ConflictException(`Seat(s) ${blockedNums} are blocked for this schedule`); + } + const stopTimes = await tx.tripStopTime.findMany({ where: { scheduleId: dto.scheduleId }, select: { stationId: true, sequence: true }, @@ -711,11 +723,18 @@ export class SeatsService { const reqFrom = seqOf(schedule.originStationId) ?? 0; const reqTo = seqOf(schedule.destinationStationId) ?? stopTimes.length; - const unavailable = await this.segmentsService.getSeatAvailabilityMap( - scheduleId, allSeatIds, stopTimes, reqFrom, reqTo, - ); + const [unavailable, scheduleBlocks] = await Promise.all([ + this.segmentsService.getSeatAvailabilityMap( + scheduleId, allSeatIds, stopTimes, reqFrom, reqTo, + ), + this.prisma.seatBlock.findMany({ + where: { scheduleId, seatId: { in: allSeatIds } }, + select: { seatId: true }, + }), + ]); + const scheduleBlockedIds = new Set(scheduleBlocks.map(b => b.seatId)); - const availableSeats = seats.filter(s => !unavailable.has(s.id)); + const availableSeats = seats.filter(s => !unavailable.has(s.id) && !scheduleBlockedIds.has(s.id)); if (availableSeats.length < count) { throw new ConflictException(`Only ${availableSeats.length} seats available, requested ${count}`); 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 3a9a65353..0dcc3c6f5 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 @@ -33,6 +33,8 @@ export default function PassengersReportPage() { const [scheduleId, setScheduleId] = useState(''); const [tab, setTab] = useState('occupancy'); const [listSearch, setListSearch] = useState(''); + const [filterCoach, setFilterCoach] = useState(''); + const [filterOrigin, setFilterOrigin] = useState(''); const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery({ queryKey: ['report-schedules'], @@ -98,7 +100,7 @@ export default function PassengersReportPage() {