From f2ee62b95f56ff0f35c4294c6dd7221a7a68e32b Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Thu, 2 Jul 2026 16:03:57 +0300 Subject: [PATCH] Refine booking flow --- .../src/app/booking/auth-check/page.tsx | 20 +- .../portal/src/app/booking/detail/page.tsx | 17 +- .../src/app/booking/passengers/page.tsx | 461 +++++++++++++----- .../portal/src/app/booking/payment/page.tsx | 120 ++++- .../booking/payment/telebirr/failure/page.tsx | 7 +- .../booking/payment/waafi/failure/page.tsx | 7 +- .../portal/src/app/booking/results/page.tsx | 136 ++++-- .../portal/src/app/booking/review/page.tsx | 54 +- .../portal/src/app/booking/search/page.tsx | 108 ++-- .../portal/src/app/booking/seats/page.tsx | 415 +++++++++------- .../portal/src/app/packages/[id]/page.tsx | 2 +- .../edr-passenger-web/portal/src/app/page.tsx | 25 +- .../portal/src/components/AppHeader.tsx | 2 +- .../portal/src/components/SearchWidget.tsx | 2 +- .../portal/src/lib/generate-voucher.ts | 4 +- .../portal/src/utils/format.ts | 12 +- 16 files changed, 921 insertions(+), 471 deletions(-) diff --git a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx index 0ef5c74ec..43775b1c7 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/lib/auth-store'; +import { useBookingStore } from '@/lib/booking-store'; import { LogIn, UserPlus, ChevronLeft } from 'lucide-react'; function Tooltip({ children, content }: { children: React.ReactNode; content: string[] }) { @@ -41,6 +42,23 @@ function Tooltip({ children, content }: { children: React.ReactNode; content: st export default function AuthCheckPage() { const router = useRouter(); const { isAuthenticated, initialize } = useAuthStore(); + const searchCriteria = useBookingStore((s) => s.searchCriteria); + + const buildResultsUrl = () => { + if (!searchCriteria) return '/booking/results'; + const params = new URLSearchParams({ + tripType: searchCriteria.tripType, + origin: searchCriteria.originStationId, + destination: searchCriteria.destinationStationId, + date: searchCriteria.departureDate, + adults: searchCriteria.adultCount.toString(), + children: searchCriteria.childCount.toString(), + nationality: searchCriteria.nationality, + }); + if (searchCriteria.returnDate) params.set('returnDate', searchCriteria.returnDate); + if (searchCriteria.promoCode) params.set('promoCode', searchCriteria.promoCode); + return `/booking/results?${params}`; + }; useEffect(() => { initialize(); @@ -94,7 +112,7 @@ export default function AuthCheckPage() {
+ {isVerifyingOther && ( +

+ Finish verifying Passenger {(verifyingIndex ?? 0) + 1} first +

+ )} @@ -889,10 +1093,16 @@ export default function PassengersPage() { {status === 'success' && (

- Verified with Fayda + Verified with Fayda — details auto-filled below

)} + {status === 'error' && faydaError && ( +
+ +

{faydaError}

+
+ )}
{/* Full Name */} @@ -915,6 +1125,7 @@ export default function PassengersPage() { value={passengers[index]?.dateOfBirth || ''} onChange={(iso) => setValue(`passengers.${index}.dateOfBirth`, iso, { shouldValidate: true })} error={errors.passengers?.[index]?.dateOfBirth?.message} + passengerType={isChildPassenger ? 'CHILD' : 'ADULT'} />
@@ -944,32 +1155,6 @@ export default function PassengersPage() { disabled />
- - {/* Phone */} -
- - setValue(`passengers.${index}.phone`, v)} - onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })} - error={errors.passengers?.[index]?.phone?.message} - /> -
- - {/* Email */} -
- - - {errors.passengers?.[index]?.email && ( -

{errors.passengers[index]?.email?.message}

- )} -
) : ( @@ -995,6 +1180,7 @@ export default function PassengersPage() { value={passengers[index]?.dateOfBirth || ''} onChange={(iso) => setValue(`passengers.${index}.dateOfBirth`, iso, { shouldValidate: true })} error={errors.passengers?.[index]?.dateOfBirth?.message} + passengerType={isChildPassenger ? 'CHILD' : 'ADULT'} /> @@ -1024,32 +1210,6 @@ export default function PassengersPage() { disabled /> - - {/* Phone */} -
- - setValue(`passengers.${index}.phone`, v)} - onNormalized={(v) => setValue(`passengers.${index}.phone`, v, { shouldValidate: true })} - error={errors.passengers?.[index]?.phone?.message} - /> -
- - {/* Email */} -
- - - {errors.passengers?.[index]?.email && ( -

{errors.passengers[index]?.email?.message}

- )} -
{/* Passport fields */} @@ -1106,7 +1266,41 @@ export default function PassengersPage() { ); })} - + +
+

Contact Information

+

+ This phone number and email will be used for booking and ticketing communication for all passengers. +

+
+ {/* Contact Phone */} +
+ + setValue('contactPhone', v)} + onNormalized={(v) => setValue('contactPhone', v, { shouldValidate: true })} + error={errors.contactPhone?.message} + /> +
+ + {/* Contact Email */} +
+ + + {errors.contactEmail && ( +

{errors.contactEmail.message}

+ )} +
+
+
+ {!isAuthenticated && (
)} + {submitError && ( +
+ +

{submitError}

+
+ )} +
-
diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index 989516839..730d2ad92 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -8,6 +8,8 @@ import { apiClient } from "@/lib/api-client"; import { useState, useEffect } from "react"; import { PaymentMethod } from "@/types"; import { format } from "date-fns"; +import { formatTime, getTimePeriod } from '@/utils/format'; +import { calculatePassengerFare, isChild, isFirstChild, formatFare } from '@/utils/fare-utils'; import { CreditCard, Smartphone, @@ -65,21 +67,21 @@ export default function PaymentPage() { enabled: !!selectedMethod && !!bookingId, }); - // Fallback: estimate from local store while API hasn't responded yet - const outboundBaseFare = isRoundTrip && outboundSchedule ? passengers.reduce( - (sum) => sum + (outboundSchedule.baseFareAdult || 0), - 0, - ) : 0; + // Fallback: estimate from local store while API hasn't responded yet. + // Uses the same first-child-free calculation as the review page so the + // breakdown shown here matches what the passenger already saw there. + const outboundBaseFare = isRoundTrip && outboundSchedule ? passengers.reduce((sum, _, i) => { + return sum + calculatePassengerFare(passengers, i, outboundSchedule.baseFareAdult || 0); + }, 0) : 0; - const inboundBaseFare = isRoundTrip && inboundSchedule ? passengers.reduce( - (sum) => sum + (inboundSchedule.baseFareAdult || 0), - 0, - ) : 0; + const inboundBaseFare = isRoundTrip && inboundSchedule ? passengers.reduce((sum, _, i) => { + return sum + calculatePassengerFare(passengers, i, inboundSchedule.baseFareAdult || 0); + }, 0) : 0; - const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce( - (sum) => sum + (selectedSchedule?.baseFareAdult || 0), - 0, - ); + const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce((sum, _, i) => { + const farePerPassenger = selectedSchedule?.baseFareAdult || (selectedSchedule as any)?.fareAdult || (selectedSchedule as any)?.price || 0; + return sum + calculatePassengerFare(passengers, i, farePerPassenger); + }, 0); // API returns amount in major units (e.g. 11602.5 DJF); convert to minor for display consistency const totalAmount = bookingAmountData != null @@ -203,10 +205,12 @@ export default function PaymentPage() {
- {schedule?.departureTime ? format(new Date(schedule.departureTime), 'HH:mm') : '--:--'} + {schedule?.departureTime ? formatTime(schedule.departureTime) : '--:--'}
- {schedule?.departureTime ? format(new Date(schedule.departureTime), 'EEE, MMM d') : ''} + {schedule?.departureTime + ? `${format(new Date(schedule.departureTime), 'EEE, MMM d')} · ${getTimePeriod(schedule.departureTime)}` + : ''}
{schedule?.origin}
@@ -216,10 +220,12 @@ export default function PaymentPage() {
- {schedule?.arrivalTime ? format(new Date(schedule.arrivalTime), 'HH:mm') : '--:--'} + {schedule?.arrivalTime ? formatTime(schedule.arrivalTime) : '--:--'}
- {schedule?.arrivalTime ? format(new Date(schedule.arrivalTime), 'EEE, MMM d') : ''} + {schedule?.arrivalTime + ? `${format(new Date(schedule.arrivalTime), 'EEE, MMM d')} · ${getTimePeriod(schedule.arrivalTime)}` + : ''}
{schedule?.destination}
@@ -240,7 +246,9 @@ export default function PaymentPage() {

Order summary - Ref: {pnr} + + Ref: {pnr} +

{isRoundTrip ? ( @@ -254,14 +262,58 @@ export default function PaymentPage() { )} -
-
- Passengers - - {passengers.length} passenger{passengers.length !== 1 ? 's' : ''} - -
-
+ {/* Fare breakdown — same first-child-free logic as the review page */} +
+

Fare breakdown

+ {passengers.map((p, i) => { + const outFare = outboundSchedule?.baseFareAdult || 0; + const inFare = inboundSchedule?.baseFareAdult || 0; + const onewayFare = selectedSchedule?.baseFareAdult || 0; + + const outboundFare = calculatePassengerFare(passengers, i, outFare); + const inboundFare = calculatePassengerFare(passengers, i, inFare); + const oneWayFare = calculatePassengerFare(passengers, i, onewayFare); + + const passengerTotal = isRoundTrip ? outboundFare + inboundFare : oneWayFare; + const isChildPassenger = isChild(p); + const isFreeChild = isChildPassenger && isFirstChild(passengers, i); + + return ( +
+
+ + {p.name || `Passenger ${i + 1}`} + {isChildPassenger && ( + + ({isFreeChild ? 'CHILD - FREE' : 'CHILD'}) + + )} + + + {formatFare(passengerTotal, displayCurrency)} + +
+ {isRoundTrip && ( +
+
+ Outbound {isFreeChild ? '(Free)' : ''} + {formatFare(outboundFare, displayCurrency)} +
+
+ Return {isFreeChild ? '(Free)' : ''} + {formatFare(inboundFare, displayCurrency)} +
+
+ )} +
+ ); + })} +
+ +
+
Total {loadingAmount && ( @@ -311,6 +363,22 @@ export default function PaymentPage() {

Complete payment

+ {/* Reservation confirmation banner */} +
+ +
+

+ Your booking is successfully reserved +

+

+ Booking Reference: {pnr} +

+

+ Please complete the payment below to confirm your booking. Your seats are held temporarily until payment is completed. +

+
+
+ {/* Payment Processing Overlay */} {isProcessing && (
diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/failure/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/failure/page.tsx index 04c2d8251..773afc79f 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/failure/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/failure/page.tsx @@ -3,7 +3,7 @@ import { useSearchParams, useRouter } from 'next/navigation'; import { usePaymentStore } from '@/lib/payment-store'; import { useEffect, Suspense } from 'react'; -import { XCircle, Loader2, RefreshCw, ChevronLeft } from 'lucide-react'; +import { XCircle, Loader2, ChevronLeft } from 'lucide-react'; function TelebirrFailureContent() { const router = useRouter(); @@ -30,11 +30,6 @@ function TelebirrFailureContent() { {merchantOrderId &&

Order ID: {merchantOrderId}

} {trxRef &&

Ref: {trxRef}

}
-

Available schedules

+ {isRoundTrip && ( +
+
+ {roundTripStep === 'inbound' + ? + : 1} + Outbound +
+
+
+ +
+
+
+ 2 + Return +
+
+ )}
@@ -718,39 +747,74 @@ export default function ResultsPage() {
- {outboundSchedules.length > 0 && ( -
- {isRoundTrip && ( + {isRoundTrip ? ( + roundTripStep === 'outbound' ? ( +

- Outbound Journey + Select Outbound Journey

{searchData.date ? format(new Date(searchData.date), 'EEEE, MMMM d, yyyy') : ''}

- )} -
- {outboundSchedules.map((schedule: Schedule) => renderScheduleCard(schedule, true))} +
+ {outboundSchedules.map((schedule: Schedule) => renderScheduleCard(schedule, true))} +
-
- )} - - {isRoundTrip && inboundSchedules.length > 0 && outboundScheduleData && ( -
-
-

- - Return Journey -

-

- {searchData.returnDate ? format(new Date(searchData.returnDate), 'EEEE, MMMM d, yyyy') : ''} -

-
-
- {inboundSchedules.map((schedule: Schedule) => renderScheduleCard(schedule))} + ) : ( +
+ {outboundScheduleData && ( +
+
+
+ +
+
+

Outbound journey selected

+

+ {outboundScheduleData.origin} → {outboundScheduleData.destination} + {outboundScheduleData.selectedSeatClassName ? ` · ${outboundScheduleData.selectedSeatClassName}` : ''} +

+
+
+ +
+ )} +
+

+ + Select Return Journey +

+

+ {searchData.returnDate ? format(new Date(searchData.returnDate), 'EEEE, MMMM d, yyyy') : ''} +

+
+
+ {inboundSchedules.map((schedule: Schedule) => renderScheduleCard(schedule, false))} +
+ ) + ) : ( +
+ {outboundSchedules.map((schedule: Schedule) => renderScheduleCard(schedule, true))}
)}
diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index 67422ad69..3e7abd2d3 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -6,6 +6,7 @@ import { useAuthStore } from '@/lib/auth-store'; import { useMutation } from '@tanstack/react-query'; import { apiClient } from '@/lib/api-client'; import { format } from 'date-fns'; +import { formatTime, getTimePeriod } from '@/utils/format'; import { useState, useEffect } from 'react'; import { ChevronLeft } from 'lucide-react'; import { calculatePassengerFare, isChild, isFirstChild, formatFare } from '@/utils/fare-utils'; @@ -93,6 +94,14 @@ export default function ReviewPage() { return suffix ? `${base}${suffix}` : base; }; + // The seat class/category is chosen once per leg (coach type selected on /booking/seats), + // so every seat on that leg shares it — no need to look it up per-seat. + const formatSeatClass = (schedule: any): string => { + const raw = schedule?.selectedSeatClassName || schedule?.seatClassName || schedule?.selectedSeatClass; + if (!raw) return 'Standard'; + return String(raw).replace(/_/g, ' '); + }; + useEffect(() => { const fetchSeatDetails = async () => { try { @@ -525,10 +534,12 @@ export default function ReviewPage() { {/* Origin */}
- {outboundSchedule.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'} + {outboundSchedule.departureTime ? formatTime(outboundSchedule.departureTime) : '--:--'}
- {outboundSchedule.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} + {outboundSchedule.departureTime + ? `${format(new Date(outboundSchedule.departureTime), 'EEE, MMM d')} · ${getTimePeriod(outboundSchedule.departureTime)}` + : 'N/A'}
{outboundSchedule.origin} @@ -556,10 +567,12 @@ export default function ReviewPage() { {/* Destination */}
- {outboundSchedule.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'} + {outboundSchedule.arrivalTime ? formatTime(outboundSchedule.arrivalTime) : '--:--'}
- {outboundSchedule.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} + {outboundSchedule.arrivalTime + ? `${format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d')} · ${getTimePeriod(outboundSchedule.arrivalTime)}` + : 'N/A'}
{outboundSchedule.destination} @@ -598,10 +611,12 @@ export default function ReviewPage() { {/* Origin */}
- {inboundSchedule.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'} + {inboundSchedule.departureTime ? formatTime(inboundSchedule.departureTime) : '--:--'}
- {inboundSchedule.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'} + {inboundSchedule.departureTime + ? `${format(new Date(inboundSchedule.departureTime), 'EEE, MMM d')} · ${getTimePeriod(inboundSchedule.departureTime)}` + : 'N/A'}
{inboundSchedule.origin} @@ -629,10 +644,12 @@ export default function ReviewPage() { {/* Destination */}
- {inboundSchedule.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'} + {inboundSchedule.arrivalTime ? formatTime(inboundSchedule.arrivalTime) : '--:--'}
- {inboundSchedule.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} + {inboundSchedule.arrivalTime + ? `${format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d')} · ${getTimePeriod(inboundSchedule.arrivalTime)}` + : 'N/A'}
{inboundSchedule.destination} @@ -671,10 +688,12 @@ export default function ReviewPage() { {/* Origin */}
- {selectedSchedule.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'} + {selectedSchedule.departureTime ? formatTime(selectedSchedule.departureTime) : '--:--'}
- {selectedSchedule.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'} + {selectedSchedule.departureTime + ? `${format(new Date(selectedSchedule.departureTime), 'EEE, MMM d')} · ${getTimePeriod(selectedSchedule.departureTime)}` + : 'N/A'}
{selectedSchedule.origin} @@ -702,10 +721,12 @@ export default function ReviewPage() { {/* Destination */}
- {selectedSchedule.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'} + {selectedSchedule.arrivalTime ? formatTime(selectedSchedule.arrivalTime) : '--:--'}
- {selectedSchedule.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'} + {selectedSchedule.arrivalTime + ? `${format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d')} · ${getTimePeriod(selectedSchedule.arrivalTime)}` + : 'N/A'}
{selectedSchedule.destination} @@ -736,18 +757,27 @@ export default function ReviewPage() {

{(p as any).outboundSeatId ? (seatDetails[`outbound-${(p as any).outboundSeatId}`] || 'Loading...') : 'Auto-assign'}

+ {(p as any).outboundSeatId && ( +

{formatSeatClass(outboundSchedule)}

+ )}

Return Seat

{(p as any).inboundSeatId ? (seatDetails[`inbound-${(p as any).inboundSeatId}`] || 'Loading...') : 'Auto-assign'}

+ {(p as any).inboundSeatId && ( +

{formatSeatClass(inboundSchedule)}

+ )}
) : (

Seat

{p.seatId ? (seatDetails[p.seatId] || 'Loading...') : 'Auto-assign'}

+ {p.seatId && ( +

{formatSeatClass(selectedSchedule)}

+ )}
)}
diff --git a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx index d173a1263..92e0ced68 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx @@ -4,14 +4,13 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useRouter, useSearchParams } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@/lib/auth-store"; import { apiClient } from "@/lib/api-client"; import { useBookingStore } from "@/lib/booking-store"; import { Station } from "@/types"; import { MapPin, - ArrowRight, ArrowLeftRight, Plus, Minus, @@ -23,7 +22,6 @@ import { X, ChevronLeft, Clock, - Zap, } from "lucide-react"; import { useEffect, useRef, useState, useCallback } from "react"; import ModernDatePicker from "@/components/ModernDatePicker"; @@ -79,12 +77,6 @@ const searchSchema = z type SearchForm = z.infer; -const POPULAR_ROUTES = [ - { from: "Sebeta", to: "Nagad", duration: "12h", icon: "🌆" }, - { from: "Sebeta", to: "Diredawa", duration: "8h", icon: "🏔️" }, - { from: "Diredawa", to: "Nagad", duration: "4h", icon: "🌊" }, -]; - // ─── Station Modal ──────────────────────────────────────────────────────────── function StationModal({ stations, @@ -530,6 +522,8 @@ export default function SearchPage() { const router = useRouter(); const searchParams = useSearchParams(); const setSearchCriteria = useBookingStore((s) => s.setSearchCriteria); + const clearBooking = useBookingStore((s) => s.clearBooking); + const queryClient = useQueryClient(); const { user, isAuthenticated } = useAuthStore(); const [passengerModalOpen, setPassengerModalOpen] = useState(false); @@ -701,7 +695,10 @@ export default function SearchPage() { const onSubmit = (data: SearchForm) => { setHasInteracted(true); + // Clear previous booking selections and search cache before starting a new search + clearBooking(); setSearchCriteria(data); + queryClient.removeQueries({ queryKey: ["search"] }); if (data.originStationId) saveRecent(data.originStationId); if (data.destinationStationId) saveRecent(data.destinationStationId); const params = new URLSearchParams({ @@ -723,20 +720,6 @@ export default function SearchPage() { const originStation = getStationById(originId); const destStation = getStationById(destId); - const handlePopularRoute = (fromName: string, toName: string) => { - const origin = stations.find((s) => - s.name.toLowerCase().includes(fromName.toLowerCase()), - ); - const dest = stations.find((s) => - s.name.toLowerCase().includes(toName.toLowerCase()), - ); - if (origin && dest) { - setValue("originStationId", origin.id); - setValue("destinationStationId", dest.id); - window.scrollTo({ top: 0, behavior: "smooth" }); - } - }; - return (
{/* Passenger modal (mobile) */} @@ -790,9 +773,15 @@ export default function SearchPage() { )} {/* ── 90vh hero with banner image ── */} + {/* Round trip stacks an extra Return Date field into the widget on mobile, which grows + upward from its bottom-anchored position — give the hero extra height there so the + widget's top edge doesn't creep up into the sticky header. */}
{/* Background image with zoom - fully isolated */}
@@ -956,7 +945,7 @@ export default function SearchPage() { -
+
Return Date -
+
{hasInteracted && errors.originStationId && ( @@ -1094,7 +1087,11 @@ export default function SearchPage() { if (s.id) saveRecent(s.id); clearErrors("destinationStationId"); }} - error={hasInteracted ? errors.destinationStationId?.message : undefined} + error={ + hasInteracted + ? errors.destinationStationId?.message + : undefined + } onOpen={scrollWidgetIntoView} /> {hasInteracted && errors.destinationStationId && ( @@ -1110,7 +1107,7 @@ export default function SearchPage() { -
+
{hasInteracted && errors.originStationId && ( @@ -1226,7 +1227,11 @@ export default function SearchPage() { if (s.id) saveRecent(s.id); clearErrors("destinationStationId"); }} - error={hasInteracted ? errors.destinationStationId?.message : undefined} + error={ + hasInteracted + ? errors.destinationStationId?.message + : undefined + } onOpen={scrollWidgetIntoView} /> {hasInteracted && errors.destinationStationId && ( @@ -1242,7 +1247,7 @@ export default function SearchPage() { -
+
Return -
+
- {/* Popular Routes — below hero */} -
-
-
-
- -

- Popular Routes -

-
-
- {POPULAR_ROUTES.map((route, idx) => ( - - ))} -
-
-
-
- - - )} + {/* Mobile summary bottom-sheet — always visible so the active passenger is clear */} +
+
+ +
+
{/* Header */} @@ -1211,15 +1287,22 @@ export default function SeatsPage() { Back -

- {isRoundTrip - ? currentJourneyType === "outbound" - ? "Select Outbound Seats" - : "Select Return Seats" - : "Select Seats"} -

+
+

+ {isRoundTrip + ? currentJourneyType === "outbound" + ? "Select Outbound Seats" + : "Select Return Seats" + : "Select Seats"} +

+ {!allSeatsAssigned && ( +

+ Now selecting: {activePassenger?.name || `Passenger ${activePassengerIndex + 1}`} +

+ )} +
- {selectedSeats.length}/{passengers.length} + {assignedCount}/{passengers.length}
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 a9b734c8f..8edd9f04a 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 @@ -104,7 +104,7 @@ function fmt(iso: string, opts?: Intl.DateTimeFormatOptions): string { function fmtTime(iso: string): string { try { - return new Date(iso).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); + return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }); } catch { return iso; } } diff --git a/apps/edr-passenger-web/portal/src/app/page.tsx b/apps/edr-passenger-web/portal/src/app/page.tsx index c6684f8f6..93bf02fc2 100644 --- a/apps/edr-passenger-web/portal/src/app/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/page.tsx @@ -1,12 +1,27 @@ import { Suspense } from 'react'; +import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import SearchPage from '@/app/booking/search/page'; import PackagesSection from '@/components/PackagesSection'; -export default function Home() { +export default async function Home() { + const queryClient = new QueryClient(); + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; + + await queryClient.prefetchQuery({ + queryKey: ['stations'], + queryFn: async () => { + const res = await fetch(`${apiUrl}/stations`, { next: { revalidate: 3600 } }); + const json = await res.json(); + return json?.data ?? json; + }, + }); + return ( - - - - + + + + + + ); } diff --git a/apps/edr-passenger-web/portal/src/components/AppHeader.tsx b/apps/edr-passenger-web/portal/src/components/AppHeader.tsx index 6422e12fa..200b07222 100644 --- a/apps/edr-passenger-web/portal/src/components/AppHeader.tsx +++ b/apps/edr-passenger-web/portal/src/components/AppHeader.tsx @@ -31,7 +31,7 @@ export default function AppHeader() { return ( -
+
diff --git a/apps/edr-passenger-web/portal/src/components/SearchWidget.tsx b/apps/edr-passenger-web/portal/src/components/SearchWidget.tsx index 85154558e..36bd545e6 100644 --- a/apps/edr-passenger-web/portal/src/components/SearchWidget.tsx +++ b/apps/edr-passenger-web/portal/src/components/SearchWidget.tsx @@ -146,7 +146,7 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
{/* Date */} -
+
{ - return format(new Date(date), 'HH:mm'); + return format(new Date(date), 'h:mm a'); +}; + +export const getTimePeriod = (date: string | Date): string => { + const hour = new Date(date).getHours(); + if (hour >= 5 && hour < 12) return 'Morning'; + if (hour >= 12 && hour < 17) return 'Afternoon'; + if (hour >= 17 && hour < 21) return 'Evening'; + return 'Night'; };