mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
205 lines
7.7 KiB
TypeScript
205 lines
7.7 KiB
TypeScript
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
|
import { PrismaService } from '../../common/prisma.service';
|
|
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto } from './routes.dto';
|
|
|
|
@Injectable()
|
|
export class RoutesService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
// ── Route CRUD ─────────────────────────────────────────────────────────────
|
|
|
|
async createRoute(dto: CreateRouteDto) {
|
|
const existing = await this.prisma.route.findUnique({ where: { code: dto.code } });
|
|
if (existing) throw new ConflictException(`Route code "${dto.code}" already exists`);
|
|
|
|
if (dto.stops.length < 2) throw new BadRequestException('A route must have at least 2 stops');
|
|
|
|
const seqs = dto.stops.map(s => s.sequence);
|
|
if (new Set(seqs).size !== seqs.length) throw new ConflictException('Duplicate sequence numbers in stop list');
|
|
|
|
const stationIds = [...new Set(dto.stops.map(s => s.stationId))];
|
|
const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
|
|
if (stations.length !== stationIds.length) throw new BadRequestException('One or more station IDs not found');
|
|
|
|
return this.prisma.route.create({
|
|
data: {
|
|
code: dto.code,
|
|
name: dto.name,
|
|
description: dto.description,
|
|
effectiveFrom: new Date(dto.effectiveFrom),
|
|
effectiveUntil: dto.effectiveUntil ? new Date(dto.effectiveUntil) : null,
|
|
stops: {
|
|
create: dto.stops.map(s => ({
|
|
stationId: s.stationId,
|
|
sequence: s.sequence,
|
|
distanceKm: s.distanceKm,
|
|
})),
|
|
},
|
|
},
|
|
include: { stops: { include: { route: false }, orderBy: { sequence: 'asc' } } },
|
|
});
|
|
}
|
|
|
|
async listRoutes(activeOnly = false) {
|
|
return this.prisma.route.findMany({
|
|
where: activeOnly ? { active: true } : undefined,
|
|
include: {
|
|
stops: { orderBy: { sequence: 'asc' } },
|
|
_count: { select: { stops: true } },
|
|
},
|
|
orderBy: { code: 'asc' },
|
|
});
|
|
}
|
|
|
|
async getRoute(id: string) {
|
|
const route = await this.prisma.route.findUnique({
|
|
where: { id },
|
|
include: {
|
|
stops: {
|
|
orderBy: { sequence: 'asc' },
|
|
include: {
|
|
route: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
|
|
// Enrich stops with station details
|
|
const stationIds = route.stops.map(s => s.stationId);
|
|
const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
|
|
const stationMap = Object.fromEntries(stations.map(s => [s.id, s]));
|
|
|
|
return {
|
|
...route,
|
|
stops: route.stops.map(s => ({ ...s, station: stationMap[s.stationId] })),
|
|
};
|
|
}
|
|
|
|
async updateRoute(id: string, dto: UpdateRouteDto) {
|
|
const route = await this.prisma.route.findUnique({ where: { id } });
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
return this.prisma.route.update({
|
|
where: { id },
|
|
data: {
|
|
name: dto.name,
|
|
description: dto.description,
|
|
active: dto.active,
|
|
effectiveUntil: dto.effectiveUntil ? new Date(dto.effectiveUntil) : undefined,
|
|
},
|
|
include: { stops: { orderBy: { sequence: 'asc' } } },
|
|
});
|
|
}
|
|
|
|
async deleteRoute(id: string) {
|
|
const route = await this.prisma.route.findUnique({ where: { id } });
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
await this.prisma.route.delete({ where: { id } });
|
|
return { deleted: true, id };
|
|
}
|
|
|
|
// ── Route Stops ────────────────────────────────────────────────────────────
|
|
|
|
async addStop(routeId: string, dto: AddRouteStopDto) {
|
|
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
|
|
const station = await this.prisma.station.findUnique({ where: { id: dto.stationId } });
|
|
if (!station) throw new NotFoundException(`Station ${dto.stationId} not found`);
|
|
|
|
const existing = await this.prisma.routeStop.findUnique({
|
|
where: { routeId_sequence: { routeId, sequence: dto.sequence } },
|
|
});
|
|
if (existing) throw new ConflictException(`Sequence ${dto.sequence} already exists on this route`);
|
|
|
|
return this.prisma.routeStop.create({
|
|
data: { routeId, stationId: dto.stationId, sequence: dto.sequence, distanceKm: dto.distanceKm },
|
|
});
|
|
}
|
|
|
|
async removeStop(routeId: string, sequence: number) {
|
|
const stop = await this.prisma.routeStop.findUnique({
|
|
where: { routeId_sequence: { routeId, sequence } },
|
|
});
|
|
if (!stop) throw new NotFoundException(`Stop at sequence ${sequence} not found on route`);
|
|
|
|
const total = await this.prisma.routeStop.count({ where: { routeId } });
|
|
if (total <= 2) throw new BadRequestException('A route must retain at least 2 stops');
|
|
|
|
await this.prisma.routeStop.delete({ where: { routeId_sequence: { routeId, sequence } } });
|
|
return { deleted: true, sequence };
|
|
}
|
|
|
|
async getStops(routeId: string) {
|
|
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
|
|
const stops = await this.prisma.routeStop.findMany({
|
|
where: { routeId },
|
|
orderBy: { sequence: 'asc' },
|
|
});
|
|
|
|
const stationIds = stops.map(s => s.stationId);
|
|
const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
|
|
const stationMap = Object.fromEntries(stations.map(s => [s.id, s]));
|
|
|
|
return stops.map(s => ({ ...s, station: stationMap[s.stationId] }));
|
|
}
|
|
|
|
async getSchedulesForRoute(routeId: string) {
|
|
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
|
if (!route) throw new NotFoundException('Route not found');
|
|
|
|
return this.prisma.trainSchedule.findMany({
|
|
where: { routeId },
|
|
include: { train: true, originStation: true, destinationStation: true },
|
|
orderBy: { departureAt: 'asc' },
|
|
});
|
|
}
|
|
|
|
// ── Used by SchedulesService ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Copies RouteStop definitions into TripStopTime rows for a schedule.
|
|
* plannedTimes maps sequence → { arrivalAt?, departureAt? } for actual timing.
|
|
*/
|
|
async applyRouteToSchedule(
|
|
routeId: string,
|
|
scheduleId: string,
|
|
plannedTimes: Record<number, { plannedArrivalAt?: string; plannedDepartureAt?: string }>,
|
|
) {
|
|
const stops = await this.prisma.routeStop.findMany({
|
|
where: { routeId },
|
|
orderBy: { sequence: 'asc' },
|
|
});
|
|
if (stops.length === 0) throw new BadRequestException('Route has no stops defined');
|
|
|
|
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId } });
|
|
|
|
await this.prisma.tripStopTime.createMany({
|
|
data: stops.map(s => {
|
|
const times = plannedTimes[s.sequence] ?? {};
|
|
return {
|
|
scheduleId,
|
|
stationId: s.stationId,
|
|
sequence: s.sequence,
|
|
plannedArrivalAt: times.plannedArrivalAt ? new Date(times.plannedArrivalAt) : null,
|
|
plannedDepartureAt: times.plannedDepartureAt ? new Date(times.plannedDepartureAt) : null,
|
|
};
|
|
}),
|
|
});
|
|
|
|
const intermediateCount = Math.max(0, stops.length - 2);
|
|
await this.prisma.trainSchedule.update({
|
|
where: { id: scheduleId },
|
|
data: { stopsCount: intermediateCount },
|
|
});
|
|
|
|
return this.prisma.tripStopTime.findMany({
|
|
where: { scheduleId },
|
|
include: { station: true },
|
|
orderBy: { sequence: 'asc' },
|
|
});
|
|
}
|
|
}
|