Update booking result page for round trip

This commit is contained in:
Roba Boru
2026-06-17 15:07:05 +03:00
parent ad2a175ff5
commit 1d34354e66
12 changed files with 1823 additions and 517 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -7,6 +7,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { apiClient } from "@/lib/api-client";
import { useState, useEffect } from "react";
import { PaymentMethod } from "@/types";
import { format } from "date-fns";
import {
CreditCard,
Smartphone,
@@ -23,12 +24,14 @@ const getIconForMethod = (methodId: string) => {
export default function PaymentPage() {
const router = useRouter();
const { bookingId, pnr, selectedSchedule, passengers } = useBookingStore();
const { bookingId, pnr, selectedSchedule, outboundSchedule, inboundSchedule, passengers, searchCriteria } = useBookingStore();
const { selectedCurrency, setPaymentIntent, updateStatus } =
usePaymentStore();
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
const { data: paymentMethods = [], isLoading: loadingMethods, error } = useQuery<PaymentMethod[]>({
queryKey: ['paymentMethods'],
queryFn: async () => {
@@ -38,10 +41,21 @@ export default function PaymentPage() {
});
// Calculate total amount
const baseFare = passengers.reduce(
const outboundBaseFare = isRoundTrip && outboundSchedule ? passengers.reduce(
(sum) => sum + (outboundSchedule.baseFareAdult || 0),
0,
) : 0;
const inboundBaseFare = isRoundTrip && inboundSchedule ? passengers.reduce(
(sum) => sum + (inboundSchedule.baseFareAdult || 0),
0,
) : 0;
const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce(
(sum) => sum + (selectedSchedule?.baseFareAdult || 0),
0,
);
const totalAmount = baseFare;
const paymentMutation = useMutation({
@@ -207,47 +221,231 @@ export default function PaymentPage() {
{/* Order Summary */}
<div className="card mb-6">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
<h2 className="text-xl font-semibold mb-6 text-gray-900 dark:text-gray-100">
Order summary
</h2>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Route</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedSchedule?.origin} {selectedSchedule?.destination}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Train</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedSchedule?.trainNumber}
</span>
</div>
{selectedSchedule?.selectedSeatClassName && (
<div className="flex justify-between">
<div className="space-y-6">
{isRoundTrip ? (
<>
{/* Outbound Journey */}
<div>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-primary rounded-full" />
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">Outbound Journey</span>
<span className="ml-auto text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium">
{outboundSchedule?.selectedSeatClassName?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
{/* Flight-style timeline */}
<div className="relative pl-6">
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
{/* Origin */}
<div className="relative pb-16">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{outboundSchedule?.origin}
</div>
</div>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{outboundSchedule?.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {outboundSchedule?.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div className="relative -mt-8">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{outboundSchedule?.destination}
</div>
</div>
</div>
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-gray-800">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Outbound fare</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">ETB {(outboundBaseFare / 100).toFixed(2)}</span>
</div>
</div>
</div>
{/* Return Journey */}
<div className="pt-4 border-t-2 border-dashed border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-blue-500 rounded-full" />
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">Return Journey</span>
<span className="ml-auto text-xs px-2 py-0.5 bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full font-medium">
{inboundSchedule?.selectedSeatClassName?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
{/* Flight-style timeline */}
<div className="relative pl-6">
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-blue-500 via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
{/* Origin */}
<div className="relative pb-16">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{inboundSchedule?.origin}
</div>
</div>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{inboundSchedule?.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {inboundSchedule?.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div className="relative -mt-8">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{inboundSchedule?.destination}
</div>
</div>
</div>
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-gray-800">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Return fare</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">ETB {(inboundBaseFare / 100).toFixed(2)}</span>
</div>
</div>
</div>
</>
) : (
<>
{/* One-Way Journey */}
<div>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-primary rounded-full" />
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">Your Journey</span>
<span className="ml-auto text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium">
{selectedSchedule?.selectedSeatClassName?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
{/* Flight-style timeline */}
<div className="relative pl-6">
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
{/* Origin */}
<div className="relative pb-16">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{selectedSchedule?.origin}
</div>
</div>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{selectedSchedule?.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {selectedSchedule?.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div className="relative -mt-8">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{selectedSchedule?.destination}
</div>
</div>
</div>
</div>
</>
)}
{/* Passengers and Total */}
<div className="pt-4 border-t-2 border-gray-200 dark:border-gray-700">
<div className="flex justify-between text-sm mb-3">
<span className="text-gray-600 dark:text-gray-400">
Class
Passengers
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedSchedule.selectedSeatClassName.replace(/_/g, " ")}
{passengers.length} passenger{passengers.length !== 1 ? "s" : ""}
</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">
Passengers
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{passengers.length} passenger
{passengers.length !== 1 ? "s" : ""}
</span>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
<div className="flex justify-between text-lg font-bold">
<span className="text-gray-900 dark:text-gray-100">
<div className="flex justify-between items-center pt-3 border-t border-gray-200 dark:border-gray-700">
<span className="text-base font-bold text-gray-900 dark:text-gray-100">
Total amount
</span>
<span className="text-primary dark:text-gray-100">
<span className="text-2xl font-bold text-primary dark:text-gray-100">
ETB {(totalAmount / 100).toFixed(2)}
</span>
</div>

View File

@@ -16,7 +16,7 @@ function TelebirrSuccessContent() {
const [status, setStatus] = useState<'processing' | 'done' | 'error'>('processing');
// Telebirr callback query params
const merchantOrderId = searchParams.get('merchantOrderId') || '';
const orderid = searchParams.get('orderid') || '';
const trxRef = searchParams.get('trxRef') || searchParams.get('outTradeNo') || '';
const bookingIdQp = searchParams.get('bookingId') || bookingId || '';
@@ -25,7 +25,7 @@ function TelebirrSuccessContent() {
try {
if (bookingIdQp) {
await apiClient.patch(`/bookings/${bookingIdQp}/confirm`, {
paymentReference: merchantOrderId || trxRef,
paymentReference: orderid || trxRef,
paymentMethod: 'TELEBIRR',
});
}
@@ -59,7 +59,7 @@ function TelebirrSuccessContent() {
<CheckCircle className="w-14 h-14 text-green-500 mx-auto mb-4" />
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Payment Successful!</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">Your Telebirr payment was received.</p>
{merchantOrderId && <p className="text-xs text-gray-400">Order ID: {merchantOrderId}</p>}
{orderid && <p className="text-xs text-gray-400">Order ID: {orderid}</p>}
{trxRef && <p className="text-xs text-gray-400">Transaction Ref: {trxRef}</p>}
<p className="text-xs text-gray-400 mt-3">Redirecting to your booking confirmation</p>
</>

View File

@@ -5,16 +5,16 @@ import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import { useBookingStore } from '@/lib/booking-store';
import { Schedule } from '@/types';
import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Loader2, Check, X, MapPin, Gift, Train } from 'lucide-react';
import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Check, X, MapPin, Gift, Train } from 'lucide-react';
import { format } from 'date-fns';
import { useState, useEffect } from 'react';
export default function ResultsPage() {
const router = useRouter();
const searchParams = useSearchParams();
const setSelectedSchedule = useBookingStore((s) => s.setSelectedSchedule);
const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore();
const [selectedClasses, setSelectedClasses] = useState<Record<string, string>>({});
const [outboundSelected, setOutboundSelected] = useState(false);
const [outboundScheduleData, setOutboundScheduleData] = useState<any>(null);
const [classModal, setClassModal] = useState<Schedule | null>(null);
const [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null);
@@ -114,18 +114,21 @@ export default function ResultsPage() {
const isRoundTrip = searchData.journeyType === 'ROUND_TRIP';
// Handle both response formats:
// 1. One-way: response is array of schedules
// 1. One-way: response can be array of schedules OR object with journeyType and outbound
// 2. Round-trip: response has journeyType, outbound, inbound properties
let outboundSchedules: Schedule[] = [];
let inboundSchedules: Schedule[] = [];
if (results) {
if (isRoundTrip && results.journeyType === 'ROUND_TRIP') {
if (results.journeyType === 'ROUND_TRIP') {
// Round trip response format
outboundSchedules = results.outbound || [];
inboundSchedules = results.inbound || [];
} else if (results.journeyType === 'ONE_WAY' && results.outbound) {
// One-way response format with outbound array
outboundSchedules = results.outbound || [];
} else if (Array.isArray(results)) {
// One-way response format (array of schedules)
// One-way response format (direct array of schedules)
outboundSchedules = results;
} else if (results.data && Array.isArray(results.data)) {
// Fallback: wrapped in data property
@@ -139,11 +142,8 @@ export default function ResultsPage() {
? (outboundSchedules.length > 0 && inboundSchedules.length > 0)
: outboundSchedules.length > 0;
const handleSelectClass = (scheduleId: string, seatClass: string, isOutbound: boolean = false) => {
const handleSelectClass = (scheduleId: string, seatClass: string) => {
setSelectedClasses(prev => ({ ...prev, [scheduleId]: seatClass }));
if (isOutbound && isRoundTrip) {
setOutboundSelected(true);
}
};
const handleSelect = (schedule: Schedule, isOutbound: boolean = false) => {
@@ -184,13 +184,28 @@ export default function ResultsPage() {
// For round trip, store outbound and wait for inbound selection
if (isRoundTrip && isOutbound) {
setOutboundSelected(true);
setOutboundScheduleData(scheduleData);
setOutboundSchedule(scheduleData);
setClassModal(null);
// Scroll to inbound section
setTimeout(() => {
const inboundSection = document.getElementById('inbound-section');
if (inboundSection) {
inboundSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
return;
}
// For round trip inbound or one-way, proceed to next step
setSelectedSchedule(scheduleData);
// For round trip inbound, proceed with both schedules
if (isRoundTrip && !isOutbound) {
setInboundSchedule(scheduleData);
setSelectedSchedule(outboundScheduleData); // Set primary as outbound
} else {
// For one-way
setSelectedSchedule(scheduleData);
}
router.push('/booking/auth-check');
};
@@ -289,10 +304,138 @@ export default function ResultsPage() {
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
<p className="text-gray-600 dark:text-gray-400 font-medium">Searching for trains...</p>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-4 md:py-6">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
{/* Progress Header */}
<div className="mb-8">
<div className="card p-6">
<div className="flex items-center gap-4">
<div className="relative">
<div className="w-12 h-12 rounded-full border-4 border-primary/20 border-t-primary animate-spin" />
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-1">
Searching for trains...
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Finding the best options for your journey
</p>
</div>
</div>
{/* Progress bar */}
<div className="mt-4 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full"
style={{
animation: 'progressBar 2s ease-in-out infinite',
}}
/>
</div>
</div>
</div>
{/* Skeleton Cards */}
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="card animate-pulse">
<div className="flex flex-col lg:flex-row lg:items-center gap-6">
<div className="flex-1">
{/* Train info skeleton */}
<div className="flex items-center gap-3 mb-4">
<div
className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div className="flex-1 space-y-2">
<div
className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-24"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
</div>
</div>
{/* Time and route skeleton */}
<div className="flex items-center gap-4">
<div className="text-center space-y-2">
<div
className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-12"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
</div>
<div className="flex-1 flex flex-col items-center">
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="w-full h-0.5 bg-gray-200 dark:bg-gray-700 relative"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
>
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-2 h-2 bg-gray-300 dark:bg-gray-600 rounded-full" />
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-2 h-2 bg-gray-300 dark:bg-gray-600 rounded-full" />
</div>
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-12 mt-2"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
</div>
<div className="text-center space-y-2">
<div
className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-12"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
</div>
</div>
</div>
<div className="lg:border-l dark:border-gray-700 lg:pl-6 lg:min-w-[220px]">
<div className="text-center lg:text-right space-y-3">
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mx-auto lg:ml-auto"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-32 mx-auto lg:ml-auto"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-16 mx-auto lg:ml-auto"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
<div
className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-full"
style={{ animation: 'shimmer 1.5s ease-in-out infinite' }}
/>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
@@ -381,7 +524,7 @@ export default function ResultsPage() {
return (
<button
key={fareClass.seatClassName}
onClick={() => isAvailable && handleSelectClass(scheduleId, fareClass.seatClassName, isOutbound)}
onClick={() => isAvailable && handleSelectClass(scheduleId, fareClass.seatClassName)}
disabled={!isAvailable}
className={`relative w-full p-4 rounded-xl border-2 text-left transition-all ${
isSelected
@@ -425,7 +568,7 @@ export default function ResultsPage() {
disabled={!selectedClass}
className="w-full flex items-center justify-center gap-2 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all disabled:opacity-40 disabled:cursor-not-allowed shadow-lg"
>
<span>{isRoundTrip && isOutbound ? 'Continue to Return' : 'Continue'}</span>
<span>{isRoundTrip && isOutbound ? 'Continue to Return Flight' : 'Continue'}</span>
<ArrowRight className="w-4 h-4" />
</button>
{!selectedClass && (
@@ -494,8 +637,8 @@ export default function ResultsPage() {
</div>
)}
{inboundSchedules.length > 0 && (!isRoundTrip || outboundSelected) && (
<div>
{isRoundTrip && inboundSchedules.length > 0 && outboundScheduleData && (
<div id="inbound-section">
<div className="mb-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<ArrowRight className="w-5 h-5 text-primary rotate-180" />

View File

@@ -51,11 +51,13 @@ function getPassengerIdFromToken(token: string): string | null {
export default function ReviewPage() {
const router = useRouter();
const { selectedSchedule, passengers, seatHold, setBookingId, setPNR, createAccount, passengerId: storedPassengerId } = useBookingStore();
const { selectedSchedule, outboundSchedule, inboundSchedule, passengers, seatHold, setBookingId, setPNR, createAccount, passengerId: storedPassengerId, searchCriteria } = useBookingStore();
const { user, isAuthenticated } = useAuthStore();
const [timeLeft, setTimeLeft] = useState<string>('');
const [seatDetails, setSeatDetails] = useState<Record<string, string>>({});
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
useEffect(() => {
if (!seatHold?.expiresAt) return;
@@ -79,22 +81,57 @@ export default function ReviewPage() {
useEffect(() => {
const fetchSeatDetails = async () => {
if (!selectedSchedule?.id) return;
try {
const seatMapData: any = await apiClient.get(`/seats/seatmap/${selectedSchedule.id}`);
const coaches = seatMapData?.coaches || [];
const allSeats = coaches.flatMap((coach: any) => coach.seats || []);
const details: Record<string, string> = {};
passengers.forEach(p => {
if (p.seatId) {
const seat = allSeats.find((s: any) => s.id === p.seatId);
if (seat) {
details[p.seatId] = seat.number || seat.label || seat.seatNumber || 'N/A';
// Fetch outbound seat details
if (isRoundTrip && outboundSchedule?.id) {
const outboundSeatMap: any = await apiClient.get(`/seats/seatmap/${outboundSchedule.id}`);
const outboundCoaches = outboundSeatMap?.coaches || [];
const outboundSeats = outboundCoaches.flatMap((coach: any) => coach.seats || []);
passengers.forEach(p => {
if ((p as any).outboundSeatId) {
const seat = outboundSeats.find((s: any) => s.id === (p as any).outboundSeatId);
if (seat) {
details[`outbound-${(p as any).outboundSeatId}`] = seat.number || seat.label || seat.seatNumber || 'N/A';
}
}
}
});
});
}
// Fetch inbound seat details
if (isRoundTrip && inboundSchedule?.id) {
const inboundSeatMap: any = await apiClient.get(`/seats/seatmap/${inboundSchedule.id}`);
const inboundCoaches = inboundSeatMap?.coaches || [];
const inboundSeats = inboundCoaches.flatMap((coach: any) => coach.seats || []);
passengers.forEach(p => {
if ((p as any).inboundSeatId) {
const seat = inboundSeats.find((s: any) => s.id === (p as any).inboundSeatId);
if (seat) {
details[`inbound-${(p as any).inboundSeatId}`] = seat.number || seat.label || seat.seatNumber || 'N/A';
}
}
});
}
// Fetch one-way seat details
if (!isRoundTrip && selectedSchedule?.id) {
const seatMapData: any = await apiClient.get(`/seats/seatmap/${selectedSchedule.id}`);
const coaches = seatMapData?.coaches || [];
const allSeats = coaches.flatMap((coach: any) => coach.seats || []);
passengers.forEach(p => {
if (p.seatId) {
const seat = allSeats.find((s: any) => s.id === p.seatId);
if (seat) {
details[p.seatId] = seat.number || seat.label || seat.seatNumber || 'N/A';
}
}
});
}
setSeatDetails(details);
} catch (error) {
console.error('Failed to fetch seat details:', error);
@@ -102,7 +139,7 @@ export default function ReviewPage() {
};
fetchSeatDetails();
}, [selectedSchedule?.id, passengers]);
}, [selectedSchedule?.id, outboundSchedule?.id, inboundSchedule?.id, passengers, isRoundTrip]);
const createBookingMutation = useMutation({
mutationFn: (data: any) => {
@@ -155,6 +192,8 @@ export default function ReviewPage() {
console.log('Search criteria:', searchCriteria);
console.log('Seat hold:', seatHold);
console.log('Selected schedule:', selectedSchedule);
console.log('Outbound schedule:', outboundSchedule);
console.log('Inbound schedule:', inboundSchedule);
console.log('Passengers:', passengers);
if (!seatHold?.holdId) {
@@ -171,12 +210,15 @@ export default function ReviewPage() {
return;
}
// Get seat class ID
let seatClassId = 'default-seat-class-id';
let returnSeatClassId = 'default-seat-class-id';
try {
const seatClasses: any = await apiClient.get('/seat-classes');
console.log('Seat classes:', seatClasses);
if (seatClasses && seatClasses.length > 0) {
seatClassId = seatClasses[0].id;
returnSeatClassId = seatClasses[0].id;
}
} catch (err) {
console.error('Failed to fetch seat classes:', err);
@@ -229,18 +271,20 @@ export default function ReviewPage() {
throw new Error('Passenger ID not found in authentication token. Please log in again.');
}
// Build booking request for authenticated users
bookingData = {
scheduleId: selectedSchedule?.id || '',
passengerId: passengerId,
scheduleId: isRoundTrip ? outboundSchedule?.id : selectedSchedule?.id,
holdId: seatHold.holdId,
originStationId: searchCriteria.originStationId,
destinationStationId: searchCriteria.destinationStationId,
seatClassId: seatClassId,
bookingType: isRoundTrip ? 'ROUND_TRIP' : 'ONE_WAY',
displayCurrency: 'ETB',
passengerId: passengerId,
passengers: passengers.map((p) => {
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
return {
seatId: p.seatId || '',
seatId: isRoundTrip ? (p as any).outboundSeatId : (p.seatId || ''),
passengerName: p.name,
dateOfBirth: p.dateOfBirth,
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
@@ -251,19 +295,34 @@ export default function ReviewPage() {
};
}),
};
// Add round trip specific fields
if (isRoundTrip && inboundSchedule) {
bookingData.returnScheduleId = inboundSchedule.id;
bookingData.returnOriginStationId = searchCriteria.destinationStationId;
bookingData.returnDestinationStationId = searchCriteria.originStationId;
bookingData.returnHoldId = seatHold.holdId; // Assuming same hold ID, adjust if needed
bookingData.returnSeatClassId = returnSeatClassId;
}
// Add promo code if exists
if (searchCriteria.promoCode) {
bookingData.promoCode = searchCriteria.promoCode;
}
} else {
// For guests: send full passenger details array
bookingData = {
scheduleId: selectedSchedule?.id || '',
scheduleId: isRoundTrip ? outboundSchedule?.id : selectedSchedule?.id,
holdId: seatHold.holdId,
originStationId: searchCriteria.originStationId,
destinationStationId: searchCriteria.destinationStationId,
seatClassId: seatClassId,
bookingType: isRoundTrip ? 'ROUND_TRIP' : 'ONE_WAY',
displayCurrency: 'ETB',
passengers: passengers.map(p => {
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
return {
seatId: p.seatId || '',
seatId: isRoundTrip ? (p as any).outboundSeatId : (p.seatId || ''),
passengerName: p.name,
dateOfBirth: p.dateOfBirth,
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
@@ -279,6 +338,20 @@ export default function ReviewPage() {
savePassengerDetails: true,
deviceId: typeof window !== 'undefined' ? (localStorage.getItem('deviceId') || `device-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) : undefined,
};
// Add round trip specific fields
if (isRoundTrip && inboundSchedule) {
bookingData.returnScheduleId = inboundSchedule.id;
bookingData.returnOriginStationId = searchCriteria.destinationStationId;
bookingData.returnDestinationStationId = searchCriteria.originStationId;
bookingData.returnHoldId = seatHold.holdId; // Assuming same hold ID, adjust if needed
bookingData.returnSeatClassId = returnSeatClassId;
}
// Add promo code if exists
if (searchCriteria.promoCode) {
bookingData.promoCode = searchCriteria.promoCode;
}
}
if (typeof window !== 'undefined' && !isAuthenticated && bookingData.deviceId && !localStorage.getItem('deviceId')) {
@@ -294,26 +367,49 @@ export default function ReviewPage() {
};
useEffect(() => {
if (!selectedSchedule || !passengers.length) {
if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) {
console.log('Redirecting to search - missing data');
router.push('/booking/search');
if (isRoundTrip) {
if (!outboundSchedule || !inboundSchedule || !passengers.length) {
if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) {
console.log('Redirecting to search - missing round trip data');
router.push('/booking/search');
}
}
} else {
if (!selectedSchedule || !passengers.length) {
if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) {
console.log('Redirecting to search - missing data');
router.push('/booking/search');
}
}
}
}, [selectedSchedule, passengers.length, createBookingMutation.isPending, createBookingMutation.isSuccess, router]);
}, [isRoundTrip, selectedSchedule, outboundSchedule, inboundSchedule, passengers.length, createBookingMutation.isPending, createBookingMutation.isSuccess, router]);
if (!selectedSchedule || !passengers.length) {
if (isRoundTrip && (!outboundSchedule || !inboundSchedule || !passengers.length)) {
return null;
}
console.log('Selected schedule:', selectedSchedule);
console.log('Base fare adult:', selectedSchedule.baseFareAdult);
if (!isRoundTrip && (!selectedSchedule || !passengers.length)) {
return null;
}
const displaySchedule = isRoundTrip ? outboundSchedule : selectedSchedule;
console.log('Selected schedule:', displaySchedule);
console.log('Base fare adult:', displaySchedule?.baseFareAdult);
console.log('Passengers:', passengers);
const baseFare = passengers.reduce((sum, p, i) => {
const farePerPassenger = selectedSchedule.baseFareAdult ||
(selectedSchedule as any).fareAdult ||
(selectedSchedule as any).price ||
const outboundBaseFare = isRoundTrip && outboundSchedule ? passengers.reduce((sum) => {
return sum + (outboundSchedule.baseFareAdult || 0);
}, 0) : 0;
const inboundBaseFare = isRoundTrip && inboundSchedule ? passengers.reduce((sum) => {
return sum + (inboundSchedule.baseFareAdult || 0);
}, 0) : 0;
const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce((sum, p, i) => {
const farePerPassenger = selectedSchedule?.baseFareAdult ||
(selectedSchedule as any)?.fareAdult ||
(selectedSchedule as any)?.price ||
0;
console.log(`Passenger ${i}: ${p.name}, fare = ${farePerPassenger}`);
@@ -340,51 +436,259 @@ export default function ReviewPage() {
)}
<div className="space-y-6">
<div className="card">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Trip details</h2>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Train</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule.trainNumber}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Route</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule.origin} {selectedSchedule.destination}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Departure</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedSchedule.departureTime ? format(new Date(selectedSchedule.departureTime), 'PPp') : 'N/A'}
{/* Outbound Trip Details */}
{isRoundTrip && outboundSchedule && (
<div className="card overflow-hidden">
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
<div className="w-2 h-2 bg-primary rounded-full" />
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100">Outbound Journey</h2>
<span className="ml-auto text-xs px-2.5 py-1 bg-primary/10 text-primary rounded-full font-semibold">
{outboundSchedule.selectedSeatClass?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Arrival</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedSchedule.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'PPp') : 'N/A'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Duration</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule.duration}</span>
{/* Flight-style timeline */}
<div className="flex">
{/* Left column: Timeline with dots and line */}
<div className="flex flex-col items-center w-8 flex-shrink-0">
{/* Origin dot */}
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
{/* Destination dot */}
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
</div>
{/* Right column: Content */}
<div className="flex-1 flex flex-col">
{/* Origin */}
<div className="pb-8">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{outboundSchedule.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{outboundSchedule.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{outboundSchedule.origin}
</div>
</div>
{/* Journey Info */}
<div className="pb-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{outboundSchedule.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {outboundSchedule.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{outboundSchedule.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{outboundSchedule.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{outboundSchedule.destination}
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Inbound Trip Details */}
{isRoundTrip && inboundSchedule && (
<div className="card overflow-hidden">
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
<div className="w-2 h-2 bg-blue-500 rounded-full" />
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100">Return Journey</h2>
<span className="ml-auto text-xs px-2.5 py-1 bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full font-semibold">
{inboundSchedule.selectedSeatClass?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
{/* Flight-style timeline */}
<div className="flex">
{/* Left column: Timeline with dots and line */}
<div className="flex flex-col items-center w-8 flex-shrink-0">
{/* Origin dot */}
<div className="w-4 h-4 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<div className="w-0.5 flex-1 bg-gradient-to-b from-blue-500 via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
{/* Destination dot */}
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
</div>
{/* Right column: Content */}
<div className="flex-1 flex flex-col">
{/* Origin */}
<div className="pb-8">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{inboundSchedule.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{inboundSchedule.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{inboundSchedule.origin}
</div>
</div>
{/* Journey Info */}
<div className="pb-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{inboundSchedule.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {inboundSchedule.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{inboundSchedule.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{inboundSchedule.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{inboundSchedule.destination}
</div>
</div>
</div>
</div>
</div>
)}
{/* One-Way Trip Details */}
{!isRoundTrip && selectedSchedule && (
<div className="card overflow-hidden">
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
<div className="w-2 h-2 bg-primary rounded-full" />
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100">Trip Details</h2>
<span className="ml-auto text-xs px-2.5 py-1 bg-primary/10 text-primary rounded-full font-semibold">
{selectedSchedule.selectedSeatClass?.replace(/_/g, ' ') || 'Standard'}
</span>
</div>
{/* Flight-style timeline */}
<div className="flex">
{/* Left column: Timeline with dots and line */}
<div className="flex flex-col items-center w-8 flex-shrink-0">
{/* Origin dot */}
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
{/* Destination dot */}
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
</div>
{/* Right column: Content */}
<div className="flex-1 flex flex-col">
{/* Origin */}
<div className="pb-8">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedSchedule.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{selectedSchedule.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{selectedSchedule.origin}
</div>
</div>
{/* Journey Info */}
<div className="pb-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">{selectedSchedule.duration}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="font-medium">Train {selectedSchedule.trainNumber}</span>
</div>
</div>
</div>
{/* Destination */}
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedSchedule.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{selectedSchedule.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
{selectedSchedule.destination}
</div>
</div>
</div>
</div>
</div>
)}
<div className="card">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Passengers</h2>
<div className="space-y-3">
{passengers.map((p, i) => (
<div key={i} className="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2 last:border-0">
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">{p.name}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'PP') : 'N/A'} {p.nationality}
</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-600 dark:text-gray-400">Seat</p>
<p className="font-medium text-gray-900 dark:text-gray-100">{p.seatId ? (seatDetails[p.seatId] || 'Loading...') : 'Auto-assign'}</p>
<div key={i} className="border-b border-gray-200 dark:border-gray-700 pb-3 last:border-0">
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">{p.name}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'PP') : 'N/A'} {p.nationality}
</p>
</div>
</div>
{isRoundTrip ? (
<div className="grid grid-cols-2 gap-3 mt-2">
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-2">
<p className="text-xs text-gray-500 dark:text-gray-400">Outbound Seat</p>
<p className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{(p as any).outboundSeatId ? (seatDetails[`outbound-${(p as any).outboundSeatId}`] || 'Loading...') : 'Auto-assign'}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-2">
<p className="text-xs text-gray-500 dark:text-gray-400">Return Seat</p>
<p className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{(p as any).inboundSeatId ? (seatDetails[`inbound-${(p as any).inboundSeatId}`] || 'Loading...') : 'Auto-assign'}
</p>
</div>
</div>
) : (
<div className="text-right">
<p className="text-sm text-gray-600 dark:text-gray-400">Seat</p>
<p className="font-medium text-gray-900 dark:text-gray-100">{p.seatId ? (seatDetails[p.seatId] || 'Loading...') : 'Auto-assign'}</p>
</div>
)}
</div>
))}
</div>
@@ -393,10 +697,23 @@ 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">
<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>
{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>
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ SeatButton.displayName = 'SeatButton';
export default function SeatsPage() {
const router = useRouter();
const { selectedSchedule, passengers, setSeatHold, setPassengers, searchCriteria, bookingId } = useBookingStore();
const { selectedSchedule, outboundSchedule, inboundSchedule, passengers, setSeatHold, setPassengers, searchCriteria, bookingId } = useBookingStore();
const [selectedSeats, setSelectedSeats] = useState<string[]>([]);
const [selectedCoach, setSelectedCoach] = useState<string | null>(null);
const [currentJourneyType, setCurrentJourneyType] = useState<'outbound' | 'inbound'>('outbound');
@@ -59,11 +59,12 @@ export default function SeatsPage() {
});
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
const currentSchedule = isRoundTrip && currentJourneyType === 'inbound' ? inboundSchedule : (isRoundTrip ? outboundSchedule : selectedSchedule);
const { data: seatMapData, isLoading, error } = useQuery({
queryKey: ['seatmap', selectedSchedule?.id],
queryFn: () => apiClient.get(`/seats/seatmap/${selectedSchedule?.id}`),
enabled: !!selectedSchedule?.id,
queryKey: ['seatmap', currentSchedule?.id, currentJourneyType],
queryFn: () => apiClient.get(`/seats/seatmap/${currentSchedule?.id}`),
enabled: !!currentSchedule?.id,
});
const holdMutation = useMutation({
@@ -79,7 +80,7 @@ export default function SeatsPage() {
const destinationId = isInbound ? searchCriteria?.originStationId : searchCriteria?.destinationStationId;
return apiClient.post(`/seats/hold`, {
scheduleId: selectedSchedule?.id,
scheduleId: currentSchedule?.id,
originStationId: originId,
destinationStationId: destinationId,
passengers: passengersForHold,
@@ -108,20 +109,20 @@ export default function SeatsPage() {
const coaches = useMemo(() => (seatMapData as any)?.coaches || [], [seatMapData]);
const filteredCoaches = useMemo(() => {
if (!selectedSchedule?.selectedSeatClass) {
if (!currentSchedule?.selectedSeatClass) {
return coaches.filter((c: any) => c.seats && c.seats.length > 0);
}
let filtered = coaches.filter((c: any) => {
const seatClasses = c.seatClasses || [c.seatClass] || [];
return seatClasses.some((seatClassName: string) =>
seatClassName === selectedSchedule.selectedSeatClass ||
seatClassName.replace(/_/g, ' ').toLowerCase() === selectedSchedule.selectedSeatClass?.toLowerCase() ||
seatClassName.toLowerCase() === selectedSchedule.selectedSeatClass?.toLowerCase()
seatClassName === currentSchedule.selectedSeatClass ||
seatClassName.replace(/_/g, ' ').toLowerCase() === currentSchedule.selectedSeatClass?.toLowerCase() ||
seatClassName.toLowerCase() === currentSchedule.selectedSeatClass?.toLowerCase()
);
});
return filtered.filter((c: any) => c.seats && c.seats.length > 0);
}, [coaches, selectedSchedule?.selectedSeatClass]);
}, [coaches, currentSchedule?.selectedSeatClass]);
useEffect(() => {
if (filteredCoaches.length > 0 && !selectedCoach) {
@@ -144,15 +145,15 @@ export default function SeatsPage() {
let seats = allSeats.filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
const isBedCoach = selectedCoachData?.seatClass?.toLowerCase().includes('bed') || selectedCoachData?.mode?.toLowerCase().includes('bed');
if (isBedCoach && selectedSchedule?.selectedSeatClass) {
const selectedBedPosition = getBedPosition(selectedSchedule.selectedSeatClass);
if (isBedCoach && currentSchedule?.selectedSeatClass) {
const selectedBedPosition = getBedPosition(currentSchedule.selectedSeatClass);
if (selectedBedPosition) {
seats = seats.filter((s: any) => s.bedPosition === selectedBedPosition);
}
}
return seats;
}, [allSeats, selectedCoachData, selectedSchedule?.selectedSeatClass]);
}, [allSeats, selectedCoachData, currentSchedule?.selectedSeatClass]);
const handleSeatClick = useCallback((seatId: string) => {
setSelectedSeats(prev => {
@@ -166,7 +167,6 @@ export default function SeatsPage() {
const handleContinue = async () => {
if (isRoundTrip && currentJourneyType === 'outbound') {
// Save outbound seats and show inbound
if (selectedSeats.length > 0) {
try {
await holdMutation.mutateAsync(selectedSeats);
@@ -174,8 +174,8 @@ export default function SeatsPage() {
const seatData = validSeats?.find((s: any) => s.id === selectedSeats[i]);
return {
...p,
seatId: selectedSeats[i],
seatNumber: seatData?.number || seatData?.label || seatData?.seatNumber || '',
outboundSeatId: selectedSeats[i],
outboundSeatNumber: seatData?.number || seatData?.label || seatData?.seatNumber || '',
};
});
setPassengers(updatedPassengers);
@@ -195,12 +195,18 @@ export default function SeatsPage() {
return;
}
// Final continue (one-way or round-trip inbound)
if (selectedSeats.length > 0) {
try {
await holdMutation.mutateAsync(selectedSeats);
const updatedPassengers = passengers.map((p, i) => {
const seatData = validSeats?.find((s: any) => s.id === selectedSeats[i]);
if (isRoundTrip && currentJourneyType === 'inbound') {
return {
...p,
inboundSeatId: selectedSeats[i],
inboundSeatNumber: seatData?.number || seatData?.label || seatData?.seatNumber || '',
};
}
return {
...p,
seatId: selectedSeats[i],
@@ -264,10 +270,16 @@ export default function SeatsPage() {
};
useEffect(() => {
if (!selectedSchedule || !passengers.length) {
router.push('/booking/search');
if (isRoundTrip) {
if (!outboundSchedule || !inboundSchedule || !passengers.length) {
router.push('/booking/search');
}
} else {
if (!selectedSchedule || !passengers.length) {
router.push('/booking/search');
}
}
}, [selectedSchedule, passengers.length, router]);
}, [isRoundTrip, selectedSchedule, outboundSchedule, inboundSchedule, passengers.length, router]);
useEffect(() => {
if (bookingId && selectedSeats.length > 0) {
@@ -595,8 +607,8 @@ export default function SeatsPage() {
const coachSeats = coach.seats?.filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-')) || [];
const isBed = coach.seatClass?.toLowerCase().includes('bed') || coach.mode?.toLowerCase().includes('bed');
let fSeats = coachSeats;
if (isBed && selectedSchedule?.selectedSeatClass) {
const bedPos = getBedPosition(selectedSchedule.selectedSeatClass);
if (isBed && currentSchedule?.selectedSeatClass) {
const bedPos = getBedPosition(currentSchedule.selectedSeatClass);
if (bedPos) fSeats = coachSeats.filter((s: any) => s.bedPosition === bedPos);
}
const available = fSeats.filter((s: any) => s.status === 'AVAILABLE').length;

View File

@@ -108,13 +108,22 @@
@keyframes shimmer {
0% {
background-position: -1000px 0;
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
background-position: 1000px 0;
opacity: 1;
}
}
@keyframes progressBar {
0% { width: 0%; }
50% { width: 60%; }
100% { width: 90%; }
}
@keyframes slide-in-left {
from {
opacity: 0;

View File

@@ -1,10 +1,11 @@
'use client';
"use client";
import { Train, Menu, X, Moon, Sun, HelpCircle } from 'lucide-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { LanguageSwitcher } from './LanguageSwitcher';
import { Menu, X, Moon, Sun, HelpCircle } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { LanguageSwitcher } from "./LanguageSwitcher";
export default function AppHeader() {
const pathname = usePathname();
@@ -12,47 +13,50 @@ export default function AppHeader() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const isDarkMode = document.documentElement.classList.contains('dark');
const isDarkMode = document.documentElement.classList.contains("dark");
setIsDark(isDarkMode);
}, []);
const toggleTheme = () => {
const html = document.documentElement;
const isDarkMode = html.classList.contains('dark');
const isDarkMode = html.classList.contains("dark");
if (isDarkMode) {
html.classList.remove('dark');
html.classList.remove("dark");
setIsDark(false);
localStorage.setItem('theme', 'light');
localStorage.setItem("theme", "light");
} else {
html.classList.add('dark');
html.classList.add("dark");
setIsDark(true);
localStorage.setItem('theme', 'dark');
localStorage.setItem("theme", "dark");
}
};
const isLandingPage = ['/', '/services', '/about', '/contact', '/help'].includes(pathname);
const isLandingPage = [
"/",
"/services",
"/about",
"/contact",
"/help",
].includes(pathname);
return (
<header className="sticky top-0 z-50 bg-[rgb(20_113_76)] dark:bg-gray-900 border-b border-[rgb(16_89_60)] dark:border-gray-800 shadow-sm">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link
href="/"
className="flex items-center gap-3 hover:opacity-80 transition-opacity"
<Link
href="/"
className="flex items-center hover:opacity-80 transition-opacity"
>
<div className="w-10 h-10 bg-white rounded-lg flex items-center justify-center">
<Train className="w-6 h-6 text-[rgb(20_113_76)]" />
</div>
<div>
<h1 className="text-lg font-bold text-white">
Ethio-Djibouti Railway
</h1>
<p className="text-xs text-gray-100">
Book your train journey with us
</p>
</div>
<Image
src="/edr-logo.png"
alt="Ethio-Djibouti Railway"
width={140}
height={48}
className="h-14 w-auto"
priority
/>
</Link>
{/* Desktop Menu - only show for landing pages */}
@@ -103,7 +107,7 @@ export default function AppHeader() {
<button
onClick={toggleTheme}
className="p-2 text-gray-100 hover:bg-white hover:bg-opacity-10 rounded-lg transition-colors"
title={isDark ? 'Light mode' : 'Dark mode'}
title={isDark ? "Light mode" : "Dark mode"}
>
{isDark ? (
<Sun className="w-5 h-5" />
@@ -117,7 +121,11 @@ export default function AppHeader() {
onClick={() => setIsOpen(!isOpen)}
className="md:hidden p-2 text-gray-100 hover:bg-white hover:bg-opacity-10 rounded-lg"
>
{isOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
{isOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
</button>
</div>
</div>
@@ -157,7 +165,7 @@ export default function AppHeader() {
</Link>
</>
)}
<Link
href="/help"
className="block px-4 py-2 text-gray-100 dark:text-gray-300 hover:bg-white hover:bg-opacity-10 dark:hover:bg-gray-800 rounded-lg transition-colors"

View File

@@ -14,14 +14,17 @@ import ModernDatePicker from '@/components/ModernDatePicker';
const searchSchema = z.object({
tripType: z.enum(['ONE_WAY', 'ROUND_TRIP']),
originStationId: z.string().min(1),
destinationStationId: z.string().min(1),
departureDate: z.string().min(1),
originStationId: z.string().min(1, 'Please select a departure station'),
destinationStationId: z.string().min(1, 'Please select an arrival station'),
departureDate: z.string().min(1, 'Please select a departure date'),
returnDate: z.string().optional(),
adultCount: z.number().min(1).max(9),
childCount: z.number().min(0).max(9),
adultCount: z.number().min(1, 'At least 1 adult is required').max(9, 'Maximum 9 adults allowed'),
childCount: z.number().min(0, 'Cannot be negative').max(9, 'Maximum 9 children allowed'),
nationality: z.enum(['ETHIOPIAN', 'DJIBOUTIAN', 'OTHER']),
}).refine((data) => data.originStationId !== data.destinationStationId, {
}).refine((data) => {
if (!data.originStationId || !data.destinationStationId) return true;
return data.originStationId !== data.destinationStationId;
}, {
message: 'Origin and destination must be different',
path: ['destinationStationId'],
});
@@ -43,10 +46,15 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
queryFn: async () => await apiClient.get('/stations') as Station[],
});
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<SearchForm>({
resolver: zodResolver(searchSchema as any),
const { register, handleSubmit, watch, setValue, clearErrors, formState: { errors } } = useForm<SearchForm>({
// @ts-ignore - ZodEffects type compatibility issue
resolver: zodResolver(searchSchema),
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: {
tripType: 'ONE_WAY',
originStationId: '',
destinationStationId: '',
adultCount: 1,
childCount: 0,
nationality: 'ETHIOPIAN',
@@ -89,8 +97,14 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-primary" />
<select
{...register('originStationId')}
className="w-full pl-11 pr-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
{...register('originStationId', {
onChange: (e) => {
if (e.target.value) clearErrors('originStationId');
}
})}
className={`w-full pl-11 pr-4 py-3.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 ${
errors.originStationId ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
disabled={isLoading}
>
<option value="">Select departure</option>
@@ -100,7 +114,7 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
</select>
</div>
{errors.originStationId && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.originStationId.message}</p>
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.originStationId.message}</p>
)}
</div>
@@ -110,8 +124,14 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-primary" />
<select
{...register('destinationStationId')}
className="w-full pl-11 pr-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
{...register('destinationStationId', {
onChange: (e) => {
if (e.target.value) clearErrors('destinationStationId');
}
})}
className={`w-full pl-11 pr-4 py-3.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 ${
errors.destinationStationId ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
disabled={isLoading}
>
<option value="">Select arrival</option>
@@ -121,7 +141,7 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
</select>
</div>
{errors.destinationStationId && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.destinationStationId.message}</p>
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.destinationStationId.message}</p>
)}
</div>
@@ -135,12 +155,13 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
setValue('departureDate', `${year}-${month}-${day}`);
clearErrors('departureDate');
}}
minDate={new Date()}
placeholder="Select date"
/>
{errors.departureDate && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.departureDate.message}</p>
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.departureDate.message}</p>
)}
</div>
</div>
@@ -267,6 +288,17 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
{/* Row 3: Search Button */}
<div>
{/* Debug info - remove after testing */}
{Object.keys(errors).length > 0 && (
<div className="mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm font-medium text-red-800 dark:text-red-200 mb-1">Validation Errors:</p>
<ul className="text-xs text-red-700 dark:text-red-300 list-disc list-inside">
{Object.entries(errors).map(([key, value]) => (
<li key={key}>{key}: {value?.message}</li>
))}
</ul>
</div>
)}
<button
type="submit"
className="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-3.5 px-6 rounded-lg transition-all duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl"

View File

@@ -56,6 +56,8 @@ export interface SeatHold {
interface BookingState {
searchCriteria: SearchCriteria | null;
selectedSchedule: SelectedSchedule | null;
outboundSchedule: SelectedSchedule | null;
inboundSchedule: SelectedSchedule | null;
passengers: PassengerDetail[];
seatHold: SeatHold | null;
bookingId: string | null;
@@ -66,6 +68,8 @@ interface BookingState {
setSearchCriteria: (criteria: SearchCriteria) => void;
setSelectedSchedule: (schedule: SelectedSchedule) => void;
setOutboundSchedule: (schedule: SelectedSchedule) => void;
setInboundSchedule: (schedule: SelectedSchedule) => void;
setPassengers: (passengers: PassengerDetail[]) => void;
setSeatHold: (hold: SeatHold | null) => void;
setBookingId: (id: string) => void;
@@ -80,6 +84,8 @@ export const useBookingStore = create<BookingState>()(persist(
(set) => (({
searchCriteria: null,
selectedSchedule: null,
outboundSchedule: null,
inboundSchedule: null,
passengers: [],
seatHold: null,
bookingId: null,
@@ -90,6 +96,8 @@ export const useBookingStore = create<BookingState>()(persist(
setSearchCriteria: (criteria) => set({ searchCriteria: criteria }),
setSelectedSchedule: (schedule) => set({ selectedSchedule: schedule }),
setOutboundSchedule: (schedule) => set({ outboundSchedule: schedule }),
setInboundSchedule: (schedule) => set({ inboundSchedule: schedule }),
setPassengers: (passengers) => set({ passengers }),
setSeatHold: (hold) => set({ seatHold: hold }),
setBookingId: (id) => set({ bookingId: id }),
@@ -100,6 +108,8 @@ export const useBookingStore = create<BookingState>()(persist(
clearBooking: () => set({
searchCriteria: null,
selectedSchedule: null,
outboundSchedule: null,
inboundSchedule: null,
passengers: [],
seatHold: null,
bookingId: null,