mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
Seat price related updates
This commit is contained in:
@@ -102,7 +102,7 @@ export default function ConfirmationPage() {
|
|||||||
const { packageTierPriceMinor } = useBookingStore.getState();
|
const { packageTierPriceMinor } = useBookingStore.getState();
|
||||||
const isPackageBooking = packageTierPriceMinor != null;
|
const isPackageBooking = packageTierPriceMinor != null;
|
||||||
const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length;
|
const adultCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length;
|
||||||
const pkgMultiplier = isPackageBooking && isRoundTrip ? 2 : 1;
|
const pkgMultiplier = isPackageBooking ? 2 : 1;
|
||||||
const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgMultiplier : 0;
|
const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgMultiplier : 0;
|
||||||
const pkgChildFare = pkgAdultFare;
|
const pkgChildFare = pkgAdultFare;
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,31 @@ export default function ReviewPage() {
|
|||||||
fetchSeatDetails();
|
fetchSeatDetails();
|
||||||
}, [selectedSchedule?.id, outboundSchedule?.id, inboundSchedule?.id, passengers, isRoundTrip]);
|
}, [selectedSchedule?.id, outboundSchedule?.id, inboundSchedule?.id, passengers, isRoundTrip]);
|
||||||
|
|
||||||
|
const isPackageBooking = packageTierPriceMinor !== null || !!packageName;
|
||||||
|
const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length;
|
||||||
|
const pkgAdultFare = isPackageBooking && packageTierPriceMinor != null ? packageTierPriceMinor * 2 : 0;
|
||||||
|
const pkgChildFare = pkgAdultFare;
|
||||||
|
|
||||||
|
const isPackageChild = (index: number) =>
|
||||||
|
isPackageBooking ? index >= adultPassengerCount : isChild(passengers[index]);
|
||||||
|
|
||||||
|
const isPkgFreeChild = (index: number) => {
|
||||||
|
if (!isPackageBooking) return false;
|
||||||
|
if (!isPackageChild(index)) return false;
|
||||||
|
const childIndex = index - adultPassengerCount;
|
||||||
|
return childIndex < adultPassengerCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPassengerSeatFare = (p: any): number | null => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
const createBookingMutation = useMutation({
|
const createBookingMutation = useMutation({
|
||||||
mutationFn: (data: any) => {
|
mutationFn: (data: any) => {
|
||||||
const endpoint = isAuthenticated ? '/bookings' : '/bookings/guest';
|
const endpoint = isAuthenticated ? '/bookings' : '/bookings/guest';
|
||||||
@@ -393,8 +418,9 @@ export default function ReviewPage() {
|
|||||||
? (isChildPassenger && (i - adultPassengerCount) < adultPassengerCount)
|
? (isChildPassenger && (i - adultPassengerCount) < adultPassengerCount)
|
||||||
: (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i)));
|
: (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i)));
|
||||||
const seatFare = getPassengerSeatFare(p);
|
const seatFare = getPassengerSeatFare(p);
|
||||||
|
const pkgFallback = isChildPassenger ? pkgChildFare : pkgAdultFare;
|
||||||
const fareMinor = isPackageBooking
|
const fareMinor = isPackageBooking
|
||||||
? (isFreeChild ? 0 : (isChildPassenger ? pkgChildFare : pkgAdultFare))
|
? (isFreeChild ? 0 : (seatFare ?? pkgFallback))
|
||||||
: (isFreeChild ? 0 : (seatFare ?? line?.fareMinor ?? 0));
|
: (isFreeChild ? 0 : (seatFare ?? line?.fareMinor ?? 0));
|
||||||
return { fareMinor, isFree: isFreeChild };
|
return { fareMinor, isFree: isFreeChild };
|
||||||
});
|
});
|
||||||
@@ -423,7 +449,7 @@ export default function ReviewPage() {
|
|||||||
|
|
||||||
const fetchFareBreakdown = useCallback(async (scheduleId: string, originStationId: string, destinationStationId: string) => {
|
const fetchFareBreakdown = useCallback(async (scheduleId: string, originStationId: string, destinationStationId: string) => {
|
||||||
// Package bookings use the stored tier price — no fare calculation needed
|
// Package bookings use the stored tier price — no fare calculation needed
|
||||||
if (packageTierPriceMinor !== null) return;
|
if (isPackageBooking) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const seatClasses: any[] = await apiClient.get('/seat-classes');
|
const seatClasses: any[] = await apiClient.get('/seat-classes');
|
||||||
@@ -433,9 +459,6 @@ export default function ReviewPage() {
|
|||||||
const fallbackSeatClassId = seatClasses.find((sc: any) => sc.name === scheduleSeatClassName)?.id || seatClasses[0]?.id;
|
const fallbackSeatClassId = seatClasses.find((sc: any) => sc.name === scheduleSeatClassName)?.id || seatClasses[0]?.id;
|
||||||
if (!fallbackSeatClassId) return;
|
if (!fallbackSeatClassId) return;
|
||||||
|
|
||||||
// Bed coaches price Upper/Middle/Lower as separate classes, so a passenger's own
|
|
||||||
// assigned berth (captured on the seats page) must resolve its own seatClassId here
|
|
||||||
// — a single shared class can't correctly price passengers in different berths.
|
|
||||||
const resolveSeatClassId = (p: any): string => {
|
const resolveSeatClassId = (p: any): string => {
|
||||||
const bedPosition: string | undefined = isRoundTrip ? (p as any).outboundBedPosition : (p as any).bedPosition;
|
const bedPosition: string | undefined = isRoundTrip ? (p as any).outboundBedPosition : (p as any).bedPosition;
|
||||||
if (!bedPosition) return fallbackSeatClassId;
|
if (!bedPosition) return fallbackSeatClassId;
|
||||||
@@ -465,7 +488,7 @@ export default function ReviewPage() {
|
|||||||
setFareBreakdown(result);
|
setFareBreakdown(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
}
|
}
|
||||||
}, [packageTierPriceMinor, passengers, selectedSchedule, outboundSchedule, isRoundTrip, searchCriteria, displayCurrency]);
|
}, [isPackageBooking, passengers, selectedSchedule, outboundSchedule, isRoundTrip, searchCriteria, displayCurrency]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchCriteria?.originStationId || !searchCriteria?.destinationStationId) return;
|
if (!searchCriteria?.originStationId || !searchCriteria?.destinationStationId) return;
|
||||||
@@ -474,37 +497,17 @@ export default function ReviewPage() {
|
|||||||
fetchFareBreakdown(scheduleId, searchCriteria.originStationId, searchCriteria.destinationStationId);
|
fetchFareBreakdown(scheduleId, searchCriteria.originStationId, searchCriteria.destinationStationId);
|
||||||
}, [fetchFareBreakdown, isRoundTrip, outboundSchedule?.id, selectedSchedule?.id, searchCriteria?.originStationId, searchCriteria?.destinationStationId]);
|
}, [fetchFareBreakdown, isRoundTrip, outboundSchedule?.id, selectedSchedule?.id, searchCriteria?.originStationId, searchCriteria?.destinationStationId]);
|
||||||
|
|
||||||
const isPackageBooking = packageTierPriceMinor !== null;
|
|
||||||
// packageTierPriceMinor is the per-adult fare for ONE leg.
|
|
||||||
// Round-trip packages multiply by 2.
|
|
||||||
// First child per adult travels FREE; additional children pay full adult fare.
|
|
||||||
const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => !isChild(p)).length;
|
|
||||||
const childPassengerCount = searchCriteria?.childCount ?? passengers.filter(p => isChild(p)).length;
|
|
||||||
const pkgRoundTripMultiplier = isPackageBooking && isRoundTrip ? 2 : 1;
|
|
||||||
const pkgAdultFare = isPackageBooking ? packageTierPriceMinor! * pkgRoundTripMultiplier : 0;
|
|
||||||
const pkgPaidChildrenCount = Math.max(0, childPassengerCount - adultPassengerCount);
|
|
||||||
// Paid children pay full adult fare
|
|
||||||
const pkgChildFare = pkgAdultFare; // full fare for paid children
|
|
||||||
|
|
||||||
// For package bookings, passengers are initialized without dateOfBirth so isChild() is
|
// For package bookings, passengers are initialized without dateOfBirth so isChild() is
|
||||||
// unreliable. Use the stored adultCount from searchCriteria to determine category by index.
|
// unreliable. Use the stored adultCount from searchCriteria to determine category by index.
|
||||||
const isPackageChild = (index: number) =>
|
|
||||||
isPackageBooking ? index >= adultPassengerCount : isChild(passengers[index]);
|
|
||||||
|
|
||||||
// Per-seat fare captured on the seats page (bed-position-aware, computed locally from
|
|
||||||
// the schedule's own coachTypes/classes) is guaranteed correct for berths, unlike the
|
|
||||||
// backend /search/fare-breakdown call whose seatClassId matching for bed positions can't
|
|
||||||
// be verified here. Prefer it whenever the passenger actually has an assigned seat.
|
|
||||||
const getPassengerSeatFare = (p: any): number | null => {
|
|
||||||
if (isRoundTrip) {
|
|
||||||
if (p.outboundSeatFareMinor == null && p.inboundSeatFareMinor == null) return null;
|
|
||||||
return (p.outboundSeatFareMinor ?? 0) + (p.inboundSeatFareMinor ?? 0);
|
|
||||||
}
|
|
||||||
return p.seatFareMinor ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = isPackageBooking
|
const total = isPackageBooking
|
||||||
? adultPassengerCount * pkgAdultFare + pkgPaidChildrenCount * pkgChildFare
|
? passengers.reduce((sum, p, i) => {
|
||||||
|
const isChild_ = isPackageChild(i);
|
||||||
|
const isFreeChild = isChild_ && (i - adultPassengerCount) < adultPassengerCount;
|
||||||
|
if (isFreeChild) return sum;
|
||||||
|
const seatFare = getPassengerSeatFare(p);
|
||||||
|
const pkgFallback = isChild_ ? pkgChildFare : pkgAdultFare;
|
||||||
|
return sum + (seatFare ?? pkgFallback);
|
||||||
|
}, 0)
|
||||||
: passengers.reduce((sum, p, i) => {
|
: passengers.reduce((sum, p, i) => {
|
||||||
const isChildPassenger = isChild(p);
|
const isChildPassenger = isChild(p);
|
||||||
const line = fareBreakdown?.passengers?.[i];
|
const line = fareBreakdown?.passengers?.[i];
|
||||||
@@ -517,16 +520,6 @@ export default function ReviewPage() {
|
|||||||
// Keep computedTotal in sync so handleConfirm can persist it to the store
|
// Keep computedTotal in sync so handleConfirm can persist it to the store
|
||||||
useEffect(() => { setComputedTotal(total); }, [total]);
|
useEffect(() => { setComputedTotal(total); }, [total]);
|
||||||
|
|
||||||
// For package bookings, determine if a child is free (first per adult) or paid.
|
|
||||||
// Children are ordered after adults in the passengers array (set on package detail page).
|
|
||||||
const isPkgFreeChild = (index: number) => {
|
|
||||||
if (!isPackageBooking) return false;
|
|
||||||
if (!isPackageChild(index)) return false;
|
|
||||||
// childIndex = position among children (0-based)
|
|
||||||
const childIndex = index - adultPassengerCount;
|
|
||||||
return childIndex < adultPassengerCount; // first adultCount children are free
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shared fare sidebar — rendered in right column (desktop) and inline (mobile)
|
// Shared fare sidebar — rendered in right column (desktop) and inline (mobile)
|
||||||
const FareSidebar = () => (
|
const FareSidebar = () => (
|
||||||
<div className="card space-y-3">
|
<div className="card space-y-3">
|
||||||
@@ -541,7 +534,7 @@ export default function ReviewPage() {
|
|||||||
: (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i)));
|
: (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i)));
|
||||||
const seatFare = getPassengerSeatFare(p);
|
const seatFare = getPassengerSeatFare(p);
|
||||||
const passengerTotal = isPackageBooking
|
const passengerTotal = isPackageBooking
|
||||||
? (isFreeChild ? 0 : (isChildPassenger ? pkgChildFare : pkgAdultFare))
|
? (isFreeChild ? 0 : (seatFare ?? (isChildPassenger ? pkgChildFare : pkgAdultFare)))
|
||||||
: (isFreeChild ? 0 : (seatFare ?? line?.fareMinor ?? 0));
|
: (isFreeChild ? 0 : (seatFare ?? line?.fareMinor ?? 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -618,9 +611,6 @@ export default function ReviewPage() {
|
|||||||
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
|
<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" />
|
<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>
|
<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>
|
||||||
|
|
||||||
{/* Flight-style timeline */}
|
{/* Flight-style timeline */}
|
||||||
@@ -695,9 +685,6 @@ export default function ReviewPage() {
|
|||||||
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
|
<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" />
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Flight-style timeline */}
|
{/* Flight-style timeline */}
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export default function SeatsPage() {
|
|||||||
packageName,
|
packageName,
|
||||||
packageId,
|
packageId,
|
||||||
priceTierId,
|
priceTierId,
|
||||||
|
packageTierPriceMinor,
|
||||||
packageDepartureStationId,
|
packageDepartureStationId,
|
||||||
packageDepartureStationName,
|
packageDepartureStationName,
|
||||||
setPackageContext,
|
setPackageContext,
|
||||||
@@ -495,7 +496,7 @@ export default function SeatsPage() {
|
|||||||
setModalState({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: "Fare Will Change",
|
title: "Fare Will Change",
|
||||||
message: `Switching to ${coach.label} (${matchedType.coachTypeName || coach.typeName || ""}) changes the fare to ETB ${(newFare / 100 * 2).toFixed(2)} per adult (currently ETB ${(currentFare / 100 * 2).toFixed(2)}). Continue?`,
|
message: `Switching to ${coach.label} (${matchedType.coachTypeName || coach.typeName || ""}) changes the fare to ETB ${(newFare / 100 * (isRoundTrip ? 2 : 1)).toFixed(2)} per adult (currently ETB ${(currentFare / 100 * (isRoundTrip ? 2 : 1)).toFixed(2)}). Continue?`,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
confirmText: "Switch Coach",
|
confirmText: "Switch Coach",
|
||||||
@@ -510,7 +511,7 @@ export default function SeatsPage() {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: "Switch Coach Type",
|
title: "Switch Coach Type",
|
||||||
message: `Switch to ${coach.label}${coachTypeName ? ` (${coachTypeName})` : ""}?${
|
message: `Switch to ${coach.label}${coachTypeName ? ` (${coachTypeName})` : ""}?${
|
||||||
newFare != null ? ` Fare: ETB ${(newFare / 100 * 2).toFixed(2)} per adult.` : " This will have a fare change."
|
newFare != null ? ` Fare: ETB ${(newFare / 100 * (isRoundTrip ? 2 : 1)).toFixed(2)} per adult.` : " This will have a fare change."
|
||||||
}`,
|
}`,
|
||||||
type: "info",
|
type: "info",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
@@ -769,26 +770,32 @@ export default function SeatsPage() {
|
|||||||
const newSeat = validSeats?.find((s: any) => s.id === seatId);
|
const newSeat = validSeats?.find((s: any) => s.id === seatId);
|
||||||
const newFare = newSeat ? getSeatFare(newSeat) : null;
|
const newFare = newSeat ? getSeatFare(newSeat) : null;
|
||||||
|
|
||||||
// Package bookings: fare-change warning on individual seat clicks is suppressed.
|
if (newFare != null) {
|
||||||
// The only fare-change confirmation is when switching coach type via Train Coach Preview.
|
|
||||||
if (newFare != null && !isPackageBooking) {
|
|
||||||
let referenceFare: number | null = null;
|
let referenceFare: number | null = null;
|
||||||
let referenceLabel = "the fare you originally selected";
|
let referenceLabel = "the fare you originally selected";
|
||||||
|
|
||||||
if (originalFareForCurrentLeg != null && originalFareForCurrentLeg !== newFare) {
|
if (isPackageBooking) {
|
||||||
referenceFare = originalFareForCurrentLeg;
|
// For package bookings, compare against the stored tier price (per leg).
|
||||||
|
const pkgLegFare = packageTierPriceMinor;
|
||||||
|
if (pkgLegFare != null && newFare !== pkgLegFare) {
|
||||||
|
referenceFare = pkgLegFare;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const differingEntry = Object.entries(passengerSeatMap).find(([idx, sid]) => {
|
if (originalFareForCurrentLeg != null && originalFareForCurrentLeg !== newFare) {
|
||||||
if (Number(idx) === activePassengerIndex) return false;
|
referenceFare = originalFareForCurrentLeg;
|
||||||
const otherSeat = validSeats?.find((s: any) => s.id === sid);
|
} else {
|
||||||
const otherFare = otherSeat ? getSeatFare(otherSeat) : null;
|
const differingEntry = Object.entries(passengerSeatMap).find(([idx, sid]) => {
|
||||||
return otherFare != null && otherFare !== newFare;
|
if (Number(idx) === activePassengerIndex) return false;
|
||||||
});
|
const otherSeat = validSeats?.find((s: any) => s.id === sid);
|
||||||
|
const otherFare = otherSeat ? getSeatFare(otherSeat) : null;
|
||||||
|
return otherFare != null && otherFare !== newFare;
|
||||||
|
});
|
||||||
|
|
||||||
if (differingEntry) {
|
if (differingEntry) {
|
||||||
const otherSeat = validSeats?.find((s: any) => s.id === differingEntry[1]);
|
const otherSeat = validSeats?.find((s: any) => s.id === differingEntry[1]);
|
||||||
referenceFare = otherSeat ? getSeatFare(otherSeat) : null;
|
referenceFare = otherSeat ? getSeatFare(otherSeat) : null;
|
||||||
referenceLabel = "another already-selected seat";
|
referenceLabel = "another already-selected seat";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,23 +804,52 @@ export default function SeatsPage() {
|
|||||||
const positionLabel = newSeat?.bedPosition
|
const positionLabel = newSeat?.bedPosition
|
||||||
? `${newSeat.bedPosition.charAt(0).toUpperCase()}${newSeat.bedPosition.slice(1)} berth`
|
? `${newSeat.bedPosition.charAt(0).toUpperCase()}${newSeat.bedPosition.slice(1)} berth`
|
||||||
: "This seat";
|
: "This seat";
|
||||||
|
const legMultiplier = isRoundTrip ? 2 : 1;
|
||||||
|
|
||||||
setModalState({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: "Fare Will Change",
|
title: "Fare Will Change",
|
||||||
message: `${positionLabel} ${seatLabel} costs ETB ${(newFare / 100 * 2).toFixed(2)}, different from ${referenceLabel}. Continue with this selection?`,
|
message: `${positionLabel} ${seatLabel} costs ETB ${(newFare / 100 * legMultiplier).toFixed(2)}, different from ${referenceLabel} (ETB ${(referenceFare / 100 * legMultiplier).toFixed(2)}). Continue with this selection?`,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
confirmText: "Continue",
|
confirmText: "Continue",
|
||||||
onConfirm: () => commitSeatAssignment(seatId),
|
onConfirm: () => {
|
||||||
|
commitSeatAssignment(seatId);
|
||||||
|
// For package bookings, sync the stored tier price to the selected berth fare
|
||||||
|
// so review/payment/confirmation pages use the correct amount.
|
||||||
|
if (isPackageBooking && packageId) {
|
||||||
|
setPackageContext(
|
||||||
|
packageId,
|
||||||
|
priceTierId ?? '',
|
||||||
|
newFare,
|
||||||
|
packageName ?? undefined,
|
||||||
|
packageDepartureStationId ?? undefined,
|
||||||
|
packageDepartureStationName ?? undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No fare change — but for package bookings still sync the tier price to the
|
||||||
|
// actual berth fare (handles the case where the first seat picked matches the
|
||||||
|
// stored price but we still want it explicitly confirmed).
|
||||||
|
if (isPackageBooking && packageId && newFare !== packageTierPriceMinor) {
|
||||||
|
setPackageContext(
|
||||||
|
packageId,
|
||||||
|
priceTierId ?? '',
|
||||||
|
newFare,
|
||||||
|
packageName ?? undefined,
|
||||||
|
packageDepartureStationId ?? undefined,
|
||||||
|
packageDepartureStationName ?? undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commitSeatAssignment(seatId);
|
commitSeatAssignment(seatId);
|
||||||
},
|
},
|
||||||
[passengerSeatMap, activePassengerIndex, validSeats, getSeatFare, originalFareForCurrentLeg, commitSeatAssignment],
|
[passengerSeatMap, activePassengerIndex, validSeats, getSeatFare, originalFareForCurrentLeg, commitSeatAssignment, isPackageBooking, packageTierPriceMinor, packageId, priceTierId, packageName, packageDepartureStationId, packageDepartureStationName, setPackageContext, isRoundTrip],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allSeatsAssigned =
|
const allSeatsAssigned =
|
||||||
@@ -1034,6 +1070,26 @@ export default function SeatsPage() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
setPassengers(updatedPassengers);
|
setPassengers(updatedPassengers);
|
||||||
|
|
||||||
|
// For one-way package bookings, sync the stored tier price with the actual berth
|
||||||
|
// fare so review/payment/confirmation pages reflect the correct amount.
|
||||||
|
if (!isRoundTrip && isPackageBooking && packageId) {
|
||||||
|
const firstEligibleIdx = seatEligibleIndices[0];
|
||||||
|
const firstSeatData = firstEligibleIdx != null
|
||||||
|
? validSeats?.find((s: any) => s.id === seatIds[firstEligibleIdx])
|
||||||
|
: null;
|
||||||
|
const berthFare = firstSeatData ? getSeatFare(firstSeatData) : null;
|
||||||
|
if (berthFare != null) {
|
||||||
|
setPackageContext(
|
||||||
|
packageId,
|
||||||
|
priceTierId ?? '',
|
||||||
|
berthFare,
|
||||||
|
packageName ?? undefined,
|
||||||
|
packageDepartureStationId ?? undefined,
|
||||||
|
packageDepartureStationName ?? undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setModalState({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@@ -1678,6 +1734,9 @@ export default function SeatsPage() {
|
|||||||
? validSeats?.find((s: any) => s.id === assignedSeatId)
|
? validSeats?.find((s: any) => s.id === assignedSeatId)
|
||||||
: null;
|
: null;
|
||||||
const seatLabel = assignedSeat ? buildSeatLabel(assignedSeat) : "";
|
const seatLabel = assignedSeat ? buildSeatLabel(assignedSeat) : "";
|
||||||
|
const seatFare = assignedSeat
|
||||||
|
? (getSeatFare(assignedSeat) ?? (isPackageBooking ? packageTierPriceMinor ?? null : null))
|
||||||
|
: isPackageBooking && assignedSeatId ? (packageTierPriceMinor ?? null) : null;
|
||||||
const isActive = i === activePassengerIndex;
|
const isActive = i === activePassengerIndex;
|
||||||
const isClickable = i <= maxSelectableIndex;
|
const isClickable = i <= maxSelectableIndex;
|
||||||
return (
|
return (
|
||||||
@@ -1719,13 +1778,20 @@ export default function SeatsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<div className="flex flex-col items-end flex-shrink-0">
|
||||||
className={`text-sm font-semibold flex-shrink-0 ${
|
<span
|
||||||
assignedSeat ? "text-[rgb(20,113,76)]" : "text-gray-400"
|
className={`text-sm font-semibold ${
|
||||||
}`}
|
assignedSeat ? "text-[rgb(20,113,76)]" : "text-gray-400"
|
||||||
>
|
}`}
|
||||||
{assignedSeat ? `Seat ${seatLabel}` : "Not Assigned"}
|
>
|
||||||
</span>
|
{assignedSeat ? `Seat ${seatLabel}` : "Not Assigned"}
|
||||||
|
</span>
|
||||||
|
{assignedSeat && seatFare != null && (
|
||||||
|
<span className="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
ETB {(seatFare / 100 * (isPackageBooking ? 2 : 1)).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export default function PackageDetailPage() {
|
|||||||
coachTypeId: g.coachTypeId,
|
coachTypeId: g.coachTypeId,
|
||||||
coachTypeName: g.coachTypeName,
|
coachTypeName: g.coachTypeName,
|
||||||
coachTypeCode: g.coachTypeCode,
|
coachTypeCode: g.coachTypeCode,
|
||||||
classes: [{ name: g.coachTypeName, baseFareMinor: g.minPrice }],
|
classes: g.tiers.map((t) => ({ name: t.label, baseFareMinor: t.priceMinor })),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user