mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 05:58:18 +00:00
Update price
This commit is contained in:
@@ -1,16 +1,16 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { useBookingStore } from '@/lib/booking-store';
|
import { useBookingStore } from "@/lib/booking-store";
|
||||||
import { usePaymentStore } from '@/lib/payment-store';
|
import { usePaymentStore } from "@/lib/payment-store";
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from "@/lib/api-client";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { CheckCircle, Clock, Copy, Train, FileText } from 'lucide-react';
|
import { CheckCircle, Clock, Copy, Train, FileText } from "lucide-react";
|
||||||
import { format } from 'date-fns';
|
import { format } from "date-fns";
|
||||||
import { isChild, isFirstChild } from '@/utils/fare-utils';
|
import { isChild, isFirstChild } from "@/utils/fare-utils";
|
||||||
|
|
||||||
type BookingWithTicket = {
|
type BookingWithTicket = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -38,11 +38,24 @@ type BookingWithTicket = {
|
|||||||
|
|
||||||
export default function ConfirmationPage() {
|
export default function ConfirmationPage() {
|
||||||
const router = useRouter();
|
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 —
|
// 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).
|
// null when no payment step ran (e.g. a fully-discounted, zero-amount booking).
|
||||||
const { selectedCurrency: paidCurrency, paidAmountMinor } = usePaymentStore();
|
const { selectedCurrency: paidCurrency, paidAmountMinor } = usePaymentStore();
|
||||||
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
|
const isRoundTrip = searchCriteria?.tripType === "ROUND_TRIP";
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [isGeneratingVoucher, setIsGeneratingVoucher] = 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
|
// 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.
|
// blocked, and awaiting a cold dynamic import is enough to fall outside that window.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import('@/lib/generate-voucher');
|
import("@/lib/generate-voucher");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: _booking } = useQuery<BookingWithTicket>({
|
const { data: _booking } = useQuery<BookingWithTicket>({
|
||||||
queryKey: ['booking', bookingId],
|
queryKey: ["booking", bookingId],
|
||||||
queryFn: async (): Promise<BookingWithTicket> => {
|
queryFn: async (): Promise<BookingWithTicket> => {
|
||||||
try {
|
try {
|
||||||
return await apiClient.get(`/bookings/${bookingId}`);
|
return await apiClient.get(`/bookings/${bookingId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
id: bookingId || '',
|
id: bookingId || "",
|
||||||
pnr: pnr || undefined,
|
pnr: pnr || undefined,
|
||||||
status: 'PENDING_PAYMENT',
|
status: "PENDING_PAYMENT",
|
||||||
totalMinor: passengers.reduce((sum) => sum + (selectedSchedule?.baseFareAdult || 0), 0),
|
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
|
// 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
|
// generates it server-side (for every payment method, wallet included); this page only
|
||||||
// ever fetches and displays whatever the booking query above already returns.
|
// ever fetches and displays whatever the booking query above already returns.
|
||||||
const isConfirmed = _booking?.status === 'CONFIRMED';
|
const isConfirmed = _booking?.status === "CONFIRMED";
|
||||||
|
|
||||||
const copyPNR = () => {
|
const copyPNR = () => {
|
||||||
if (pnr) {
|
if (pnr) {
|
||||||
@@ -88,46 +104,53 @@ export default function ConfirmationPage() {
|
|||||||
|
|
||||||
const handleDownloadVoucher = async () => {
|
const handleDownloadVoucher = async () => {
|
||||||
if (!pnr) {
|
if (!pnr) {
|
||||||
alert('Booking data not available. Please try again.');
|
alert("Booking data not available. Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!passengers.length) {
|
if (!passengers.length) {
|
||||||
alert('No passenger data found.');
|
alert("No passenger data found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsGeneratingVoucher(true);
|
setIsGeneratingVoucher(true);
|
||||||
try {
|
try {
|
||||||
const { generatePassengerVoucherPDF } = await import('@/lib/generate-voucher');
|
const { generatePassengerVoucherPDF } =
|
||||||
|
await import("@/lib/generate-voucher");
|
||||||
|
|
||||||
const activeSchedule = isRoundTrip ? outboundSchedule : selectedSchedule;
|
const activeSchedule = isRoundTrip ? outboundSchedule : selectedSchedule;
|
||||||
// The server-confirmed settled amount/currency (what was actually charged) is
|
// The server-confirmed settled amount/currency (what was actually charged) is
|
||||||
// authoritative — prefer it over the ETB booking fare once it's available.
|
// authoritative — prefer it over the ETB booking fare once it's available.
|
||||||
const settledAmountMinor = _booking?.payment?.amountMinor;
|
const settledAmountMinor = _booking?.payment?.amountMinor;
|
||||||
const settledCurrency = _booking?.payment?.currency;
|
const settledCurrency = _booking?.payment?.currency;
|
||||||
const voucherCurrency = settledCurrency || 'ETB';
|
const voucherCurrency = settledCurrency || "ETB";
|
||||||
const createdAt = _booking?.createdAt || new Date().toISOString();
|
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
|
// Compute per-passenger fares (in ETB) using the same logic as the review/payment
|
||||||
// pages. reviewedPassengerFares is the authoritative source; rebuild from package
|
// pages. reviewedPassengerFares is the authoritative source; rebuild from package
|
||||||
// context as a fallback so free children always show 0 on their voucher.
|
// context as a fallback so free children always show 0 on their voucher.
|
||||||
const { packageTierPriceMinor } = useBookingStore.getState();
|
const { packageTierPriceMinor } = useBookingStore.getState();
|
||||||
const isPackageBooking = packageTierPriceMinor != null;
|
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 pkgMultiplier = isPackageBooking ? 2 : 1;
|
||||||
const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgMultiplier : 0;
|
const pkgAdultFare = isPackageBooking
|
||||||
|
? packageTierPriceMinor! * pkgMultiplier
|
||||||
|
: 0;
|
||||||
const pkgChildFare = pkgAdultFare;
|
const pkgChildFare = pkgAdultFare;
|
||||||
|
|
||||||
const getEtbFare = (idx: number): number => {
|
const getEtbFare = (idx: number): number => {
|
||||||
if (reviewedPassengerFares?.[idx] != null) return reviewedPassengerFares[idx].fareMinor;
|
if (reviewedPassengerFares?.[idx] != null)
|
||||||
|
return reviewedPassengerFares[idx].fareMinor;
|
||||||
if (isPackageBooking) {
|
if (isPackageBooking) {
|
||||||
const isPkgChild = idx >= adultCount;
|
const isPkgChild = idx >= adultCount;
|
||||||
const isFreeChild = isPkgChild && (idx - adultCount) < adultCount;
|
const isFreeChild = isPkgChild && idx - adultCount < adultCount;
|
||||||
if (isFreeChild) return 0;
|
if (isFreeChild) return 0;
|
||||||
return isPkgChild ? pkgChildFare : pkgAdultFare;
|
return isPkgChild ? pkgChildFare : pkgAdultFare;
|
||||||
}
|
}
|
||||||
const totalFare = reviewedTotalMinor ?? paidAmountMinor ?? _booking?.totalMinor ?? 0;
|
const totalFare =
|
||||||
|
reviewedTotalMinor ?? paidAmountMinor ?? _booking?.totalMinor ?? 0;
|
||||||
return Math.round(totalFare / passengers.length);
|
return Math.round(totalFare / passengers.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,31 +159,54 @@ export default function ConfirmationPage() {
|
|||||||
// ETB-denominated numbers next to a foreign currency label.
|
// ETB-denominated numbers next to a foreign currency label.
|
||||||
const etbFares = passengers.map((_, idx) => getEtbFare(idx));
|
const etbFares = passengers.map((_, idx) => getEtbFare(idx));
|
||||||
const etbTotal = etbFares.reduce((sum, f) => sum + f, 0);
|
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 => {
|
const getVoucherFare = (idx: number): number => {
|
||||||
if (!needsConversion) return etbFares[idx];
|
if (!needsConversion) return etbFares[idx];
|
||||||
return Math.round(etbFares[idx] * (settledAmountMinor! / etbTotal));
|
return Math.round(etbFares[idx] * (settledAmountMinor! / etbTotal));
|
||||||
};
|
};
|
||||||
|
|
||||||
const outbound = {
|
const outbound = {
|
||||||
trainNumber: activeSchedule?.trainNumber || 'N/A',
|
trainNumber: activeSchedule?.trainNumber || "N/A",
|
||||||
trainName: 'EDR Express',
|
trainName: "EDR Express",
|
||||||
origin: { name: activeSchedule?.origin || 'Origin', code: 'ORG', city: activeSchedule?.origin || 'Origin' },
|
origin: {
|
||||||
destination: { name: activeSchedule?.destination || 'Destination', code: 'DST', city: activeSchedule?.destination || 'Destination' },
|
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(),
|
departureAt: activeSchedule?.departureTime || new Date().toISOString(),
|
||||||
arrivalAt: activeSchedule?.arrivalTime || new Date().toISOString(),
|
arrivalAt: activeSchedule?.arrivalTime || new Date().toISOString(),
|
||||||
seatClass: activeSchedule?.selectedSeatClassName,
|
seatClass: activeSchedule?.selectedSeatClassName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const inbound = inboundSchedule ? {
|
const inbound = inboundSchedule
|
||||||
trainNumber: inboundSchedule.trainNumber || 'N/A',
|
? {
|
||||||
trainName: 'EDR Express',
|
trainNumber: inboundSchedule.trainNumber || "N/A",
|
||||||
origin: { name: inboundSchedule.origin, code: 'ORG', city: inboundSchedule.origin },
|
trainName: "EDR Express",
|
||||||
destination: { name: inboundSchedule.destination, code: 'DST', city: inboundSchedule.destination },
|
origin: {
|
||||||
departureAt: inboundSchedule.departureTime || new Date().toISOString(),
|
name: inboundSchedule.origin,
|
||||||
arrivalAt: inboundSchedule.arrivalTime || new Date().toISOString(),
|
code: "ORG",
|
||||||
seatClass: inboundSchedule.selectedSeatClassName,
|
city: inboundSchedule.origin,
|
||||||
} : undefined;
|
},
|
||||||
|
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)
|
// 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
|
// 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
|
// 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).
|
// fabricated placeholder if there's no backend ticket data (see generate-voucher.ts).
|
||||||
const matchedTicket =
|
const matchedTicket =
|
||||||
_booking?.tickets?.find((t) => t.passengerName === p.name) ?? _booking?.tickets?.[i] ?? null;
|
_booking?.tickets?.find((t) => t.passengerName === p.name) ??
|
||||||
const ticketNumber = matchedTicket?.barcodePayload || 'Not yet issued';
|
_booking?.tickets?.[i] ??
|
||||||
|
null;
|
||||||
|
const ticketNumber = matchedTicket?.barcodePayload || "Not yet issued";
|
||||||
|
|
||||||
await generatePassengerVoucherPDF({
|
await generatePassengerVoucherPDF({
|
||||||
bookingRef: pnr,
|
bookingRef: pnr,
|
||||||
ticketNumber,
|
ticketNumber,
|
||||||
passengerName: p.name || `Passenger ${i + 1}`,
|
passengerName: p.name || `Passenger ${i + 1}`,
|
||||||
dateOfBirth: p.dateOfBirth,
|
dateOfBirth: p.dateOfBirth,
|
||||||
nationality: p.nationality,
|
nationality: p.nationality,
|
||||||
seatNumber: p.seatNumber,
|
seatNumber: p.seatNumber,
|
||||||
outboundSeatNumber: (p as any).outboundSeatNumber,
|
outboundSeatNumber: (p as any).outboundSeatNumber,
|
||||||
inboundSeatNumber: (p as any).inboundSeatNumber,
|
inboundSeatNumber: (p as any).inboundSeatNumber,
|
||||||
status,
|
status,
|
||||||
outboundSchedule: outbound,
|
outboundSchedule: outbound,
|
||||||
inboundSchedule: inbound,
|
inboundSchedule: inbound,
|
||||||
isRoundTrip,
|
isRoundTrip,
|
||||||
fareMinor: getVoucherFare(i),
|
fareMinor: getVoucherFare(i),
|
||||||
currency: voucherCurrency,
|
currency: voucherCurrency,
|
||||||
createdAt,
|
createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setIsGeneratingVoucher(false);
|
setIsGeneratingVoucher(false);
|
||||||
}
|
}
|
||||||
@@ -200,12 +250,12 @@ export default function ConfirmationPage() {
|
|||||||
|
|
||||||
const handleNewBooking = () => {
|
const handleNewBooking = () => {
|
||||||
clearBooking();
|
clearBooking();
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bookingId || !pnr) {
|
if (!bookingId || !pnr) {
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
}, [bookingId, pnr, router]);
|
}, [bookingId, pnr, router]);
|
||||||
|
|
||||||
@@ -231,9 +281,13 @@ export default function ConfirmationPage() {
|
|||||||
{isConfirmed ? (
|
{isConfirmed ? (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">
|
<h1 className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">
|
||||||
{packageName ? `${packageName} booking confirmed!` : 'Booking confirmed!'}
|
{packageName
|
||||||
|
? `${packageName} booking confirmed!`
|
||||||
|
: "Booking confirmed!"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 text-lg">Your train tickets are ready</p>
|
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
||||||
|
Your train tickets are ready
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -241,7 +295,8 @@ export default function ConfirmationPage() {
|
|||||||
Booking received — payment pending
|
Booking received — payment pending
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -250,11 +305,15 @@ export default function ConfirmationPage() {
|
|||||||
{/* PNR Card */}
|
{/* PNR Card */}
|
||||||
<div className="card mb-6 bg-primary text-white">
|
<div className="card mb-6 bg-primary text-white">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm font-medium mb-2">Booking reference (PNR)</p>
|
<p className="text-sm font-medium mb-2">
|
||||||
|
Booking reference (PNR)
|
||||||
|
</p>
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<span className="text-5xl font-bold tracking-widest">{pnr}</span>
|
<span className="text-5xl font-bold tracking-widest">
|
||||||
<button
|
{pnr}
|
||||||
onClick={copyPNR}
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={copyPNR}
|
||||||
className="p-2 hover:bg-white hover:bg-opacity-20 rounded transition-colors"
|
className="p-2 hover:bg-white hover:bg-opacity-20 rounded transition-colors"
|
||||||
title="Copy PNR"
|
title="Copy PNR"
|
||||||
>
|
>
|
||||||
@@ -265,149 +324,231 @@ export default function ConfirmationPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium mt-2">Save this reference number for future use</p>
|
<p className="text-sm font-medium mt-2">
|
||||||
|
Save this reference number for future use
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trip Details */}
|
{/* Trip Details */}
|
||||||
<div className="card mb-6">
|
<div className="card mb-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-10 h-10 bg-primary-100 dark:bg-primary-900/30 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-primary-100 dark:bg-primary-900/30 rounded-lg flex items-center justify-center">
|
||||||
<Train className="w-6 h-6 text-primary dark:text-primary-400" />
|
<Train className="w-6 h-6 text-primary dark:text-primary-400" />
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{isRoundTrip ? 'Round trip details' : 'Trip details'}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{isRoundTrip ? "Round trip details" : "Trip details"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Outbound journey (round trip) or single journey */}
|
{/* Outbound journey (round trip) or single journey */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const schedule = isRoundTrip ? outboundSchedule : selectedSchedule;
|
const schedule = isRoundTrip
|
||||||
if (!schedule) return null;
|
? outboundSchedule
|
||||||
return (
|
: selectedSchedule;
|
||||||
<div className="mb-4">
|
if (!schedule) return null;
|
||||||
{isRoundTrip && (
|
return (
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-primary mb-2">Outbound</p>
|
<div className="mb-4">
|
||||||
)}
|
{isRoundTrip && (
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<p className="text-xs font-bold uppercase tracking-wide text-primary mb-2">
|
||||||
<div className="space-y-3">
|
Outbound
|
||||||
<div>
|
</p>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Train number</p>
|
)}
|
||||||
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{schedule.trainNumber}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Route</p>
|
|
||||||
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{schedule.origin} → {schedule.destination}</p>
|
|
||||||
</div>
|
|
||||||
{schedule.selectedSeatClassName && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Class</p>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{schedule.selectedSeatClassName.replace(/_/g, ' ')}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Departure</p>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{schedule.departureTime && format(new Date(schedule.departureTime), 'PPp')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Arrival</p>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{schedule.arrivalTime && format(new Date(schedule.arrivalTime), 'PPp')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Duration</p>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{schedule.duration}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Return journey (round trip only) */}
|
|
||||||
{isRoundTrip && inboundSchedule && (
|
|
||||||
<div className="border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
|
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-blue-500 mb-2">Return</p>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Train number</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{inboundSchedule.trainNumber}</p>
|
Train number
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||||
|
{schedule.trainNumber}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Route</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{inboundSchedule.origin} → {inboundSchedule.destination}</p>
|
Route
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||||
|
{schedule.origin} → {schedule.destination}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{inboundSchedule.selectedSeatClassName && (
|
{schedule.selectedSeatClassName && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Class</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{inboundSchedule.selectedSeatClassName.replace(/_/g, ' ')}</p>
|
Class
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{schedule.selectedSeatClassName.replace(
|
||||||
|
/_/g,
|
||||||
|
" ",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Departure</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Departure
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{inboundSchedule.departureTime && format(new Date(inboundSchedule.departureTime), 'PPp')}
|
{schedule.departureTime &&
|
||||||
|
format(new Date(schedule.departureTime), "PPp")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Arrival</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Arrival
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{inboundSchedule.arrivalTime && format(new Date(inboundSchedule.arrivalTime), 'PPp')}
|
{schedule.arrivalTime &&
|
||||||
|
format(new Date(schedule.arrivalTime), "PPp")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Duration</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{inboundSchedule.duration}</p>
|
Duration
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{schedule.duration}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Return journey (round trip only) */}
|
||||||
|
{isRoundTrip && inboundSchedule && (
|
||||||
|
<div className="border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-wide text-blue-500 mb-2">
|
||||||
|
Return
|
||||||
|
</p>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Train number
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.trainNumber}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Route
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.origin} →{" "}
|
||||||
|
{inboundSchedule.destination}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{inboundSchedule.selectedSeatClassName && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Class
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.selectedSeatClassName.replace(
|
||||||
|
/_/g,
|
||||||
|
" ",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Departure
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.departureTime &&
|
||||||
|
format(
|
||||||
|
new Date(inboundSchedule.departureTime),
|
||||||
|
"PPp",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Arrival
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.arrivalTime &&
|
||||||
|
format(
|
||||||
|
new Date(inboundSchedule.arrivalTime),
|
||||||
|
"PPp",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Duration
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{inboundSchedule.duration}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Booking date & payment summary */}
|
{/* Booking date & payment summary */}
|
||||||
<div className="card mb-6">
|
<div className="card mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Booking details</h2>
|
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
|
Booking details
|
||||||
|
</h2>
|
||||||
<div className="grid sm:grid-cols-2 gap-4">
|
<div className="grid sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Booking date</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Booking date
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{format(new Date(_booking?.createdAt || new Date()), 'PPp')}
|
{format(new Date(_booking?.createdAt || new Date()), "PPp")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Status</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className={`font-semibold ${isConfirmed ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}`}>
|
Status
|
||||||
{_booking?.status || 'PENDING_PAYMENT'}
|
</p>
|
||||||
|
<p
|
||||||
|
className={`font-semibold ${isConfirmed ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-400"}`}
|
||||||
|
>
|
||||||
|
{_booking?.status || "PENDING_PAYMENT"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Passengers</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{passengers.length}</p>
|
Passengers
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{passengers.length}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Total paid</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Total paid
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{(() => {
|
{(() => {
|
||||||
// The server-confirmed settled amount is authoritative — prefer it over
|
// The server-confirmed settled amount is authoritative — prefer it over
|
||||||
// any client-side session state, which can go stale (e.g. after a refresh).
|
// any client-side session state, which can go stale (e.g. after a refresh).
|
||||||
if (_booking?.payment?.amountMinor != null) {
|
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 (reviewedTotalMinor != null)
|
||||||
if (paidAmountMinor != null) return `${paidCurrency} ${(paidAmountMinor / 100).toFixed(2)}`;
|
return `ETB ${(reviewedTotalMinor / 100).toFixed(2)}`;
|
||||||
if (_booking?.totalMinor != null) return `ETB ${(_booking.totalMinor / 100).toFixed(2)}`;
|
if (paidAmountMinor != null)
|
||||||
return 'ETB 0.00';
|
return `${paidCurrency} ${(paidAmountMinor / 100).toFixed(2)}`;
|
||||||
|
if (_booking?.totalMinor != null)
|
||||||
|
return `ETB ${(_booking.totalMinor / 100).toFixed(2)}`;
|
||||||
|
return "ETB 0.00";
|
||||||
})()}
|
})()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,70 +557,132 @@ export default function ConfirmationPage() {
|
|||||||
|
|
||||||
{/* Tickets */}
|
{/* Tickets */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Your tickets</h2>
|
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
|
Your tickets
|
||||||
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{passengers.map((passenger, index) => {
|
{passengers.map((passenger, index) => {
|
||||||
// Match by name first (tickets aren't necessarily created/ordered the same
|
// 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.
|
// way as this passengers array) — fall back to position if no name match.
|
||||||
const backendTicket =
|
const backendTicket =
|
||||||
_booking?.tickets?.find((t) => t.passengerName === passenger.name) ??
|
_booking?.tickets?.find(
|
||||||
|
(t) => t.passengerName === passenger.name,
|
||||||
|
) ??
|
||||||
_booking?.tickets?.[index] ??
|
_booking?.tickets?.[index] ??
|
||||||
null;
|
null;
|
||||||
// No fabricated placeholder — a made-up TKT-... number reads as real and is
|
// No fabricated placeholder — a made-up TKT-... number reads as real and is
|
||||||
// misleading if it doesn't match what's actually on file.
|
// 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 (
|
return (
|
||||||
<div key={index} className="card hover:shadow-lg transition-shadow">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="card hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
{/* Ticket Info */}
|
{/* Ticket Info */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">{passenger.name}</h3>
|
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">Passenger {index + 1}</p>
|
{passenger.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Passenger {index + 1}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isConfirmed ? (
|
{isConfirmed ? (
|
||||||
<span className="badge badge-success">CONFIRMED</span>
|
<span className="badge badge-success">CONFIRMED</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="badge badge-warning">AWAITING PAYMENT</span>
|
<span className="badge badge-warning">
|
||||||
|
AWAITING PAYMENT
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 dark:text-gray-400">Ticket Number</p>
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Ticket Number
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{ticketNumber || (isConfirmed ? 'Not yet issued' : 'Pending payment')}
|
{ticketNumber ||
|
||||||
|
(isConfirmed
|
||||||
|
? "Not yet issued"
|
||||||
|
: "Pending payment")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 dark:text-gray-400">Date of Birth</p>
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{format(new Date(passenger.dateOfBirth), 'PP')}</p>
|
Date of Birth
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{format(new Date(passenger.dateOfBirth), "PP")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 dark:text-gray-400">Nationality</p>
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{passenger.nationality}</p>
|
Nationality
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{passenger.nationality}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 dark:text-gray-400">Seat(s)</p>
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Seat(s)
|
||||||
|
</p>
|
||||||
{(() => {
|
{(() => {
|
||||||
const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length;
|
const adultCount =
|
||||||
|
searchCriteria?.adultCount ??
|
||||||
|
passengers.filter((p) => !isChild(p)).length;
|
||||||
const isFreeChild = packageId
|
const isFreeChild = packageId
|
||||||
? index >= adultCount && (index - adultCount) < adultCount
|
? index >= adultCount &&
|
||||||
: isChild(passenger) && isFirstChild(passengers, index);
|
index - adultCount < adultCount
|
||||||
if (isFreeChild) return <p className="font-semibold text-gray-900 dark:text-gray-100">—</p>;
|
: isChild(passenger) &&
|
||||||
|
isFirstChild(passengers, index);
|
||||||
|
if (isFreeChild)
|
||||||
|
return (
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
—
|
||||||
|
</p>
|
||||||
|
);
|
||||||
return isRoundTrip ? (
|
return isRoundTrip ? (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Outbound: {(passenger as any).outboundCoachNumber && <span className='text-xs text-gray-500 dark:text-gray-400 ml-1'>{(passenger as any).outboundCoachNumber}</span>} — {(passenger as any).outboundSeatNumber || 'Auto-assigned at boarding'}
|
Outbound:{" "}
|
||||||
|
{(passenger as any).outboundCoachNumber && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
|
{(passenger as any).outboundCoachNumber}
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
—{" "}
|
||||||
|
{(passenger as any).outboundSeatNumber ||
|
||||||
|
"Auto-assigned at boarding"}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Return: {(passenger as any).inboundCoachNumber && <span className='text-xs text-gray-500 dark:text-gray-400 ml-1'>{(passenger as any).inboundCoachNumber}</span>} — {(passenger as any).inboundSeatNumber || 'Auto-assigned at boarding'}
|
Return:{" "}
|
||||||
|
{(passenger as any).inboundCoachNumber && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
|
{(passenger as any).inboundCoachNumber}
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
—{" "}
|
||||||
|
{(passenger as any).inboundSeatNumber ||
|
||||||
|
"Auto-assigned at boarding"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{passenger.coachNumber && <span className='text-xs text-gray-500 dark:text-gray-400 ml-1'>(Coach {passenger.coachNumber})</span>} — {passenger.seatNumber || 'Auto-assigned at boarding'}
|
{passenger.coachNumber && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
|
(Coach {passenger.coachNumber})
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
—{" "}
|
||||||
|
{passenger.seatNumber ||
|
||||||
|
"Auto-assigned at boarding"}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -517,8 +720,8 @@ export default function ConfirmationPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* New Booking Button */}
|
{/* New Booking Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleNewBooking}
|
onClick={handleNewBooking}
|
||||||
className="btn-primary w-full py-4 text-lg font-semibold"
|
className="btn-primary w-full py-4 text-lg font-semibold"
|
||||||
>
|
>
|
||||||
Book another trip
|
Book another trip
|
||||||
@@ -528,12 +731,14 @@ export default function ConfirmationPage() {
|
|||||||
<div className="mt-6 space-y-3">
|
<div className="mt-6 space-y-3">
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg">
|
<div className="p-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300">
|
<p className="text-sm text-blue-800 dark:text-blue-300">
|
||||||
📧 A confirmation email with your tickets has been sent to your registered email address.
|
📧 A confirmation email with your tickets has been sent to your
|
||||||
|
registered email address.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
<div className="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
<p className="text-sm text-green-800 dark:text-green-300">
|
<p className="text-sm text-green-800 dark:text-green-300">
|
||||||
✅ Please arrive at the station at least 30 minutes before departure.
|
✅ Please arrive at the station at least 30 minutes before
|
||||||
|
departure.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user