Update seatmap configuration

This commit is contained in:
Roba Boru
2026-06-24 11:50:44 +03:00
parent 998282b3b5
commit 9320e0e9ab
10 changed files with 1316 additions and 389 deletions

View File

@@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiBody, ApiResponse } from '@nestjs/swagger';
import { FleetService } from './fleet.service';
import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoachesDto, CreateCoachTypeDto, UpdateCoachTypeDto, CreateClassDto, UpdateClassDto } from './fleet.dto';
import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoachesDto, CreateCoachTypeDto, UpdateCoachTypeDto, CreateClassDto, UpdateClassDto, GenerateSeatMapDto } from './fleet.dto';
import { JwtGuard } from '../../common/jwt.guard';
@ApiTags('Fleet')
@@ -317,6 +317,39 @@ export class FleetController {
return this.service.removeAssignment(id);
}
@Post('seatmap/generate')
@ApiOperation({
summary: 'Preview bed seat map — ECONOMY_BED or VIP_BED',
description: `Generates a structured seat map for bed coaches without persisting anything.
**ECONOMY_BED**: 6 beds per room — Left(Lower/Middle/Upper) + Right(Lower/Middle/Upper)
**VIP_BED**: 4 beds per room — Left(Lower/Upper) + Right(Lower/Upper)
Use this to preview the full flat seat list before creating coaches.`,
})
@ApiBody({ type: GenerateSeatMapDto })
@ApiResponse({
status: 201,
description: 'Generated seat map preview',
schema: {
example: {
coachCount: 1, roomsPerCoach: 2, roomType: 'ECONOMY_BED', bedsPerRoom: 6, totalBeds: 12,
seats: [
{ seat_id: 'C1-C1-R1-S1', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'LEFT', bed_type: 'LOWER', sequence_number: 1, status: 'AVAILABLE' },
{ seat_id: 'C1-C1-R1-S2', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'LEFT', bed_type: 'MIDDLE', sequence_number: 2, status: 'AVAILABLE' },
{ seat_id: 'C1-C1-R1-S3', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'LEFT', bed_type: 'UPPER', sequence_number: 3, status: 'AVAILABLE' },
{ seat_id: 'C1-C1-R1-S4', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'RIGHT', bed_type: 'LOWER', sequence_number: 4, status: 'AVAILABLE' },
{ seat_id: 'C1-C1-R1-S5', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'RIGHT', bed_type: 'MIDDLE', sequence_number: 5, status: 'AVAILABLE' },
{ seat_id: 'C1-C1-R1-S6', coach_id: 'C1', room_id: 'C1-R1', category: 'ECONOMY_BED', position: 'RIGHT', bed_type: 'UPPER', sequence_number: 6, status: 'AVAILABLE' },
],
},
},
})
generateSeatMap(@Body() dto: GenerateSeatMapDto) {
return this.service.generateSeatMapPreview(dto);
}
@Get('analytics')
@ApiOperation({ summary: 'Fleet analytics and occupancy metrics' })
@ApiResponse({ status: 200, description: 'Occupancy statistics' })

View File

@@ -12,9 +12,20 @@ export class CreateTrainDto {
export class CreateCoachDto {
@ApiProperty({ example: 'A-001', description: 'Unique coach number' }) @IsString() number: string;
@ApiProperty({ example: 'coach-type-uuid', description: 'Coach Type UUID' }) @IsString() coachTypeId: string;
@ApiProperty({ example: '2+2', description: 'Seat arrangement (e.g., "2+2", "3+2")' }) @IsString() arrangement: string;
@ApiProperty({ example: 60, description: 'Total seat capacity' }) @IsInt() capacity: number;
@ApiProperty({ example: '2+2', description: 'Seat arrangement for regular coaches (e.g., "2+2", "3+2"). Ignored for bed coaches.' }) @IsString() arrangement: string;
@ApiProperty({ example: 60, description: 'Total seat/bed capacity' }) @IsInt() capacity: number;
@ApiPropertyOptional({ example: 'ACTIVE', description: 'Status: ACTIVE, INACTIVE' }) @IsOptional() @IsString() status?: string;
@ApiPropertyOptional({
enum: ['ECONOMY_BED', 'VIP_BED'],
description: 'Bed coach category. Set to generate bed/sleeper compartments instead of regular seats. Overrides name-based detection.',
example: 'VIP_BED',
})
@IsOptional() @IsString() bedCategory?: 'ECONOMY_BED' | 'VIP_BED';
@ApiPropertyOptional({
example: 4,
description: 'Beds per compartment/room. Must be even (split equally left/right). Defaults: VIP_BED=4, ECONOMY_BED=6. Only applies when bedCategory is set.',
})
@IsOptional() @IsInt() bedsPerRoom?: number;
}
export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['number'] as const)) {
@@ -67,3 +78,14 @@ export class UpdateClassDto {
@IsBoolean()
isActive?: boolean;
}
export class GenerateSeatMapDto {
@ApiProperty({ example: 2, description: 'Number of coaches' })
@IsInt() coachCount: number;
@ApiProperty({ example: 9, description: 'Number of rooms (compartments) per coach' })
@IsInt() roomsPerCoach: number;
@ApiProperty({ enum: ['ECONOMY_BED', 'VIP_BED'], example: 'ECONOMY_BED', description: 'ECONOMY_BED = 6 beds/room (L/M/U × Left/Right), VIP_BED = 4 beds/room (L/U × Left/Right)' })
@IsString() roomType: 'ECONOMY_BED' | 'VIP_BED';
}

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoachesDto, CreateCoachTypeDto, UpdateCoachTypeDto, CreateClassDto, UpdateClassDto } from './fleet.dto';
import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoachesDto, CreateCoachTypeDto, UpdateCoachTypeDto, CreateClassDto, UpdateClassDto, GenerateSeatMapDto } from './fleet.dto';
import { SeatKind } from '@prisma/client';
// Parses '2+2' → [2, 2], '2+2+2' → [2, 2, 2]
@@ -33,41 +33,97 @@ function isAisleCol(colIndex: number, groups: number[]): boolean {
return false;
}
function buildSeats(coachId: string, coachNumber: string, arrangement: string, capacity: number, seatClass?: string): SeatRow[] {
type BedCategory = 'ECONOMY_BED' | 'VIP_BED' | null;
// Default beds per room for each category when not explicitly configured
const DEFAULT_BEDS_PER_ROOM: Record<'ECONOMY_BED' | 'VIP_BED', number> = {
VIP_BED: 4,
ECONOMY_BED: 6,
};
// Name-based fallback: checks if 'vip' is present for any bed/sleeper coach type
function detectBedCategory(coachTypeName: string): BedCategory {
const name = coachTypeName.toLowerCase();
const isBed = name.includes('bed') || name.includes('sleeper') || name.includes('couchette');
if (!isBed) return null;
if (name.includes('vip')) return 'VIP_BED';
return 'ECONOMY_BED';
}
// Resolves bed type names per side from beds-per-side count:
// 2/side → ['LOWER','UPPER'] (VIP style)
// 3/side → ['LOWER','MIDDLE','UPPER'] (Economy style)
function resolveBedTypes(bedsPerSide: number): string[] {
if (bedsPerSide === 1) return ['LOWER'];
if (bedsPerSide === 2) return ['LOWER', 'UPPER'];
if (bedsPerSide === 3) return ['LOWER', 'MIDDLE', 'UPPER'];
return Array.from({ length: bedsPerSide }, (_, i) => {
if (i === 0) return 'LOWER';
if (i === bedsPerSide - 1) return 'UPPER';
return 'MIDDLE';
});
}
// Generates the flat seat/bed list for a bed coach.
// Row = room number; col = position-relative label (L1, L2 … R1, R2 …).
function buildBedSeats(
coachId: string,
capacity: number,
bedsPerRoom: number,
): SeatRow[] {
const bedsPerSide = bedsPerRoom / 2;
const bedTypeNames = resolveBedTypes(bedsPerSide);
const layout: Array<{ position: 'LEFT' | 'RIGHT'; bedType: string }> = [
...bedTypeNames.map(bt => ({ position: 'LEFT' as const, bedType: bt })),
...bedTypeNames.map(bt => ({ position: 'RIGHT' as const, bedType: bt })),
];
const roomCount = Math.ceil(capacity / bedsPerRoom);
const seats: SeatRow[] = [];
let seatNumber = 1;
for (let room = 1; room <= roomCount; room++) {
const posCount: Record<string, number> = {};
for (let slot = 0; slot < bedsPerRoom && seats.length < capacity; slot++) {
const { position, bedType } = layout[slot];
posCount[position] = (posCount[position] ?? 0) + 1;
const col = `${position[0]}${posCount[position]}`;
seats.push({
coachId,
row: room,
col,
seatNumber: `${seatNumber}`,
kind: SeatKind.STANDARD,
bedPosition: bedType.toLowerCase(),
isWindow: false,
isAisle: false,
});
seatNumber++;
}
}
return seats;
}
function buildRegularSeats(coachId: string, arrangement: string, capacity: number): SeatRow[] {
const cols = seatCols(arrangement);
const groups = parseArrangement(arrangement);
const seats: SeatRow[] = [];
let row = 1;
let seatNumber = 1;
let seatIndex = 0;
const isBedCoach = seatClass?.toLowerCase().includes('bed');
const totalCols = cols.length;
while (seatIndex < capacity) {
for (let ci = 0; ci < cols.length && seatIndex < capacity; ci++) {
const col = cols[ci];
let bedPosition = null;
// Set bedPosition for bed coaches based on ROW cycling (not seat number)
if (isBedCoach) {
if (totalCols === 3) {
// Economy bed (3-row cycle): upper, middle, lower
if (row % 3 === 1) bedPosition = 'upper';
else if (row % 3 === 2) bedPosition = 'middle';
else bedPosition = 'lower';
} else if (totalCols === 2) {
// VIP bed (2-row cycle): upper, lower
bedPosition = row % 2 === 1 ? 'upper' : 'lower';
}
}
seats.push({
coachId,
row,
col,
seatNumber: `${seatNumber}`,
kind: SeatKind.STANDARD,
bedPosition,
bedPosition: null,
isWindow: isWindowCol(ci, groups),
isAisle: isAisleCol(ci, groups),
});
seatNumber++;
seatIndex++;
@@ -84,6 +140,8 @@ type SeatRow = {
seatNumber: string;
kind: SeatKind;
bedPosition?: string | null;
isWindow?: boolean;
isAisle?: boolean;
};
@Injectable()
@@ -332,8 +390,20 @@ export class FleetService {
});
if (dto.capacity > 0) {
const seatClass = coach.coachType?.name || '';
const seats = buildSeats(coach.id, coach.number, dto.arrangement, dto.capacity, seatClass);
// dto.bedCategory takes priority; fall back to name-based detection
const bedCategory: BedCategory = dto.bedCategory ?? detectBedCategory(coach.coachType?.name || '');
let seats: SeatRow[];
if (bedCategory) {
const bedsPerRoom = dto.bedsPerRoom ?? DEFAULT_BEDS_PER_ROOM[bedCategory];
if (bedsPerRoom < 2 || bedsPerRoom % 2 !== 0) {
throw new BadRequestException('bedsPerRoom must be an even number ≥ 2');
}
seats = buildBedSeats(coach.id, dto.capacity, bedsPerRoom);
} else {
seats = buildRegularSeats(coach.id, dto.arrangement, dto.capacity);
}
await this.prisma.seat.createMany({ data: seats });
}
@@ -425,6 +495,50 @@ export class FleetService {
return this.prisma.coachAssignment.delete({ where: { id } });
}
async generateSeatMapPreview(dto: GenerateSeatMapDto) {
const { coachCount, roomsPerCoach, roomType } = dto;
const bedsPerRoom = DEFAULT_BEDS_PER_ROOM[roomType];
const bedTypeNames = resolveBedTypes(bedsPerRoom / 2);
const layout: Array<{ position: 'LEFT' | 'RIGHT'; bedType: string }> = [
...bedTypeNames.map(bt => ({ position: 'LEFT' as const, bedType: bt })),
...bedTypeNames.map(bt => ({ position: 'RIGHT' as const, bedType: bt })),
];
const seats: object[] = [];
let globalSeq = 1;
for (let c = 1; c <= coachCount; c++) {
const coachLabel = `C${c}`;
for (let r = 1; r <= roomsPerCoach; r++) {
const roomLabel = `R${r}`;
const posCount: Record<string, number> = {};
for (let s = 0; s < bedsPerRoom; s++) {
const { position, bedType } = layout[s];
posCount[position] = (posCount[position] ?? 0) + 1;
seats.push({
seat_id: `${coachLabel}-${roomLabel}-S${globalSeq}`,
coach_id: coachLabel,
room_id: `${coachLabel}-${roomLabel}`,
category: roomType,
position,
col: `${position[0]}${posCount[position]}`,
bed_type: bedType,
sequence_number: globalSeq,
status: 'AVAILABLE',
});
globalSeq++;
}
}
}
return {
coachCount,
roomsPerCoach,
roomType,
bedsPerRoom,
totalBeds: seats.length,
seats,
};
}
async getAnalytics() {
const [totalTrains, totalSchedules, totalSeats, bookedSeats] = await Promise.all([
this.prisma.train.count(),

View File

@@ -835,7 +835,7 @@ export class PaymentsService {
status: 'CONFIRMED',
totalMinor: booking.totalMinor,
currency: booking.currency,
},
} as any,
});
const journeySegments: any[] = [];

View File

@@ -12,19 +12,19 @@ export class SeatsController {
// ── Seat Map ──────────────────────────────────────────────────────────────
@Get('seatmap/:scheduleId')
@ApiOperation({
summary: 'Get seat map with real-time availability by class',
description: `Returns seat map for a schedule with availability by seat class:
- Economy Regular
- Economy Bed
- VIP Bed
Shows seat status: AVAILABLE, BOOKED, HELD, BLOCKED`
@ApiOperation({
summary: 'Get seat map filtered by coach type',
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches.`
})
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
@ApiQuery({ name: 'coachId', required: false, description: 'Filter by coach UUID' })
@ApiResponse({ status: 200, description: 'Returns coaches with seats and seat class info' })
getSeatMap(@Param('scheduleId') scheduleId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(scheduleId, coachId); }
@ApiQuery({ name: 'coachTypeId', required: false, description: 'Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)' })
@ApiResponse({ status: 200, description: 'List of coaches of the given type with their seats and availability' })
getSeatMap(
@Param('scheduleId') scheduleId: string,
@Query('coachTypeId') coachTypeId?: string,
) {
return this.service.getSeatMap(scheduleId, coachTypeId);
}
// ── Hold / Release ────────────────────────────────────────────────────────
@Get('holds')

View File

@@ -11,9 +11,18 @@ export class SeatsService {
private segmentsService: SegmentsService,
) {}
async getSeatMap(scheduleId: string, coachId?: string, originStationId?: string, destinationStationId?: string) {
async getSeatMap(scheduleId: string, coachTypeId?: string) {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: scheduleId },
select: { originStationId: true, destinationStationId: true },
});
if (!schedule) throw new NotFoundException('Schedule not found');
const assignments = await this.prisma.coachAssignment.findMany({
where: { scheduleId, ...(coachId ? { coachId } : {}) },
where: {
scheduleId,
...(coachTypeId ? { coach: { coachTypeId } } : {}),
},
include: {
coach: {
include: {
@@ -26,48 +35,110 @@ export class SeatsService {
});
const allSeatIds = assignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id));
const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds, originStationId, destinationStationId);
const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds, schedule.originStationId, schedule.destinationStationId);
return {
coaches: assignments.map((a) => {
const allSeats = a.coach.seats;
const allSeats = a.coach.seats;
const coachTypeName = a.coach.coachType?.name ?? '';
const seatClassNames = a.coach.coachType.seatClasses.map((sc: any) => sc.name);
const isBedCoach = this.isBedCoach(coachTypeName);
// Compute actual beds-per-room from first room to correctly identify VIP (4) vs Economy (6)
const bedsPerRoom = isBedCoach
? allSeats.filter((s: any) => s.row === (allSeats[0] as any)?.row).length
: 0;
const bedCategory = isBedCoach ? this.getBedCategory(coachTypeName, bedsPerRoom) : null;
return {
id: a.coach.id,
assignmentId: a.id,
coachNumber: a.coach.number,
label: a.coach.number,
mode: a.coach.status,
name: `Coach ${a.coach.number}`,
seatClasses: seatClassNames,
seatClass: seatClassNames.length > 0 ? seatClassNames[0] : 'Standard',
const mappedSeats = allSeats.map((s: any) => ({
id: s.id,
seatNumber: s.seatNumber,
label: s.seatNumber,
status: effectiveStatuses.get(s.id) ?? s.status,
kind: s.kind,
row: s.row,
col: s.col,
isWindow: s.isWindow,
isAisle: s.isAisle,
// Bed-specific fields
...(isBedCoach ? {
room_id: `${a.coach.id}-R${s.row}`,
category: bedCategory,
position: this.colToPosition(s.col),
bed_type: this.bedPositionToType(s.bedPosition),
bedPosition: s.bedPosition,
} : {
bedPosition: s.bedPosition,
}),
}));
const base = {
id: a.coach.id,
assignmentId: a.id,
coachNumber: a.coach.number,
label: a.coach.number,
mode: a.coach.status,
name: `Coach ${a.coach.number}`,
coachTypeName,
isBedCoach,
bedCategory,
seatClasses: seatClassNames,
seatClass: seatClassNames.length > 0 ? seatClassNames[0] : 'Standard',
positionNumber: a.positionNumber,
seatArrangement: a.coach.arrangement,
totalSeats: a.coach.capacity,
seats: allSeats.map((s) => ({
id: s.id,
seatNumber: s.seatNumber,
number: s.seatNumber,
label: s.seatNumber,
status: effectiveStatuses.get(s.id) ?? s.status,
kind: s.kind,
row: s.row,
col: s.col,
isWindow: s.isWindow,
isAisle: s.isAisle,
bedPosition: s.bedPosition,
coach: {
id: a.coach.id,
coachNumber: a.coach.number,
label: a.coach.number,
},
})),
totalSeats: a.coach.capacity,
};
if (isBedCoach) {
// Group seats into rooms; row = room number
const roomMap = new Map<number, any[]>();
for (const seat of mappedSeats) {
if (!roomMap.has(seat.row)) roomMap.set(seat.row, []);
roomMap.get(seat.row)!.push(seat);
}
const rooms = Array.from(roomMap.entries())
.sort(([a], [b]) => a - b)
.map(([roomNumber, beds]) => ({
room_id: `${a.coach.id}-R${roomNumber}`,
roomNumber,
category: bedCategory,
totalBeds: beds.length,
beds,
}));
return { ...base, rooms, seats: mappedSeats };
}
return { ...base, seats: mappedSeats };
}),
};
}
private isBedCoach(coachTypeName: string): boolean {
const n = coachTypeName.toLowerCase();
return n.includes('bed') || n.includes('sleeper') || n.includes('couchette');
}
private getBedCategory(coachTypeName: string, bedsPerRoom?: number): 'ECONOMY_BED' | 'VIP_BED' {
const n = coachTypeName.toLowerCase();
// Explicit VIP name check first
if (n.includes('vip')) return 'VIP_BED';
// Fall back to actual beds-per-room count: 4 = VIP, 6 = Economy
if (bedsPerRoom === 4) return 'VIP_BED';
return 'ECONOMY_BED';
}
// col format: L1, L2, L3, R1, R2, R3
private colToPosition(col: string): 'LEFT' | 'RIGHT' {
return col?.startsWith('R') ? 'RIGHT' : 'LEFT';
}
private bedPositionToType(bedPosition: string | null): 'LOWER' | 'MIDDLE' | 'UPPER' | null {
if (!bedPosition) return null;
const map: Record<string, 'LOWER' | 'MIDDLE' | 'UPPER'> = {
lower: 'LOWER', middle: 'MIDDLE', upper: 'UPPER',
};
return map[bedPosition.toLowerCase()] ?? null;
}
async resolveEffectiveStatuses(
scheduleId: string,
seatIds: string[],
@@ -426,7 +497,7 @@ export class SeatsService {
// Delete the Journey (and its JourneySegments) scoped to this booking.
async releaseSeats(bookingId: string) {
await this.prisma.journey.deleteMany({ where: { bookingId } });
await this.prisma.journey.deleteMany({ where: { bookingId } as any });
}
async autoAssignSeats(scheduleId: string, count: number, seatClassName: string): Promise<string[]> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -142,8 +142,8 @@ export default function ResultsPage() {
? (outboundSchedules.length > 0 && inboundSchedules.length > 0)
: outboundSchedules.length > 0;
const handleSelectCoachType = (scheduleId: string, coachId: string, coachTypeCode: string, coachTypeName: string) => {
setSelectedCoachTypes(prev => ({ ...prev, [scheduleId]: { id: coachId, code: coachTypeCode, name: coachTypeName } }));
const handleSelectCoachType = (scheduleId: string, coachTypeCode: string, coachTypeName: string) => {
setSelectedCoachTypes(prev => ({ ...prev, [scheduleId]: { id: coachTypeCode, code: coachTypeCode, name: coachTypeName } }));
};
const handleSelect = (schedule: Schedule, isOutbound: boolean = false) => {
@@ -156,7 +156,7 @@ export default function ResultsPage() {
}
// Find the coach type to get pricing info
const coachType = schedule.coachTypes?.find(ct => ct.coachId === selectedCoachType.id);
const coachType = schedule.coachTypes?.find(ct => ct.coachTypeCode === selectedCoachType.id);
const minFare = coachType?.classes.length ? Math.min(...coachType.classes.map(c => c.baseFareMinor)) : 0;
const hours = Math.floor((schedule.durationMinutes || 0) / 60);
@@ -175,7 +175,7 @@ export default function ResultsPage() {
baseFareChild: minFare,
selectedSeatClass: selectedCoachType.name,
selectedSeatClassName: selectedCoachType.name,
selectedCoachId: selectedCoachType.id,
selectedCoachTypeId: selectedCoachType.id,
selectedCoachTypeCode: selectedCoachType.code,
selectedCoachTypeName: selectedCoachType.name,
};
@@ -533,14 +533,14 @@ export default function ResultsPage() {
{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 isSelected = selectedCoachType?.id === coachType.coachTypeCode;
const minPrice = coachType.classes.length ? Math.min(...coachType.classes.map((c: any) => c.baseFareMinor)) : 0;
const CoachIcon = getCoachIcon(coachType.coachTypeName);
return (
<button
key={coachType.coachId}
onClick={() => handleSelectCoachType(scheduleId, coachType.coachId, coachType.coachTypeCode, coachType.coachTypeName)}
onClick={() => handleSelectCoachType(scheduleId, 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-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]'

View File

@@ -364,11 +364,10 @@ export default function ReviewPage() {
return null;
}
const displaySchedule = isRoundTrip ? outboundSchedule : selectedSchedule;
const outboundBaseFare = isRoundTrip && outboundSchedule ? passengers.reduce((sum) => sum + (outboundSchedule.baseFareAdult || 0), 0) : 0;
const inboundBaseFare = isRoundTrip && inboundSchedule ? passengers.reduce((sum) => sum + (inboundSchedule.baseFareAdult || 0), 0) : 0;
const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce((sum, _, i) => {
const baseFare = isRoundTrip ? (outboundBaseFare + inboundBaseFare) : passengers.reduce((sum) => {
const farePerPassenger = selectedSchedule?.baseFareAdult || (selectedSchedule as any)?.fareAdult || (selectedSchedule as any)?.price || 0;
return sum + farePerPassenger;
}, 0);

File diff suppressed because it is too large Load Diff