mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -412,6 +412,103 @@ function BookingDetailContent() {
|
||||
return displayTotal / etbTotal;
|
||||
})();
|
||||
|
||||
// Flight-style origin → train → destination timeline for a single leg's schedule.
|
||||
// Shared by both the pending-payment "Trip Summary" card and the confirmed booking's
|
||||
// "Journey Details" card so a round trip's outbound and return legs render identically —
|
||||
// each of those cards calls this once per leg instead of hardcoding booking.schedule only.
|
||||
const renderJourneyTimeline = (schedule: any) => (
|
||||
<div className="flex">
|
||||
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
|
||||
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="pb-8">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{schedule?.departureAt ? formatTime(schedule.departureAt) : "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{schedule?.departureAt
|
||||
? `${format(toZonedDate(new Date(schedule.departureAt)), "EEE, MMM d")} · ${getTimePeriod(schedule.departureAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{schedule?.origin?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{schedule?.origin?.city}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-8">
|
||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium">Train {schedule?.trainNumber}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{schedule?.arrivalAt ? formatTime(schedule.arrivalAt) : "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{schedule?.arrivalAt
|
||||
? `${format(toZonedDate(new Date(schedule.arrivalAt)), "EEE, MMM d")} · ${getTimePeriod(schedule.arrivalAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{schedule?.destination?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{schedule?.destination?.city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Renders one or both legs' timelines with an "Outbound Journey"/"Return Journey"
|
||||
// heading pair when the booking has a return leg, or a single "Your Journey" heading
|
||||
// for one-way bookings — used by both the pending-payment and confirmed views.
|
||||
const renderJourneyLegs = () => (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 bg-primary rounded-full" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{isRoundTripBooking ? "Outbound Journey" : "Your Journey"}
|
||||
</span>
|
||||
{booking.passengers?.[0]?.seat?.seatClass && (
|
||||
<span className="ml-auto text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium">
|
||||
{booking.passengers[0].seat.seatClass}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{renderJourneyTimeline(booking.schedule)}
|
||||
|
||||
{isRoundTripBooking && booking.returnSchedule && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 bg-primary rounded-full" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Return Journey
|
||||
</span>
|
||||
</div>
|
||||
{renderJourneyTimeline(booking.returnSchedule)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Order summary card — mirrors /booking/payment's OrderSummary: fare breakdown per
|
||||
// passenger, Total with a loading spinner while a currency conversion is in flight, and
|
||||
// a note confirming what will actually be charged once a payment method is selected.
|
||||
@@ -586,137 +683,62 @@ function BookingDetailContent() {
|
||||
Trip Summary
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 bg-primary rounded-full" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Your Journey
|
||||
</span>
|
||||
{booking.passengers?.[0]?.seat?.seatClass && (
|
||||
<span className="ml-auto text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium">
|
||||
{booking.passengers[0].seat.seatClass}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Flight-style timeline */}
|
||||
<div className="flex">
|
||||
{/* Left column: Timeline with dots and line */}
|
||||
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||
{/* Origin dot */}
|
||||
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
|
||||
{/* Vertical line */}
|
||||
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||
{/* Destination dot */}
|
||||
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||
</div>
|
||||
|
||||
{/* Right column: Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Origin */}
|
||||
<div className="pb-8">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{booking.schedule?.departureAt
|
||||
? formatTime(booking.schedule.departureAt)
|
||||
: "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{booking.schedule?.departureAt
|
||||
? `${format(toZonedDate(new Date(booking.schedule.departureAt)), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.departureAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{booking.schedule?.origin?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule?.origin?.city}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Journey Info */}
|
||||
<div className="pb-8">
|
||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium">
|
||||
Train {booking.schedule?.trainNumber}
|
||||
</span>
|
||||
</div>
|
||||
{booking.schedule?.trainName && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule.trainName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Destination */}
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{booking.schedule?.arrivalAt
|
||||
? formatTime(booking.schedule.arrivalAt)
|
||||
: "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{booking.schedule?.arrivalAt
|
||||
? `${format(toZonedDate(new Date(booking.schedule.arrivalAt)), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.arrivalAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{booking.schedule?.destination?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule?.destination?.city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderJourneyLegs()}
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Users className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{booking.passengers?.length || 0} Passenger(s)
|
||||
{groupedPassengers.length} Passenger(s)
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{booking.passengers?.map(
|
||||
(passenger: any, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-between text-sm py-2 px-3 bg-gray-50 dark:bg-gray-900 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<div className="text-gray-900 dark:text-white font-medium">
|
||||
{passenger.fullName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{passenger.category} • Coach{" "}
|
||||
{passenger.seat?.coach}
|
||||
</div>
|
||||
{groupedPassengers.map((passenger: any, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-sm py-2 px-3 bg-gray-50 dark:bg-gray-900 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-gray-900 dark:text-white font-medium">
|
||||
{passenger.fullName}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">
|
||||
Seat {passenger.seat?.number}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{passenger.seat?.seatClass}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{passenger.category}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{isRoundTripBooking ? (
|
||||
<div className="mt-1.5 grid grid-cols-2 gap-2">
|
||||
{(
|
||||
[
|
||||
{ legLabel: "Outbound", seat: passenger.outboundSeat },
|
||||
{ legLabel: "Return", seat: passenger.returnSeat },
|
||||
] as const
|
||||
).map(({ legLabel, seat }) => (
|
||||
<div key={legLabel}>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{legLabel}
|
||||
</div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">
|
||||
Seat {seat?.number ?? "--"}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{seat?.seatClass}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1.5 text-right">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">
|
||||
Seat {passenger.outboundSeat?.number}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{passenger.outboundSeat?.seatClass}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -994,102 +1016,7 @@ function BookingDetailContent() {
|
||||
Journey Details
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 bg-primary rounded-full" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Your Journey
|
||||
</span>
|
||||
{booking.passengers?.[0]?.seat?.seatClass && (
|
||||
<span className="ml-auto text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium">
|
||||
{booking.passengers[0].seat.seatClass}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Flight-style timeline */}
|
||||
<div className="flex">
|
||||
{/* Left column: Timeline with dots and line */}
|
||||
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||
{/* Origin dot */}
|
||||
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
|
||||
{/* Vertical line */}
|
||||
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||
{/* Destination dot */}
|
||||
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||
</div>
|
||||
|
||||
{/* Right column: Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Origin */}
|
||||
<div className="pb-8">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{booking.schedule?.departureAt
|
||||
? formatTime(booking.schedule.departureAt)
|
||||
: "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{booking.schedule?.departureAt
|
||||
? `${format(toZonedDate(new Date(booking.schedule.departureAt)), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.departureAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{booking.schedule?.origin?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule?.origin?.city}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Journey Info */}
|
||||
<div className="pb-8">
|
||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium">
|
||||
Train {booking.schedule?.trainNumber}
|
||||
</span>
|
||||
</div>
|
||||
{booking.schedule?.trainName && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule.trainName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Destination */}
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{booking.schedule?.arrivalAt
|
||||
? formatTime(booking.schedule.arrivalAt)
|
||||
: "--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{booking.schedule?.arrivalAt
|
||||
? `${format(toZonedDate(new Date(booking.schedule.arrivalAt)), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.arrivalAt)}`
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||
{booking.schedule?.destination?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{booking.schedule?.destination?.city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderJourneyLegs()}
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user