mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|