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 c85b49bbf..340900fdd 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -224,16 +224,27 @@ export class SeatsService { } } - const availability = await this.segmentsService.getSeatAvailabilityMap( - scheduleId, seatIds, stopTimes, reqFrom, reqTo, journeyDirection || JourneyDirection.ONE_WAY, - ); + const [availability, persistedSeats] = await Promise.all([ + this.segmentsService.getSeatAvailabilityMap( + scheduleId, seatIds, stopTimes, reqFrom, reqTo, journeyDirection || JourneyDirection.ONE_WAY, + ), + this.prisma.seat.findMany({ + where: { id: { in: seatIds } }, + select: { id: true, status: true }, + }), + ]); + + const persistedStatus = new Map(persistedSeats.map(s => [s.id, s.status])); - // Every requested seat defaults to AVAILABLE — this also guards against a stale - // persisted Seat.status column (e.g. a leftover 'BOOKED'/'BLOCKED' value) bleeding - // through getSeatMap's own fallback, since that fallback only triggers when this - // map has no entry at all for a given seat. for (const seatId of seatIds) { - statusMap.set(seatId, availability.get(seatId) ?? 'AVAILABLE'); + const persisted = persistedStatus.get(seatId); + // BLOCKED and UNDER_MAINTENANCE are cross-schedule flags set by admins — + // always honour them regardless of hold/booking state. + if ((persisted as string) === 'BLOCKED' || (persisted as string) === 'UNDER_MAINTENANCE') { + statusMap.set(seatId, persisted!); + } else { + statusMap.set(seatId, availability.get(seatId) ?? 'AVAILABLE'); + } } return statusMap; diff --git a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx index 413383b7a..9c9ee3286 100644 --- a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx @@ -35,6 +35,7 @@ export default function SeatsPage() { queryKey: ['seatmap', selectedSchedule], queryFn: () => selectedSchedule ? seatsApi.getSeatMap(selectedSchedule) : Promise.resolve(null), enabled: !!selectedSchedule, + staleTime: 0, }); const { data: coachTypesData } = useQuery({ @@ -49,6 +50,7 @@ export default function SeatsPage() { const { data: routeCoachesData, isLoading: routeCoachesLoading } = useQuery({ queryKey: ['routeCoaches', selectedRoute], + staleTime: 0, queryFn: async () => { if (!selectedRoute) return null; const template: any[] = await routeCoachTemplatesApi.get(selectedRoute); @@ -79,10 +81,15 @@ export default function SeatsPage() { enabled: !!selectedRoute, }); + const invalidateSeatData = () => { + queryClient.refetchQueries({ queryKey: ['seatmap', selectedSchedule] }); + queryClient.refetchQueries({ queryKey: ['routeCoaches', selectedRoute] }); + }; + const blockMutation = useMutation({ mutationFn: ({ seatId, reason }: any) => seatsApi.block(seatId, { reason }), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); setShowBlockModal(false); setSelectedSeat(null); setBlockReason(''); @@ -92,14 +99,14 @@ export default function SeatsPage() { const unblockMutation = useMutation({ mutationFn: (seatId: string) => seatsApi.unblock(seatId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); }, }); const removeSeatMutation = useMutation({ mutationFn: (seatId: string) => seatsApi.removeSeat(seatId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); setShowRemoveModal(false); setSelectedSeat(null); }, @@ -108,7 +115,7 @@ export default function SeatsPage() { const undoRemoveMutation = useMutation({ mutationFn: (seatId: string) => seatsApi.undoRemove(seatId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); }, }); @@ -116,7 +123,7 @@ export default function SeatsPage() { mutationFn: ({ seatId, reason }: { seatId: string; reason: string }) => seatsApi.setMaintenance(seatId, reason), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); setShowMaintenanceModal(false); setSelectedSeat(null); setMaintenanceReason(''); @@ -125,7 +132,7 @@ export default function SeatsPage() { const clearMaintenanceMutation = useMutation({ mutationFn: (seatId: string) => seatsApi.clearMaintenance(seatId), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['seatmap'] }), + onSuccess: () => invalidateSeatData(), }); const schedules = schedulesData?.items || schedulesData?.data || []; @@ -139,7 +146,7 @@ export default function SeatsPage() { return Promise.all(seatIds.map((seatId: string) => seatsApi.block(seatId, { reason }))); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); setShowBlockCoachModal(false); setSelectedCoach(null); setBlockCoachReason(''); @@ -153,7 +160,7 @@ export default function SeatsPage() { return Promise.all(seatIds.map((seatId: string) => seatsApi.unblock(seatId))); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['seatmap'] }); + invalidateSeatData(); setShowUnblockCoachModal(false); setCoachToUnblock(null); }, @@ -1015,7 +1022,7 @@ function SeatIcon({ const color = getSeatColor(status); const canBlock = status === 'AVAILABLE'; const canUnblock = status === 'BLOCKED'; - const canMaintenance = status === 'AVAILABLE' || status === 'BLOCKED'; + const canMaintenance = false; const canClearMaintenance = status === 'UNDER_MAINTENANCE'; return (