feat(group-booking): draft schedules, coach-scoped seating, mixed classes

This commit is contained in:
Abubeker Yasin
2026-09-08 16:07:46 +03:00
parent b08fc85aaf
commit 3fb019fd07
28 changed files with 1068 additions and 107 deletions

View File

@@ -70,10 +70,10 @@ function emptySearchMessage(reason: SearchEmptyReason | undefined): string {
case 'PACKAGE_ONLY':
return `Departures on this date are reserved for travel packages, not regular ticketing.`;
case 'GROUP_BOOKING_ONLY':
// isGroupBookingOnly is an exclusive partition — this page only ever sees group-booking
// schedules, so an empty result here means a regular (non-group) train runs on this date
// but nothing has been set up for group booking specifically.
return `A regular train runs from ${o} to ${d} on this date, but no schedule has been set up for group booking yet — ask fleet/schedule management to create one, or try another date.`;
// The staff channel is a superset of the public one — it sees ordinary, group-reserved
// and unpublished DRAFT trips alike — so the backend never returns this code here. Kept
// only so the switch stays exhaustive against the shared SearchEmptyReasonCode union.
return `No departures from ${o} to ${d} are available for booking on this date.`;
case 'CHECKIN_CLOSED':
return `Check-in has already closed for every departure on this date.`;
case 'FULLY_BOOKED':
@@ -285,7 +285,8 @@ function GroupBookingPageContent() {
journeyType: tripType,
returnDate: tripType === 'ROUND_TRIP' ? returnDate : undefined,
nationality: fareTier === 'LOCAL' ? 'Ethiopian' : 'Other',
channel: 'GROUP_BOOKING',
// The staff channel comes from the route groupBookingApi.searchTrips calls
// (POST /search/group), not from a field in the body.
}),
});

View File

@@ -13,10 +13,9 @@ export interface SearchTripsRequest {
returnDate?: string;
/** Drives which fare tier (Local vs International) gets quoted — see fareTier in the page component. */
nationality?: string;
/** Always 'GROUP_BOOKING' for this app — search returns ONLY schedules marked
* isGroupBookingOnly (an exclusive partition, not additive): normal passenger-facing
* schedules never show up here, and group-only schedules never show up in the portal. */
channel?: 'PORTAL' | 'GROUP_BOOKING';
// There is no `channel` field any more. It used to select the staff view from the request
// BODY of the public `POST /search`, which meant anyone could ask for it. The channel is now
// decided by the route: `POST /search/group` is permission-guarded (see searchTrips below).
}
export interface ScheduleClassOption {
@@ -227,8 +226,14 @@ export interface InitiatePaymentResponse {
}
export const groupBookingApi = {
/**
* The staff search channel. Same body and response as the public `POST /search`, but this
* route also returns unpublished DRAFT trips and trips reserved for groups — and, unlike
* before, ordinary scheduled trips too, so a group can be booked onto a normal service.
* Requires bookings:create / bookings:manage; the shared apiClient sends the IAM token.
*/
searchTrips: (dto: SearchTripsRequest) =>
apiClient.post<SearchTripsResponse>('/search', dto),
apiClient.post<SearchTripsResponse>('/search/group', dto),
getSeatClasses: () => apiClient.get<SeatClassOption[]>('/seat-classes'),