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 5c237e6ba..495c394ee 100644
--- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts
+++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts
@@ -61,11 +61,12 @@ export class SeatsService {
const resolvedBedPosition = isBedCoach
? this.resolveBedPosition(s.col, s.bedPosition)
: s.bedPosition;
+ const effectiveStatus = effectiveStatuses.get(s.id) ?? (s.status === 'BLOCKED' || s.status === 'BOOKED' ? s.status : 'AVAILABLE');
return {
id: s.id,
seatNumber: s.seatNumber,
label: s.seatNumber,
- status: effectiveStatuses.get(s.id) ?? s.status,
+ status: effectiveStatus,
kind: s.kind,
row: s.row,
col: s.col,
@@ -245,11 +246,16 @@ export class SeatsService {
holdFrom === undefined || holdTo === undefined ||
(holdFrom < reqTo && reqFrom < holdTo);
- if (!legsOverlap) continue;
-
// Check direction conflict
const directionsConflict = this.checkDirectionConflict(reqDirection, holdDirection);
- if (!directionsConflict) continue;
+
+ if (!legsOverlap || !directionsConflict) {
+ // This hold does not conflict with the requested leg/direction.
+ // Explicitly mark AVAILABLE so the DB's HELD status (set by the
+ // opposing-direction hold) does not bleed through via the fallback.
+ if (!statusMap.has(seatId)) statusMap.set(seatId, 'AVAILABLE');
+ continue;
+ }
statusMap.set(seatId, 'HELD');
}
@@ -653,7 +659,7 @@ export class SeatsService {
const seats = a.coach.seats.filter(s => s.seatNumber && !s.seatNumber.startsWith('-'));
const totalSeats = seats.length;
const unavailable = seats.filter(s => {
- const status = effectiveStatuses.get(s.id) ?? s.status;
+ const status = effectiveStatuses.get(s.id) ?? (s.status === 'BLOCKED' || s.status === 'BOOKED' ? s.status : 'AVAILABLE');
return status === 'HELD' || status === 'BOOKED' || status === 'BLOCKED';
}).length;
@@ -665,8 +671,8 @@ export class SeatsService {
seatClasses: a.coach.coachType?.seatClasses.map(sc => sc.name) ?? [],
totalSeats,
availableSeats: totalSeats - unavailable,
- heldSeats: seats.filter(s => (effectiveStatuses.get(s.id) ?? s.status) === 'HELD').length,
- bookedSeats: seats.filter(s => (effectiveStatuses.get(s.id) ?? s.status) === 'BOOKED').length,
+ heldSeats: seats.filter(s => (effectiveStatuses.get(s.id) ?? (s.status === 'BLOCKED' || s.status === 'BOOKED' ? s.status : 'AVAILABLE')) === 'HELD').length,
+ bookedSeats: seats.filter(s => (effectiveStatuses.get(s.id) ?? (s.status === 'BLOCKED' || s.status === 'BOOKED' ? s.status : 'AVAILABLE')) === 'BOOKED').length,
};
});
}
@@ -861,11 +867,21 @@ export class SeatsService {
if (expired.length === 0) return;
const expiredSeatIds = expired.flatMap(h => h.seatIds as string[]);
- // Only reset seats that are still HELD — BOOKED seats have been confirmed and must not be touched.
- await this.prisma.seat.updateMany({
- where: { id: { in: expiredSeatIds }, status: 'HELD' },
- data: { status: 'AVAILABLE' },
+
+ // Only reset seats that have no remaining active holds
+ const stillHeld = await this.prisma.seatHold.findMany({
+ where: { expiresAt: { gte: new Date() }, seatIds: { hasSome: expiredSeatIds } },
+ select: { seatIds: true },
});
+ const stillHeldIds = new Set(stillHeld.flatMap(h => h.seatIds as string[]));
+ const toRelease = expiredSeatIds.filter(id => !stillHeldIds.has(id));
+
+ if (toRelease.length > 0) {
+ await this.prisma.seat.updateMany({
+ where: { id: { in: toRelease }, status: 'HELD' },
+ data: { status: 'AVAILABLE' },
+ });
+ }
await this.prisma.seatHold.deleteMany({ where: { expiresAt: { lt: new Date() } } });
}
}
diff --git a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx
index c89edf5ec..974c25470 100644
--- a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx
+++ b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx
@@ -13,7 +13,7 @@ import { usePermission } from '@/lib/use-permission';
import { PERMS } from '@/lib/permissions';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import { bookingsApi, apiClient } from '@/lib/api';
-import { formatCurrency, formatDateTime } from '@/lib/utils';
+import { formatCurrency, formatDateTime, formatDateTimeShort } from '@/lib/utils';
import { BookingFilters } from '@/types';
const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => (
@@ -151,12 +151,38 @@ function BookingsPageContent() {
)}
{booking.isPackageBooking
- ?
From: {booking.departureStationName || booking.schedule?.originStation?.name || '—'}
+ ? Boarding at: {booking.departureStationName || booking.schedule?.originStation?.name || '—'}
: {booking.bookingType || 'ONE_WAY'}
}
),
},
+ {
+ key: 'trip',
+ label: 'Trip',
+ render: (booking: any) => {
+ const isRoundTrip = booking?.bookingType === 'ROUND_TRIP' || booking?.bookingType === 'ROUND_TRIP_TRANSIT';
+ const returnDeparture = booking?.returnSchedule?.departureAt;
+ console.log(JSON.stringify(booking.packageId));
+ return (
+
+
+ {booking.schedule?.originStation?.name || 'N/A'} → {booking.schedule?.destinationStation?.name || 'N/A'}
+
+
+ {!isRoundTrip ? (
+ {booking.schedule?.departureAt ? formatDateTimeShort(booking.schedule.departureAt) : 'N/A'}
+ ) : (
+
+ {booking.schedule?.departureAt ? formatDateTimeShort(booking.schedule.departureAt) : 'N/A'} ·
+ {returnDeparture ? formatDateTimeShort(returnDeparture) : ''}
+
+ )}
+
+
+ );
+ },
+ },
{
key: 'passengerNames', label: 'Names',
render: (booking: any) => {
@@ -203,14 +229,6 @@ function BookingsPageContent() {
),
},
- {
- key: 'passengerCount', label: 'Passengers',
- render: (booking: any) => {
- const adults = booking.adultCount || 0, children = booking.childCount || 0;
- if (!adults && !children) return '—';
- return <>Adult: {adults}
Child: {children}
>;
- },
- },
{
key: 'paymentStatus', label: 'Payment',
render: (booking: any) => (
diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx
index c56ccd304..6b5bcb715 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx
@@ -153,7 +153,7 @@ export default function ConfirmationPage() {
const handleNewBooking = () => {
clearBooking();
- router.push('/booking/search');
+ window.location.href = '/';
};
useEffect(() => {
diff --git a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx
index d5cdcf3af..9d289bbe5 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx
@@ -143,7 +143,7 @@ export default function SeatsPage() {
const [passengerSeatMap, setPassengerSeatMap] = useState>({});
// Outbound seat IDs locked in after the outbound hold — used to prevent the
// same physical seat being picked again on the inbound leg.
- const [outboundLockedSeatIds, setOutboundLockedSeatIds] = useState>(new Set());
+
const [activePassengerIndex, setActivePassengerIndex] = useState(0);
const [selectedCoach, setSelectedCoach] = useState(null);
const [currentJourneyType, setCurrentJourneyType] = useState<
@@ -173,9 +173,12 @@ export default function SeatsPage() {
isLoading,
error,
} = useQuery({
- queryKey: ["seatmap", currentSchedule?.id, coachTypeId, currentJourneyType],
+ queryKey: ["seatmap", currentSchedule?.id, coachTypeId, currentJourneyType, (currentSchedule as any)?.originStationId, (currentSchedule as any)?.destinationStationId],
queryFn: async () => {
- const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachTypeId=${coachTypeId}&journeyDirection=${journeyDirection}`;
+ const scheduleForMap = isRoundTrip && currentJourneyType === "inbound" ? inboundSchedule : (isRoundTrip ? outboundSchedule : selectedSchedule);
+ const originId = (scheduleForMap as any)?.originStationId || searchCriteria?.originStationId;
+ const destinationId = (scheduleForMap as any)?.destinationStationId || searchCriteria?.destinationStationId;
+ const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachTypeId=${coachTypeId}&journeyDirection=${journeyDirection}${originId ? `&originStationId=${originId}` : ''}${destinationId ? `&destinationStationId=${destinationId}` : ''}`;
const response = await apiClient.get(endpoint);
@@ -337,10 +340,9 @@ export default function SeatsPage() {
}, [allSeats, selectedCoachData, currentSchedule?.selectedSeatClass]);
// Seats already claimed by any passenger in this journey leg, plus outbound
- // locked seats (so inbound cannot reuse the same physical seat IDs).
const assignedSeatIds = useMemo(
- () => new Set([...Object.values(passengerSeatMap), ...outboundLockedSeatIds]),
- [passengerSeatMap, outboundLockedSeatIds],
+ () => new Set(Object.values(passengerSeatMap)),
+ [passengerSeatMap],
);
// The furthest passenger a user is allowed to jump to — cannot skip ahead of the
@@ -359,11 +361,10 @@ export default function SeatsPage() {
const isSeatAssignedToOther = useCallback(
(seatId: string) =>
- outboundLockedSeatIds.has(seatId) ||
Object.entries(passengerSeatMap).some(
([idx, sid]) => Number(idx) !== activePassengerIndex && sid === seatId,
),
- [passengerSeatMap, activePassengerIndex, outboundLockedSeatIds],
+ [passengerSeatMap, activePassengerIndex],
);
const handleSelectPassenger = useCallback(
@@ -376,12 +377,10 @@ export default function SeatsPage() {
const handleSeatClick = useCallback(
(seatId: string) => {
- // Seat already claimed by a different passenger or locked from outbound — never allow duplicate assignment
- const takenByOther =
- outboundLockedSeatIds.has(seatId) ||
- Object.entries(passengerSeatMap).some(
- ([idx, sid]) => Number(idx) !== activePassengerIndex && sid === seatId,
- );
+ // Seat already claimed by a different passenger — never allow duplicate assignment
+ const takenByOther = Object.entries(passengerSeatMap).some(
+ ([idx, sid]) => Number(idx) !== activePassengerIndex && sid === seatId,
+ );
if (takenByOther) return;
const isDeselecting = passengerSeatMap[activePassengerIndex] === seatId;
@@ -401,7 +400,7 @@ export default function SeatsPage() {
if (nextUnassigned !== -1) setActivePassengerIndex(nextUnassigned);
}
},
- [passengerSeatMap, activePassengerIndex, passengers, outboundLockedSeatIds],
+ [passengerSeatMap, activePassengerIndex, passengers],
);
const allSeatsAssigned =
@@ -436,8 +435,6 @@ export default function SeatsPage() {
});
return;
}
- // Lock outbound seat IDs so they cannot be selected on the inbound leg
- setOutboundLockedSeatIds(new Set(seatIds));
setCurrentJourneyType("inbound");
setPassengerSeatMap({});
setActivePassengerIndex(0);
diff --git a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx
index 5c8d14891..aee655dd5 100644
--- a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx
@@ -21,7 +21,6 @@ import {
Tag,
Shield,
X,
- Navigation,
} from "lucide-react";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -322,7 +321,7 @@ function PassengerCountModal({
-
Number of passengers
+
Number of passengers & boarding station selection
@@ -374,8 +373,7 @@ function PassengerCountModal({
{/* Departure Station */}
-