-
- {formatCoachTypeLabel(group.coachTypeType)}
-
-
- From {formatPrice(group.minPrice * priceMultiplier, group.currency)}
- {allSoldOut && · Sold out}
-
-
+
Choose Coach Type
+
+ {groups.map((group, index) => {
+ const CoachIcon = getCoachIcon(group.coachTypeType);
+ const allSoldOut = group.tiers.every((t) => t.availableSeats === 0);
+ const isSelected = selectedId === group.coachTypeId;
+ return (
+
!allSoldOut && setSelectedId(isSelected ? null : group.coachTypeId)}
+ onKeyDown={(e) => {
+ if (!allSoldOut && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ setSelectedId(isSelected ? null : group.coachTypeId);
+ }
+ }}
+ className={`group relative w-full p-2 rounded-2xl border-2 text-left transition-all duration-200 ${allSoldOut
+ ? 'border-gray-200 dark:border-gray-700 opacity-50 cursor-not-allowed'
+ : isSelected
+ ? 'border-primary bg-gradient-to-br from-primary/8 to-primary/3 dark:from-primary/15 dark:to-primary/5 shadow-lg shadow-primary/20 scale-[1.02] cursor-pointer'
+ : 'border-gray-200 dark:border-gray-700 shadow-sm hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50 cursor-pointer'
+ }`}
+ style={{ animation: `fade-in-up 0.3s ease-out ${index * 0.08}s both` }}
+ >
+ {/* Radio indicator */}
{!allSoldOut && (
-
- {isSelected && }
-
+
+ {isSelected && }
+
)}
-
- {/* All available classes for this coach type */}
-
- {group.tiers.map((tier) => {
- const soldOut = tier.availableSeats === 0;
- return (
-
-
-
-
{tier.seatType.trim()}
-
- {formatPrice(tier.priceMinor * priceMultiplier, tier.currency)}
- {soldOut ? (
- Sold out
- ) : (
- {tier.availableSeats} left
- )}
-
+
+
+
+
+
+
+
+
+ {formatCoachTypeLabel(group.coachTypeType)}
+
+ {allSoldOut && (
+
Sold out
+ )}
+
+ From
+
+ {((group.minPrice * priceMultiplier) / 100).toFixed(2)}
+
+ {group.currency}
- );
- })}
-
+
- {/* Book Now — only when this group is selected */}
- {isSelected && !allSoldOut && (
-
-
+ {/* Class options — always visible, matching results page style */}
+ {group.tiers.length > 0 && (
+
+
+ {group.tiers.map((tier) => {
+ const soldOut = tier.availableSeats === 0;
+ return (
+
+
{tier.seatType.trim()}
+ {soldOut ? (
+
Sold out
+ ) : (
+
+
+ {((tier.priceMinor * priceMultiplier) / 100).toFixed(2)}
+
+ {tier.currency}
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ {!isSelected && !allSoldOut && (
+
+ Click to select this coach
+
+ )}
+
+ {isSelected && !allSoldOut && (
+
+ )}
- )}
-
- );
- })}
+
+ );
+ })}
+
+
);
}
@@ -407,7 +438,7 @@ function PassengerCountModal({
Coach type
-
{tier.seatClass?.coachType?.type ? formatCoachTypeLabel(tier.seatClass.coachType.type) : tier.label.trim()}
+
{tier.seatClass?.coachType?.name ?? tier.label.trim()}
{remaining} seats remaining · from {formatPrice(minPriceMinor * priceMultiplier, tier.currency)} per adult · 1st child per adult free (no seat)
@@ -416,26 +447,26 @@ function PassengerCountModal({
{ label: "Adults", sub: `Age 5+ · max ${PKG_MAX_ADULTS}`, value: adultCount, min: 1, max: Math.min(PKG_MAX_ADULTS, remaining), set: (v: number) => { setAdultCount(v); const newMax = Math.min(v * PKG_CHILDREN_PER_ADULT, v + Math.max(0, remaining - v)); setChildCount(c => Math.min(c, newMax)); } },
{ label: "Children", sub: `Under 5 · max ${PKG_CHILDREN_PER_ADULT} per adult · 1st per adult FREE (no seat)`, value: childCount, min: 0, max: Math.min(adultCount * PKG_CHILDREN_PER_ADULT, adultCount + Math.max(0, remaining - adultCount)), set: setChildCount },
].map(({ label, sub, value, min, max, set }) => (
-
-
-
-
- {value}
-
-
+
+
- ))}
+
+
+ {value}
+
+
+
+ ))}
{freeChildren > 0 && (
@@ -457,15 +488,14 @@ function PassengerCountModal({
{/* Departure Station */}
-
diff --git a/apps/edr-passenger-web/portal/src/lib/booking-store.ts b/apps/edr-passenger-web/portal/src/lib/booking-store.ts
index e5d1d83aa..ce578b7a9 100644
--- a/apps/edr-passenger-web/portal/src/lib/booking-store.ts
+++ b/apps/edr-passenger-web/portal/src/lib/booking-store.ts
@@ -128,7 +128,7 @@ interface BookingState {
reviewedTotalMinor: number | null;
// Per-passenger fare breakdown computed on the review page — guarantees line items
// on the payment page sum to exactly reviewedTotalMinor.
- reviewedPassengerFares: Array<{ fareMinor: number; isFree: boolean }> | null;
+ reviewedPassengerFares: Array<{ fareMinor: number; isFree: boolean; outboundFareMinor?: number; inboundFareMinor?: number }> | null;
setSearchCriteria: (criteria: SearchCriteria) => void;
setSelectedSchedule: (schedule: SelectedSchedule) => void;
@@ -143,7 +143,7 @@ interface BookingState {
setCreateAccount: (create: boolean) => void;
setPassengerId: (id: string | null) => void;
setPackageContext: (packageId: string, priceTierId: string, priceMinor: number, packageName?: string, departureStationId?: string, departureStationName?: string) => void;
- setReviewedTotal: (totalMinor: number, passengerFares: Array<{ fareMinor: number; isFree: boolean }>) => void;
+ setReviewedTotal: (totalMinor: number, passengerFares: Array<{ fareMinor: number; isFree: boolean; outboundFareMinor?: number; inboundFareMinor?: number }>) => void;
clearBooking: () => void;
}