diff --git a/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts index e50cf6c64..ea91f89a2 100644 --- a/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts +++ b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts @@ -34,7 +34,7 @@ export class SingleMessageDto { }) @IsString() @IsNotEmpty() - sms: string; + message: string; } export class BulkMessagesDto { diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index 8960f1a87..595633e93 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -21,7 +21,7 @@ export class NotificationsService { ) { this.channels = new Map([ ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then(() => true) }], - ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, sms: body }).then(() => true) }], + ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }], ['PUSH', this.pushAdapter as NotificationChannel], ]); } diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 0ca9a8723..9087ee7ac 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -500,11 +500,12 @@ export class SearchService { coachTypeId: string; coachTypeName: string; coachTypeCode: string; + coachId: string; classes: Array<{ name: string; baseFareMinor: number }>; }>> { const coachTypeMap = new Map< string, - { coachType: any; classNames: Set } + { coachType: any; classNames: Set; coachId: string } >(); for (const assignment of schedule.coachAssignments) { @@ -515,6 +516,7 @@ export class SearchService { coachTypeMap.set(coachType.id, { coachType, classNames: new Set(), + coachId: assignment.coach.id, }); } @@ -523,7 +525,7 @@ export class SearchService { } const result = []; - for (const [, { coachType, classNames }] of coachTypeMap) { + for (const [, { coachType, classNames, coachId }] of coachTypeMap) { const classes = Array.from(classNames) .map((className) => { const fareInfo = faresByClass.find((f) => f.seatClassName === className); @@ -537,6 +539,7 @@ export class SearchService { coachTypeId: coachType.id, coachTypeName: coachType.name, coachTypeCode: coachType.code, + coachId, classes, }); } diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 2c57a59e3..4cf4ce9d3 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -281,7 +281,7 @@ export class TicketsService { if (resolvedLeg !== 'LEG1' && resolvedLeg !== 'LEG2') { throw new BadRequestException('For TRANSIT bookings supply leg=LEG1 or leg=LEG2'); } - const logs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }); + const logs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }) as any[]; const alreadyValidated = logs.some(l => l.leg === resolvedLeg); if (alreadyValidated) { await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: `${resolvedLeg}_ALREADY_USED` } as any }); @@ -333,7 +333,7 @@ export class TicketsService { if (!validLegs.includes(resolvedLeg)) { throw new BadRequestException(`For ROUND_TRIP_TRANSIT supply leg=${validLegs.join('|')}`); } - const logs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }); + const logs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }) as any[]; if (logs.some(l => l.leg === resolvedLeg)) { await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: `${resolvedLeg}_ALREADY_USED` } as any }); throw new BadRequestException(`${resolvedLeg} already validated`); @@ -435,7 +435,7 @@ export class TicketsService { booking.bookingType === 'TRANSIT' || booking.bookingType === 'ROUND_TRIP_TRANSIT'; if (isMultiLeg && offlineLeg) { - const existingLogs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }); + const existingLogs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }) as any[]; if (existingLogs.some(l => l.leg === offlineLeg)) { results.duplicate++; continue; 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 2e8d9e950..1e906be2c 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 @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/lib/api-client'; import { useBookingStore } from '@/lib/booking-store'; import { Schedule } from '@/types'; -import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Check, X, MapPin, Gift, Train } from 'lucide-react'; +import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Check, X, MapPin, Gift, Train, Bed, Armchair, Star } from 'lucide-react'; import { format } from 'date-fns'; import { useState, useEffect } from 'react'; @@ -13,7 +13,7 @@ export default function ResultsPage() { const router = useRouter(); const searchParams = useSearchParams(); const { setSelectedSchedule, setOutboundSchedule, setInboundSchedule } = useBookingStore(); - const [selectedClasses, setSelectedClasses] = useState>({}); + const [selectedCoachTypes, setSelectedCoachTypes] = useState>({}); const [outboundScheduleData, setOutboundScheduleData] = useState(null); const [classModal, setClassModal] = useState(null); const [promoData, setPromoData] = useState<{ code: string; discount: string; message: string } | null>(null); @@ -142,27 +142,22 @@ export default function ResultsPage() { ? (outboundSchedules.length > 0 && inboundSchedules.length > 0) : outboundSchedules.length > 0; - const handleSelectClass = (scheduleId: string, seatClass: string) => { - setSelectedClasses(prev => ({ ...prev, [scheduleId]: seatClass })); + const handleSelectCoachType = (scheduleId: string, coachId: string, coachTypeCode: string, coachTypeName: string) => { + setSelectedCoachTypes(prev => ({ ...prev, [scheduleId]: { id: coachId, code: coachTypeCode, name: coachTypeName } })); }; const handleSelect = (schedule: Schedule, isOutbound: boolean = false) => { const scheduleId = schedule.scheduleId || schedule.id || ''; - const selectedClass = selectedClasses[scheduleId]; + const selectedCoachType = selectedCoachTypes[scheduleId]; - if (!selectedClass) { - alert('Please select a seat class before continuing'); + if (!selectedCoachType) { + alert('Please select a coach type before continuing'); return; } - const selectedClassFare = schedule.faresByClass?.find( - (f: any) => f.seatClassName === selectedClass - ); - - if (!selectedClassFare) { - alert('Unable to find fare for selected class'); - return; - } + // Find the coach type to get pricing info + const coachType = schedule.coachTypes?.find(ct => ct.coachId === selectedCoachType.id); + const minFare = coachType?.classes.length ? Math.min(...coachType.classes.map(c => c.baseFareMinor)) : 0; const hours = Math.floor((schedule.durationMinutes || 0) / 60); const minutes = (schedule.durationMinutes || 0) % 60; @@ -176,10 +171,13 @@ export default function ResultsPage() { departureTime: schedule.departureAt || schedule.departureTime || '', arrivalTime: schedule.arrivalAt || schedule.arrivalTime || '', duration: durationStr, - baseFareAdult: selectedClassFare.baseFareMinor, - baseFareChild: selectedClassFare.baseFareMinor, - selectedSeatClass: selectedClass, - selectedSeatClassName: selectedClass, + baseFareAdult: minFare, + baseFareChild: minFare, + selectedSeatClass: selectedCoachType.name, + selectedSeatClassName: selectedCoachType.name, + selectedCoachId: selectedCoachType.id, + selectedCoachTypeCode: selectedCoachType.code, + selectedCoachTypeName: selectedCoachType.name, }; // For round trip, store outbound and wait for inbound selection @@ -211,10 +209,16 @@ export default function ResultsPage() { const renderScheduleCard = (schedule: Schedule, isOutbound: boolean = false) => { const scheduleId = schedule.scheduleId || schedule.id || ''; - const selectedClass = selectedClasses[scheduleId]; - const lowestFare = schedule.faresByClass && Array.isArray(schedule.faresByClass) && schedule.faresByClass.length > 0 - ? Math.min(...schedule.faresByClass.map((f: any) => f.baseFareMinor).filter((fare: number) => fare > 0)) - : null; + const selectedCoachType = selectedCoachTypes[scheduleId]; + + // Calculate lowest fare from coach types + let lowestFare = null; + if (schedule.coachTypes?.length) { + const allFares = schedule.coachTypes.flatMap(ct => ct.classes.map(c => c.baseFareMinor)).filter(f => f > 0); + lowestFare = allFares.length ? Math.min(...allFares) : null; + } else if (schedule.faresByClass?.length) { + lowestFare = Math.min(...schedule.faresByClass.map((f: any) => f.baseFareMinor).filter((fare: number) => fare > 0)); + } const hours = Math.floor((schedule.durationMinutes || 0) / 60); const minutes = (schedule.durationMinutes || 0) % 60; const durationStr = `${hours}h ${minutes}m`; @@ -284,16 +288,16 @@ export default function ResultsPage() { {lowestFare ? `ETB ${(lowestFare / 100).toFixed(2)}` : 'N/A'}
per adult
- {selectedClass && ( + {selectedCoachType && (

- {selectedClass.replace(/_/g, ' ')} selected + {selectedCoachType.name} selected

)} @@ -488,95 +492,177 @@ export default function ResultsPage() { {classModal && (() => { const scheduleId = classModal.scheduleId || classModal.id || ''; - const selectedClass = selectedClasses[scheduleId]; + 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)} /> -
-
+
-

Select Class

-

- - {classModal.trainNumber} ยท {classModal.origin?.name} โ†’ {classModal.destination?.name} +

Choose Your Coach

+

+ + {classModal.trainNumber} + ยท + {classModal.origin?.name} โ†’ {classModal.destination?.name}

-
- {classModal.faresByClass && Array.isArray(classModal.faresByClass) && classModal.faresByClass.length > 0 ? ( -
- {classModal.faresByClass.map((fareClass: any) => { - const isSelected = selectedClass === fareClass.seatClassName; - const availableSeats = classModal.availabilityByClass?.[fareClass.seatClassName] || 0; - const isAvailable = availableSeats > 0; - const isBedClass = fareClass.seatClassName.toLowerCase().includes('bed'); +
+ {coachTypes.length > 0 ? ( +
+ {coachTypes.map((coachType: any, index: number) => { + const isSelected = selectedCoachType?.id === coachType.coachId; + const minPrice = coachType.classes.length ? Math.min(...coachType.classes.map((c: any) => c.baseFareMinor)) : 0; + const CoachIcon = getCoachIcon(coachType.coachTypeName); + return ( ); })}
) : ( -

No seat classes available

+
+
+ +
+

No coach types available for this journey

+
)}
-
- - {!selectedClass && ( -

Please select a class to continue

- )} +
+
+ + {!selectedCoachType && ( +

+ + Select a coach type to continue +

+ )} +
- + ); })()} diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index d51410dff..0ce5a340f 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -144,9 +144,14 @@ export default function ReviewPage() { const createBookingMutation = useMutation({ mutationFn: (data: any) => { const endpoint = isAuthenticated ? '/bookings' : '/bookings/guest'; + console.log('=== API REQUEST ==='); + console.log('Endpoint:', endpoint); + console.log('Request Data:', JSON.stringify(data, null, 2)); return apiClient.post(endpoint, data); }, onSuccess: (data: any) => { + console.log('=== API RESPONSE SUCCESS ==='); + console.log('Response Data:', JSON.stringify(data, null, 2)); console.log('Booking created successfully:', data); const bookingIdValue = data.bookingId || data.id; const pnrValue = data.pnr || data.bookingReference || data.bookingRef; @@ -178,7 +183,12 @@ export default function ReviewPage() { }, 100); }, onError: (error: any) => { - console.error('Booking creation failed:', error); + console.log('=== API RESPONSE ERROR ==='); + console.error('Error Object:', error); + console.error('Error Response:', error?.response); + console.error('Error Response Data:', JSON.stringify(error?.response?.data, null, 2)); + console.error('Error Status:', error?.response?.status); + console.error('Error Message:', error?.message); const errorMessage = error?.response?.data?.message || error?.message || 'Failed to create booking. Please try again.'; alert(errorMessage); }, @@ -196,7 +206,7 @@ export default function ReviewPage() { console.log('Inbound schedule:', inboundSchedule); console.log('Passengers:', passengers); - if (!seatHold?.holdId) { + if (!seatHold?.holdId && (passengers.some(p => p.seatId) || passengers.some(p => (p as any).outboundSeatId || (p as any).inboundSeatId))) { console.error('No seat hold found'); alert('Please select seats before continuing.'); router.push('/booking/seats'); @@ -275,7 +285,7 @@ export default function ReviewPage() { bookingData = { passengerId: passengerId, scheduleId: isRoundTrip ? outboundSchedule?.id : selectedSchedule?.id, - holdId: seatHold.holdId, + holdId: seatHold?.holdId || '', originStationId: searchCriteria.originStationId, destinationStationId: searchCriteria.destinationStationId, seatClassId: seatClassId, @@ -302,7 +312,7 @@ export default function ReviewPage() { bookingData.returnScheduleId = inboundSchedule.id; bookingData.returnOriginStationId = searchCriteria.destinationStationId; bookingData.returnDestinationStationId = searchCriteria.originStationId; - bookingData.returnHoldId = seatHold.holdId; // Assuming same hold ID, adjust if needed + bookingData.returnHoldId = seatHold?.holdId || ''; // Assuming same hold ID, adjust if needed bookingData.returnSeatClassId = returnSeatClassId; } @@ -314,7 +324,7 @@ export default function ReviewPage() { // For guests: send full passenger details array bookingData = { scheduleId: isRoundTrip ? outboundSchedule?.id : selectedSchedule?.id, - holdId: seatHold.holdId, + holdId: seatHold?.holdId || '', originStationId: searchCriteria.originStationId, destinationStationId: searchCriteria.destinationStationId, seatClassId: seatClassId, @@ -346,7 +356,7 @@ export default function ReviewPage() { bookingData.returnScheduleId = inboundSchedule.id; bookingData.returnOriginStationId = searchCriteria.destinationStationId; bookingData.returnDestinationStationId = searchCriteria.originStationId; - bookingData.returnHoldId = seatHold.holdId; // Assuming same hold ID, adjust if needed + bookingData.returnHoldId = seatHold?.holdId || ''; // Assuming same hold ID, adjust if needed bookingData.returnSeatClassId = returnSeatClassId; } @@ -360,7 +370,8 @@ export default function ReviewPage() { localStorage.setItem('deviceId', bookingData.deviceId); } - console.log('Creating booking with payload:', bookingData); + console.log('Creating booking with payload:', JSON.stringify(bookingData, null, 2)); + console.log('API endpoint:', isAuthenticated ? '/bookings' : '/bookings/guest'); await createBookingMutation.mutateAsync(bookingData); } catch (error) { console.error('Error in handleConfirm:', error); diff --git a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx index d3c39a32e..3cd9db800 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx @@ -60,11 +60,35 @@ export default function SeatsPage() { const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP'; const currentSchedule = isRoundTrip && currentJourneyType === 'inbound' ? inboundSchedule : (isRoundTrip ? outboundSchedule : selectedSchedule); + const coachId = (currentSchedule as any)?.selectedCoachId; + const coachTypeCode = (currentSchedule as any)?.selectedCoachTypeCode; const { data: seatMapData, isLoading, error } = useQuery({ - queryKey: ['seatmap', currentSchedule?.id, currentJourneyType], - queryFn: () => apiClient.get(`/seats/seatmap/${currentSchedule?.id}`), - enabled: !!currentSchedule?.id, + queryKey: ['seatmap', currentSchedule?.id, coachId, currentJourneyType], + queryFn: async () => { + const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachId=${coachId}`; + console.log('๐Ÿช‘ Seatmap Request:', { + endpoint, + scheduleId: currentSchedule?.id, + coachId, + coachTypeCode, + currentJourneyType, + }); + + const response = await apiClient.get(endpoint); + + console.log('โœ… Seatmap Response:', { + endpoint, + fullResponse: response, + dataCoaches: (response as any)?.data?.coaches?.length || 0, + rootCoaches: (response as any)?.coaches?.length || 0, + }); + + const finalData = (response as any)?.data || response; + console.log('๐ŸŽฏ Final data structure:', finalData); + return finalData; + }, + enabled: !!currentSchedule?.id && !!coachId, }); const holdMutation = useMutation({ @@ -106,22 +130,40 @@ export default function SeatsPage() { }, }); - const coaches = useMemo(() => (seatMapData as any)?.coaches || [], [seatMapData]); + const coaches = useMemo(() => { + const rawCoaches = (seatMapData as any)?.coaches || (seatMapData as any)?.data?.coaches || []; + console.log('๐Ÿ“ฆ Raw coaches data:', { + fromRoot: (seatMapData as any)?.coaches?.length || 0, + fromData: (seatMapData as any)?.data?.coaches?.length || 0, + using: rawCoaches.length, + seatMapData + }); + return rawCoaches; + }, [seatMapData]); const filteredCoaches = useMemo(() => { + console.log('๐Ÿ” Filtering coaches:', { + totalCoaches: coaches.length, + selectedSeatClass: currentSchedule?.selectedSeatClass, + coachesData: coaches.map((c: any) => ({ + id: c.id, + name: c.name, + label: c.label, + seatClass: c.seatClass, + seatClasses: c.seatClasses, + seatsCount: c.seats?.length || 0 + })) + }); + + const coachesWithSeats = coaches.filter((c: any) => c.seats && c.seats.length > 0); + if (!currentSchedule?.selectedSeatClass) { - return coaches.filter((c: any) => c.seats && c.seats.length > 0); + console.log('โœ… No filter applied, returning all coaches:', coachesWithSeats.length); + return coachesWithSeats; } - let filtered = coaches.filter((c: any) => { - const seatClasses = c.seatClasses || [c.seatClass] || []; - return seatClasses.some((seatClassName: string) => - seatClassName === currentSchedule.selectedSeatClass || - seatClassName.replace(/_/g, ' ').toLowerCase() === currentSchedule.selectedSeatClass?.toLowerCase() || - seatClassName.toLowerCase() === currentSchedule.selectedSeatClass?.toLowerCase() - ); - }); - return filtered.filter((c: any) => c.seats && c.seats.length > 0); + console.log('โœ… No seat class filter - returning all coaches with seats:', coachesWithSeats.length); + return coachesWithSeats; }, [coaches, currentSchedule?.selectedSeatClass]); useEffect(() => { @@ -142,7 +184,10 @@ export default function SeatsPage() { }; const validSeats = useMemo(() => { - let seats = allSeats.filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-')); + let seats = allSeats.filter((s: any) => { + const seatLabel = s.label || s.number || s.seatNumber || ''; + return seatLabel && !seatLabel.startsWith('-'); + }); const isBedCoach = selectedCoachData?.seatClass?.toLowerCase().includes('bed') || selectedCoachData?.mode?.toLowerCase().includes('bed'); if (isBedCoach && currentSchedule?.selectedSeatClass) { @@ -288,10 +333,22 @@ export default function SeatsPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [bookingId]); - const parseSeatArrangement = (arrangement: string | null): number[] => { + const parseSeatArrangement = (arrangement: string | null, seatClasses?: string[]): number[] => { if (!arrangement) return [2, 2]; - const parts = arrangement.split('+').map(p => parseInt(p.trim())); - return parts.length === 2 ? parts : [2, 2]; + + // Check if this is a bed coach based on seat classes + const isBedCoach = seatClasses?.some(sc => sc?.toLowerCase().includes('bed')); + + if (isBedCoach) { + // For bed coaches, arrangement like "3+0" means 3 beds stacked vertically + // We want to render them as single column, so return [1] + const parts = arrangement.split('+').map(p => parseInt(p.trim())).filter(n => !isNaN(n) && n > 0); + return parts.length > 0 ? [Math.max(...parts)] : [3]; + } + + // For regular seats, parse normally (e.g., "3+2" -> [3, 2]) + const parts = arrangement.split('+').map(p => parseInt(p.trim())).filter(n => !isNaN(n) && n > 0); + return parts.length >= 2 ? parts : parts.length === 1 ? [parts[0]] : [2, 2]; }; const getBedLabel = (bedPosition: string | null): string => { @@ -302,8 +359,7 @@ export default function SeatsPage() { }; const renderCoachSeats = (coach: any, isBedCoach: boolean) => { - const arrangement = parseSeatArrangement(coach.seatArrangement); - const leftCount = arrangement[0]; + const arrangement = parseSeatArrangement(coach.seatArrangement, coach.seatClasses || [coach.seatClass]); if (validSeats.length === 0) { return
No seats
; @@ -312,36 +368,91 @@ export default function SeatsPage() { const hasBedPositionData = validSeats.some((s: any) => s.bedPosition); const seatClassStr = typeof selectedCoachData?.seatClass === 'string' ? selectedCoachData.seatClass : (selectedCoachData?.seatClass?.name || ''); + // Bed coach with bed positions (Upper, Middle, Lower) if (isBedCoach && hasBedPositionData) { + // Group by the base seat number (column), not by row + // For beds, seats with same number but different positions should be grouped together + const seatGroups = new Map(); + + for (const seat of validSeats) { + const baseNumber = seat.seatNumber || seat.number || seat.label || ''; + if (!seatGroups.has(baseNumber)) { + seatGroups.set(baseNumber, []); + } + seatGroups.get(baseNumber)!.push(seat); + } + + // Sort groups by seat number + const sortedGroups = Array.from(seatGroups.entries()) + .sort(([a], [b]) => { + const numA = parseInt(a) || 0; + const numB = parseInt(b) || 0; + return numA - numB; + }); + return ( -
- {validSeats.map((seat: any) => { - const rowNumber = seat.row || 1; - const shouldFlipIcon = rowNumber % 2 === 0; +
+ {sortedGroups.map(([seatNumber, beds], idx) => { + const shouldFlipIcon = idx % 2 === 0; + + // Order: lower, middle, upper (bottom to top) + const orderedBeds = ['lower', 'middle', 'upper'] + .map(pos => beds.find(seat => seat.bedPosition === pos)) + .filter(seat => seat !== undefined); + + if (orderedBeds.length === 0) return null; return ( -
- {shouldFlipIcon && ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''} +
+
+ {shouldFlipIcon && ( +
+ {orderedBeds.map((seat: any) => { + const seatLabel = seat.seatNumber || seat.number || seat.label || ''; + const bedLabelFull = seat.bedPosition ? ( + seat.bedPosition === 'upper' ? 'Upper' : + seat.bedPosition === 'middle' ? 'Middle' : 'Lower' + ) : ''; + return ( +
+ {seatLabel ? `${seatLabel} ${bedLabelFull}` : ''} +
+ ); + })} +
+ )} + +
+ {orderedBeds.map((seat: any) => ( + + ))}
- )} -
- + + {!shouldFlipIcon && ( +
+ {orderedBeds.map((seat: any) => { + const seatLabel = seat.seatNumber || seat.number || seat.label || ''; + const bedLabelFull = seat.bedPosition ? ( + seat.bedPosition === 'upper' ? 'Upper' : + seat.bedPosition === 'middle' ? 'Middle' : 'Lower' + ) : ''; + return ( +
+ {seatLabel ? `${seatLabel} ${bedLabelFull}` : ''} +
+ ); + })} +
+ )}
- {!shouldFlipIcon && ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''} -
- )}
); })} @@ -349,68 +460,64 @@ export default function SeatsPage() { ); } - const rows = []; - const processedRows = new Set(); + // Regular seats with row/column arrangement + const rowMap = new Map(); for (const seat of validSeats) { - if (!processedRows.has(seat.row)) { - rows.push(validSeats.filter((s: any) => s.row === seat.row).sort((a: any, b: any) => { - const colA = a.col.charCodeAt(0); - const colB = b.col.charCodeAt(0); - return colA - colB; - })); - processedRows.add(seat.row); + if (!rowMap.has(seat.row)) { + rowMap.set(seat.row, []); } + rowMap.get(seat.row)!.push(seat); } + const rows = Array.from(rowMap.entries()) + .sort(([a], [b]) => a - b) + .map(([_, seats]) => seats.sort((a, b) => a.col.localeCompare(b.col))); + return (
{rows.map((rowSeats: any[], rowIdx: number) => { - const leftSeats = rowSeats.slice(0, leftCount); - const rightSeats = rowSeats.slice(leftCount); + const groups: any[][] = []; + + // Split seats into groups based on arrangement + if (arrangement.length === 1) { + // Single group (all seats together) + groups.push(rowSeats); + } else { + // Multiple groups with aisle separation + arrangement.forEach((_groupSize, groupIdx) => { + const startIdx = arrangement.slice(0, groupIdx).reduce((sum, size) => sum + size, 0); + const endIdx = arrangement.slice(0, groupIdx + 1).reduce((sum, size) => sum + size, 0); + const currentGroup = rowSeats.slice(startIdx, endIdx); + if (currentGroup.length > 0) groups.push(currentGroup); + }); + } + const rowNumber = rowSeats[0]?.row || 1; const shouldFlipArmchair = rowNumber % 2 === 0; const showSpacing = rowIdx % 2 === 1; return ( -
+
{shouldFlipArmchair && ( -
-
- {leftSeats.map((seat: any) => ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''} -
- ))} -
- {rightSeats.length > 0 &&
} - {rightSeats.length > 0 && ( -
- {rightSeats.map((seat: any) => ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''} -
- ))} +
+ {groups.map((group, gIdx) => ( +
+ {group.map((seat: any) => { + const seatLabel = seat.label || seat.number || seat.seatNumber || ''; + return ( +
+ {seatLabel} +
+ ); + })}
- )} -
- )} -
-
- {leftSeats.map((seat: any) => ( - ))}
- {rightSeats.length > 0 &&
} - {rightSeats.length > 0 && ( -
- {rightSeats.map((seat: any) => ( + )} +
+ {groups.map((group, gIdx) => ( +
+ {group.map((seat: any) => ( ))}
- )} + ))}
{!shouldFlipArmchair && ( -
-
- {leftSeats.map((seat: any) => ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''} -
- ))} -
- {rightSeats.length > 0 &&
} - {rightSeats.length > 0 && ( -
- {rightSeats.map((seat: any) => ( -
- {seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''} -
- ))} +
+ {groups.map((group, gIdx) => ( +
+ {group.map((seat: any) => { + const seatLabel = seat.label || seat.number || seat.seatNumber || ''; + return ( +
+ {seatLabel} +
+ ); + })}
- )} + ))}
)} - {showSpacing &&
} + {showSpacing &&
}
); })} @@ -456,6 +559,23 @@ export default function SeatsPage() { if (!selectedSchedule || !passengers.length) return null; + if (!coachId) { + return ( +
+
+

No coach selected

+

Please go back and select a coach type

+ +
+
+ ); + } + const allSelected = selectedSeats.length === passengers.length; const isBedCoach = selectedCoachData?.seatClass?.toLowerCase().includes('bed') || selectedCoachData?.mode?.toLowerCase().includes('bed'); diff --git a/apps/edr-passenger-web/portal/src/types/index.ts b/apps/edr-passenger-web/portal/src/types/index.ts index 532bef8d5..08203494a 100644 --- a/apps/edr-passenger-web/portal/src/types/index.ts +++ b/apps/edr-passenger-web/portal/src/types/index.ts @@ -39,6 +39,15 @@ export interface Schedule { availableSeats?: number; availabilityByClass?: Record; // API returns this faresByClass?: Array<{ seatClassName: string; baseFareMinor: number }>; // API returns this + coachTypes?: Array<{ + coachId: string; + coachTypeName: string; + coachTypeCode: string; + classes: Array<{ + name: string; + baseFareMinor: number; + }>; + }>; serviceClass?: string; status?: string; hasAvailability?: boolean;