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 48e29968f..f5d71229d 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 @@ -1,16 +1,16 @@ -'use client'; +"use client"; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; -import { useRouter } from 'next/navigation'; -import { useBookingStore } from '@/lib/booking-store'; -import { usePaymentStore } from '@/lib/payment-store'; -import { useQuery } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api-client'; -import { useEffect, useState } from 'react'; -import { CheckCircle, Clock, Copy, Train, FileText } from 'lucide-react'; -import { format } from 'date-fns'; -import { isChild, isFirstChild } from '@/utils/fare-utils'; +import { useRouter } from "next/navigation"; +import { useBookingStore } from "@/lib/booking-store"; +import { usePaymentStore } from "@/lib/payment-store"; +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/lib/api-client"; +import { useEffect, useState } from "react"; +import { CheckCircle, Clock, Copy, Train, FileText } from "lucide-react"; +import { format } from "date-fns"; +import { isChild, isFirstChild } from "@/utils/fare-utils"; type BookingWithTicket = { id: string; @@ -38,11 +38,24 @@ type BookingWithTicket = { export default function ConfirmationPage() { const router = useRouter(); - const { bookingId, pnr, selectedSchedule, outboundSchedule, inboundSchedule, searchCriteria, passengers, clearBooking, packageName, packageId, reviewedTotalMinor, reviewedPassengerFares } = useBookingStore(); + const { + bookingId, + pnr, + selectedSchedule, + outboundSchedule, + inboundSchedule, + searchCriteria, + passengers, + clearBooking, + packageName, + packageId, + reviewedTotalMinor, + reviewedPassengerFares, + } = useBookingStore(); // The currency/amount actually confirmed for the payment option the user selected — // null when no payment step ran (e.g. a fully-discounted, zero-amount booking). const { selectedCurrency: paidCurrency, paidAmountMinor } = usePaymentStore(); - const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP'; + const isRoundTrip = searchCriteria?.tripType === "ROUND_TRIP"; const [copied, setCopied] = useState(false); const [isGeneratingVoucher, setIsGeneratingVoucher] = useState(false); @@ -51,20 +64,23 @@ export default function ConfirmationPage() { // too long after the originating click's synchronous execution window is silently // blocked, and awaiting a cold dynamic import is enough to fall outside that window. useEffect(() => { - import('@/lib/generate-voucher'); + import("@/lib/generate-voucher"); }, []); const { data: _booking } = useQuery({ - queryKey: ['booking', bookingId], + queryKey: ["booking", bookingId], queryFn: async (): Promise => { try { return await apiClient.get(`/bookings/${bookingId}`); } catch (error) { return { - id: bookingId || '', + id: bookingId || "", pnr: pnr || undefined, - status: 'PENDING_PAYMENT', - totalMinor: passengers.reduce((sum) => sum + (selectedSchedule?.baseFareAdult || 0), 0), + status: "PENDING_PAYMENT", + totalMinor: passengers.reduce( + (sum) => sum + (selectedSchedule?.baseFareAdult || 0), + 0, + ), }; } }, @@ -76,7 +92,7 @@ export default function ConfirmationPage() { // Ticket generation itself is never triggered from this page — the payment webhook // generates it server-side (for every payment method, wallet included); this page only // ever fetches and displays whatever the booking query above already returns. - const isConfirmed = _booking?.status === 'CONFIRMED'; + const isConfirmed = _booking?.status === "CONFIRMED"; const copyPNR = () => { if (pnr) { @@ -88,46 +104,53 @@ export default function ConfirmationPage() { const handleDownloadVoucher = async () => { if (!pnr) { - alert('Booking data not available. Please try again.'); + alert("Booking data not available. Please try again."); return; } if (!passengers.length) { - alert('No passenger data found.'); + alert("No passenger data found."); return; } setIsGeneratingVoucher(true); try { - const { generatePassengerVoucherPDF } = await import('@/lib/generate-voucher'); + const { generatePassengerVoucherPDF } = + await import("@/lib/generate-voucher"); const activeSchedule = isRoundTrip ? outboundSchedule : selectedSchedule; // The server-confirmed settled amount/currency (what was actually charged) is // authoritative — prefer it over the ETB booking fare once it's available. const settledAmountMinor = _booking?.payment?.amountMinor; const settledCurrency = _booking?.payment?.currency; - const voucherCurrency = settledCurrency || 'ETB'; + const voucherCurrency = settledCurrency || "ETB"; const createdAt = _booking?.createdAt || new Date().toISOString(); - const status = _booking?.status || 'CONFIRMED'; + const status = _booking?.status || "CONFIRMED"; // Compute per-passenger fares (in ETB) using the same logic as the review/payment // pages. reviewedPassengerFares is the authoritative source; rebuild from package // context as a fallback so free children always show 0 on their voucher. const { packageTierPriceMinor } = useBookingStore.getState(); const isPackageBooking = packageTierPriceMinor != null; - const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length; + const adultCount = + searchCriteria?.adultCount ?? + passengers.filter((p) => !isChild(p)).length; const pkgMultiplier = isPackageBooking ? 2 : 1; - const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgMultiplier : 0; + const pkgAdultFare = isPackageBooking + ? packageTierPriceMinor! * pkgMultiplier + : 0; const pkgChildFare = pkgAdultFare; const getEtbFare = (idx: number): number => { - if (reviewedPassengerFares?.[idx] != null) return reviewedPassengerFares[idx].fareMinor; + if (reviewedPassengerFares?.[idx] != null) + return reviewedPassengerFares[idx].fareMinor; if (isPackageBooking) { const isPkgChild = idx >= adultCount; - const isFreeChild = isPkgChild && (idx - adultCount) < adultCount; + const isFreeChild = isPkgChild && idx - adultCount < adultCount; if (isFreeChild) return 0; return isPkgChild ? pkgChildFare : pkgAdultFare; } - const totalFare = reviewedTotalMinor ?? paidAmountMinor ?? _booking?.totalMinor ?? 0; + const totalFare = + reviewedTotalMinor ?? paidAmountMinor ?? _booking?.totalMinor ?? 0; return Math.round(totalFare / passengers.length); }; @@ -136,31 +159,54 @@ export default function ConfirmationPage() { // ETB-denominated numbers next to a foreign currency label. const etbFares = passengers.map((_, idx) => getEtbFare(idx)); const etbTotal = etbFares.reduce((sum, f) => sum + f, 0); - const needsConversion = settledAmountMinor != null && settledCurrency && settledCurrency !== 'ETB' && etbTotal > 0; + const needsConversion = + settledAmountMinor != null && + settledCurrency && + settledCurrency !== "ETB" && + etbTotal > 0; const getVoucherFare = (idx: number): number => { if (!needsConversion) return etbFares[idx]; return Math.round(etbFares[idx] * (settledAmountMinor! / etbTotal)); }; const outbound = { - trainNumber: activeSchedule?.trainNumber || 'N/A', - trainName: 'EDR Express', - origin: { name: activeSchedule?.origin || 'Origin', code: 'ORG', city: activeSchedule?.origin || 'Origin' }, - destination: { name: activeSchedule?.destination || 'Destination', code: 'DST', city: activeSchedule?.destination || 'Destination' }, + trainNumber: activeSchedule?.trainNumber || "N/A", + trainName: "EDR Express", + origin: { + name: activeSchedule?.origin || "Origin", + code: "ORG", + city: activeSchedule?.origin || "Origin", + }, + destination: { + name: activeSchedule?.destination || "Destination", + code: "DST", + city: activeSchedule?.destination || "Destination", + }, departureAt: activeSchedule?.departureTime || new Date().toISOString(), - arrivalAt: activeSchedule?.arrivalTime || new Date().toISOString(), - seatClass: activeSchedule?.selectedSeatClassName, + arrivalAt: activeSchedule?.arrivalTime || new Date().toISOString(), + seatClass: activeSchedule?.selectedSeatClassName, }; - const inbound = inboundSchedule ? { - trainNumber: inboundSchedule.trainNumber || 'N/A', - trainName: 'EDR Express', - origin: { name: inboundSchedule.origin, code: 'ORG', city: inboundSchedule.origin }, - destination: { name: inboundSchedule.destination, code: 'DST', city: inboundSchedule.destination }, - departureAt: inboundSchedule.departureTime || new Date().toISOString(), - arrivalAt: inboundSchedule.arrivalTime || new Date().toISOString(), - seatClass: inboundSchedule.selectedSeatClassName, - } : undefined; + const inbound = inboundSchedule + ? { + trainNumber: inboundSchedule.trainNumber || "N/A", + trainName: "EDR Express", + origin: { + name: inboundSchedule.origin, + code: "ORG", + city: inboundSchedule.origin, + }, + destination: { + name: inboundSchedule.destination, + code: "DST", + city: inboundSchedule.destination, + }, + departureAt: + inboundSchedule.departureTime || new Date().toISOString(), + arrivalAt: inboundSchedule.arrivalTime || new Date().toISOString(), + seatClass: inboundSchedule.selectedSeatClassName, + } + : undefined; // Separate file per passenger, saved back-to-back with no macrotask (setTimeout) // between them — a setTimeout delay would push later saves outside the click's @@ -170,29 +216,33 @@ export default function ConfirmationPage() { // Same match-by-name-then-position as the on-screen ticket list above — no // fabricated placeholder if there's no backend ticket data (see generate-voucher.ts). const matchedTicket = - _booking?.tickets?.find((t) => t.passengerName === p.name) ?? _booking?.tickets?.[i] ?? null; - const ticketNumber = matchedTicket?.barcodePayload || 'Not yet issued'; + _booking?.tickets?.find((t) => t.passengerName === p.name) ?? + _booking?.tickets?.[i] ?? + null; + const ticketNumber = matchedTicket?.barcodePayload || "Not yet issued"; await generatePassengerVoucherPDF({ - bookingRef: pnr, + bookingRef: pnr, ticketNumber, - passengerName: p.name || `Passenger ${i + 1}`, - dateOfBirth: p.dateOfBirth, - nationality: p.nationality, - seatNumber: p.seatNumber, - outboundSeatNumber: (p as any).outboundSeatNumber, - inboundSeatNumber: (p as any).inboundSeatNumber, + passengerName: p.name || `Passenger ${i + 1}`, + dateOfBirth: p.dateOfBirth, + nationality: p.nationality, + seatNumber: p.seatNumber, + outboundSeatNumber: (p as any).outboundSeatNumber, + inboundSeatNumber: (p as any).inboundSeatNumber, status, - outboundSchedule: outbound, - inboundSchedule: inbound, + outboundSchedule: outbound, + inboundSchedule: inbound, isRoundTrip, - fareMinor: getVoucherFare(i), - currency: voucherCurrency, + fareMinor: getVoucherFare(i), + currency: voucherCurrency, createdAt, }); } } catch (error) { - alert(`Failed to generate voucher: ${error instanceof Error ? error.message : 'Unknown error'}`); + alert( + `Failed to generate voucher: ${error instanceof Error ? error.message : "Unknown error"}`, + ); } finally { setIsGeneratingVoucher(false); } @@ -200,12 +250,12 @@ export default function ConfirmationPage() { const handleNewBooking = () => { clearBooking(); - window.location.href = '/'; + window.location.href = "/"; }; useEffect(() => { if (!bookingId || !pnr) { - window.location.href = '/'; + window.location.href = "/"; } }, [bookingId, pnr, router]); @@ -231,9 +281,13 @@ export default function ConfirmationPage() { {isConfirmed ? ( <>

- {packageName ? `${packageName} booking confirmed!` : 'Booking confirmed!'} + {packageName + ? `${packageName} booking confirmed!` + : "Booking confirmed!"}

-

Your train tickets are ready

+

+ Your train tickets are ready +

) : ( <> @@ -241,7 +295,8 @@ export default function ConfirmationPage() { Booking received — payment pending

- We haven't confirmed your payment yet. Your tickets will be issued once payment is completed. + We haven't confirmed your payment yet. Your tickets will + be issued once payment is completed.

)} @@ -250,11 +305,15 @@ export default function ConfirmationPage() { {/* PNR Card */}
-

Booking reference (PNR)

+

+ Booking reference (PNR) +

- {pnr} -
-

Save this reference number for future use

+

+ Save this reference number for future use +

{/* Trip Details */}
-
-
- -
-

- {isRoundTrip ? 'Round trip details' : 'Trip details'} -

+
+
+
+

+ {isRoundTrip ? "Round trip details" : "Trip details"} +

+
- {/* Outbound journey (round trip) or single journey */} - {(() => { - const schedule = isRoundTrip ? outboundSchedule : selectedSchedule; - if (!schedule) return null; - return ( -
- {isRoundTrip && ( -

Outbound

- )} -
-
-
-

Train number

-

{schedule.trainNumber}

-
-
-

Route

-

{schedule.origin} → {schedule.destination}

-
- {schedule.selectedSeatClassName && ( -
-

Class

-

{schedule.selectedSeatClassName.replace(/_/g, ' ')}

-
- )} -
-
-
-

Departure

-

- {schedule.departureTime && format(new Date(schedule.departureTime), 'PPp')} -

-
-
-

Arrival

-

- {schedule.arrivalTime && format(new Date(schedule.arrivalTime), 'PPp')} -

-
-
-

Duration

-

{schedule.duration}

-
-
-
-
- ); - })()} - - {/* Return journey (round trip only) */} - {isRoundTrip && inboundSchedule && ( -
-

Return

+ {/* Outbound journey (round trip) or single journey */} + {(() => { + const schedule = isRoundTrip + ? outboundSchedule + : selectedSchedule; + if (!schedule) return null; + return ( +
+ {isRoundTrip && ( +

+ Outbound +

+ )}
-

Train number

-

{inboundSchedule.trainNumber}

+

+ Train number +

+

+ {schedule.trainNumber} +

-

Route

-

{inboundSchedule.origin} → {inboundSchedule.destination}

+

+ Route +

+

+ {schedule.origin} → {schedule.destination} +

- {inboundSchedule.selectedSeatClassName && ( + {schedule.selectedSeatClassName && (
-

Class

-

{inboundSchedule.selectedSeatClassName.replace(/_/g, ' ')}

+

+ Class +

+

+ {schedule.selectedSeatClassName.replace( + /_/g, + " ", + )} +

)}
-

Departure

+

+ Departure +

- {inboundSchedule.departureTime && format(new Date(inboundSchedule.departureTime), 'PPp')} + {schedule.departureTime && + format(new Date(schedule.departureTime), "PPp")}

-

Arrival

+

+ Arrival +

- {inboundSchedule.arrivalTime && format(new Date(inboundSchedule.arrivalTime), 'PPp')} + {schedule.arrivalTime && + format(new Date(schedule.arrivalTime), "PPp")}

-

Duration

-

{inboundSchedule.duration}

+

+ Duration +

+

+ {schedule.duration} +

- )} + ); + })()} + + {/* Return journey (round trip only) */} + {isRoundTrip && inboundSchedule && ( +
+

+ Return +

+
+
+
+

+ Train number +

+

+ {inboundSchedule.trainNumber} +

+
+
+

+ Route +

+

+ {inboundSchedule.origin} →{" "} + {inboundSchedule.destination} +

+
+ {inboundSchedule.selectedSeatClassName && ( +
+

+ Class +

+

+ {inboundSchedule.selectedSeatClassName.replace( + /_/g, + " ", + )} +

+
+ )} +
+
+
+

+ Departure +

+

+ {inboundSchedule.departureTime && + format( + new Date(inboundSchedule.departureTime), + "PPp", + )} +

+
+
+

+ Arrival +

+

+ {inboundSchedule.arrivalTime && + format( + new Date(inboundSchedule.arrivalTime), + "PPp", + )} +

+
+
+

+ Duration +

+

+ {inboundSchedule.duration} +

+
+
+
+
+ )}
{/* Booking date & payment summary */}
-

Booking details

+

+ Booking details +

-

Booking date

+

+ Booking date +

- {format(new Date(_booking?.createdAt || new Date()), 'PPp')} + {format(new Date(_booking?.createdAt || new Date()), "PPp")}

-

Status

-

- {_booking?.status || 'PENDING_PAYMENT'} +

+ Status +

+

+ {_booking?.status || "PENDING_PAYMENT"}

-

Passengers

-

{passengers.length}

+

+ Passengers +

+

+ {passengers.length} +

-

Total paid

+

+ Total paid +

{(() => { // The server-confirmed settled amount is authoritative — prefer it over // any client-side session state, which can go stale (e.g. after a refresh). if (_booking?.payment?.amountMinor != null) { - return `${_booking.payment.currency || 'ETB'} ${(_booking.payment.amountMinor / 100).toFixed(2)}`; + return `${_booking.payment.currency || "ETB"} ${_booking.payment.amountMinor}`; } - if (reviewedTotalMinor != null) return `ETB ${(reviewedTotalMinor / 100).toFixed(2)}`; - if (paidAmountMinor != null) return `${paidCurrency} ${(paidAmountMinor / 100).toFixed(2)}`; - if (_booking?.totalMinor != null) return `ETB ${(_booking.totalMinor / 100).toFixed(2)}`; - return 'ETB 0.00'; + if (reviewedTotalMinor != null) + return `ETB ${(reviewedTotalMinor / 100).toFixed(2)}`; + if (paidAmountMinor != null) + return `${paidCurrency} ${(paidAmountMinor / 100).toFixed(2)}`; + if (_booking?.totalMinor != null) + return `ETB ${(_booking.totalMinor / 100).toFixed(2)}`; + return "ETB 0.00"; })()}

@@ -416,70 +557,132 @@ export default function ConfirmationPage() { {/* Tickets */}
-

Your tickets

+

+ Your tickets +

{passengers.map((passenger, index) => { // Match by name first (tickets aren't necessarily created/ordered the same // way as this passengers array) — fall back to position if no name match. const backendTicket = - _booking?.tickets?.find((t) => t.passengerName === passenger.name) ?? + _booking?.tickets?.find( + (t) => t.passengerName === passenger.name, + ) ?? _booking?.tickets?.[index] ?? null; // No fabricated placeholder — a made-up TKT-... number reads as real and is // misleading if it doesn't match what's actually on file. - const ticketNumber = isConfirmed ? backendTicket?.barcodePayload || null : null; + const ticketNumber = isConfirmed + ? backendTicket?.barcodePayload || null + : null; return ( -
+
{/* Ticket Info */}
-

{passenger.name}

-

Passenger {index + 1}

+

+ {passenger.name} +

+

+ Passenger {index + 1} +

{isConfirmed ? ( CONFIRMED ) : ( - AWAITING PAYMENT + + AWAITING PAYMENT + )}
-

Ticket Number

+

+ Ticket Number +

- {ticketNumber || (isConfirmed ? 'Not yet issued' : 'Pending payment')} + {ticketNumber || + (isConfirmed + ? "Not yet issued" + : "Pending payment")}

-

Date of Birth

-

{format(new Date(passenger.dateOfBirth), 'PP')}

+

+ Date of Birth +

+

+ {format(new Date(passenger.dateOfBirth), "PP")} +

-

Nationality

-

{passenger.nationality}

+

+ Nationality +

+

+ {passenger.nationality} +

-

Seat(s)

+

+ Seat(s) +

{(() => { - const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length; + const adultCount = + searchCriteria?.adultCount ?? + passengers.filter((p) => !isChild(p)).length; const isFreeChild = packageId - ? index >= adultCount && (index - adultCount) < adultCount - : isChild(passenger) && isFirstChild(passengers, index); - if (isFreeChild) return

; + ? index >= adultCount && + index - adultCount < adultCount + : isChild(passenger) && + isFirstChild(passengers, index); + if (isFreeChild) + return ( +

+ — +

+ ); return isRoundTrip ? (

- Outbound: {(passenger as any).outboundCoachNumber && {(passenger as any).outboundCoachNumber}} — {(passenger as any).outboundSeatNumber || 'Auto-assigned at boarding'} + Outbound:{" "} + {(passenger as any).outboundCoachNumber && ( + + {(passenger as any).outboundCoachNumber} + + )}{" "} + —{" "} + {(passenger as any).outboundSeatNumber || + "Auto-assigned at boarding"}

- Return: {(passenger as any).inboundCoachNumber && {(passenger as any).inboundCoachNumber}} — {(passenger as any).inboundSeatNumber || 'Auto-assigned at boarding'} + Return:{" "} + {(passenger as any).inboundCoachNumber && ( + + {(passenger as any).inboundCoachNumber} + + )}{" "} + —{" "} + {(passenger as any).inboundSeatNumber || + "Auto-assigned at boarding"}

) : (

- {passenger.coachNumber && (Coach {passenger.coachNumber})} — {passenger.seatNumber || 'Auto-assigned at boarding'} + {passenger.coachNumber && ( + + (Coach {passenger.coachNumber}) + + )}{" "} + —{" "} + {passenger.seatNumber || + "Auto-assigned at boarding"}

); })()} @@ -517,8 +720,8 @@ export default function ConfirmationPage() { )} {/* New Booking Button */} - - + +
); } - const isPendingPayment = booking.status === 'PENDING_PAYMENT' || booking.status === 'DRAFT'; - const isConfirmed = booking.status === 'TICKETED' || booking.status === 'CONFIRMED'; - const isExpired = booking.status === 'EXPIRED'; - const isCancelled = booking.status === 'CANCELLED'; - + const isPendingPayment = + booking.status === "PENDING_PAYMENT" || booking.status === "DRAFT"; + const isConfirmed = + booking.status === "TICKETED" || booking.status === "CONFIRMED"; + const isExpired = booking.status === "EXPIRED"; + const isCancelled = booking.status === "CANCELLED"; const StatusBadge = () => { const statusConfig = { - PENDING_PAYMENT: { color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', label: 'Pending Payment' }, - DRAFT: { color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', label: 'Pending Payment' }, - CONFIRMED: { color: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', label: 'Confirmed' }, - TICKETED: { color: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', label: 'Ticketed' }, - EXPIRED: { color: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', label: 'Expired' }, - CANCELLED: { color: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', label: 'Cancelled' }, + PENDING_PAYMENT: { + color: + "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", + label: "Pending Payment", + }, + DRAFT: { + color: + "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", + label: "Pending Payment", + }, + CONFIRMED: { + color: + "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", + label: "Confirmed", + }, + TICKETED: { + color: + "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", + label: "Ticketed", + }, + EXPIRED: { + color: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", + label: "Expired", + }, + CANCELLED: { + color: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300", + label: "Cancelled", + }, }; - const config = statusConfig[booking.status as keyof typeof statusConfig] || statusConfig.DRAFT; + const config = + statusConfig[booking.status as keyof typeof statusConfig] || + statusConfig.DRAFT; return ( - + {isConfirmed && } {config.label} @@ -238,16 +309,33 @@ function BookingDetailContent() { // combined row per passenger with an Outbound/Return sub-split. Group leg rows back // together here so both pages present the same per-passenger total, not a doubled list // of half-fare rows. - const isRoundTripBooking = booking.bookingType === 'ROUND_TRIP'; + const isRoundTripBooking = booking.bookingType === "ROUND_TRIP"; const farePassengers = (() => { const rows: any[] = booking.passengers || []; if (!isRoundTripBooking) { - return rows.map((p) => ({ fullName: p.fullName, category: p.category, fareMinor: p.fareMinor ?? 0 })); + return rows.map((p) => ({ + fullName: p.fullName, + category: p.category, + fareMinor: p.fareMinor ?? 0, + })); } - const grouped = new Map(); + const grouped = new Map< + string, + { + fullName: string; + category: string; + outboundFareMinor: number; + returnFareMinor: number; + } + >(); rows.forEach((p) => { const key = `${p.fullName}|${p.dateOfBirth}|${p.category}`; - const entry = grouped.get(key) || { fullName: p.fullName, category: p.category, outboundFareMinor: 0, returnFareMinor: 0 }; + const entry = grouped.get(key) || { + fullName: p.fullName, + category: p.category, + outboundFareMinor: 0, + returnFareMinor: 0, + }; if (p.leg === 2) entry.returnFareMinor = p.fareMinor ?? 0; else entry.outboundFareMinor = p.fareMinor ?? 0; grouped.set(key, entry); @@ -269,23 +357,34 @@ function BookingDetailContent() {

Order summary - Ref: {booking.bookingRef} + Ref:{" "} + + {booking.bookingRef} +

-

Fare breakdown

+

+ Fare breakdown +

{farePassengers.map((passenger: any, idx: number) => { - const isChildPassenger = passenger.category === 'CHILD'; - const isFreeChild = isChildPassenger && (passenger.fareMinor ?? 0) === 0; + const isChildPassenger = passenger.category === "CHILD"; + const isFreeChild = + isChildPassenger && (passenger.fareMinor ?? 0) === 0; return ( -
+
{passenger.fullName || `Passenger ${idx + 1}`} {isChildPassenger && ( - - ({isFreeChild ? 'CHILD - FREE' : 'CHILD - FULL FARE'}) + + ({isFreeChild ? "CHILD - FREE" : "CHILD - FULL FARE"}) )} @@ -297,11 +396,21 @@ function BookingDetailContent() {
Outbound - {formatFare(passenger.outboundFareMinor ?? 0, displayCurrency)} + + {formatFare( + passenger.outboundFareMinor ?? 0, + displayCurrency, + )} +
Return - {formatFare(passenger.returnFareMinor ?? 0, displayCurrency)} + + {formatFare( + passenger.returnFareMinor ?? 0, + displayCurrency, + )} +
)} @@ -312,18 +421,24 @@ function BookingDetailContent() {
- Total + + Total + {awaitingAmount ? ( ) : ( - <>{confirmedCurrency} {(totalAmountDisplay ?? 0).toFixed(2)} + <> + {confirmedCurrency} {(totalAmountDisplay ?? 0).toFixed(2)} + )}
{selectedPaymentMethod && !awaitingAmount && (

- You will be charged {confirmedCurrency} {(totalAmountDisplay ?? 0).toFixed(2)} via {selectedPaymentMethod.displayName} + You will be charged {confirmedCurrency}{" "} + {(totalAmountDisplay ?? 0).toFixed(2)} via{" "} + {selectedPaymentMethod.displayName}

)}
@@ -331,11 +446,15 @@ function BookingDetailContent() { {/* Pay + back buttons — desktop sidebar only */}
{paymentError && ( -

⚠️ {paymentError}

+

+ ⚠️ {paymentError} +

)} - @@ -366,17 +489,23 @@ function BookingDetailContent() {
-

Complete payment

+

+ Complete payment +

- Booking Reference: {booking.bookingRef} + Booking Reference:{" "} + + {booking.bookingRef} +

{booking.createdAt && (

- Booking created on {format(new Date(booking.createdAt), 'PPpp')} + Booking created on{" "} + {format(new Date(booking.createdAt), "PPpp")}

)}
@@ -385,16 +514,18 @@ function BookingDetailContent() { {/* Two-column grid — matches /booking/payment's layout */}
- {/* Left column — trip/payment method (2/3 width) */}
-
-

Trip Summary

- +

+ Trip Summary +

+
- Your Journey + + Your Journey + {booking.passengers?.[0]?.seat?.seatClass && ( {booking.passengers[0].seat.seatClass} @@ -413,16 +544,20 @@ function BookingDetailContent() { {/* Destination dot */}
- + {/* Right column: Content */}
{/* Origin */}
- {booking.schedule?.departureAt ? formatTime(booking.schedule.departureAt) : '--:--'} + {booking.schedule?.departureAt + ? formatTime(booking.schedule.departureAt) + : "--:--"}
- {booking.schedule?.departureAt ? `${format(new Date(booking.schedule.departureAt), 'EEE, MMM d')} · ${getTimePeriod(booking.schedule.departureAt)}` : 'N/A'} + {booking.schedule?.departureAt + ? `${format(new Date(booking.schedule.departureAt), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.departureAt)}` + : "N/A"}
{booking.schedule?.origin?.name} @@ -436,10 +571,22 @@ function BookingDetailContent() {
- - + + - Train {booking.schedule?.trainNumber} + + Train {booking.schedule?.trainNumber} +
{booking.schedule?.trainName && ( @@ -452,10 +599,14 @@ function BookingDetailContent() { {/* Destination */}
- {booking.schedule?.arrivalAt ? formatTime(booking.schedule.arrivalAt) : '--:--'} + {booking.schedule?.arrivalAt + ? formatTime(booking.schedule.arrivalAt) + : "--:--"}
- {booking.schedule?.arrivalAt ? `${format(new Date(booking.schedule.arrivalAt), 'EEE, MMM d')} · ${getTimePeriod(booking.schedule.arrivalAt)}` : 'N/A'} + {booking.schedule?.arrivalAt + ? `${format(new Date(booking.schedule.arrivalAt), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.arrivalAt)}` + : "N/A"}
{booking.schedule?.destination?.name} @@ -475,32 +626,44 @@ function BookingDetailContent() {
- {booking.passengers?.map((passenger: any, idx: number) => ( -
-
-
{passenger.fullName}
-
- {passenger.category} • Coach {passenger.seat?.coach} + {booking.passengers?.map( + (passenger: any, idx: number) => ( +
+
+
+ {passenger.fullName} +
+
+ {passenger.category} • Coach{" "} + {passenger.seat?.coach} +
+
+
+
+ Seat {passenger.seat?.number} +
+
+ {passenger.seat?.seatClass} +
-
-
- Seat {passenger.seat?.number} -
-
- {passenger.seat?.seatClass} -
-
-
- ))} + ), + )}
-

Select payment method

+

+ Select payment method +

- {paymentMethods && Array.isArray(paymentMethods) && paymentMethods.length > 0 ? ( + {paymentMethods && + Array.isArray(paymentMethods) && + paymentMethods.length > 0 ? (
{paymentMethods.map((method: any) => { const Icon = getIconForMethod(method.type); @@ -508,21 +671,37 @@ function BookingDetailContent() { return (
{/* end grid */} +
+ {/* end grid */}
{/* Mobile sticky bottom bar */}
- Total + + Total + {awaitingAmount ? ( ) : ( - <>{confirmedCurrency} {(totalAmountDisplay ?? 0).toFixed(2)} + <> + {confirmedCurrency} {(totalAmountDisplay ?? 0).toFixed(2)} + )}
{paymentError && ( -

⚠️ {paymentError}

+

+ ⚠️ {paymentError} +

)}
-
{isConfirmed && (
- Total paid:{' '} + Total paid:{" "} {booking?.payment?.amountMinor != null - ? `${booking.payment.currency || 'ETB'} ${(booking.payment.amountMinor / 100).toFixed(2)}` + ? `${booking.payment.currency || "ETB"} ${booking.payment.amountMinor}` : `ETB ${((booking?.totalMinor ?? 0) / 100).toFixed(2)}`} {booking?.payment?.method && ( - via {booking.payment.method} + + {" "} + via {booking.payment.method} + )}
)} @@ -673,11 +884,15 @@ function BookingDetailContent() {
-

Journey Details

- +

+ Journey Details +

+
- Your Journey + + Your Journey + {booking.passengers?.[0]?.seat?.seatClass && ( {booking.passengers[0].seat.seatClass} @@ -696,16 +911,20 @@ function BookingDetailContent() { {/* Destination dot */}
- + {/* Right column: Content */}
{/* Origin */}
- {booking.schedule?.departureAt ? formatTime(booking.schedule.departureAt) : '--:--'} + {booking.schedule?.departureAt + ? formatTime(booking.schedule.departureAt) + : "--:--"}
- {booking.schedule?.departureAt ? `${format(new Date(booking.schedule.departureAt), 'EEE, MMM d')} · ${getTimePeriod(booking.schedule.departureAt)}` : 'N/A'} + {booking.schedule?.departureAt + ? `${format(new Date(booking.schedule.departureAt), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.departureAt)}` + : "N/A"}
{booking.schedule?.origin?.name} @@ -719,10 +938,22 @@ function BookingDetailContent() {
- - + + - Train {booking.schedule?.trainNumber} + + Train {booking.schedule?.trainNumber} +
{booking.schedule?.trainName && ( @@ -735,10 +966,14 @@ function BookingDetailContent() { {/* Destination */}
- {booking.schedule?.arrivalAt ? formatTime(booking.schedule.arrivalAt) : '--:--'} + {booking.schedule?.arrivalAt + ? formatTime(booking.schedule.arrivalAt) + : "--:--"}
- {booking.schedule?.arrivalAt ? `${format(new Date(booking.schedule.arrivalAt), 'EEE, MMM d')} · ${getTimePeriod(booking.schedule.arrivalAt)}` : 'N/A'} + {booking.schedule?.arrivalAt + ? `${format(new Date(booking.schedule.arrivalAt), "EEE, MMM d")} · ${getTimePeriod(booking.schedule.arrivalAt)}` + : "N/A"}
{booking.schedule?.destination?.name} @@ -755,39 +990,50 @@ function BookingDetailContent() {

Passenger Details ({booking.passengers?.length || 0})

- +
{booking.passengers?.map((passenger: any, idx: number) => ( -
+
{idx + 1} -

{passenger.fullName}

+

+ {passenger.fullName} +

{passenger.category}
- +
- Coach: + + Coach: +
- {passenger.seat?.coach || 'N/A'} + {passenger.seat?.coach || "N/A"}
- Seat Number: + + Seat Number: +
- {passenger.seat?.number || 'N/A'} + {passenger.seat?.number || "N/A"}
- Class: + + Class: +
- {passenger.seat?.seatClass || 'N/A'} + {passenger.seat?.seatClass || "N/A"}
@@ -811,7 +1057,10 @@ function BookingDetailContent() {
-
@@ -828,11 +1077,18 @@ function BookingDetailContent() {
-

Unknown Booking Status

+

+ Unknown Booking Status +

Booking status: {booking.status}

- +
); @@ -840,14 +1096,18 @@ function BookingDetailContent() { export default function BookingDetailPage() { return ( - -
-
-

Loading...

+ +
+
+

+ Loading... +

+
-
- }> + } + > );