mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
Fare and route-coach, production checklist updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||
import { RoutesService } from './routes.service';
|
||||
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto } from './routes.dto';
|
||||
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto, SetRouteCoachTemplateDto } from './routes.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@ApiTags('Routes')
|
||||
@@ -93,4 +93,35 @@ Route stops carry distanceKm for fare-by-distance calculations.`,
|
||||
@ApiResponse({ status: 200, description: 'Schedules with train and terminal station details' })
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
getSchedules(@Param('id') id: string) { return this.service.getSchedulesForRoute(id); }
|
||||
|
||||
// ── Route Coach Template ───────────────────────────────────────────────────
|
||||
|
||||
@Get(':id/coaches')
|
||||
@ApiOperation({ summary: 'Get the default coach lineup for this route' })
|
||||
@ApiParam({ name: 'id', description: 'Route UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Ordered coach template with coach and coach type details' })
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
getCoachTemplate(@Param('id') id: string) { return this.service.getRouteCoachTemplate(id); }
|
||||
|
||||
@Put(':id/coaches')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Set the default coach lineup for this route',
|
||||
description: 'Replaces the entire coach template. Coaches are auto-assigned in this order when a new schedule is created for this route.',
|
||||
})
|
||||
@ApiParam({ name: 'id', description: 'Route UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Updated coach template' })
|
||||
@ApiResponse({ status: 400, description: 'Duplicate positions or inactive coach' })
|
||||
@ApiResponse({ status: 404, description: 'Route or coach not found' })
|
||||
setCoachTemplate(@Param('id') id: string, @Body() dto: SetRouteCoachTemplateDto) {
|
||||
return this.service.setRouteCoachTemplate(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/coaches')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Clear the default coach lineup for this route' })
|
||||
@ApiParam({ name: 'id', description: 'Route UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Template cleared' })
|
||||
@ApiResponse({ status: 404, description: 'Route not found' })
|
||||
clearCoachTemplate(@Param('id') id: string) { return this.service.removeRouteCoachTemplate(id); }
|
||||
}
|
||||
|
||||
@@ -44,3 +44,14 @@ export class UpdateRouteDto {
|
||||
@ApiPropertyOptional({ example: '2027-12-31T23:59:59Z' }) @IsOptional() @IsDateString() effectiveUntil?: string;
|
||||
@ApiPropertyOptional({ type: [RouteStopInputDto] }) @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => RouteStopInputDto) stops?: RouteStopInputDto[];
|
||||
}
|
||||
|
||||
export class RouteCoachTemplateItemDto {
|
||||
@ApiProperty({ example: 'coach-uuid', description: 'Coach UUID' }) @IsString() coachId: string;
|
||||
@ApiProperty({ example: 1, description: 'Position in the train consist (1 = first coach)' }) @IsInt() @Min(1) positionNumber: number;
|
||||
}
|
||||
|
||||
export class SetRouteCoachTemplateDto {
|
||||
@ApiProperty({ type: [RouteCoachTemplateItemDto], description: 'Ordered list of coaches for this route. Replaces the existing template.' })
|
||||
@IsArray() @ValidateNested({ each: true }) @Type(() => RouteCoachTemplateItemDto)
|
||||
coaches: RouteCoachTemplateItemDto[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto } from './routes.dto';
|
||||
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto, SetRouteCoachTemplateDto } from './routes.dto';
|
||||
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
|
||||
|
||||
@Injectable()
|
||||
@@ -202,6 +202,44 @@ export class RoutesService {
|
||||
});
|
||||
}
|
||||
|
||||
async getRouteCoachTemplate(routeId: string) {
|
||||
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
return this.prisma.routeCoachTemplate.findMany({
|
||||
where: { routeId },
|
||||
include: { coach: { include: { coachType: true } } },
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async setRouteCoachTemplate(routeId: string, dto: SetRouteCoachTemplateDto) {
|
||||
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
|
||||
const coachIds = dto.coaches.map(c => c.coachId);
|
||||
const coaches = await this.prisma.coach.findMany({ where: { id: { in: coachIds } } });
|
||||
if (coaches.length !== coachIds.length) throw new NotFoundException('One or more coaches not found');
|
||||
const inactive = coaches.find(c => c.status !== 'ACTIVE');
|
||||
if (inactive) throw new BadRequestException(`Coach ${inactive.number} is not active`);
|
||||
|
||||
const positions = dto.coaches.map(c => c.positionNumber);
|
||||
if (new Set(positions).size !== positions.length) throw new BadRequestException('Duplicate positionNumber values');
|
||||
|
||||
await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId } });
|
||||
await this.prisma.routeCoachTemplate.createMany({
|
||||
data: dto.coaches.map(c => ({ routeId, coachId: c.coachId, positionNumber: c.positionNumber })),
|
||||
});
|
||||
|
||||
return this.getRouteCoachTemplate(routeId);
|
||||
}
|
||||
|
||||
async removeRouteCoachTemplate(routeId: string) {
|
||||
const route = await this.prisma.route.findUnique({ where: { id: routeId } });
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId } });
|
||||
return { deleted: true, routeId };
|
||||
}
|
||||
|
||||
// ── Used by SchedulesService ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,7 +119,7 @@ export class BulkCreateSchedulesDto {
|
||||
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlannedStopTimeDto)
|
||||
plannedTimes?: PlannedStopTimeDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: [String], description: 'Optional coach UUIDs to assign to every generated schedule' })
|
||||
@ApiPropertyOptional({ type: [String], description: 'Optional coach UUIDs to assign to every generated schedule. Overrides the route coach template if provided.' })
|
||||
@IsOptional() @IsArray() @IsString({ each: true })
|
||||
coachIds?: string[];
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ export class SchedulesService {
|
||||
const schedule = await this.createSchedule(createDto);
|
||||
scheduleIds.push(schedule.id);
|
||||
|
||||
// createSchedule already auto-applies the route coach template;
|
||||
// only override if explicit coachIds are provided
|
||||
if (dto.coachIds && dto.coachIds.length > 0) {
|
||||
await this.assignCoaches(
|
||||
schedule.id,
|
||||
@@ -177,6 +179,18 @@ export class SchedulesService {
|
||||
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
|
||||
await this.routesService.applyRouteToSchedule(dto.routeId, schedule.id, plannedTimesMap);
|
||||
|
||||
// Auto-apply route coach template if one is defined
|
||||
const coachTemplates = await this.prisma.routeCoachTemplate.findMany({
|
||||
where: { routeId: dto.routeId },
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
});
|
||||
if (coachTemplates.length > 0) {
|
||||
await this.assignCoaches(
|
||||
schedule.id,
|
||||
coachTemplates.map(t => ({ coachId: t.coachId, positionNumber: t.positionNumber })),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getSchedule(schedule.id);
|
||||
}
|
||||
|
||||
@@ -583,10 +597,10 @@ export class SchedulesService {
|
||||
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });
|
||||
|
||||
const data = coaches.map((c, idx) => ({
|
||||
const data = coaches.map((c) => ({
|
||||
scheduleId,
|
||||
coachId: c.coachId,
|
||||
positionNumber: idx + 1,
|
||||
positionNumber: c.positionNumber,
|
||||
isOperational: true,
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user