Fix test findings

This commit is contained in:
Roba Boru
2026-06-25 19:58:47 +03:00
parent 1a152b0e75
commit bd82e95f9f
6 changed files with 210 additions and 78 deletions

View File

@@ -16,6 +16,8 @@ type BookingWithTicket = {
pnr?: string | null; pnr?: string | null;
status?: string; status?: string;
totalMinor?: number; totalMinor?: number;
createdAt?: string;
paymentMethod?: string;
ticket?: { ticket?: {
barcodePayload?: string; barcodePayload?: string;
qrPayload?: string; qrPayload?: string;
@@ -24,7 +26,8 @@ type BookingWithTicket = {
export default function ConfirmationPage() { export default function ConfirmationPage() {
const router = useRouter(); const router = useRouter();
const { bookingId, pnr, selectedSchedule, passengers, clearBooking } = useBookingStore(); const { bookingId, pnr, selectedSchedule, outboundSchedule, inboundSchedule, searchCriteria, passengers, clearBooking } = useBookingStore();
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);
const confirmAttempted = useRef(false); const confirmAttempted = useRef(false);
@@ -193,12 +196,7 @@ export default function ConfirmationPage() {
<div className="flex flex-col md:flex-row gap-6"> <div className="flex flex-col md:flex-row gap-6">
{/* QR Code Section */} {/* QR Code Section */}
<div className="flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-800 rounded-lg p-6 md:w-48 flex-shrink-0"> <div className="flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-800 rounded-lg p-6 md:w-48 flex-shrink-0">
<QRCodeSVG <QRCodeSVG value={pnr} size={160} level="H" includeMargin={true} />
value={pnr}
size={160}
level="H"
includeMargin={true}
/>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-2 text-center font-semibold">Scan at gate</p> <p className="text-xs text-gray-600 dark:text-gray-400 mt-2 text-center font-semibold">Scan at gate</p>
</div> </div>
@@ -208,44 +206,129 @@ export default function ConfirmationPage() {
<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> </div>
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">Trip details</h2> <h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{isRoundTrip ? 'Round trip details' : 'Trip details'}
</h2>
</div> </div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-3"> {/* Outbound journey (round trip) or single journey */}
<div> {(() => {
<p className="text-sm text-gray-600 dark:text-gray-400">Train number</p> const schedule = isRoundTrip ? outboundSchedule : selectedSchedule;
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{selectedSchedule?.trainNumber}</p> if (!schedule) return null;
</div> return (
<div> <div className="mb-4">
<p className="text-sm text-gray-600 dark:text-gray-400">Route</p> {isRoundTrip && (
<p className="font-semibold text-lg text-gray-900 dark:text-gray-100">{selectedSchedule?.origin} {selectedSchedule?.destination}</p> <p className="text-xs font-bold uppercase tracking-wide text-primary mb-2">Outbound</p>
</div> )}
{selectedSchedule?.selectedSeatClassName && ( <div className="grid md:grid-cols-2 gap-4">
<div> <div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">Class</p> <div>
<p className="font-semibold text-gray-900 dark:text-gray-100">{selectedSchedule.selectedSeatClassName.replace(/_/g, ' ')}</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>
)}
</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">
{selectedSchedule?.departureTime && format(new Date(selectedSchedule.departureTime), 'PPp')}
</p>
</div> </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">
{selectedSchedule?.arrivalTime && format(new Date(selectedSchedule.arrivalTime), 'PPp')} {/* Return journey (round trip only) */}
</p> {isRoundTrip && inboundSchedule && (
</div> <div className="border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
<div> <p className="text-xs font-bold uppercase tracking-wide text-blue-500 mb-2">Return</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Duration</p> <div className="grid md:grid-cols-2 gap-4">
<p className="font-semibold text-gray-900 dark:text-gray-100">{selectedSchedule?.duration}</p> <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>
</div>
</div>
{/* Booking date & payment summary */}
<div className="card mb-6">
<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>
<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">
{format(new Date(_booking?.createdAt || new Date()), 'PPp')}
</p>
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">Status</p>
<p className="font-semibold text-green-600 dark:text-green-400">{_booking?.status || 'CONFIRMED'}</p>
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">Passengers</p>
<p className="font-semibold text-gray-900 dark:text-gray-100">{passengers.length}</p>
</div>
<div>
<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">
ETB {((_booking?.totalMinor || passengers.reduce((s) => s + (selectedSchedule?.baseFareAdult || 0), 0)) / 100).toFixed(2)}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -378,7 +378,7 @@ type FormData = z.infer<typeof formSchema>;
export default function PassengersPage() { export default function PassengersPage() {
const router = useRouter(); const router = useRouter();
const { searchCriteria, setPassengers, setCreateAccount } = useBookingStore(); const { searchCriteria, passengers: storedPassengers, setPassengers, setCreateAccount } = useBookingStore();
const { user, isAuthenticated, updateUser } = useAuthStore(); const { user, isAuthenticated, updateUser } = useAuthStore();
const isInitialized = useAuthStore((s) => s.isInitialized); const isInitialized = useAuthStore((s) => s.isInitialized);
const [faydaEnabled, setFaydaEnabled] = useState(true); const [faydaEnabled, setFaydaEnabled] = useState(true);
@@ -393,22 +393,43 @@ export default function PassengersPage() {
resolver: zodResolver(formSchema as any), resolver: zodResolver(formSchema as any),
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
passengers: Array.from({ length: totalPassengers }, () => ({ passengers: Array.from({ length: totalPassengers }, (_, i) => {
name: '', const stored = storedPassengers[i];
dateOfBirth: '', if (stored?.name) {
gender: undefined, return {
nationality: searchCriteria?.nationality || 'ETHIOPIAN', name: stored.name,
phone: '', dateOfBirth: stored.dateOfBirth || '',
email: '', gender: (stored.gender as any) || undefined,
nationalId: '', nationality: stored.nationality || searchCriteria?.nationality || 'ETHIOPIAN',
passportNumber: '', phone: stored.phone || '',
passportCountry: '', email: stored.email || '',
passportIssueDate: '', nationalId: stored.nationalId || '',
passportExpiryDate: '', passportNumber: stored.passportNumber || '',
passportIssuingAuthority: '', passportCountry: stored.passportCountry || '',
faydaVerified: false, passportIssueDate: stored.passportIssueDate || '',
formExpanded: false, passportExpiryDate: stored.passportExpiryDate || '',
})), passportIssuingAuthority: stored.passportIssuingAuthority || '',
faydaVerified: stored.faydaVerified || false,
formExpanded: true,
};
}
return {
name: '',
dateOfBirth: '',
gender: undefined,
nationality: searchCriteria?.nationality || 'ETHIOPIAN',
phone: '',
email: '',
nationalId: '',
passportNumber: '',
passportCountry: '',
passportIssueDate: '',
passportExpiryDate: '',
passportIssuingAuthority: '',
faydaVerified: false,
formExpanded: false,
};
}),
createAccount: false, createAccount: false,
}, },
}); });

View File

@@ -14,7 +14,9 @@ export default function ResultsPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore(); const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore();
const [selectedCoachTypes, setSelectedCoachTypes] = useState<Record<string, { id: string; code: string; name: string }>>({}); const [selectedCoachTypes, setSelectedCoachTypes] = useState<Record<string, { id: string; code: string; name: string }>>({});
const [outboundScheduleData, setOutboundScheduleData] = useState<any>(null); const [outboundScheduleData, setOutboundScheduleData] = useState<any>(
() => useBookingStore.getState().outboundSchedule,
);
const [classModal, setClassModal] = useState<Schedule | null>(null); const [classModal, setClassModal] = useState<Schedule | null>(null);
const [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null); const [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null);

View File

@@ -648,24 +648,37 @@ export default function ReviewPage() {
<div className="card"> <div className="card">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Fare breakdown</h2> <h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Fare breakdown</h2>
<div className="space-y-2"> <div className="space-y-3">
{isRoundTrip ? ( {passengers.map((p, i) => {
<> const outFare = outboundSchedule?.baseFareAdult || 0;
<div className="flex justify-between"> const inFare = inboundSchedule?.baseFareAdult || 0;
<span className="text-gray-600 dark:text-gray-400">Outbound fare</span> const onewayFare = selectedSchedule?.baseFareAdult || 0;
<span className="text-gray-900 dark:text-gray-100">ETB {(outboundBaseFare / 100).toFixed(2)}</span> const passengerTotal = isRoundTrip ? outFare + inFare : onewayFare;
return (
<div key={i} className="border-b border-gray-100 dark:border-gray-800 pb-3 last:border-0">
<div className="flex justify-between mb-1">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{p.name || `Passenger ${i + 1}`}
</span>
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
ETB {(passengerTotal / 100).toFixed(2)}
</span>
</div>
{isRoundTrip && (
<div className="pl-3 space-y-0.5 text-xs text-gray-500 dark:text-gray-400">
<div className="flex justify-between">
<span>Outbound</span>
<span>ETB {(outFare / 100).toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>Return</span>
<span>ETB {(inFare / 100).toFixed(2)}</span>
</div>
</div>
)}
</div> </div>
<div className="flex justify-between"> );
<span className="text-gray-600 dark:text-gray-400">Return fare</span> })}
<span className="text-gray-900 dark:text-gray-100">ETB {(inboundBaseFare / 100).toFixed(2)}</span>
</div>
</>
) : (
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Base fare</span>
<span className="text-gray-900 dark:text-gray-100">ETB {(baseFare / 100).toFixed(2)}</span>
</div>
)}
<div className="flex justify-between text-lg font-bold border-t border-gray-200 dark:border-gray-700 pt-2"> <div className="flex justify-between text-lg font-bold border-t border-gray-200 dark:border-gray-700 pt-2">
<span className="text-gray-900 dark:text-gray-100">Total</span> <span className="text-gray-900 dark:text-gray-100">Total</span>
<span className="text-primary dark:text-gray-100">ETB {(total / 100).toFixed(2)}</span> <span className="text-primary dark:text-gray-100">ETB {(total / 100).toFixed(2)}</span>

View File

@@ -65,6 +65,16 @@ const searchSchema = z
message: "Return date must be after departure date", message: "Return date must be after departure date",
path: ["returnDate"], path: ["returnDate"],
}, },
)
.refine(
(d) =>
!d.originStationId ||
!d.destinationStationId ||
d.originStationId !== d.destinationStationId,
{
message: "Departure and destination cannot be the same station",
path: ["destinationStationId"],
},
); );
type SearchForm = z.infer<typeof searchSchema>; type SearchForm = z.infer<typeof searchSchema>;
@@ -583,7 +593,7 @@ export default function SearchPage() {
adultCount: 1, adultCount: 1,
childCount: 0, childCount: 0,
nationality: "ETHIOPIAN", nationality: "ETHIOPIAN",
departureDate: new Date().toISOString().split("T")[0], departureDate: "",
promoCode: "", promoCode: "",
}, },
}); });
@@ -609,6 +619,8 @@ export default function SearchPage() {
const adults = searchParams.get("adults"); const adults = searchParams.get("adults");
const children = searchParams.get("children"); const children = searchParams.get("children");
const nat = searchParams.get("nationality"); const nat = searchParams.get("nationality");
const tripType = searchParams.get("tripType");
const returnDate = searchParams.get("returnDate");
if (o) setValue("originStationId", o); if (o) setValue("originStationId", o);
if (d) setValue("destinationStationId", d); if (d) setValue("destinationStationId", d);
if (date) setValue("departureDate", date); if (date) setValue("departureDate", date);
@@ -616,6 +628,8 @@ export default function SearchPage() {
if (children) setValue("childCount", parseInt(children)); if (children) setValue("childCount", parseInt(children));
if (nat) if (nat)
setValue("nationality", nat as "ETHIOPIAN" | "DJIBOUTIAN" | "OTHER"); setValue("nationality", nat as "ETHIOPIAN" | "DJIBOUTIAN" | "OTHER");
if (tripType) setValue("tripType", tripType as "ONE_WAY" | "ROUND_TRIP");
if (returnDate) setValue("returnDate", returnDate);
}, [searchParams, setValue]); }, [searchParams, setValue]);
useEffect(() => { useEffect(() => {

View File

@@ -133,7 +133,6 @@ export default function SeatsPage() {
? outboundSchedule ? outboundSchedule
: selectedSchedule; : selectedSchedule;
const coachTypeId = (currentSchedule as any)?.selectedCoachTypeId; const coachTypeId = (currentSchedule as any)?.selectedCoachTypeId;
const coachTypeCode = (currentSchedule as any)?.selectedCoachTypeCode;
const { const {
data: seatMapData, data: seatMapData,
@@ -142,7 +141,7 @@ export default function SeatsPage() {
} = useQuery({ } = useQuery({
queryKey: ["seatmap", currentSchedule?.id, coachTypeId, currentJourneyType], queryKey: ["seatmap", currentSchedule?.id, coachTypeId, currentJourneyType],
queryFn: async () => { queryFn: async () => {
const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachTypeId=${coachTypeCode}`; const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachTypeId=${coachTypeId}`;
console.log("🪑 Seatmap Request:", { console.log("🪑 Seatmap Request:", {
endpoint, endpoint,
}); });