Merge pull request #779 from Tria-plc/alpha

Package pricing issue resolution, passenger report updates
This commit is contained in:
Stephanos A.
2026-07-17 23:51:49 +03:00
committed by GitHub
3 changed files with 12 additions and 14 deletions

View File

@@ -18,8 +18,8 @@ export class ReportsController {
return this.service.generateReport(dto); return this.service.generateReport(dto);
} }
@Get('occupancy') @Get('passengers')
@ApiOperation({ summary: 'Occupancy report for a specific schedule' }) @ApiOperation({ summary: 'Passengers report for a specific schedule' })
getOccupancyReport(@Query('scheduleId') scheduleId: string) { getOccupancyReport(@Query('scheduleId') scheduleId: string) {
return this.service.getOccupancyBySchedule(scheduleId); return this.service.getOccupancyBySchedule(scheduleId);
} }

View File

@@ -949,10 +949,9 @@ export default function SeatsPage() {
return; return;
} }
// No fare change — but for package bookings still sync the tier price to the // No fare change — for package bookings still sync the tier price to the
// actual berth fare (handles the case where the first seat picked matches the // actual berth fare so the correct amount is always stored.
// stored price but we still want it explicitly confirmed). if (isPackageBooking && packageId) {
if (isPackageBooking && packageId && newFare !== packageTierPriceMinor) {
setPackageContext( setPackageContext(
packageId, packageId,
priceTierId ?? '', priceTierId ?? '',
@@ -962,6 +961,8 @@ export default function SeatsPage() {
packageDepartureStationName ?? undefined, packageDepartureStationName ?? undefined,
); );
} }
} else if (isPackageBooking && packageId) {
// newFare is null (seat class data unavailable) — leave stored price as-is.
} }
commitSeatAssignment(seatId); commitSeatAssignment(seatId);

View File

@@ -401,7 +401,6 @@ const PKG_CHILDREN_PER_ADULT = 5;
function PassengerCountModal({ function PassengerCountModal({
tier, tier,
minPriceMinor,
onClose, onClose,
onConfirm, onConfirm,
loading, loading,
@@ -410,7 +409,6 @@ function PassengerCountModal({
stations, stations,
}: { }: {
tier: PriceTier; tier: PriceTier;
minPriceMinor: number;
onClose: () => void; onClose: () => void;
onConfirm: (adultCount: number, childCount: number, departureStationId: string, departureStationName: string) => void; onConfirm: (adultCount: number, childCount: number, departureStationId: string, departureStationName: string) => void;
loading: boolean; loading: boolean;
@@ -423,11 +421,9 @@ function PassengerCountModal({
const [departureStationId, setDepartureStationId] = useState(''); const [departureStationId, setDepartureStationId] = useState('');
const [showStationError, setShowStationError] = useState(false); const [showStationError, setShowStationError] = useState(false);
const remaining = tier.availableSeats - tier.bookedSeats; const remaining = tier.availableSeats - tier.bookedSeats;
// First child per adult travels free (no seat); additional children pay full adult fare
const freeChildren = Math.min(childCount, adultCount); const freeChildren = Math.min(childCount, adultCount);
const paidChildren = Math.max(0, childCount - adultCount); const paidChildren = Math.max(0, childCount - adultCount);
// Only paid children need seats; free children share with an adult const totalMinor = (adultCount * tier.priceMinor + paidChildren * tier.priceMinor) * priceMultiplier;
const totalMinor = (adultCount * minPriceMinor + paidChildren * minPriceMinor) * priceMultiplier;
return ( return (
<> <>
@@ -444,7 +440,7 @@ function PassengerCountModal({
<div className="px-6 py-3 bg-primary/5 border-b border-primary/10"> <div className="px-6 py-3 bg-primary/5 border-b border-primary/10">
<p className="text-xs text-gray-400 uppercase tracking-wide font-semibold">Coach type</p> <p className="text-xs text-gray-400 uppercase tracking-wide font-semibold">Coach type</p>
<p className="text-sm font-bold text-gray-900 dark:text-white mt-0.5">{tier.seatClass?.coachType?.name ?? tier.label.trim()}</p> <p className="text-sm font-bold text-gray-900 dark:text-white mt-0.5">{tier.seatClass?.coachType?.name ?? tier.label.trim()}</p>
<p className="text-xs text-gray-400 mt-0.5">{remaining} seats remaining · from {formatPrice(minPriceMinor * priceMultiplier, tier.currency)} per adult · 1st child per adult free (no seat)</p> <p className="text-xs text-gray-400 mt-0.5">{remaining} seats remaining · from {formatPrice(tier.priceMinor * priceMultiplier, tier.currency)} per adult · 1st child per adult free (no seat)</p>
</div> </div>
<div className="px-6 py-5 space-y-4"> <div className="px-6 py-5 space-y-4">
@@ -646,6 +642,8 @@ export default function PackageDetailPage() {
); );
// Store per-adult tier price (×1 leg); review page applies round-trip multiplier and child pricing // Store per-adult tier price (×1 leg); review page applies round-trip multiplier and child pricing
// Store the group's cheapest tier price as a placeholder — the actual berth fare
// (Upper/Middle/Lower) will be resolved and saved when the user picks a seat.
setPackageContext(id, representativeTier.id, representativeTier.priceMinor, pkg.name, departureStationId, departureStationName); setPackageContext(id, representativeTier.id, representativeTier.priceMinor, pkg.name, departureStationId, departureStationName);
router.push("/booking/passengers"); router.push("/booking/passengers");
@@ -696,7 +694,6 @@ export default function PackageDetailPage() {
{passengerModalOpen && representativeTier && ( {passengerModalOpen && representativeTier && (
<PassengerCountModal <PassengerCountModal
tier={representativeTier} tier={representativeTier}
minPriceMinor={selectedGroup?.minPrice ?? representativeTier.priceMinor}
onClose={() => { setPassengerModalOpen(false); setBookingContextError(null); }} onClose={() => { setPassengerModalOpen(false); setBookingContextError(null); }}
onConfirm={handleBookNow} onConfirm={handleBookNow}
loading={bookingContextLoading} loading={bookingContextLoading}
@@ -896,7 +893,7 @@ export default function PackageDetailPage() {
{/* ── Sidebar — desktop ── */} {/* ── Sidebar — desktop ── */}
<div className="hidden lg:block"> <div className="hidden lg:block">
<div className="sticky top-20"> <div>
<PriceTiersPanel <PriceTiersPanel
tiers={pkg.priceTiers} tiers={pkg.priceTiers}
onBookNow={(coachTypeId) => { setSelectedCoachTypeId(coachTypeId); setPassengerModalOpen(true); }} onBookNow={(coachTypeId) => { setSelectedCoachTypeId(coachTypeId); setPassengerModalOpen(true); }}