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 { SeatKind } from '@prisma/client'; // Parses '2+2' → [2, 2], '2+2+2' → [2, 2, 2] function parseArrangement(arrangement: string): number[] { return arrangement.split('+').map((n) => parseInt(n, 10)); } // Derives column labels from arrangement: '2+2' → ['A','B','C','D'] function seatCols(arrangement: string): string[] { const groups = parseArrangement(arrangement); const total = groups.reduce((s, n) => s + n, 0); return Array.from({ length: total }, (_, i) => String.fromCharCode(65 + i)); } // Returns true if column is a window seat function isWindowCol(colIndex: number, groups: number[]): boolean { const total = groups.reduce((s, n) => s + n, 0); return colIndex === 0 || colIndex === total - 1; } // Returns true if column is an aisle seat function isAisleCol(colIndex: number, groups: number[]): boolean { let cursor = 0; for (const g of groups) { cursor += g; const leftAisle = cursor - 1; const rightAisle = cursor; if (colIndex === leftAisle || colIndex === rightAisle) return true; } return false; } function buildSeats(coachId: string, coachNumber: string, arrangement: string, capacity: number, seatClass?: string): 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, }); seatNumber++; seatIndex++; } row++; } return seats; } type SeatRow = { coachId: string; row: number; col: string; seatNumber: string; kind: SeatKind; bedPosition?: string | null; }; @Injectable() export class FleetService { constructor(private prisma: PrismaService) {} async createCoachType(dto: CreateCoachTypeDto) { return this.prisma.coachType.create({ data: { code: dto.code, name: dto.name, type: dto.type || 'passenger', }, include: { seatClasses: true, coaches: true, }, }); } async getCoachTypes() { return this.prisma.coachType.findMany({ include: { seatClasses: true, coaches: true, }, orderBy: { createdAt: 'desc' }, }); } async updateCoachType(id: string, dto: UpdateCoachTypeDto) { const coachType = await this.prisma.coachType.findUnique({ where: { id } }); if (!coachType) throw new NotFoundException('Coach type not found'); const data: any = {}; if (dto.code !== undefined) data.code = dto.code; if (dto.name !== undefined) data.name = dto.name; if (dto.type !== undefined) data.type = dto.type; return this.prisma.coachType.update({ where: { id }, data, include: { seatClasses: true, coaches: true, }, }); } async deleteCoachType(id: string) { const coachType = await this.prisma.coachType.findUnique({ where: { id } }); if (!coachType) throw new NotFoundException('Coach type not found'); return this.prisma.coachType.delete({ where: { id } }); } async createClass(dto: CreateClassDto) { return this.prisma.seatClass.create({ data: { coachTypeId: dto.coachTypeId, name: dto.name, description: dto.description, baseFareMinor: dto.baseFareMinor, }, }); } async getClasses(coachTypeId?: string) { const where = coachTypeId ? { coachTypeId } : {}; return this.prisma.seatClass.findMany({ where, include: { coachType: true }, orderBy: { createdAt: 'desc' }, }); } async updateClass(id: string, dto: UpdateClassDto) { const seatClass = await this.prisma.seatClass.findUnique({ where: { id } }); if (!seatClass) throw new NotFoundException('Seat class not found'); const updateData: any = { coachTypeId: dto.coachTypeId, name: dto.name, description: dto.description, baseFareMinor: dto.baseFareMinor, }; if (dto.isActive !== undefined) { updateData.isActive = dto.isActive; } return this.prisma.seatClass.update({ where: { id }, data: updateData, include: { coachType: true }, }); } async deleteClass(id: string) { const seatClass = await this.prisma.seatClass.findUnique({ where: { id } }); if (!seatClass) throw new NotFoundException('Seat class not found'); return this.prisma.seatClass.delete({ where: { id } }); } createSeatClass(dto: CreateClassDto) { return this.createClass(dto); } getSeatClasses(coachTypeId?: string) { return this.getClasses(coachTypeId); } async updateSeatClass(id: string, dto: UpdateClassDto) { return this.updateClass(id, dto); } async deleteSeatClass(id: string) { return this.deleteClass(id); } getTrains() { return this.prisma.train.findMany({ include: { schedules: { take: 5, orderBy: { departureAt: 'desc' } } } }); } createTrain(dto: CreateTrainDto) { return this.prisma.train.create({ data: dto }); } async updateTrain(id: string, dto: CreateTrainDto) { const train = await this.prisma.train.findUnique({ where: { id } }); if (!train) throw new NotFoundException('Train not found'); return this.prisma.train.update({ where: { id }, data: dto }); } async deleteTrain(id: string) { const train = await this.prisma.train.findUnique({ where: { id } }); if (!train) throw new NotFoundException('Train not found'); return this.prisma.train.delete({ where: { id } }); } async getCoach(id: string) { const coach = await this.prisma.coach.findUnique({ where: { id }, include: { coachType: true, seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] }, assignments: { include: { schedule: { include: { originStation: true, destinationStation: true } } }, orderBy: { schedule: { departureAt: 'desc' } }, take: 5, }, }, }); if (!coach) throw new NotFoundException('Coach not found'); return coach; } async listCoaches(dto: ListCoachesDto) { const where: any = {}; if (dto.status) where.status = dto.status; if (dto.scheduleId) where.assignments = { some: { scheduleId: dto.scheduleId } }; return this.prisma.coach.findMany({ where, include: { coachType: true }, orderBy: { sequence: 'asc' }, }); } async createCoach(dto: CreateCoachDto) { const groups = parseArrangement(dto.arrangement); if (groups.some(isNaN)) { throw new BadRequestException(`Invalid arrangement format "${dto.arrangement}". Use e.g. "2+2"`); } // Get the next sequence number for this coach type const lastCoach = await this.prisma.coach.findFirst({ where: { coachTypeId: dto.coachTypeId }, orderBy: { sequence: 'desc' }, }); const nextSequence = (lastCoach?.sequence ?? 0) + 1; const coach = await this.prisma.coach.create({ data: { coachTypeId: dto.coachTypeId, number: dto.number, sequence: nextSequence, arrangement: dto.arrangement, capacity: dto.capacity, status: dto.status || 'ACTIVE', }, include: { coachType: true }, }); if (dto.capacity > 0) { const seatClass = coach.coachType?.name || ''; const seats = buildSeats(coach.id, coach.number, dto.arrangement, dto.capacity, seatClass); await this.prisma.seat.createMany({ data: seats }); } return coach; } async updateCoach(id: string, dto: UpdateCoachDto) { const coach = await this.prisma.coach.findUnique({ where: { id } }); if (!coach) throw new NotFoundException('Coach not found'); return this.prisma.coach.update({ where: { id }, data: { arrangement: dto.arrangement, capacity: dto.capacity, status: dto.status, }, include: { coachType: true }, }); } async deleteCoach(id: string) { const coach = await this.prisma.coach.findUnique({ where: { id } }); if (!coach) throw new NotFoundException('Coach not found'); return this.prisma.coach.delete({ where: { id } }); } async assignCoach(dto: AssignCoachDto) { const [schedule, coach] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId } }), this.prisma.coach.findUnique({ where: { id: dto.coachId } }), ]); if (!schedule) throw new NotFoundException('Schedule not found'); if (!coach) throw new NotFoundException('Coach not found'); return this.prisma.coachAssignment.create({ data: dto }); } async removeAssignment(id: string) { const assignment = await this.prisma.coachAssignment.findUnique({ where: { id } }); if (!assignment) throw new NotFoundException('Assignment not found'); return this.prisma.coachAssignment.delete({ where: { id } }); } async getAnalytics() { const [totalTrains, totalSchedules, totalSeats, bookedSeats] = await Promise.all([ this.prisma.train.count(), this.prisma.trainSchedule.count(), this.prisma.seat.count(), this.prisma.seat.count({ where: { status: 'BOOKED' } }), ]); return { totalTrains, totalSchedules, totalSeats, bookedSeats, occupancyRate: totalSeats > 0 ? +((bookedSeats / totalSeats) * 100).toFixed(2) : 0, }; } }