Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha

This commit is contained in:
Abubeker Yasin
2026-07-15 21:33:17 +03:00
140 changed files with 6841 additions and 1433 deletions

View File

@@ -35,6 +35,7 @@ export default function SeatsPage() {
queryKey: ['seatmap', selectedSchedule],
queryFn: () => selectedSchedule ? seatsApi.getSeatMap(selectedSchedule) : Promise.resolve(null),
enabled: !!selectedSchedule,
staleTime: 0,
});
const { data: coachTypesData } = useQuery({
@@ -49,6 +50,7 @@ export default function SeatsPage() {
const { data: routeCoachesData, isLoading: routeCoachesLoading } = useQuery({
queryKey: ['routeCoaches', selectedRoute],
staleTime: 0,
queryFn: async () => {
if (!selectedRoute) return null;
const template: any[] = await routeCoachTemplatesApi.get(selectedRoute);
@@ -79,10 +81,15 @@ export default function SeatsPage() {
enabled: !!selectedRoute,
});
const invalidateSeatData = () => {
queryClient.refetchQueries({ queryKey: ['seatmap', selectedSchedule] });
queryClient.refetchQueries({ queryKey: ['routeCoaches', selectedRoute] });
};
const blockMutation = useMutation({
mutationFn: ({ seatId, reason }: any) => seatsApi.block(seatId, { reason }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
@@ -92,14 +99,14 @@ export default function SeatsPage() {
const unblockMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.unblock(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
},
});
const removeSeatMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.removeSeat(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
setShowRemoveModal(false);
setSelectedSeat(null);
},
@@ -108,7 +115,7 @@ export default function SeatsPage() {
const undoRemoveMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.undoRemove(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
},
});
@@ -116,7 +123,7 @@ export default function SeatsPage() {
mutationFn: ({ seatId, reason }: { seatId: string; reason: string }) =>
seatsApi.setMaintenance(seatId, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
setShowMaintenanceModal(false);
setSelectedSeat(null);
setMaintenanceReason('');
@@ -125,7 +132,7 @@ export default function SeatsPage() {
const clearMaintenanceMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.clearMaintenance(seatId),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['seatmap'] }),
onSuccess: () => invalidateSeatData(),
});
const schedules = schedulesData?.items || schedulesData?.data || [];
@@ -139,7 +146,7 @@ export default function SeatsPage() {
return Promise.all(seatIds.map((seatId: string) => seatsApi.block(seatId, { reason })));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
setShowBlockCoachModal(false);
setSelectedCoach(null);
setBlockCoachReason('');
@@ -153,7 +160,7 @@ export default function SeatsPage() {
return Promise.all(seatIds.map((seatId: string) => seatsApi.unblock(seatId)));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
invalidateSeatData();
setShowUnblockCoachModal(false);
setCoachToUnblock(null);
},
@@ -1015,7 +1022,7 @@ function SeatIcon({
const color = getSeatColor(status);
const canBlock = status === 'AVAILABLE';
const canUnblock = status === 'BLOCKED';
const canMaintenance = status === 'AVAILABLE' || status === 'BLOCKED';
const canMaintenance = false;
const canClearMaintenance = status === 'UNDER_MAINTENANCE';
return (

View File

@@ -427,6 +427,7 @@ function BookingDetailContent() {
</span>
</h2>
{!booking.isPackageBooking && booking.bookingType !== "PACKAGE" && (
<div className="space-y-2">
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100">
Fare breakdown
@@ -481,6 +482,7 @@ function BookingDetailContent() {
);
})}
</div>
)}
<div className="pt-2 border-t-2 border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center">

View File

@@ -34,7 +34,6 @@ export default function PaymentPage() {
const { bookingId, pnr, selectedSchedule, outboundSchedule, inboundSchedule, passengers, searchCriteria, packageName, reviewedTotalMinor, reviewedPassengerFares } = useBookingStore();
const { setPaymentIntent, updateStatus, setCurrency, setPaidAmount } = usePaymentStore();
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
const [selectedMethodCurrency, setSelectedMethodCurrency] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
// CAC Bank OTP debit: on Pay, collect the payer's mobile in a modal, then the SMS'd OTP.
@@ -47,40 +46,40 @@ export default function PaymentPage() {
const [otpError, setOtpError] = useState<string | null>(null);
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
const isPackage = !!packageName;
// Use the same display currency as the review page (derived from nationality)
// Use the same display currency as the review page — stored on the schedule at search time.
const scheduleCurrency = isRoundTrip
? outboundSchedule?.displayCurrency
: selectedSchedule?.displayCurrency;
const nat = (searchCriteria?.nationality ?? '').toUpperCase();
const displayCurrency = nat === 'DJIBOUTIAN' ? 'DJF' : nat === 'ETHIOPIAN' ? 'ETB' : 'USD';
const displayCurrency = scheduleCurrency || (nat === 'DJIBOUTIAN' ? 'DJF' : nat === 'ETHIOPIAN' ? 'ETB' : 'USD');
const { data: paymentMethods = [], isLoading: loadingMethods, error } = useQuery<PaymentMethod[]>({
queryKey: ['paymentMethods', displayCurrency],
queryKey: ['paymentMethods'],
queryFn: async () => {
const response = await apiClient.get<PaymentMethod[]>(`/payments/methods?currency=${displayCurrency}`);
const response = await apiClient.get<PaymentMethod[]>(`/payments/methods`);
return Array.isArray(response) ? response : [];
},
});
const selectedPaymentMethod = paymentMethods.find(m => m.type === selectedMethod) || null;
// A payment method only needs a currency conversion when its own currency differs from
// the default booking currency (e.g. Waafi settles in USD) — otherwise the reviewed ETB
// total already shown on the review page is exact and there's nothing to convert.
const isConversionNeeded = !!selectedMethodCurrency && selectedMethodCurrency !== displayCurrency;
const amountCurrency = isConversionNeeded ? selectedMethodCurrency! : displayCurrency;
// Derive charge currency directly from the selected method — no separate state that can lag.
const amountCurrency = (selectedPaymentMethod?.currency || 'ETB').toUpperCase();
// Fetch the converted booking amount from the booking-amount-changer API whenever a
// currency-specific payment method is selected.
const { data: bookingAmountData, isLoading: loadingAmount } = useQuery<{ amount: number; currency: string; booking_id: string }>({
const { data: bookingAmountData, isFetching: fetchingAmount } = useQuery<{ amount: number; currency: string; booking_id: string }>({
queryKey: ['bookingAmount', bookingId, amountCurrency],
queryFn: async () => {
const url = `/payments/booking-amount?bookingId=${bookingId}&currency=${amountCurrency}`;
const response: any = await apiClient.get(url);
const response: any = await apiClient.get(`/payments/booking-amount?bookingId=${bookingId}&currency=${amountCurrency}`);
return response;
},
enabled: !!bookingId && isConversionNeeded,
enabled: !!bookingId && !!selectedMethod,
staleTime: 30_000,
});
// Data is only usable when it belongs to the currently-selected method's currency.
const dataReady = !fetchingAmount && bookingAmountData != null && bookingAmountData.currency.toUpperCase() === amountCurrency.toUpperCase();
// Per-leg subtotals for the journey header — sum each paying passenger's reviewed fare
// split equally across both legs. This guarantees leg totals are consistent with the
// per-passenger breakdown rows and the overall reviewed total.
@@ -91,39 +90,33 @@ export default function PaymentPage() {
? (reviewedPassengerFares ?? []).reduce((sum, f) => sum + (f.isFree ? 0 : (f.inboundFareMinor ?? Math.round(f.fareMinor / 2))), 0)
: 0;
// reviewedPassengerFares / reviewedTotalMinor are the single source of truth for display
// in the booking's default currency (ETB) — they were computed and shown to the user on
// the review page. But once a payment method with its own currency is selected (e.g.
// Waafi/USD), the converted amount from the booking-amount API takes over so the user
// sees the actual amount they'll be charged in that currency.
// reviewedTotalMinor is in display-currency minor units — matches what was shown on the review page.
// When a method with a different currency is selected, bookingAmountData gives the converted charge amount.
// When the method's currency matches displayCurrency (or no method selected), use reviewedTotal directly.
const reviewedTotal = reviewedTotalMinor ?? (reviewedPassengerFares?.reduce((s, f) => s + f.fareMinor, 0) ?? null);
const totalAmountDisplay = isConversionNeeded
? (bookingAmountData != null ? bookingAmountData.amount : null)
: (reviewedTotal != null ? reviewedTotal / 100 : (bookingAmountData != null ? bookingAmountData.amount : null));
const totalAmount = isConversionNeeded
? (bookingAmountData != null ? Math.round(bookingAmountData.amount * 100) : (reviewedTotal ?? 0))
: (reviewedTotal ?? (bookingAmountData != null ? Math.round(bookingAmountData.amount * 100) : 0));
const confirmedCurrency = isConversionNeeded ? (bookingAmountData?.currency || amountCurrency) : displayCurrency;
// Show loading spinner while the converted amount is still in flight for a
// currency-specific method; ETB methods always have the reviewed total instantly.
const awaitingAmount = !isPackage && isConversionNeeded && loadingAmount && totalAmountDisplay === null;
// When a method is selected: show spinner until dataReady, then show converted amount.
// When no method is selected: show the reviewed total in displayCurrency.
const totalAmountDisplay = selectedMethod
? (dataReady ? bookingAmountData!.amount : null)
: (reviewedTotal != null ? reviewedTotal / 100 : null);
const totalAmount = selectedMethod && dataReady
? Math.round(bookingAmountData!.amount * 100)
: (reviewedTotal ?? 0);
const confirmedCurrency = selectedMethod
? (dataReady ? bookingAmountData!.currency : amountCurrency)
: displayCurrency;
const awaitingAmount = !!selectedMethod && !dataReady;
useEffect(() => {
// Once a currency-specific payment method's converted amount has loaded, that's the
// real charge amount and currency — store it as the paid amount. Otherwise fall back
// to the reviewed ETB total shown on the review page.
if (isConversionNeeded && bookingAmountData != null) {
setCurrency(confirmedCurrency as 'ETB' | 'DJF' | 'USD');
setPaidAmount(Math.round(bookingAmountData.amount * 100));
} else if (reviewedTotal != null) {
setCurrency('ETB');
if (selectedMethod && dataReady) {
setCurrency(bookingAmountData!.currency as 'ETB' | 'DJF' | 'USD');
setPaidAmount(Math.round(bookingAmountData!.amount * 100));
} else if (!selectedMethod && reviewedTotal != null) {
setCurrency(displayCurrency as 'ETB' | 'DJF' | 'USD');
setPaidAmount(reviewedTotal);
} else if (bookingAmountData != null) {
setCurrency(confirmedCurrency as 'ETB' | 'DJF' | 'USD');
setPaidAmount(Math.round(bookingAmountData.amount * 100));
}
}, [isConversionNeeded, bookingAmountData, confirmedCurrency, reviewedTotal, setCurrency, setPaidAmount]);
}, [selectedMethod, dataReady, bookingAmountData, reviewedTotal, displayCurrency, setCurrency, setPaidAmount]);
const paymentMutation = useMutation({
mutationFn: async (data: any) => {
@@ -603,7 +596,7 @@ export default function PaymentPage() {
return (
<button
key={method.id}
onClick={() => { setSelectedMethod(method.type); setSelectedMethodCurrency(method.currency ?? null); }}
onClick={() => setSelectedMethod(method.type)}
disabled={isProcessing || !method.enabled}
className={`w-full p-4 rounded-xl border-2 transition-all text-left ${
isSelected

View File

@@ -1065,23 +1065,36 @@ export default function ResultsPage() {
}
if (isOneWayNoOutbound) {
const hasAlternatives = alternativeOutbound.length > 0;
return (
<div className="booking-page">
{renderClassModal()}
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between gap-4 px-4 py-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 mb-8">
<div className="flex items-center gap-2.5 text-sm text-red-800 dark:text-red-300">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>No trains available on <span className="font-semibold">{searchData.date ? format(new Date(`${searchData.date}T00:00:00`), "EEEE, MMMM d, yyyy") : "your selected date"}</span>.</span>
<div className="card max-w-lg mx-auto text-center py-10 px-6 mb-8">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-5">
<Calendar className="w-8 h-8 text-red-500 dark:text-red-400" />
</div>
<button onClick={() => router.push(buildSearchUrl())} className="text-sm font-semibold text-primary hover:underline flex-shrink-0">
Change date
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
No trains available
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
There are no trains scheduled on{" "}
<span className="font-semibold text-gray-900 dark:text-gray-100">
{searchData.date ? format(new Date(`${searchData.date}T00:00:00`), "EEEE, MMMM d, yyyy") : "your selected date"}
</span>
. Try a different date to see available trains.
</p>
<button
onClick={() => router.push(buildSearchUrl())}
className="btn-primary inline-flex items-center gap-2"
>
<Calendar className="w-4 h-4" />
Change Date
</button>
</div>
{/* Alternative Travel Options — commented out for the time being;
only the "No trains available" banner above is shown.
{hasAlternatives && (
<div>
<div className="mb-4">
@@ -1100,6 +1113,7 @@ export default function ResultsPage() {
</div>
</div>
)}
*/}
</div>
</div>
</div>
@@ -1233,30 +1247,32 @@ export default function ResultsPage() {
renderScheduleCard(schedule, true),
)}
</div>
{outboundSchedules.length === 0 &&
alternativeOutbound.length > 0 && (
<div className="mt-6">
<div className="flex items-center justify-between gap-4 px-4 py-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 mb-6">
<div className="flex items-center gap-2.5 text-sm text-red-800 dark:text-red-300">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>No trains on <span className="font-semibold">{searchData.date ? format(new Date(`${searchData.date}T00:00:00`), "EEEE, MMMM d") : "your selected date"}</span>.</span>
</div>
<button onClick={() => router.push(buildSearchUrl())} className="text-sm font-semibold text-primary hover:underline flex-shrink-0">
Change dates
</button>
</div>
<div className="mb-3">
<h3 className="text-base font-bold text-gray-900 dark:text-white">
Alternative Outbound Options
</h3>
</div>
<div className="space-y-4">
{alternativeOutbound.map((schedule: Schedule) =>
renderScheduleCard(schedule, true, true),
)}
{outboundSchedules.length === 0 && (
<div className="mt-6">
<div className="flex items-center justify-between gap-4 px-4 py-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-center gap-2.5 text-sm text-red-800 dark:text-red-300">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>No trains on <span className="font-semibold">{searchData.date ? format(new Date(`${searchData.date}T00:00:00`), "EEEE, MMMM d") : "your selected date"}</span>.</span>
</div>
<button onClick={() => router.push(buildSearchUrl())} className="text-sm font-semibold text-primary hover:underline flex-shrink-0">
Change dates
</button>
</div>
)}
{/* Alternative Outbound Options — commented out for the time being;
only the "No trains" banner above is shown.
<div className="mb-3">
<h3 className="text-base font-bold text-gray-900 dark:text-white">
Alternative Outbound Options
</h3>
</div>
<div className="space-y-4">
{alternativeOutbound.map((schedule: Schedule) =>
renderScheduleCard(schedule, true, true),
)}
</div>
*/}
</div>
)}
</div>
) : (
<div>
@@ -1317,30 +1333,32 @@ export default function ResultsPage() {
renderScheduleCard(schedule, false),
)}
</div>
{inboundSchedules.length === 0 &&
alternativeInbound.length > 0 && (
<div className="mt-6">
<div className="flex items-center justify-between gap-4 px-4 py-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 mb-6">
<div className="flex items-center gap-2.5 text-sm text-red-800 dark:text-red-300">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>No trains on <span className="font-semibold">{searchData.returnDate ? format(new Date(`${searchData.returnDate}T00:00:00`), "EEEE, MMMM d") : "your selected return date"}</span>.</span>
</div>
<button onClick={() => router.push(buildSearchUrl())} className="text-sm font-semibold text-primary hover:underline flex-shrink-0">
Change dates
</button>
</div>
<div className="mb-3">
<h3 className="text-base font-bold text-gray-900 dark:text-white">
Alternative Return Options
</h3>
</div>
<div className="space-y-4">
{alternativeInbound.map((schedule: Schedule) =>
renderScheduleCard(schedule, false, true),
)}
{inboundSchedules.length === 0 && (
<div className="mt-6">
<div className="flex items-center justify-between gap-4 px-4 py-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-center gap-2.5 text-sm text-red-800 dark:text-red-300">
<Calendar className="w-4 h-4 flex-shrink-0" />
<span>No trains on <span className="font-semibold">{searchData.returnDate ? format(new Date(`${searchData.returnDate}T00:00:00`), "EEEE, MMMM d") : "your selected return date"}</span>.</span>
</div>
<button onClick={() => router.push(buildSearchUrl())} className="text-sm font-semibold text-primary hover:underline flex-shrink-0">
Change dates
</button>
</div>
)}
{/* Alternative Return Options — commented out for the time being;
only the "No trains" banner above is shown.
<div className="mb-3">
<h3 className="text-base font-bold text-gray-900 dark:text-white">
Alternative Return Options
</h3>
</div>
<div className="space-y-4">
{alternativeInbound.map((schedule: Schedule) =>
renderScheduleCard(schedule, false, true),
)}
</div>
*/}
</div>
)}
</div>
)
) : (

View File

@@ -52,6 +52,7 @@ export default function ReviewPage() {
const [timeLeft, setTimeLeft] = useState<string>('');
const [seatDetails, setSeatDetails] = useState<Record<string, string>>({});
const [fareBreakdown, setFareBreakdown] = useState<any>(null);
const [returnFareBreakdown, setReturnFareBreakdown] = useState<any>(null);
const [computedTotal, setComputedTotal] = useState<number>(0);
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
@@ -173,16 +174,27 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p =>
// Prefers displayFareMinor (converted) from the fare breakdown API when available.
// Falls back to raw ETB seat fares (which are always in minor units).
const getPassengerSeatFare = (p: any, index?: number): number | null => {
if (isRoundTrip) {
// Seat-specific fares (set during seat selection) cover each leg separately — use them first.
if (p.outboundSeatFareMinor != null || p.inboundSeatFareMinor != null) {
if (isPackageBooking) return (p.outboundSeatFareMinor ?? 0) * 2;
return (p.outboundSeatFareMinor ?? 0) + (p.inboundSeatFareMinor ?? 0);
}
// No seat-specific fares: fall back to the per-leg fare-breakdown totals for both legs.
if (!isPackageBooking && fareBreakdown?.passengers && returnFareBreakdown?.passengers && index != null) {
const obLine = fareBreakdown.passengers[index];
const retLine = returnFareBreakdown.passengers[index];
const obFare = obLine?.displayFareMinor ?? obLine?.fareMinor;
const retFare = retLine?.displayFareMinor ?? retLine?.fareMinor;
if (obFare != null && retFare != null) return obFare + retFare;
}
return null;
}
if (!isPackageBooking && fareBreakdown?.passengers && index != null) {
const line = fareBreakdown.passengers[index];
const displayFare = line?.displayFareMinor ?? line?.fareMinor;
if (displayFare != null) return displayFare;
}
if (isRoundTrip) {
if (p.outboundSeatFareMinor == null && p.inboundSeatFareMinor == null) return null;
if (isPackageBooking) return (p.outboundSeatFareMinor ?? 0) * 2;
return (p.outboundSeatFareMinor ?? 0) + (p.inboundSeatFareMinor ?? 0);
}
if (p.seatFareMinor == null) return null;
return isPackageBooking ? p.seatFareMinor * 2 : p.seatFareMinor;
};
@@ -541,6 +553,22 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p =>
const result: any = await apiClient.get(`/search/fare-breakdown?${params}`);
setFareBreakdown(result);
// For round-trips, also fetch the return leg's fare breakdown so the review page
// can display and send the correct combined total (outbound + return per passenger).
if (isRoundTrip && inboundSchedule) {
const returnScheduleId = (inboundSchedule as any).id;
const returnParams = new URLSearchParams({
scheduleId: returnScheduleId,
originStationId: searchCriteria.destinationStationId,
destinationStationId: searchCriteria.originStationId,
passengers: passengersParam,
displayCurrency: displayCurrencyCode,
...(searchCriteria.promoCode ? { promoCode: searchCriteria.promoCode } : {}),
});
const returnResult: any = await apiClient.get(`/search/fare-breakdown?${returnParams}`);
setReturnFareBreakdown(returnResult);
}
} catch (err) {
}
})();
@@ -566,7 +594,11 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p =>
const isFreeChild = line?.isFree ?? (isChildPassenger && isFirstChild(passengers, i));
if (isFreeChild) return sum;
const seatFare = getPassengerSeatFare(p, i);
const displayFare = line?.displayFareMinor ?? line?.fareMinor;
// For round-trips the fallback must combine both legs; for one-way it's the single-leg fare.
const obFare = line?.displayFareMinor ?? line?.fareMinor;
const retLine = returnFareBreakdown?.passengers?.[i];
const retFare = retLine?.displayFareMinor ?? retLine?.fareMinor;
const displayFare = isRoundTrip && retFare != null ? (obFare ?? 0) + retFare : obFare;
return sum + (seatFare ?? displayFare ?? 0);
}, 0);
@@ -586,26 +618,27 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p =>
? isPkgFreeChild(i)
: (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i)));
// Per-leg fares for round trips — use converted amounts from fareBreakdown when available
const displayFare = line?.displayFareMinor ?? line?.fareMinor;
// Per-leg fares for round trips — prefer seat-specific fares, then per-leg breakdowns.
const retLine = returnFareBreakdown?.passengers?.[i];
const obBreakdownFare = line?.displayFareMinor ?? line?.fareMinor;
const retBreakdownFare = retLine?.displayFareMinor ?? retLine?.fareMinor;
const outboundFare: number | null = isRoundTrip
? (isPackageBooking
? (packageTierPriceMinor ?? null)
: (displayFare != null
? Math.round(displayFare / 2)
: ((p as any).outboundSeatFareMinor ?? null)))
: ((p as any).outboundSeatFareMinor ?? obBreakdownFare ?? null))
: null;
const inboundFare: number | null = isRoundTrip
? (isPackageBooking
? (packageTierPriceMinor ?? null)
: (displayFare != null
? Math.round(displayFare / 2)
: ((p as any).inboundSeatFareMinor ?? null)))
: ((p as any).inboundSeatFareMinor ?? retBreakdownFare ?? null))
: null;
const seatFare = getPassengerSeatFare(p, i);
const combinedDisplayFare = isRoundTrip && retBreakdownFare != null
? (obBreakdownFare ?? 0) + retBreakdownFare
: obBreakdownFare;
const passengerTotal = isPackageBooking
? (isFreeChild ? 0 : (seatFare ?? (isChildPassenger ? pkgChildFare : pkgAdultFare)))
: (isFreeChild ? 0 : (seatFare ?? displayFare ?? 0));
: (isFreeChild ? 0 : (seatFare ?? combinedDisplayFare ?? 0));
return (
<div key={i} className="border-b border-gray-100 dark:border-gray-800 pb-2 last:border-0">

View File

@@ -52,7 +52,7 @@ const BedCard = memo(({ bed, isSelected, isAssignedToOther, onToggle }: any) =>
? "bg-purple-50 border-purple-300 cursor-not-allowed dark:bg-purple-900/20 dark:border-purple-700"
: bed.status === "AVAILABLE"
? "bg-green-50 border-green-300 hover:bg-green-100 hover:border-green-400 dark:bg-green-900/20 dark:border-green-700"
: bed.status === "BOOKED"
: bed.status === "BOOKED" || bed.status === "BLOCKED"
? "bg-red-50 border-red-300 cursor-not-allowed dark:bg-red-900/20 dark:border-red-700"
: "bg-gray-100 border-gray-300 cursor-not-allowed dark:bg-gray-800 dark:border-gray-700"
}`}