mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Fix test findings
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user