mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 10:45:44 +00:00
First passenger and back office portal commit
This commit is contained in:
@@ -47,6 +47,14 @@ Route stops carry distanceKm for fare-by-distance calculations.`,
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
updateRoute(@Param('id') id: string, @Body() dto: UpdateRouteDto) { return this.service.updateRoute(id, dto); }
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete a route' })
|
||||
@ApiParam({ name: 'id', description: 'Route UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Route deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
deleteRoute(@Param('id') id: string) { return this.service.deleteRoute(id); }
|
||||
|
||||
// ── Route Stops ────────────────────────────────────────────────────────────
|
||||
|
||||
@Get(':id/stops')
|
||||
|
||||
@@ -91,6 +91,13 @@ export class RoutesService {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||
import { SchedulesService } from './schedules.service';
|
||||
import { CreateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto } from './schedules.dto';
|
||||
@@ -54,6 +54,16 @@ Origin and destination are derived from the first and last route stop — no nee
|
||||
@ApiResponse({ status: 404, description: 'Schedule not found' })
|
||||
getSchedule(@Param('id') id: string) { return this.service.getSchedule(id); }
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Update a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Schedule updated' })
|
||||
@ApiResponse({ status: 404, description: 'Schedule not found' })
|
||||
updateSchedule(@Param('id') id: string, @Body() dto: CreateScheduleDto) {
|
||||
return this.service.updateSchedule(id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Update schedule status (SCHEDULED → BOARDING → EN_ROUTE → ARRIVED)' })
|
||||
@@ -64,6 +74,16 @@ Origin and destination are derived from the first and last route stop — no nee
|
||||
return this.service.updateScheduleStatus(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Schedule deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Schedule not found' })
|
||||
deleteSchedule(@Param('id') id: string) {
|
||||
return this.service.deleteSchedule(id);
|
||||
}
|
||||
|
||||
// ── Stop Times ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Get(':id/stops')
|
||||
@@ -130,4 +150,43 @@ Origin and destination are derived from the first and last route stop — no nee
|
||||
syncFares(@Param('id') id: string) {
|
||||
return this.service.syncFaresFromEngine(id);
|
||||
}
|
||||
|
||||
// ── Coach Assignments ──────────────────────────────────────────────────────
|
||||
|
||||
@Post(':id/coaches')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Assign coaches to a schedule',
|
||||
description: 'Assigns selected coaches to a schedule with their position numbers. Replaces any existing coach assignments.'
|
||||
})
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiResponse({ status: 201, description: 'Coaches assigned successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Schedule or coach not found' })
|
||||
assignCoaches(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: { coaches: Array<{ coachId: string; positionNumber: number }> },
|
||||
) {
|
||||
return this.service.assignCoaches(id, dto.coaches);
|
||||
}
|
||||
|
||||
@Get(':id/coaches')
|
||||
@ApiOperation({ summary: 'Get assigned coaches for a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiResponse({ status: 200, description: 'List of assigned coaches with seat details' })
|
||||
getAssignedCoaches(@Param('id') id: string) {
|
||||
return this.service.getAssignedCoaches(id);
|
||||
}
|
||||
|
||||
@Delete(':id/coaches/:coachId')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Remove a coach assignment from a schedule' })
|
||||
@ApiParam({ name: 'id', description: 'TrainSchedule UUID' })
|
||||
@ApiParam({ name: 'coachId', description: 'Coach UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Coach assignment removed' })
|
||||
removeCoachAssignment(
|
||||
@Param('id') id: string,
|
||||
@Param('coachId') coachId: string,
|
||||
) {
|
||||
return this.service.removeCoachAssignment(id, coachId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,14 @@ export class SchedulesService {
|
||||
where,
|
||||
include: {
|
||||
train: true,
|
||||
route: true,
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
|
||||
coachAssignments: {
|
||||
include: { coach: true },
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
},
|
||||
_count: { select: { coachAssignments: true, bookings: true } },
|
||||
},
|
||||
orderBy: { departureAt: 'asc' },
|
||||
@@ -53,8 +58,38 @@ export class SchedulesService {
|
||||
if (!route.active) throw new BadRequestException('Route is not active');
|
||||
if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');
|
||||
|
||||
// Auto-generate plannedTimes if not provided or empty
|
||||
let plannedTimes = dto.plannedTimes;
|
||||
if (!plannedTimes || plannedTimes.length === 0) {
|
||||
const totalDuration = arr.getTime() - dep.getTime();
|
||||
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
|
||||
|
||||
plannedTimes = route.stops.map((stop, index) => {
|
||||
let stopTime: Date;
|
||||
|
||||
if (index === 0) {
|
||||
// First stop - use departure time
|
||||
stopTime = dep;
|
||||
} else if (index === route.stops.length - 1) {
|
||||
// Last stop - use arrival time
|
||||
stopTime = arr;
|
||||
} else {
|
||||
// Intermediate stops - calculate based on distance proportion
|
||||
const stopDistance = stop.distanceKm || 0;
|
||||
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
|
||||
stopTime = new Date(dep.getTime() + totalDuration * progress);
|
||||
}
|
||||
|
||||
return {
|
||||
sequence: stop.sequence,
|
||||
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
|
||||
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Validate all route stop sequences are covered by plannedTimes
|
||||
const providedSeqs = new Set(dto.plannedTimes.map(t => t.sequence));
|
||||
const providedSeqs = new Set(plannedTimes.map(t => t.sequence));
|
||||
const missingSeqs = route.stops.map(s => s.sequence).filter(seq => !providedSeqs.has(seq));
|
||||
if (missingSeqs.length > 0) {
|
||||
throw new BadRequestException(`Missing planned times for stop sequences: ${missingSeqs.join(', ')}`);
|
||||
@@ -80,7 +115,7 @@ export class SchedulesService {
|
||||
|
||||
// Copy route stops into TripStopTime with the provided planned times
|
||||
const plannedTimesMap = Object.fromEntries(
|
||||
dto.plannedTimes.map(t => [t.sequence, t]),
|
||||
plannedTimes.map(t => [t.sequence, t]),
|
||||
);
|
||||
await this.routesService.applyRouteToSchedule(dto.routeId, schedule.id, plannedTimesMap);
|
||||
|
||||
@@ -161,10 +196,94 @@ export class SchedulesService {
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
async updateSchedule(id: string, dto: CreateScheduleDto) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
const dep = new Date(dto.departureAt);
|
||||
const arr = new Date(dto.arrivalAt);
|
||||
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
|
||||
|
||||
// Validate route exists and has stops
|
||||
const route = await this.prisma.route.findUnique({
|
||||
where: { id: dto.routeId },
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
});
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
if (!route.active) throw new BadRequestException('Route is not active');
|
||||
if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');
|
||||
|
||||
// Derive origin and destination from first and last route stop
|
||||
const firstStop = route.stops[0];
|
||||
const lastStop = route.stops[route.stops.length - 1];
|
||||
|
||||
await this.prisma.trainSchedule.update({
|
||||
where: { id },
|
||||
data: {
|
||||
trainId: dto.trainId,
|
||||
routeId: dto.routeId,
|
||||
originStationId: firstStop.stationId,
|
||||
destinationStationId: lastStop.stationId,
|
||||
departureAt: dep,
|
||||
arrivalAt: arr,
|
||||
durationMinutes: Math.round((arr.getTime() - dep.getTime()) / 60_000),
|
||||
stopsCount: Math.max(0, route.stops.length - 2),
|
||||
},
|
||||
});
|
||||
|
||||
// Delete existing stop times and recreate
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
|
||||
|
||||
// Auto-generate plannedTimes if not provided
|
||||
let plannedTimes = dto.plannedTimes;
|
||||
if (!plannedTimes || plannedTimes.length === 0) {
|
||||
const totalDuration = arr.getTime() - dep.getTime();
|
||||
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
|
||||
|
||||
plannedTimes = route.stops.map((stop, index) => {
|
||||
let stopTime: Date;
|
||||
|
||||
if (index === 0) {
|
||||
stopTime = dep;
|
||||
} else if (index === route.stops.length - 1) {
|
||||
stopTime = arr;
|
||||
} else {
|
||||
const stopDistance = stop.distanceKm || 0;
|
||||
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
|
||||
stopTime = new Date(dep.getTime() + totalDuration * progress);
|
||||
}
|
||||
|
||||
return {
|
||||
sequence: stop.sequence,
|
||||
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
|
||||
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const plannedTimesMap = Object.fromEntries(
|
||||
plannedTimes.map(t => [t.sequence, t]),
|
||||
);
|
||||
await this.routesService.applyRouteToSchedule(dto.routeId, id, plannedTimesMap);
|
||||
|
||||
return this.getSchedule(id);
|
||||
}
|
||||
|
||||
updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) {
|
||||
return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } });
|
||||
}
|
||||
|
||||
async deleteSchedule(id: string) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
// Delete related records first
|
||||
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
|
||||
|
||||
return this.prisma.trainSchedule.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// ── Stop Times (per-schedule overrides) ───────────────────────────────────
|
||||
|
||||
getStops(scheduleId: string) {
|
||||
@@ -254,4 +373,63 @@ export class SchedulesService {
|
||||
|
||||
return { synced, errors };
|
||||
}
|
||||
|
||||
// ── Coach Assignments ──────────────────────────────────────────────────────
|
||||
|
||||
async assignCoaches(
|
||||
scheduleId: string,
|
||||
coaches: Array<{ coachId: string; positionNumber: number }>,
|
||||
) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: scheduleId } });
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
// Validate all coaches exist
|
||||
const coachIds = coaches.map(c => c.coachId);
|
||||
const existingCoaches = await this.prisma.coach.findMany({
|
||||
where: { id: { in: coachIds } },
|
||||
});
|
||||
if (existingCoaches.length !== coachIds.length) {
|
||||
throw new NotFoundException('One or more coaches not found');
|
||||
}
|
||||
|
||||
// Remove existing assignments
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });
|
||||
|
||||
// Create new assignments
|
||||
await this.prisma.coachAssignment.createMany({
|
||||
data: coaches.map(c => ({
|
||||
scheduleId,
|
||||
coachId: c.coachId,
|
||||
positionNumber: c.positionNumber,
|
||||
isOperational: true,
|
||||
})),
|
||||
});
|
||||
|
||||
return { message: 'Coaches assigned successfully', count: coaches.length };
|
||||
}
|
||||
|
||||
async getAssignedCoaches(scheduleId: string) {
|
||||
return this.prisma.coachAssignment.findMany({
|
||||
where: { scheduleId },
|
||||
include: {
|
||||
coach: {
|
||||
include: {
|
||||
seatClass: true,
|
||||
seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async removeCoachAssignment(scheduleId: string, coachId: string) {
|
||||
const assignment = await this.prisma.coachAssignment.findFirst({
|
||||
where: { scheduleId, coachId },
|
||||
});
|
||||
if (!assignment) throw new NotFoundException('Coach assignment not found');
|
||||
|
||||
await this.prisma.coachAssignment.delete({ where: { id: assignment.id } });
|
||||
return { message: 'Coach assignment removed' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user