diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx
index 810e0689a..d1952da23 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx
@@ -1,4 +1,4 @@
-'use client';
+'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
@@ -147,10 +147,16 @@ export default function ResultsPage() {
// For one-way, check if outbound has results
// For round-trip, check if BOTH outbound and inbound have results
- const hasResults = isRoundTrip
+ const hasResults = isRoundTrip
? (outboundSchedules.length > 0 && inboundSchedules.length > 0)
: outboundSchedules.length > 0;
+ // One-way searches that come back with an empty outbound list may still include
+ // date-shifted alternatives from the API — surface those instead of a dead end.
+ const isOneWayNoOutbound = !isRoundTrip && !!results && outboundSchedules.length === 0;
+ const alternativeOutbound: Schedule[] = isOneWayNoOutbound ? (results.alternativeOutbound || []) : [];
+ const requestedDate: string = (results && results.requestedDate) || searchData.date;
+
const handleSelectCoachType = (scheduleId: string, coachTypeId: string, coachTypeCode: string, coachTypeName: string, seatClassName: string) => {
setSelectedCoachTypes(prev => ({ ...prev, [scheduleId]: { id: coachTypeId, code: coachTypeCode, name: coachTypeName, seatClassName } }));
};
@@ -217,7 +223,190 @@ export default function ResultsPage() {
router.push('/booking/auth-check');
};
- const renderScheduleCard = (schedule: Schedule, isOutbound: boolean = false) => {
+ // Shared "Choose Your Coach" drawer — used by both the normal results view and the
+ // Alternative Travel Options fallback, so selecting an alternative opens the exact
+ // same coach-type picker as a normal schedule.
+ const renderClassModal = () => {
+ if (!classModal) return null;
+
+ const scheduleId = classModal.scheduleId || classModal.id || '';
+ const selectedCoachType = selectedCoachTypes[scheduleId];
+ const isOutbound = (classModal as any).isOutbound;
+ const coachTypes = classModal.coachTypes || [];
+
+ const getCoachIcon = (typeName: string) => {
+ const lower = typeName.toLowerCase();
+ if (lower.includes('soft') || lower.includes('vip')) return Star;
+ if (lower.includes('bed')) return Bed;
+ return Armchair;
+ };
+
+ return (
+ <>
+
setClassModal(null)} />
+
+
+
+
Choose Your Coach
+
+
+ {classModal.trainNumber}
+ ·
+ {classModal.origin?.name} → {classModal.destination?.name}
+
+
+
setClassModal(null)}
+ className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all"
+ aria-label="Close"
+ >
+
+
+
+
+
+ {coachTypes.length > 0 ? (
+
+ {coachTypes.map((coachType: any, index: number) => {
+ const isSelected = selectedCoachType?.id === coachType.coachTypeId;
+ const minPrice = coachType.classes.length ? Math.min(...coachType.classes.map((c: any) => c.baseFareMinor)) : 0;
+ const coachCurrency = 'ETB';
+ const CoachIcon = getCoachIcon(coachType.coachTypeName);
+
+ return (
+
handleSelectCoachType(scheduleId, coachType.coachTypeId, coachType.coachTypeCode, coachType.coachTypeName, coachType.classes?.[0]?.name || coachType.coachTypeName)}
+ className={`group relative w-full p-5 rounded-2xl border-2 text-left transition-all duration-200 ${
+ 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-gray-200 dark:border-gray-700 hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50'
+ }`}
+ style={{ animation: `fade-in-up 0.3s ease-out ${index * 0.1}s both` }}
+ >
+ {isSelected && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {coachType.coachTypeName}
+
+
+
+
+
+
+ From
+
+ {(minPrice / 100).toFixed(2)}
+
+ {coachCurrency}
+
+
+
+
+
+ {coachType.classes.length > 0 && (
+
+
+
+ Class Options
+
+
+ {coachType.classes.length} available
+
+
+
+ {coachType.classes.map((cls: any, idx: number) => (
+
+
+
+
+ {cls.name}
+
+
+
+
+ {(cls.baseFareMinor / 100).toFixed(2)}
+
+
+ {coachCurrency}
+
+
+
+ ))}
+
+
+ )}
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+
+
No coach types available for this journey
+
+ )}
+
+
+
+
+
{ 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]"
+ >
+ {isRoundTrip && isOutbound ? 'Continue to Return Journey' : 'Continue to Passenger Details'}
+
+
+ {!selectedCoachType && (
+
+
+ Select a coach type to continue
+
+ )}
+
+
+
+
+ >
+ );
+ };
+
+ const renderScheduleCard = (schedule: Schedule, isOutbound: boolean = false, isAlternative: boolean = false) => {
const scheduleId = schedule.scheduleId || schedule.id || '';
const selectedCoachType = selectedCoachTypes[scheduleId];
@@ -242,7 +431,22 @@ export default function ResultsPage() {
const isNextDay = departureDate && arrivalDate && departureDate.toDateString() !== arrivalDate.toDateString();
return (
-
+
+ {isAlternative && (
+
+
+
+ Different date — {schedule.departureAt ? format(new Date(schedule.departureAt), 'EEEE, MMMM d, yyyy') : 'N/A'}
+
+
+ {schedule.hasAvailability ? 'Seats available' : 'Fully booked'}
+
+
+ )}
@@ -310,9 +514,10 @@ export default function ResultsPage() {
)}
setClassModal({ ...schedule, isOutbound } as any)}
- className="btn-secondary w-full flex items-center justify-center gap-2"
+ disabled={isAlternative && schedule.hasAvailability === false}
+ className="btn-secondary w-full flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
- {selectedCoachType ? 'Change' : 'Select'}
+ {isAlternative && schedule.hasAvailability === false ? 'Unavailable' : selectedCoachType ? 'Change' : 'Select'}
@@ -478,6 +683,54 @@ export default function ResultsPage() {
}
if (!hasResults) {
+ // ONE_WAY search with an explicit empty outbound list — surface any date-shifted
+ // alternatives the API suggests instead of a dead-end "no trains found" screen.
+ if (isOneWayNoOutbound) {
+ const requestedDateLabel = requestedDate
+ ? format(new Date(`${requestedDate}T00:00:00`), 'EEEE, MMMM d, yyyy')
+ : 'your selected date';
+ const hasAlternatives = alternativeOutbound.length > 0;
+
+ return (
+
+ {renderClassModal()}
+
+
+
+
+
+
+
No trains available
+
+ No trains are available on {requestedDateLabel} . This
+ may be due to no scheduled service or full capacity. {hasAlternatives
+ ? 'Please check the alternative options below or try a different date.'
+ : 'Please try a different date.'}
+
+
router.push(buildSearchUrl())} className="btn-primary">
+ Change travel date
+
+
+
+ {hasAlternatives && (
+
+
+
Alternative Travel Options
+
+ These trains run on different dates than requested — adjust your travel date to book one of them.
+
+
+
+ {alternativeOutbound.map((schedule) => renderScheduleCard(schedule, true, true))}
+
+
+ )}
+
+
+
+ );
+ }
+
return (
@@ -505,183 +758,7 @@ export default function ResultsPage() {
- {classModal && (() => {
- const scheduleId = classModal.scheduleId || classModal.id || '';
- const selectedCoachType = selectedCoachTypes[scheduleId];
- const isOutbound = (classModal as any).isOutbound;
- const coachTypes = classModal.coachTypes || [];
-
- const getCoachIcon = (typeName: string) => {
- const lower = typeName.toLowerCase();
- if (lower.includes('soft') || lower.includes('vip')) return Star;
- if (lower.includes('bed')) return Bed;
- return Armchair;
- };
-
- return (
- <>
-
setClassModal(null)} />
-
-
-
-
Choose Your Coach
-
-
- {classModal.trainNumber}
- ·
- {classModal.origin?.name} → {classModal.destination?.name}
-
-
-
setClassModal(null)}
- className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all"
- aria-label="Close"
- >
-
-
-
-
-
- {coachTypes.length > 0 ? (
-
- {coachTypes.map((coachType: any, index: number) => {
- const isSelected = selectedCoachType?.id === coachType.coachTypeId;
- const minPrice = coachType.classes.length ? Math.min(...coachType.classes.map((c: any) => c.baseFareMinor)) : 0;
- const coachCurrency = 'ETB';
- const CoachIcon = getCoachIcon(coachType.coachTypeName);
-
- return (
-
handleSelectCoachType(scheduleId, coachType.coachTypeId, coachType.coachTypeCode, coachType.coachTypeName, coachType.classes?.[0]?.name || coachType.coachTypeName)}
- className={`group relative w-full p-5 rounded-2xl border-2 text-left transition-all duration-200 ${
- 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-gray-200 dark:border-gray-700 hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50'
- }`}
- style={{ animation: `fade-in-up 0.3s ease-out ${index * 0.1}s both` }}
- >
- {isSelected && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {coachType.coachTypeName}
-
-
-
-
-
-
- From
-
- {(minPrice / 100).toFixed(2)}
-
- {coachCurrency}
-
-
-
-
-
- {coachType.classes.length > 0 && (
-
-
-
- Class Options
-
-
- {coachType.classes.length} available
-
-
-
- {coachType.classes.map((cls: any, idx: number) => (
-
-
-
-
- {cls.name}
-
-
-
-
- {(cls.baseFareMinor / 100).toFixed(2)}
-
-
- {coachCurrency}
-
-
-
- ))}
-
-
- )}
-
-
- );
- })}
-
- ) : (
-
-
-
-
-
No coach types available for this journey
-
- )}
-
-
-
-
-
{ 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]"
- >
- {isRoundTrip && isOutbound ? 'Continue to Return Journey' : 'Continue to Passenger Details'}
-
-
- {!selectedCoachType && (
-
-
- Select a coach type to continue
-
- )}
-
-
-
-
- >
- );
- })()}
+ {renderClassModal()}
{promoData && (