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;
status?: string;
totalMinor?: number;
createdAt?: string;
paymentMethod?: string;
ticket?: {
barcodePayload?: string;
qrPayload?: string;
@@ -24,7 +26,8 @@ type BookingWithTicket = {
export default function ConfirmationPage() {
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 [isGeneratingVoucher, setIsGeneratingVoucher] = useState(false);
const confirmAttempted = useRef(false);
@@ -193,12 +196,7 @@ export default function ConfirmationPage() {
<div className="flex flex-col md:flex-row gap-6">
{/* 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">
<QRCodeSVG
value={pnr}
size={160}
level="H"
includeMargin={true}
/>
<QRCodeSVG 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>
</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">
<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">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 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">{selectedSchedule?.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">{selectedSchedule?.origin} {selectedSchedule?.destination}</p>
</div>
{selectedSchedule?.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">{selectedSchedule.selectedSeatClassName.replace(/_/g, ' ')}</p>
{/* Outbound journey (round trip) or single journey */}
{(() => {
const schedule = isRoundTrip ? outboundSchedule : selectedSchedule;
if (!schedule) return null;
return (
<div className="mb-4">
{isRoundTrip && (
<p className="text-xs font-bold uppercase tracking-wide text-primary mb-2">Outbound</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">{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 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>
<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')}
</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">{selectedSchedule?.duration}</p>
);
})()}
{/* 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 */}
<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>

View File

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

View File

@@ -14,7 +14,9 @@ export default function ResultsPage() {
const searchParams = useSearchParams();
const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore();
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 [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null);

View File

@@ -648,24 +648,37 @@ export default function ReviewPage() {
<div className="card">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Fare breakdown</h2>
<div className="space-y-2">
{isRoundTrip ? (
<>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Outbound fare</span>
<span className="text-gray-900 dark:text-gray-100">ETB {(outboundBaseFare / 100).toFixed(2)}</span>
<div className="space-y-3">
{passengers.map((p, i) => {
const outFare = outboundSchedule?.baseFareAdult || 0;
const inFare = inboundSchedule?.baseFareAdult || 0;
const onewayFare = selectedSchedule?.baseFareAdult || 0;
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 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">
<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>

View File

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

View File

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