From 47c91cad0376b89cf2d3c1080d6c4725cdf37b43 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sun, 28 Jun 2026 23:02:40 +0000 Subject: [PATCH] feat: enhance locomotive creation and management with optional code generation and default max pull weight --- .../locomotives/dto/create-locomotive.dto.ts | 15 ++++-- .../locomotives/locomotives.service.ts | 30 ++++++++++-- .../src/modules/wagons/wagons.service.ts | 5 +- .../bookings/BookingActionsToolbar.tsx | 22 +-------- .../src/components/fleet/FleetFormDialog.tsx | 10 +++- .../bookings/booking-actions.config.ts | 47 ++----------------- .../bookings/BookingRequestDetailPage.tsx | 11 +++++ .../src/pages/fleet/FleetResourcePage.tsx | 1 - .../src/pages/fleet/config/resources.ts | 5 +- 9 files changed, 66 insertions(+), 80 deletions(-) diff --git a/apps/edr-freight-api/src/modules/locomotives/dto/create-locomotive.dto.ts b/apps/edr-freight-api/src/modules/locomotives/dto/create-locomotive.dto.ts index 5745d3037..66d0100d0 100644 --- a/apps/edr-freight-api/src/modules/locomotives/dto/create-locomotive.dto.ts +++ b/apps/edr-freight-api/src/modules/locomotives/dto/create-locomotive.dto.ts @@ -8,10 +8,13 @@ import { } from '../entities/locomotive.entity'; export class CreateLocomotiveDto { - @ApiProperty({ example: 'LOCO-001' }) + // Optional on input — the service auto-generates a sequential LOCO-NNN code + // when none is supplied. + @ApiPropertyOptional({ example: 'LOCO-001' }) + @IsOptional() @IsString() @MaxLength(32) - code!: string; + code?: string; @ApiPropertyOptional() @IsOptional() @@ -32,11 +35,13 @@ export class CreateLocomotiveDto { @IsUUID() currentYardId?: string; - @ApiProperty({ example: 3500 }) - @Transform(({ value }) => Number(value)) + // Defaults to 2500 tons when omitted (see service). + @ApiPropertyOptional({ example: 2500, default: 2500 }) + @IsOptional() + @Transform(({ value }) => (value === '' || value == null ? undefined : Number(value))) @IsNumber() @Min(0) - maxPullWeightTons!: number; + maxPullWeightTons?: number; @ApiProperty({ example: 760 }) @Transform(({ value }) => Number(value)) diff --git a/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts b/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts index ebcb09bce..ae9a41608 100644 --- a/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts +++ b/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts @@ -29,20 +29,40 @@ export class LocomotivesService { }); } - async create(dto: CreateLocomotiveDto): Promise { - const [existing] = await this.locomotivesRepository.findAll({ where: { code: dto.code } }); + /** Default max pull weight (tons) applied when the caller omits it. */ + private static readonly DEFAULT_MAX_PULL_WEIGHT_TONS = 2500; + /** + * Generate the next sequential locomotive code (LOCO-001, LOCO-002, …) by + * scanning the highest existing LOCO-NNN number. Used when the caller does not + * supply a code. + */ + private async generateCode(): Promise { + const all = await this.locomotivesRepository.findAll({}); + let max = 0; + for (const loco of all) { + const match = /^LOCO-(\d+)$/.exec(loco.code ?? ''); + if (match) max = Math.max(max, Number(match[1])); + } + return `LOCO-${String(max + 1).padStart(3, '0')}`; + } + + async create(dto: CreateLocomotiveDto): Promise { + const code = dto.code?.trim() || (await this.generateCode()); + + const [existing] = await this.locomotivesRepository.findAll({ where: { code } }); if (existing) { - throw new ConflictException(`Locomotive code ${dto.code} already exists`); + throw new ConflictException(`Locomotive code ${code} already exists`); } return this.locomotivesRepository.create({ - code: dto.code, + code, name: dto.name?.trim() || null, locomotiveType: dto.locomotiveType as LocomotiveType, status: dto.status as LocomotiveStatus, currentYardId: dto.currentYardId ?? null, - maxPullWeightTons: dto.maxPullWeightTons, + maxPullWeightTons: + dto.maxPullWeightTons ?? LocomotivesService.DEFAULT_MAX_PULL_WEIGHT_TONS, maxTrainLengthMeters: dto.maxTrainLengthMeters, powerKw: dto.powerKw ?? null, tractionForceKn: dto.tractionForceKn ?? null, diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.service.ts b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts index b2b1df275..1350a7fd0 100644 --- a/apps/edr-freight-api/src/modules/wagons/wagons.service.ts +++ b/apps/edr-freight-api/src/modules/wagons/wagons.service.ts @@ -77,7 +77,10 @@ export class WagonsService { async update(id: string, dto: UpdateWagonDto): Promise { const wagon = await this.findById(id); Object.assign(wagon, dto); - return this.wagonRepo.save(wagon); + await this.wagonRepo.save(wagon); + // Re-read with the relation so the response reflects the new yard label + // instead of the stale relation object loaded before the assign. + return this.findById(id); } async remove(id: string): Promise { diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx index 4589ad7fb..e451f2d2c 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingActionsToolbar.tsx @@ -1,15 +1,10 @@ -import { useState } from "react"; import { Download, Zap, FileText, Clock } from "lucide-react"; import { Stack, Text, Button } from "@mantine/core"; -import { AllocateBookingWizard } from "@/components/trainScheduling/AllocateBookingWizard"; import type { BookingDetail } from "@/types/booking"; import { BookingActionsMenu } from "./BookingActionsMenu"; import { SectionCard } from "./detail/SectionCard"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; -import { canAllocateBooking } from "@/features/bookings/booking-actions.config"; -import { useAuth } from "@/auth/useAuth"; -import { canManageScheduling } from "@/lib/permissions"; import type { useBookingMutations } from "@/hooks/bookings/useBookings"; type Mutations = ReturnType; @@ -21,11 +16,8 @@ interface BookingActionsToolbarProps { /** Detail-page actions: primary toolbar + downloads. */ export function BookingActionsToolbar({ booking, mutations }: BookingActionsToolbarProps) { - const { user } = useAuth(); const row = toBookingListRow(booking); const { status } = booking; - const [allocateOpen, setAllocateOpen] = useState(false); - const canAllocate = canManageScheduling(user); const downloadBlob = async (fn: () => Promise, filename: string) => { const blob = await fn(); @@ -106,11 +98,7 @@ export function BookingActionsToolbar({ booking, mutations }: BookingActionsTool Confirm each step before it is applied. - setAllocateOpen(true)} - /> + @@ -130,14 +118,6 @@ export function BookingActionsToolbar({ booking, mutations }: BookingActionsTool )} - - {canAllocate && canAllocateBooking(booking) ? ( - setAllocateOpen(false)} - /> - ) : null} ); } diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx index ac3332258..83e711451 100644 --- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx @@ -66,12 +66,20 @@ const FleetFormDialog = ({ const [values, setValues] = useState>({}); const [errors, setErrors] = useState>({}); + // Seed the form ONLY when the dialog opens or the edited record changes — NOT + // when `fields`/`emptyValues` get new object refs (they're rebuilt whenever the + // dynamic select options finish loading). Re-seeding on those would wipe the + // user's in-progress edits (e.g. a changed Current Yard / status) the moment + // the yard or wagon-type options resolve. + const recordId = + initialRecord && "id" in initialRecord ? String(initialRecord.id) : null; useEffect(() => { if (open) { setValues(buildInitialValues(fields, emptyValues, initialRecord)); setErrors({}); } - }, [open, fields, emptyValues, initialRecord]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, recordId]); const shortFields = useMemo( () => fields.filter((f) => f.type !== "textarea"), diff --git a/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts b/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts index df50b5ed2..6928cc4d2 100644 --- a/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts +++ b/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts @@ -7,8 +7,6 @@ import { MessageSquareWarning, Play, ShieldCheck, - TrainTrack, - Truck, XCircle, } from "lucide-react"; @@ -388,47 +386,10 @@ export function getBookingActions( actions = withCancel(OPERATION_REVIEW_ACTIONS); break; case "PAID": - if ( - canAllocateBooking({ status, schedulingStatus: ctx.schedulingStatus }) - ) { - actions = [ - { - id: "allocateBooking", - label: "Allocate booking", - shortLabel: "Allocate", - description: "Assign to train, wagons, and finalize schedule", - confirmTitle: "Allocate booking?", - confirmDescription: "Opens the train allocation wizard.", - variant: "default", - icon: TrainTrack, - primary: true, - }, - { - id: "startTransit", - label: "Start transit", - shortLabel: "Transit", - description: "Begin rail movement", - confirmTitle: "Start transit?", - confirmDescription: "The booking will move to in transit status.", - variant: "default", - icon: Truck, - }, - ]; - } else { - actions = [ - { - id: "startTransit", - label: "Start transit", - shortLabel: "Transit", - description: "Begin rail movement", - confirmTitle: "Start transit?", - confirmDescription: "The booking will move to in transit status.", - variant: "default", - icon: Truck, - primary: true, - }, - ]; - } + // Allocate is handled by the Operations "Ready to allocate" queue, not the + // per-booking action menu. Start transit was removed entirely. No per-row + // action remains in the PAID state. + actions = []; break; case "IN_TRANSIT": actions = [ diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx index bd8ed9a98..75badc5b3 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -4,6 +4,7 @@ import { FileSignature, Layers, LayoutGrid, + Milestone, Package, ShieldCheck, } from "lucide-react"; @@ -288,6 +289,16 @@ export default function BookingRequestDetailPage() { booking={booking} mutations={mutations} /> + {showContractButton && (