mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Update seat map and sms payload
This commit is contained in:
@@ -34,7 +34,7 @@ export class SingleMessageDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
sms: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class BulkMessagesDto {
|
||||
|
||||
@@ -21,7 +21,7 @@ export class NotificationsService {
|
||||
) {
|
||||
this.channels = new Map<NotificationChannelType, NotificationChannel>([
|
||||
['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],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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<string> }
|
||||
{ coachType: any; classNames: Set<string>; 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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [selectedCoachTypes, setSelectedCoachTypes] = useState<Record<string, { id: string; code: string; name: string }>>({});
|
||||
const [outboundScheduleData, setOutboundScheduleData] = useState<any>(null);
|
||||
const [classModal, setClassModal] = useState<Schedule | null>(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'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 mb-4">per adult</div>
|
||||
{selectedClass && (
|
||||
{selectedCoachType && (
|
||||
<p className="text-xs text-primary font-semibold mb-2">
|
||||
{selectedClass.replace(/_/g, ' ')} selected
|
||||
{selectedCoachType.name} selected
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setClassModal({ ...schedule, isOutbound } as any)}
|
||||
className="btn-secondary w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
{selectedClass ? 'Change class' : 'Select class'}
|
||||
{selectedCoachType ? 'Change' : 'Select'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="fixed inset-0 z-[99] bg-black/50 backdrop-blur-sm" onClick={() => setClassModal(null)} />
|
||||
<div className="fixed inset-y-0 right-0 z-[100] w-full sm:w-[640px] bg-white dark:bg-gray-900 shadow-2xl flex flex-col"
|
||||
style={{ animation: 'drawer-slide-in 0.25s cubic-bezier(0.32,0.72,0,1)' }}
|
||||
<div className="fixed inset-y-0 right-0 z-[100] w-full sm:w-[680px] lg:w-[95vw] xl:w-[90vw] 2xl:w-[85vw] max-w-[1400px] bg-white dark:bg-gray-900 shadow-2xl flex flex-col"
|
||||
style={{ animation: 'drawer-slide-in 0.3s cubic-bezier(0.22,1,0.36,1)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-gray-800 flex-shrink-0">
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-100 dark:border-gray-800 flex-shrink-0">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-gray-900 dark:text-white">Select Class</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 flex items-center gap-1">
|
||||
<Train className="w-3 h-3" />
|
||||
{classModal.trainNumber} · {classModal.origin?.name} → {classModal.destination?.name}
|
||||
<h2 className="text-lg font-bold text-gray-900 dark:text-white">Choose Your Coach</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-1.5">
|
||||
<Train className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{classModal.trainNumber}</span>
|
||||
<span className="text-gray-400">·</span>
|
||||
<span>{classModal.origin?.name} → {classModal.destination?.name}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClassModal(null)}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
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"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{classModal.faresByClass && Array.isArray(classModal.faresByClass) && classModal.faresByClass.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{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');
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{coachTypes.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 pt-2">
|
||||
{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 (
|
||||
<button
|
||||
key={fareClass.seatClassName}
|
||||
onClick={() => isAvailable && handleSelectClass(scheduleId, fareClass.seatClassName)}
|
||||
disabled={!isAvailable}
|
||||
className={`relative w-full p-4 rounded-xl border-2 text-left transition-all ${
|
||||
key={coachType.coachId}
|
||||
onClick={() => handleSelectCoachType(scheduleId, coachType.coachId, coachType.coachTypeCode, coachType.coachTypeName)}
|
||||
className={`group relative w-full p-5 rounded-2xl border-2 text-left transition-all duration-200 ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 dark:bg-primary/10 shadow-sm'
|
||||
: isAvailable
|
||||
? 'border-gray-200 dark:border-gray-700 hover:border-primary/50 hover:shadow-sm'
|
||||
: 'border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 opacity-50 cursor-not-allowed'
|
||||
? '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 && (
|
||||
<div className="absolute top-3 right-3 w-6 h-6 bg-primary rounded-full flex items-center justify-center">
|
||||
<Check className="w-3.5 h-3.5 text-white" />
|
||||
<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">
|
||||
<Check className="w-4 h-4 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
<p className="font-semibold text-gray-900 dark:text-white pr-8">
|
||||
{fareClass.seatClassName.replace(/_/g, ' ')}
|
||||
</p>
|
||||
<p className="text-xl font-bold text-primary dark:text-white mt-2">
|
||||
ETB {((fareClass.baseFareMinor || 0) / 100).toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">per adult</p>
|
||||
<p className={`text-xs mt-2 font-medium ${
|
||||
isAvailable ? 'text-green-600 dark:text-green-400' : 'text-red-500'
|
||||
}`}>
|
||||
{isAvailable
|
||||
? `${availableSeats} ${isBedClass ? 'bed' : 'seat'}${availableSeats !== 1 ? 's' : ''} available`
|
||||
: 'Sold out'}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start gap-4 pr-2">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 transition-all ${
|
||||
isSelected
|
||||
? 'bg-primary/15 dark:bg-primary/25 shadow-inner'
|
||||
: 'bg-gray-100 dark:bg-gray-700 group-hover:bg-primary/10'
|
||||
}`}>
|
||||
<CoachIcon className={`w-6 h-6 transition-colors ${
|
||||
isSelected ? 'text-primary' : 'text-gray-600 dark:text-gray-400 group-hover:text-primary'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<div>
|
||||
<h3 className="font-bold text-base text-gray-900 dark:text-white leading-tight">
|
||||
{coachType.coachTypeName}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">From</span>
|
||||
<span className={`text-2xl font-bold tracking-tight ${
|
||||
isSelected ? 'text-primary' : 'text-gray-900 dark:text-white'
|
||||
}`}>
|
||||
{(minPrice / 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-600 dark:text-gray-400">ETB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{coachType.classes.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200/60 dark:border-gray-700/60">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-xs font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Class Options
|
||||
</p>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
{coachType.classes.length} available
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{coachType.classes.map((cls: any, idx: number) => (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<CoachIcon className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{cls.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-base font-bold tabular-nums text-gray-900 dark:text-white">
|
||||
{(cls.baseFareMinor / 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
ETB
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center py-8 text-gray-400 text-sm">No seat classes available</p>
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<Train className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">No coach types available for this journey</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-5 py-4 border-t border-gray-100 dark:border-gray-800 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => { if (selectedClass) { handleSelect(classModal, isOutbound); } }}
|
||||
disabled={!selectedClass}
|
||||
className="w-full flex items-center justify-center gap-2 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all disabled:opacity-40 disabled:cursor-not-allowed shadow-lg"
|
||||
>
|
||||
<span>{isRoundTrip && isOutbound ? 'Continue to Return Flight' : 'Continue'}</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
{!selectedClass && (
|
||||
<p className="text-center text-xs text-gray-400 mt-2">Please select a class to continue</p>
|
||||
)}
|
||||
<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>
|
||||
<style>{`@keyframes drawer-slide-in{from{transform:translateX(100%)}to{transform:translateX(0)}}`}</style>
|
||||
<style>{`
|
||||
@keyframes drawer-slide-in{from{transform:translateX(100%)}to{transform:translateX(0)}}
|
||||
@keyframes fade-in-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes scale-in{from{transform:scale(0)}to{transform:scale(1)}}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -285,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,
|
||||
@@ -312,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;
|
||||
}
|
||||
|
||||
@@ -324,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,
|
||||
@@ -356,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <div className="text-xs text-muted-foreground">No seats</div>;
|
||||
@@ -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<string, any[]>();
|
||||
|
||||
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 (
|
||||
<div className="space-y-2 w-40">
|
||||
{validSeats.map((seat: any) => {
|
||||
const rowNumber = seat.row || 1;
|
||||
const shouldFlipIcon = rowNumber % 2 === 0;
|
||||
<div className="space-y-4">
|
||||
{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 (
|
||||
<div key={seat.id}>
|
||||
{shouldFlipIcon && (
|
||||
<div className="w-24 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''}
|
||||
<div key={`bed-group-${seatNumber}`} className="pb-4 border-b-2 border-dashed border-gray-300 dark:border-gray-600 last:border-b-0">
|
||||
<div className="flex items-center gap-3">
|
||||
{shouldFlipIcon && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{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 (
|
||||
<div key={seat.id} className="w-20 text-xs font-bold text-foreground text-right">
|
||||
{seatLabel ? `${seatLabel} ${bedLabelFull}` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{orderedBeds.map((seat: any) => (
|
||||
<SeatButton
|
||||
key={seat.id}
|
||||
seat={seat}
|
||||
isSelected={selectedSeats.includes(seat.id)}
|
||||
onToggle={handleSeatClick}
|
||||
isBedCoach={true}
|
||||
bedLabel={getBedLabel(seat.bedPosition)}
|
||||
coachSeatClass={seatClassStr}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<SeatButton
|
||||
key={seat.id}
|
||||
seat={seat}
|
||||
isSelected={selectedSeats.includes(seat.id)}
|
||||
onToggle={handleSeatClick}
|
||||
isBedCoach={true}
|
||||
bedLabel={getBedLabel(seat.bedPosition)}
|
||||
coachSeatClass={seatClassStr}
|
||||
/>
|
||||
|
||||
{!shouldFlipIcon && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{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 (
|
||||
<div key={seat.id} className="w-20 text-xs font-bold text-foreground">
|
||||
{seatLabel ? `${seatLabel} ${bedLabelFull}` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!shouldFlipIcon && (
|
||||
<div className="w-24 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -349,68 +460,64 @@ export default function SeatsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const processedRows = new Set();
|
||||
// Regular seats with row/column arrangement
|
||||
const rowMap = new Map<number, any[]>();
|
||||
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 (
|
||||
<div className="space-y-0">
|
||||
{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 (
|
||||
<div key={`row-${rowSeats[0]?.id}`}>
|
||||
<div key={`row-${rowNumber}-${rowSeats[0]?.id}`}>
|
||||
{shouldFlipArmchair && (
|
||||
<div className="flex gap-0.5 justify-start text-xs text-muted-foreground mb-1">
|
||||
<div className="flex gap-0.5">
|
||||
{leftSeats.map((seat: any) => (
|
||||
<div key={`num-before-left-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{rightSeats.length > 0 && <div className="w-3" />}
|
||||
{rightSeats.length > 0 && (
|
||||
<div className="flex gap-0.5">
|
||||
{rightSeats.map((seat: any) => (
|
||||
<div key={`num-before-right-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-3 justify-start text-xs text-muted-foreground mb-1">
|
||||
{groups.map((group, gIdx) => (
|
||||
<div key={`num-before-group-${gIdx}`} className="flex gap-0.5">
|
||||
{group.map((seat: any) => {
|
||||
const seatLabel = seat.label || seat.number || seat.seatNumber || '';
|
||||
return (
|
||||
<div key={`num-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold leading-3 text-foreground">
|
||||
{seatLabel}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-0.5 justify-start">
|
||||
<div className="flex gap-0.5">
|
||||
{leftSeats.map((seat: any) => (
|
||||
<SeatButton
|
||||
key={seat.id}
|
||||
seat={seat}
|
||||
isSelected={selectedSeats.includes(seat.id)}
|
||||
onToggle={handleSeatClick}
|
||||
isBedCoach={false}
|
||||
bedLabel=""
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{rightSeats.length > 0 && <div className="w-3" />}
|
||||
{rightSeats.length > 0 && (
|
||||
<div className="flex gap-0.5">
|
||||
{rightSeats.map((seat: any) => (
|
||||
)}
|
||||
<div className="flex gap-3 justify-start">
|
||||
{groups.map((group, gIdx) => (
|
||||
<div key={`group-${gIdx}`} className="flex gap-0.5">
|
||||
{group.map((seat: any) => (
|
||||
<SeatButton
|
||||
key={seat.id}
|
||||
seat={seat}
|
||||
@@ -418,35 +525,31 @@ export default function SeatsPage() {
|
||||
onToggle={handleSeatClick}
|
||||
isBedCoach={false}
|
||||
bedLabel=""
|
||||
coachSeatClass={seatClassStr}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!shouldFlipArmchair && (
|
||||
<div className="flex gap-0.5 justify-start text-xs text-muted-foreground mb-1">
|
||||
<div className="flex gap-0.5">
|
||||
{leftSeats.map((seat: any) => (
|
||||
<div key={`num-left-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{rightSeats.length > 0 && <div className="w-3" />}
|
||||
{rightSeats.length > 0 && (
|
||||
<div className="flex gap-0.5">
|
||||
{rightSeats.map((seat: any) => (
|
||||
<div key={`num-right-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
|
||||
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-3 justify-start text-xs text-muted-foreground mb-1">
|
||||
{groups.map((group, gIdx) => (
|
||||
<div key={`num-after-group-${gIdx}`} className="flex gap-0.5">
|
||||
{group.map((seat: any) => {
|
||||
const seatLabel = seat.label || seat.number || seat.seatNumber || '';
|
||||
return (
|
||||
<div key={`num-${seat.id}`} className="w-10 h-4 flex items-center justify-center text-xs font-bold leading-3 text-foreground">
|
||||
{seatLabel}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSpacing && <div className="h-2" />}
|
||||
{showSpacing && <div className="h-3 border-b border-gray-200 dark:border-gray-700" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -456,6 +559,23 @@ export default function SeatsPage() {
|
||||
|
||||
if (!selectedSchedule || !passengers.length) return null;
|
||||
|
||||
if (!coachId) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-8 text-center max-w-md">
|
||||
<p className="text-red-500 font-medium mb-2">No coach selected</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">Please go back and select a coach type</p>
|
||||
<button
|
||||
onClick={() => router.push('/booking/results')}
|
||||
className="btn-primary"
|
||||
>
|
||||
Back to Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const allSelected = selectedSeats.length === passengers.length;
|
||||
const isBedCoach = selectedCoachData?.seatClass?.toLowerCase().includes('bed') || selectedCoachData?.mode?.toLowerCase().includes('bed');
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@ export interface Schedule {
|
||||
availableSeats?: number;
|
||||
availabilityByClass?: Record<string, number>; // 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;
|
||||
|
||||
Reference in New Issue
Block a user