mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
|
import { usePaymentStore } from '@/lib/payment-store';
|
||||||
|
import { consumeManageBookingPaymentReturn } from '@/utils/manage-booking-return';
|
||||||
|
import { useEffect, useState, Suspense } from 'react';
|
||||||
|
import { XCircle, Loader2, ChevronLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
function DmoneyFailureContent() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { updateStatus } = usePaymentStore();
|
||||||
|
// A Manage Booking payment (paying for an already-existing booking) has no in-progress
|
||||||
|
// booking-store session to go "back to review" from — send it back to that booking's
|
||||||
|
// detail view instead, where the user can pick a different payment method.
|
||||||
|
const [backTarget, setBackTarget] = useState('/booking/review');
|
||||||
|
|
||||||
|
// D-Money callback query params (mirrors Telebirr)
|
||||||
|
const merchantOrderId = searchParams.get('merchantOrderId') || '';
|
||||||
|
const trxRef = searchParams.get('trxRef') || searchParams.get('outTradeNo') || '';
|
||||||
|
const resultCode = searchParams.get('resultCode') || searchParams.get('code') || '';
|
||||||
|
const resultMsg = searchParams.get('resultMsg') || searchParams.get('message') || 'Payment was not completed.';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const manageBookingRef = consumeManageBookingPaymentReturn();
|
||||||
|
if (manageBookingRef) {
|
||||||
|
setBackTarget(`/booking/detail?ref=${encodeURIComponent(manageBookingRef)}`);
|
||||||
|
}
|
||||||
|
updateStatus('FAILED');
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
|
||||||
|
<XCircle className="w-14 h-14 text-red-500 mx-auto mb-4" />
|
||||||
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Payment Failed</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{resultMsg}</p>
|
||||||
|
{resultCode && <p className="text-xs text-gray-400 mb-1">Code: {resultCode}</p>}
|
||||||
|
{merchantOrderId && <p className="text-xs text-gray-400 mb-1">Order ID: {merchantOrderId}</p>}
|
||||||
|
{trxRef && <p className="text-xs text-gray-400 mb-4">Ref: {trxRef}</p>}
|
||||||
|
<div className="flex flex-col gap-3 mt-4">
|
||||||
|
<button onClick={() => router.push(backTarget)}
|
||||||
|
className="btn-secondary w-full flex items-center justify-center gap-2">
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DmoneyFailurePage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center"><Loader2 className="w-10 h-10 animate-spin text-primary" /></div>}>
|
||||||
|
<DmoneyFailureContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -379,36 +379,50 @@ export default function ResultsPage() {
|
|||||||
const coachCurrency = "ETB";
|
const coachCurrency = "ETB";
|
||||||
const CoachIcon = getCoachIcon(coachType.coachTypeName);
|
const CoachIcon = getCoachIcon(coachType.coachTypeName);
|
||||||
|
|
||||||
|
const selectThisCoach = () =>
|
||||||
|
handleSelectCoachType(
|
||||||
|
scheduleId,
|
||||||
|
coachType.coachTypeId,
|
||||||
|
coachType.coachTypeCode,
|
||||||
|
coachType.coachTypeName,
|
||||||
|
coachType.classes?.[0]?.name ||
|
||||||
|
coachType.coachTypeName,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={coachType.coachId}
|
key={coachType.coachId}
|
||||||
onClick={() =>
|
role="button"
|
||||||
handleSelectCoachType(
|
tabIndex={0}
|
||||||
scheduleId,
|
onClick={selectThisCoach}
|
||||||
coachType.coachTypeId,
|
onKeyDown={(e) => {
|
||||||
coachType.coachTypeCode,
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
coachType.coachTypeName,
|
e.preventDefault();
|
||||||
coachType.classes?.[0]?.name ||
|
selectThisCoach();
|
||||||
coachType.coachTypeName,
|
}
|
||||||
)
|
}}
|
||||||
}
|
className={`group relative w-full p-5 rounded-2xl border-2 text-left transition-all duration-200 cursor-pointer ${
|
||||||
className={`group relative w-full p-5 rounded-2xl border-2 text-left transition-all duration-200 ${
|
|
||||||
isSelected
|
isSelected
|
||||||
? "border-primary bg-gradient-to-br from-primary/8 to-primary/3 dark:from-primary/15 dark:to-primary/5 shadow-lg shadow-primary/20 scale-[1.02]"
|
? "border-primary bg-gradient-to-br from-primary/8 to-primary/3 dark:from-primary/15 dark:to-primary/5 shadow-lg shadow-primary/20 scale-[1.02]"
|
||||||
: "border-gray-200 dark:border-gray-700 hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50"
|
: "border-gray-200 dark:border-gray-700 shadow-sm hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
animation: `fade-in-up 0.3s ease-out ${index * 0.1}s both`,
|
animation: `fade-in-up 0.3s ease-out ${index * 0.1}s both`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isSelected && (
|
{/* Radio indicator — top-right, persistent (not hover-only) so the
|
||||||
<div className="absolute top-4 right-4 w-7 h-7 bg-primary rounded-full flex items-center justify-center shadow-lg animate-scale-in">
|
card's selection state is clear on touch too. */}
|
||||||
<Check
|
<span
|
||||||
className="w-4 h-4 text-white"
|
className={`absolute top-4 right-4 w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-all ${
|
||||||
strokeWidth={3}
|
isSelected
|
||||||
/>
|
? "border-primary"
|
||||||
</div>
|
: "border-gray-300 dark:border-gray-600 group-hover:border-primary/50"
|
||||||
)}
|
}`}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-primary animate-scale-in" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-start gap-4 pr-2">
|
<div className="flex items-start gap-4 pr-2">
|
||||||
@@ -474,7 +488,7 @@ export default function ResultsPage() {
|
|||||||
(cls: any, idx: number) => (
|
(cls: any, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="flex items-center justify-between py-2 px-3 rounded-lg bg-gray-50/80 dark:bg-gray-800/40 hover:bg-gray-100/80 dark:hover:bg-gray-800/60 transition-colors"
|
className="flex items-center justify-between py-2 px-3 rounded-lg bg-gray-50/80 dark:bg-gray-800/40"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<CoachIcon className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
<CoachIcon className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||||
@@ -496,8 +510,37 @@ export default function ResultsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Note — only shown while unselected; once picked, the Continue
|
||||||
|
button below takes its place. */}
|
||||||
|
{!isSelected && (
|
||||||
|
<p className="mt-4 pt-3 border-t border-gray-200/60 dark:border-gray-700/60 text-xs text-gray-400 dark:text-gray-500 italic text-center">
|
||||||
|
Click to select this coach
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Continue only appears on the card the user has actually picked —
|
||||||
|
a real nested button (the outer card is a div, not a button, so
|
||||||
|
this doesn't create invalid/ambiguous nested-button behavior). */}
|
||||||
|
{isSelected && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSelect(classModal, isOutbound);
|
||||||
|
}}
|
||||||
|
className="mt-3 w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-gradient-to-r from-[rgb(20,113,76)] to-[rgb(16,95,65)] hover:from-[rgb(16,89,60)] hover:to-[rgb(12,75,50)] text-white font-bold text-sm rounded-xl transition-all shadow-md shadow-primary/30 hover:shadow-lg active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{isRoundTrip && isOutbound
|
||||||
|
? "Continue to Return Journey"
|
||||||
|
: "Continue to Passenger Details"}
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -512,33 +555,6 @@ export default function ResultsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-5 border-t border-gray-100 dark:border-gray-800 flex-shrink-0 bg-gray-50/50 dark:bg-gray-800/30 flex justify-center">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedCoachType) {
|
|
||||||
handleSelect(classModal, isOutbound);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!selectedCoachType}
|
|
||||||
className="w-full flex items-center justify-center gap-2.5 px-6 py-3.5 bg-gradient-to-r from-[rgb(20,113,76)] to-[rgb(16,95,65)] hover:from-[rgb(16,89,60)] hover:to-[rgb(12,75,50)] text-white font-bold text-sm rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-primary/30 disabled:shadow-none hover:shadow-xl hover:scale-[1.02] active:scale-[0.98]"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{isRoundTrip && isOutbound
|
|
||||||
? "Continue to Return Journey"
|
|
||||||
: "Continue to Passenger Details"}
|
|
||||||
</span>
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
{!selectedCoachType && (
|
|
||||||
<p className="text-center text-xs text-gray-500 dark:text-gray-400 mt-3 flex items-center justify-center gap-1">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-pulse" />
|
|
||||||
Select a coach type to continue
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes drawer-slide-in{from{transform:translateX(100%)}to{transform:translateX(0)}}
|
@keyframes drawer-slide-in{from{transform:translateX(100%)}to{transform:translateX(0)}}
|
||||||
|
|||||||
@@ -315,6 +315,11 @@ export default function SeatsPage() {
|
|||||||
// first real pick (no fare-change modal) even though the map already has an entry for
|
// first real pick (no fare-change modal) even though the map already has an entry for
|
||||||
// them — only a click AFTER that (replacing their own real pick) is an actual change.
|
// them — only a click AFTER that (replacing their own real pick) is an actual change.
|
||||||
const restoredIndicesRef = useRef<Set<number>>(new Set());
|
const restoredIndicesRef = useRef<Set<number>>(new Set());
|
||||||
|
// Set by handleAutoAssign right before it updates passengerSeatMap, so the effect below
|
||||||
|
// can proceed straight to Continue once that state update actually lands — calling
|
||||||
|
// handleContinue() synchronously in the same tick would still see the pre-update
|
||||||
|
// passengerSeatMap/allSeatsAssigned from this render's closure.
|
||||||
|
const autoContinueAfterAssignRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const legKey = `${currentSchedule?.id || ''}-${currentJourneyType}`;
|
const legKey = `${currentSchedule?.id || ''}-${currentJourneyType}`;
|
||||||
if (restoredLegRef.current === legKey) return;
|
if (restoredLegRef.current === legKey) return;
|
||||||
@@ -689,10 +694,6 @@ export default function SeatsPage() {
|
|||||||
() => filteredCoaches.find((c: any) => c.id === selectedCoach),
|
() => filteredCoaches.find((c: any) => c.id === selectedCoach),
|
||||||
[filteredCoaches, selectedCoach],
|
[filteredCoaches, selectedCoach],
|
||||||
);
|
);
|
||||||
const allSeats = useMemo(
|
|
||||||
() => selectedCoachData?.seats || [],
|
|
||||||
[selectedCoachData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getBedPosition = (selectedClass: string): string | null => {
|
const getBedPosition = (selectedClass: string): string | null => {
|
||||||
const lowerClass = selectedClass.toLowerCase();
|
const lowerClass = selectedClass.toLowerCase();
|
||||||
@@ -702,59 +703,87 @@ export default function SeatsPage() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validSeats = useMemo(() => {
|
// Extracted so it can be applied to ANY coach, not just the one currently expanded —
|
||||||
// If coach has rooms, extract all beds from rooms
|
// Auto Assign needs to look across every coach of this type, not just selectedCoachData.
|
||||||
if (selectedCoachData?.rooms?.length > 0) {
|
const getValidSeatsForCoach = useCallback(
|
||||||
const allBeds: any[] = [];
|
(coachData: any): any[] => {
|
||||||
selectedCoachData.rooms.forEach((room: any) => {
|
if (!coachData) return [];
|
||||||
if (room.beds) {
|
|
||||||
allBeds.push(...room.beds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let beds = allBeds.filter((s: any) => {
|
// If coach has rooms, extract all beds from rooms
|
||||||
|
if (coachData.rooms?.length > 0) {
|
||||||
|
const allBeds: any[] = [];
|
||||||
|
coachData.rooms.forEach((room: any) => {
|
||||||
|
if (room.beds) {
|
||||||
|
allBeds.push(...room.beds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let beds = allBeds.filter((s: any) => {
|
||||||
|
const seatLabel = s.label || s.number || s.seatNumber || "";
|
||||||
|
return seatLabel && !seatLabel.startsWith("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
const isBedCoach =
|
||||||
|
coachData.seatClass?.toLowerCase().includes("bed") ||
|
||||||
|
coachData.mode?.toLowerCase().includes("bed");
|
||||||
|
|
||||||
|
if (isBedCoach && currentSchedule?.selectedSeatClass) {
|
||||||
|
const selectedBedPosition = getBedPosition(
|
||||||
|
currentSchedule.selectedSeatClass,
|
||||||
|
);
|
||||||
|
if (selectedBedPosition) {
|
||||||
|
beds = beds.filter((s: any) => s.bedPosition === selectedBedPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return beds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old seat structure
|
||||||
|
let seats = (coachData.seats || []).filter((s: any) => {
|
||||||
const seatLabel = s.label || s.number || s.seatNumber || "";
|
const seatLabel = s.label || s.number || s.seatNumber || "";
|
||||||
return seatLabel && !seatLabel.startsWith("-");
|
return seatLabel && !seatLabel.startsWith("-");
|
||||||
});
|
});
|
||||||
|
|
||||||
const isBedCoach =
|
const isBedCoach =
|
||||||
selectedCoachData?.seatClass?.toLowerCase().includes("bed") ||
|
coachData.isBedCoach === true ||
|
||||||
selectedCoachData?.mode?.toLowerCase().includes("bed");
|
seats.some((s: any) => s.bedPosition) ||
|
||||||
|
coachData.seatClass?.toLowerCase().includes("bed") ||
|
||||||
|
coachData.mode?.toLowerCase().includes("bed");
|
||||||
|
|
||||||
if (isBedCoach && currentSchedule?.selectedSeatClass) {
|
if (isBedCoach && currentSchedule?.selectedSeatClass) {
|
||||||
const selectedBedPosition = getBedPosition(
|
const selectedBedPosition = getBedPosition(
|
||||||
currentSchedule.selectedSeatClass,
|
currentSchedule.selectedSeatClass,
|
||||||
);
|
);
|
||||||
if (selectedBedPosition) {
|
if (selectedBedPosition) {
|
||||||
beds = beds.filter((s: any) => s.bedPosition === selectedBedPosition);
|
seats = seats.filter((s: any) => s.bedPosition === selectedBedPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return beds;
|
return seats;
|
||||||
}
|
},
|
||||||
|
[currentSchedule?.selectedSeatClass],
|
||||||
|
);
|
||||||
|
|
||||||
// Fallback to old seat structure
|
const validSeats = useMemo(
|
||||||
let seats = allSeats.filter((s: any) => {
|
() => getValidSeatsForCoach(selectedCoachData),
|
||||||
const seatLabel = s.label || s.number || s.seatNumber || "";
|
[getValidSeatsForCoach, selectedCoachData],
|
||||||
return seatLabel && !seatLabel.startsWith("-");
|
);
|
||||||
});
|
|
||||||
const isBedCoach =
|
|
||||||
selectedCoachData?.isBedCoach === true ||
|
|
||||||
seats.some((s: any) => s.bedPosition) ||
|
|
||||||
selectedCoachData?.seatClass?.toLowerCase().includes("bed") ||
|
|
||||||
selectedCoachData?.mode?.toLowerCase().includes("bed");
|
|
||||||
|
|
||||||
if (isBedCoach && currentSchedule?.selectedSeatClass) {
|
// Every valid seat across every coach of the current type, tagged with which coach it
|
||||||
const selectedBedPosition = getBedPosition(
|
// belongs to — lets Auto Assign search the whole train (not just the one expanded coach)
|
||||||
currentSchedule.selectedSeatClass,
|
// and lets handleContinue resolve a passenger's coach label even when their seat came
|
||||||
);
|
// from a coach they never manually expanded.
|
||||||
if (selectedBedPosition) {
|
const allCoachSeats = useMemo(
|
||||||
seats = seats.filter((s: any) => s.bedPosition === selectedBedPosition);
|
() =>
|
||||||
}
|
filteredCoaches.flatMap((c: any) =>
|
||||||
}
|
getValidSeatsForCoach(c).map((s: any) => ({
|
||||||
|
...s,
|
||||||
return seats;
|
_coachId: c.id,
|
||||||
}, [allSeats, selectedCoachData, currentSchedule?.selectedSeatClass]);
|
_coachLabel: c.label || c.name || c.coachNumber || c.number || "",
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
[filteredCoaches, getValidSeatsForCoach],
|
||||||
|
);
|
||||||
|
|
||||||
// Seats already claimed by any passenger in this journey leg, plus outbound
|
// Seats already claimed by any passenger in this journey leg, plus outbound
|
||||||
const assignedSeatIds = useMemo(
|
const assignedSeatIds = useMemo(
|
||||||
@@ -989,12 +1018,15 @@ export default function SeatsPage() {
|
|||||||
try {
|
try {
|
||||||
await ensureLegHold(seatIdsForHold);
|
await ensureLegHold(seatIdsForHold);
|
||||||
const updatedPassengers = passengers.map((p, i) => {
|
const updatedPassengers = passengers.map((p, i) => {
|
||||||
const seatData = validSeats?.find((s: any) => s.id === seatIds[i]);
|
// Look up from allCoachSeats (every coach of this type), not just the currently
|
||||||
|
// expanded one — Auto Assign can place passengers in a coach that was never
|
||||||
|
// manually expanded.
|
||||||
|
const seatData = allCoachSeats?.find((s: any) => s.id === seatIds[i]);
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
outboundSeatId: seatIds[i],
|
outboundSeatId: seatIds[i],
|
||||||
outboundSeatNumber: seatData ? buildSeatLabel(seatData) : '',
|
outboundSeatNumber: seatData ? buildSeatLabel(seatData) : '',
|
||||||
outboundCoachNumber: selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
outboundCoachNumber: seatData?._coachLabel || selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
||||||
outboundSeatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
outboundSeatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
||||||
outboundBedPosition: seatData?.bedPosition || undefined,
|
outboundBedPosition: seatData?.bedPosition || undefined,
|
||||||
};
|
};
|
||||||
@@ -1170,13 +1202,16 @@ export default function SeatsPage() {
|
|||||||
try {
|
try {
|
||||||
await ensureLegHold(seatIdsForHold);
|
await ensureLegHold(seatIdsForHold);
|
||||||
const updatedPassengers = passengers.map((p, i) => {
|
const updatedPassengers = passengers.map((p, i) => {
|
||||||
const seatData = validSeats?.find((s: any) => s.id === seatIds[i]);
|
// See the outbound branch above — resolve from allCoachSeats so a passenger whose
|
||||||
|
// seat came from a coach Auto Assign picked (but was never manually expanded)
|
||||||
|
// still gets the right coach label.
|
||||||
|
const seatData = allCoachSeats?.find((s: any) => s.id === seatIds[i]);
|
||||||
if (isRoundTrip && currentJourneyType === "inbound") {
|
if (isRoundTrip && currentJourneyType === "inbound") {
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
inboundSeatId: seatIds[i],
|
inboundSeatId: seatIds[i],
|
||||||
inboundSeatNumber: seatData ? buildSeatLabel(seatData) : '',
|
inboundSeatNumber: seatData ? buildSeatLabel(seatData) : '',
|
||||||
inboundCoachNumber: selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
inboundCoachNumber: seatData?._coachLabel || selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
||||||
inboundSeatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
inboundSeatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
||||||
inboundBedPosition: seatData?.bedPosition || undefined,
|
inboundBedPosition: seatData?.bedPosition || undefined,
|
||||||
};
|
};
|
||||||
@@ -1185,7 +1220,7 @@ export default function SeatsPage() {
|
|||||||
...p,
|
...p,
|
||||||
seatId: seatIds[i],
|
seatId: seatIds[i],
|
||||||
seatNumber: seatData ? buildSeatLabel(seatData) : '',
|
seatNumber: seatData ? buildSeatLabel(seatData) : '',
|
||||||
coachNumber: selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
coachNumber: seatData?._coachLabel || selectedCoachData?.label || selectedCoachData?.name || selectedCoachData?.number || '',
|
||||||
seatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
seatFareMinor: seatData ? (getSeatFare(seatData) ?? undefined) : undefined,
|
||||||
bedPosition: seatData?.bedPosition || undefined,
|
bedPosition: seatData?.bedPosition || undefined,
|
||||||
};
|
};
|
||||||
@@ -1197,7 +1232,7 @@ export default function SeatsPage() {
|
|||||||
if (!isRoundTrip && isPackageBooking && packageId) {
|
if (!isRoundTrip && isPackageBooking && packageId) {
|
||||||
const firstEligibleIdx = seatEligibleIndices[0];
|
const firstEligibleIdx = seatEligibleIndices[0];
|
||||||
const firstSeatData = firstEligibleIdx != null
|
const firstSeatData = firstEligibleIdx != null
|
||||||
? validSeats?.find((s: any) => s.id === seatIds[firstEligibleIdx])
|
? allCoachSeats?.find((s: any) => s.id === seatIds[firstEligibleIdx])
|
||||||
: null;
|
: null;
|
||||||
const berthFare = firstSeatData ? getSeatFare(firstSeatData) : null;
|
const berthFare = firstSeatData ? getSeatFare(firstSeatData) : null;
|
||||||
if (berthFare != null) {
|
if (berthFare != null) {
|
||||||
@@ -1228,20 +1263,38 @@ export default function SeatsPage() {
|
|||||||
router.push("/booking/review");
|
router.push("/booking/review");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fires once the passengerSeatMap update from handleAutoAssign has actually landed (and
|
||||||
|
// every eligible passenger now has a seat) — proceeds straight to Continue so the user
|
||||||
|
// doesn't have to click it separately after auto-assigning.
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoContinueAfterAssignRef.current && allSeatsAssigned) {
|
||||||
|
autoContinueAfterAssignRef.current = false;
|
||||||
|
handleContinue();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [passengerSeatMap, allSeatsAssigned]);
|
||||||
|
|
||||||
const handleAutoAssign = () => {
|
const handleAutoAssign = () => {
|
||||||
const unassignedIndices = seatEligibleIndices.filter((i) => !passengerSeatMap[i]);
|
const unassignedIndices = seatEligibleIndices.filter((i) => !passengerSeatMap[i]);
|
||||||
|
|
||||||
if (unassignedIndices.length === 0) return;
|
if (unassignedIndices.length === 0) return;
|
||||||
|
|
||||||
const availableSeats = (
|
// Search the whole train (every coach of this type), not just whichever one happens to
|
||||||
validSeats?.filter((s: any) => s.status === "AVAILABLE") || []
|
// be expanded — fill the currently-expanded coach first so the result stays visible
|
||||||
).filter((s: any) => !assignedSeatIds.has(s.id));
|
// without switching coaches, then spill over into other coaches only if needed.
|
||||||
|
const availableAcrossTrain = allCoachSeats
|
||||||
|
.filter((s: any) => s.status === "AVAILABLE" && !assignedSeatIds.has(s.id))
|
||||||
|
.sort((a: any, b: any) => {
|
||||||
|
const aCurrent = a._coachId === selectedCoach ? 0 : 1;
|
||||||
|
const bCurrent = b._coachId === selectedCoach ? 0 : 1;
|
||||||
|
return aCurrent - bCurrent;
|
||||||
|
});
|
||||||
|
|
||||||
if (availableSeats.length < unassignedIndices.length) {
|
if (availableAcrossTrain.length < unassignedIndices.length) {
|
||||||
setModalState({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: "Not Enough Seats",
|
title: "Not Enough Seats",
|
||||||
message: `Only ${availableSeats.length} seat(s) available in this coach, but you need ${unassignedIndices.length} more seat(s). Please select another coach.`,
|
message: `Only ${availableAcrossTrain.length} seat(s) available across this coach type, but you need ${unassignedIndices.length} more seat(s). Please select another coach type.`,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
onConfirm: undefined,
|
onConfirm: undefined,
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
@@ -1251,11 +1304,21 @@ export default function SeatsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const next = { ...passengerSeatMap };
|
const next = { ...passengerSeatMap };
|
||||||
|
let lastAssignedCoachId: string | null = null;
|
||||||
unassignedIndices.forEach((passengerIndex, offset) => {
|
unassignedIndices.forEach((passengerIndex, offset) => {
|
||||||
next[passengerIndex] = availableSeats[offset].id;
|
const seat = availableAcrossTrain[offset];
|
||||||
|
next[passengerIndex] = seat.id;
|
||||||
|
lastAssignedCoachId = seat._coachId;
|
||||||
});
|
});
|
||||||
|
// Proceed straight to Continue once this update lands — no separate click needed.
|
||||||
|
autoContinueAfterAssignRef.current = true;
|
||||||
setPassengerSeatMap(next);
|
setPassengerSeatMap(next);
|
||||||
setActivePassengerIndex(seatEligibleIndices[seatEligibleIndices.length - 1] ?? 0);
|
setActivePassengerIndex(seatEligibleIndices[seatEligibleIndices.length - 1] ?? 0);
|
||||||
|
// Bring whichever coach the last passenger landed in into view, since seats may now
|
||||||
|
// span coaches beyond the one that was expanded when Auto Assign was clicked.
|
||||||
|
if (lastAssignedCoachId && lastAssignedCoachId !== selectedCoach) {
|
||||||
|
setSelectedCoach(lastAssignedCoachId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToPassengers = () => {
|
const handleBackToPassengers = () => {
|
||||||
@@ -1857,8 +1920,10 @@ export default function SeatsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assignedSeatId = passengerSeatMap[i];
|
const assignedSeatId = passengerSeatMap[i];
|
||||||
|
// allCoachSeats (not validSeats) so a seat Auto Assign placed in a coach the user
|
||||||
|
// never manually expanded still shows correctly here.
|
||||||
const assignedSeat = assignedSeatId
|
const assignedSeat = assignedSeatId
|
||||||
? validSeats?.find((s: any) => s.id === assignedSeatId)
|
? allCoachSeats?.find((s: any) => s.id === assignedSeatId)
|
||||||
: null;
|
: null;
|
||||||
const seatLabel = assignedSeat ? buildSeatLabel(assignedSeat) : "";
|
const seatLabel = assignedSeat ? buildSeatLabel(assignedSeat) : "";
|
||||||
const seatFare = assignedSeat
|
const seatFare = assignedSeat
|
||||||
|
|||||||
Reference in New Issue
Block a user