mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 02:30:55 +00:00
train
This commit is contained in:
@@ -208,6 +208,8 @@ export interface BatchBoardScheduleDetail {
|
||||
docReviewEndsAt: string | null;
|
||||
paymentPhaseEndsAt: string | null;
|
||||
bookingCycleNo: number;
|
||||
/** Built train (Train Builder) behind this departure, when scheduled by train. */
|
||||
train: BatchBoardSchedule["train"];
|
||||
locomotive: BatchBoardSchedule["locomotive"];
|
||||
capacity: BatchBoardSchedule["capacity"];
|
||||
counts: BatchBoardSchedule["counts"];
|
||||
@@ -235,6 +237,12 @@ export interface BatchBoardSchedule {
|
||||
docReviewEndsAt: string | null;
|
||||
paymentPhaseEndsAt: string | null;
|
||||
bookingCycleNo: number;
|
||||
/** Built train (Train Builder) behind this departure, when scheduled by train. */
|
||||
train: {
|
||||
id: string;
|
||||
code: string;
|
||||
trainName: string | null;
|
||||
} | null;
|
||||
locomotive: {
|
||||
code: string;
|
||||
name: string | null;
|
||||
@@ -877,7 +885,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const [schedules, total] = await this.trainSchedulesRepository.findAndCount({
|
||||
where,
|
||||
relations: {
|
||||
trainSet: { locomotive: true },
|
||||
trainSet: { locomotive: true, train: true },
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
// Yards supply the route's display name for `routeName` below;
|
||||
@@ -1128,6 +1136,13 @@ export class BookingBatchService implements OnModuleInit {
|
||||
? s.paymentPhaseEndsAt.toISOString()
|
||||
: null,
|
||||
bookingCycleNo: s.bookingCycleNo ?? 0,
|
||||
train: s.trainSet?.train
|
||||
? {
|
||||
id: s.trainSet.train.id,
|
||||
code: s.trainSet.train.code,
|
||||
trainName: s.trainSet.train.trainName ?? null,
|
||||
}
|
||||
: null,
|
||||
locomotive: loco
|
||||
? {
|
||||
code: loco.code,
|
||||
@@ -1247,6 +1262,13 @@ export class BookingBatchService implements OnModuleInit {
|
||||
? s.paymentPhaseEndsAt.toISOString()
|
||||
: null,
|
||||
bookingCycleNo: s.bookingCycleNo ?? 0,
|
||||
train: s.trainSet?.train
|
||||
? {
|
||||
id: s.trainSet.train.id,
|
||||
code: s.trainSet.train.code,
|
||||
trainName: s.trainSet.train.trainName ?? null,
|
||||
}
|
||||
: null,
|
||||
locomotive: loco
|
||||
? {
|
||||
code: loco.code,
|
||||
|
||||
@@ -282,6 +282,8 @@ interface BookingWindowRow {
|
||||
origin_code: string | null;
|
||||
destination_label: string | null;
|
||||
destination_code: string | null;
|
||||
/** Full ordered corridor (origin → milestones → destination) from the schedule's route. */
|
||||
route_stations: string[] | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -1329,9 +1331,15 @@ export class TrainSchedulingService {
|
||||
limitLoco.maxPullWeightTons + (Number(limitLoco.overageToleranceTons) || 0);
|
||||
const lengthCapWithOverage =
|
||||
limitLoco.maxTrainLengthMeters + (Number(limitLoco.overageToleranceMeters) || 0);
|
||||
if (!dto.forceAssign && weightCapWithOverage < totalWeightTons) {
|
||||
// The locomotives pull GROSS weight: the customers' cargo plus the empty
|
||||
// weight of every planned wagon — cargo-only comparison understates the load.
|
||||
const planTareTons = roundTons(
|
||||
wagonPlan.reduce((sum, slot) => sum + Number(slot.tareWeightTons ?? 0), 0),
|
||||
);
|
||||
const grossWeightTons = roundTons(totalWeightTons + planTareTons);
|
||||
if (!dto.forceAssign && weightCapWithOverage < grossWeightTons) {
|
||||
throw new BadRequestException(
|
||||
`Train set locomotives cannot pull ${totalWeightTons}T`,
|
||||
`Train set locomotives cannot pull ${grossWeightTons}T gross (${totalWeightTons}T cargo + ${planTareTons}T wagon tare)`,
|
||||
);
|
||||
}
|
||||
if (!dto.forceAssign && lengthCapWithOverage < totalLengthMeters) {
|
||||
@@ -4605,14 +4613,8 @@ export class TrainSchedulingService {
|
||||
name: link.locomotive!.name ?? null,
|
||||
})),
|
||||
wagonCount: wagons.length,
|
||||
maxGrossTons: roundTons(
|
||||
wagons.reduce(
|
||||
(sum, w) =>
|
||||
sum +
|
||||
(Number(w.wagonType?.tareWeightTons) || 0) +
|
||||
(Number(w.wagonType?.capacityTons) || 0),
|
||||
0,
|
||||
),
|
||||
totalTareTons: roundTons(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.tareWeightTons) || 0), 0),
|
||||
),
|
||||
totalLengthMeters: roundTons(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.lengthMeters) || 0), 0),
|
||||
@@ -4725,7 +4727,11 @@ export class TrainSchedulingService {
|
||||
ts.booking_cycle_no,
|
||||
ts.scheduled_departure_date,
|
||||
oy.label AS origin_label, oy.code AS origin_code,
|
||||
dy.label AS destination_label, dy.code AS destination_code
|
||||
dy.label AS destination_label, dy.code AS destination_code,
|
||||
(SELECT array_agg(COALESCE(rmy.label, rmy.code) ORDER BY rm.sequence_no)
|
||||
FROM freight.route_milestones rm
|
||||
JOIN freight.yards rmy ON rmy.id = rm.yard_id
|
||||
WHERE rm.route_id = ts.route_id) AS route_stations
|
||||
FROM freight.train_schedules ts
|
||||
LEFT JOIN freight.contract_routes cr
|
||||
ON cr.deleted_at IS NULL
|
||||
@@ -4777,7 +4783,11 @@ export class TrainSchedulingService {
|
||||
ts.booking_cycle_no,
|
||||
ts.scheduled_departure_date,
|
||||
oy.label AS origin_label, oy.code AS origin_code,
|
||||
dy.label AS destination_label, dy.code AS destination_code
|
||||
dy.label AS destination_label, dy.code AS destination_code,
|
||||
(SELECT array_agg(COALESCE(rmy.label, rmy.code) ORDER BY rm.sequence_no)
|
||||
FROM freight.route_milestones rm
|
||||
JOIN freight.yards rmy ON rmy.id = rm.yard_id
|
||||
WHERE rm.route_id = ts.route_id) AS route_stations
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.contract_routes cr
|
||||
ON cr.contract_id = $1
|
||||
@@ -4823,7 +4833,11 @@ export class TrainSchedulingService {
|
||||
ts.booking_cycle_no,
|
||||
ts.scheduled_departure_date,
|
||||
oy.label AS origin_label, oy.code AS origin_code,
|
||||
dy.label AS destination_label, dy.code AS destination_code
|
||||
dy.label AS destination_label, dy.code AS destination_code,
|
||||
(SELECT array_agg(COALESCE(rmy.label, rmy.code) ORDER BY rm.sequence_no)
|
||||
FROM freight.route_milestones rm
|
||||
JOIN freight.yards rmy ON rmy.id = rm.yard_id
|
||||
WHERE rm.route_id = ts.route_id) AS route_stations
|
||||
FROM freight.train_schedules ts
|
||||
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
|
||||
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
||||
@@ -4845,6 +4859,17 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
private mapBookingWindowRow(r: BookingWindowRow) {
|
||||
const origin = r.origin_label ?? r.origin_code ?? null;
|
||||
const destination = r.destination_label ?? r.destination_code ?? null;
|
||||
// Full corridor from the route's milestones (origin → stops → destination).
|
||||
// Falls back to the schedule's origin/destination when no milestones exist.
|
||||
const milestoneStops = (r.route_stations ?? []).filter(
|
||||
(s): s is string => Boolean(s),
|
||||
);
|
||||
const routeStations =
|
||||
milestoneStops.length >= 2
|
||||
? milestoneStops
|
||||
: [origin, destination].filter((s): s is string => Boolean(s));
|
||||
return {
|
||||
scheduleId: r.schedule_id,
|
||||
reference: r.reference ?? null,
|
||||
@@ -4860,8 +4885,9 @@ export class TrainSchedulingService {
|
||||
bookingWindowStatus: r.booking_window_status,
|
||||
bookingCycleNo: r.booking_cycle_no,
|
||||
departureDate: r.scheduled_departure_date,
|
||||
origin: r.origin_label ?? r.origin_code ?? null,
|
||||
destination: r.destination_label ?? r.destination_code ?? null,
|
||||
origin,
|
||||
destination,
|
||||
routeStations,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class UpdateTrainYardDto {
|
||||
@ApiProperty({
|
||||
format: 'uuid',
|
||||
description:
|
||||
'Yard the train now sits in. The coupled locomotives and wagons are relocated with it.',
|
||||
})
|
||||
@IsUUID()
|
||||
currentYardId!: string;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
@@ -19,6 +20,7 @@ import { BuildTrainDto } from './dto/build-train.dto';
|
||||
import { ListBuiltTrainsQueryDto } from './dto/list-built-trains-query.dto';
|
||||
import { ReorderTrainWagonsDto } from './dto/reorder-train-wagons.dto';
|
||||
import { UpdateTrainLocomotivesDto } from './dto/update-train-locomotives.dto';
|
||||
import { UpdateTrainYardDto } from './dto/update-train-yard.dto';
|
||||
import { TrainBuilderService } from './train-builder.service';
|
||||
|
||||
@ApiTags('train-builder')
|
||||
@@ -57,6 +59,15 @@ export class TrainBuilderController {
|
||||
return this.trainBuilderService.setLocomotives(id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/yard')
|
||||
@FleetManage()
|
||||
@ApiOperation({
|
||||
summary: 'Relocate the train — its locomotives and wagons move to the new yard with it',
|
||||
})
|
||||
setYard(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateTrainYardDto) {
|
||||
return this.trainBuilderService.setYard(id, dto.currentYardId);
|
||||
}
|
||||
|
||||
@Post(':id/wagons')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Freight, WagonStatus } from '@edr/types';
|
||||
import { Freight, WagonMovementKind, WagonStatus } from '@edr/types';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
@@ -10,6 +10,7 @@ import { DataSource, EntityManager, ILike, In } from 'typeorm';
|
||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { minLocomotiveLimits } from '../train-scheduling/train-capacity.util';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { AssignTrainWagonsDto } from './dto/assign-train-wagons.dto';
|
||||
import { BuildTrainDto } from './dto/build-train.dto';
|
||||
@@ -202,7 +203,6 @@ export class TrainBuilderService {
|
||||
const totalLengthMeters = round(
|
||||
wagons.reduce((sum, w) => sum + (w.wagonType?.lengthMeters ?? 0), 0),
|
||||
);
|
||||
const maxGrossTons = round(totalTareTons + totalCapacityTons);
|
||||
const maxPullWeightTons = round(limits?.maxPullWeightTons ?? 0);
|
||||
const maxTrainLengthMeters = round(limits?.maxTrainLengthMeters ?? 0);
|
||||
|
||||
@@ -221,14 +221,17 @@ export class TrainBuilderService {
|
||||
totals: {
|
||||
wagonCount: wagons.length,
|
||||
totalTareTons,
|
||||
// Informational only — building never checks against full capacity;
|
||||
// the real gross check (cargo + tare vs haul limit) runs at allocation.
|
||||
totalCapacityTons,
|
||||
maxGrossTons,
|
||||
totalLengthMeters,
|
||||
maxPullWeightTons,
|
||||
maxTrainLengthMeters,
|
||||
// Fully loaded gross vs. what the weakest locomotive can haul.
|
||||
weightUtilizationPct: maxPullWeightTons
|
||||
? round((maxGrossTons / maxPullWeightTons) * 100)
|
||||
// Cargo the locomotives can still haul once pulling the empty consist.
|
||||
payloadCapacityTons: round(Math.max(0, maxPullWeightTons - totalTareTons)),
|
||||
// Share of the haul limit consumed by the empty wagons alone.
|
||||
tareUtilizationPct: maxPullWeightTons
|
||||
? round((totalTareTons / maxPullWeightTons) * 100)
|
||||
: null,
|
||||
lengthUtilizationPct: maxTrainLengthMeters
|
||||
? round((totalLengthMeters / maxTrainLengthMeters) * 100)
|
||||
@@ -269,6 +272,54 @@ export class TrainBuilderService {
|
||||
return this.getComposition(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relocate the train to another yard. The consist moves as one unit: every
|
||||
* coupled locomotive and wagon follows to the new yard (so their current
|
||||
* yards always match the train's), and each wagon gets a movement-ledger row.
|
||||
* Blocked while the train is out on a dispatched run.
|
||||
*/
|
||||
async setYard(id: string, currentYardId: string) {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const train = await this.getEditableTrain(manager, id);
|
||||
if (train.currentYardId === currentYardId) return;
|
||||
const yard = await manager.getRepository(Yard).findOne({ where: { id: currentYardId } });
|
||||
if (!yard) throw new NotFoundException(`Yard ${currentYardId} not found`);
|
||||
|
||||
await manager.getRepository(Train).update(train.id, { currentYardId: yard.id });
|
||||
|
||||
const links = await manager
|
||||
.getRepository(TrainLocomotive)
|
||||
.find({ where: { trainId: train.id } });
|
||||
if (links.length) {
|
||||
await manager
|
||||
.getRepository(Locomotive)
|
||||
.update(
|
||||
{ id: In(links.map((link) => link.locomotiveId)) },
|
||||
{ currentYardId: yard.id },
|
||||
);
|
||||
}
|
||||
|
||||
const wagons = await manager.getRepository(Wagon).find({ where: { trainId: train.id } });
|
||||
const now = new Date();
|
||||
for (const wagon of wagons) {
|
||||
if (wagon.currentYardId === yard.id) continue;
|
||||
await manager.getRepository(Wagon).update(wagon.id, { currentYardId: yard.id });
|
||||
// Ledger row keeps the wagon's yard history auditable (mirrors the
|
||||
// manual-relocation path in the wagons service).
|
||||
await manager.getRepository(WagonMovement).save(
|
||||
manager.getRepository(WagonMovement).create({
|
||||
wagonId: wagon.id,
|
||||
fromYardId: wagon.currentYardId ?? null,
|
||||
toYardId: yard.id,
|
||||
kind: WagonMovementKind.Manual,
|
||||
occurredAt: now,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return this.getComposition(id);
|
||||
}
|
||||
|
||||
/** Append AVAILABLE wagons from the train's own yard to the consist. */
|
||||
async assignWagons(id: string, dto: AssignTrainWagonsDto) {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
@@ -361,12 +412,8 @@ export class TrainBuilderService {
|
||||
.map((link) => link.locomotive)
|
||||
.filter((loco): loco is Locomotive => Boolean(loco));
|
||||
const wagons = train.wagons ?? [];
|
||||
const maxGrossTons = round(
|
||||
wagons.reduce(
|
||||
(sum, w) =>
|
||||
sum + (Number(w.wagonType?.tareWeightTons) || 0) + (Number(w.wagonType?.capacityTons) || 0),
|
||||
0,
|
||||
),
|
||||
const totalTareTons = round(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.tareWeightTons) || 0), 0),
|
||||
);
|
||||
return {
|
||||
id: train.id,
|
||||
@@ -379,7 +426,7 @@ export class TrainBuilderService {
|
||||
: null,
|
||||
locomotives: locomotives.map((loco) => ({ id: loco.id, code: loco.code, name: loco.name ?? null })),
|
||||
wagonCount: wagons.length,
|
||||
maxGrossTons,
|
||||
totalTareTons,
|
||||
totalLengthMeters: round(
|
||||
wagons.reduce((sum, w) => sum + (Number(w.wagonType?.lengthMeters) || 0), 0),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { Yard } from '../../rule-engine/entities/yard.entity';
|
||||
import { Wagon } from './wagon.entity';
|
||||
import { WagonTransferRequest } from './wagon-transfer-request.entity';
|
||||
|
||||
/**
|
||||
* Ledger of every physical wagon relocation between yards — one row per move.
|
||||
@@ -51,6 +52,14 @@ export class WagonMovement extends BaseEntity {
|
||||
@Column({ name: 'moved_by_user_id', type: 'uuid', nullable: true })
|
||||
movedByUserId?: string | null;
|
||||
|
||||
/** The transfer request this move fulfilled, when it came from one. */
|
||||
@Column({ name: 'transfer_request_id', type: 'uuid', nullable: true })
|
||||
transferRequestId?: string | null;
|
||||
|
||||
@ManyToOne(() => WagonTransferRequest, { nullable: true })
|
||||
@JoinColumn({ name: 'transfer_request_id' })
|
||||
transferRequest?: WagonTransferRequest | null;
|
||||
|
||||
@Column({ name: 'occurred_at', type: 'timestamptz' })
|
||||
occurredAt!: Date;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FleetManage,
|
||||
FleetView,
|
||||
WagonTransferFulfill,
|
||||
WagonTransferHistoryAll,
|
||||
WagonTransferRequest,
|
||||
} from '../../common/booking-guards';
|
||||
import { CreateTransferRequestDto } from './dto/create-transfer-request.dto';
|
||||
@@ -50,6 +51,30 @@ export class WagonTransferRequestsController {
|
||||
return this.service.listRequests(status);
|
||||
}
|
||||
|
||||
// NOTE: the two `history` routes MUST stay above `@Get(':id')` — Express
|
||||
// matches in declaration order, so `/history` would otherwise be captured by
|
||||
// the `:id` param route (and rejected by ParseUUIDPipe).
|
||||
@Get('history')
|
||||
@ApiOperation({
|
||||
summary: "Caller's own transfer history (requests filed/fulfilled + wagons moved)",
|
||||
})
|
||||
myHistory(@CurrentUser() user: TCurrentUser) {
|
||||
// Never fall through to the all-staff view: getHistory(undefined) means
|
||||
// "everyone", so a missing caller id must return empty, not leak scope.
|
||||
if (!user?.id) return { requests: [], movements: [] };
|
||||
return this.service.getHistory(user.id);
|
||||
}
|
||||
|
||||
@Get('history/all')
|
||||
@WagonTransferHistoryAll()
|
||||
@ApiQuery({ name: 'userId', required: false })
|
||||
@ApiOperation({
|
||||
summary: "Admin: any/all staff's transfer history (optional ?userId filter)",
|
||||
})
|
||||
allHistory(@Query('userId') userId?: string) {
|
||||
return this.service.getHistory(userId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get one transfer request' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
|
||||
@@ -6,14 +6,24 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { CreateTransferRequestDto } from './dto/create-transfer-request.dto';
|
||||
import { FulfillTransferRequestDto } from './dto/fulfill-transfer-request.dto';
|
||||
import { Wagon } from './entities/wagon.entity';
|
||||
import { WagonMovement } from './entities/wagon-movement.entity';
|
||||
import { WagonTransferRequest } from './entities/wagon-transfer-request.entity';
|
||||
import { WagonsService } from './wagons.service';
|
||||
|
||||
/** Bundled per-user activity: requests they touched + wagons they moved. */
|
||||
export interface TransferHistory {
|
||||
requests: WagonTransferRequest[];
|
||||
movements: WagonMovement[];
|
||||
}
|
||||
|
||||
/** How many ledger rows the history returns at most (newest first). */
|
||||
const HISTORY_LIMIT = 500;
|
||||
|
||||
const REQUEST_RELATIONS = {
|
||||
fromYard: true,
|
||||
toYard: true,
|
||||
@@ -33,6 +43,8 @@ export class WagonTransferRequestsService {
|
||||
private readonly requestRepo: Repository<WagonTransferRequest>,
|
||||
@InjectRepository(Wagon)
|
||||
private readonly wagonRepo: Repository<Wagon>,
|
||||
@InjectRepository(WagonMovement)
|
||||
private readonly movementRepo: Repository<WagonMovement>,
|
||||
private readonly wagonsService: WagonsService,
|
||||
) {}
|
||||
|
||||
@@ -125,10 +137,12 @@ export class WagonTransferRequestsService {
|
||||
);
|
||||
}
|
||||
|
||||
// Reuse the audited bulk-transfer path (writes wagon_movements ledger rows).
|
||||
// Reuse the audited bulk-transfer path (writes wagon_movements ledger rows,
|
||||
// each stamped with this request's id so history can link them back).
|
||||
await this.wagonsService.bulkTransfer(
|
||||
{ wagonIds, toYardId: request.toYardId },
|
||||
userId,
|
||||
{ transferRequestId: request.id },
|
||||
);
|
||||
|
||||
request.status = WagonTransferRequestStatus.Fulfilled;
|
||||
@@ -138,6 +152,38 @@ export class WagonTransferRequestsService {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user transfer history: the requests a user filed OR fulfilled, plus the
|
||||
* individual wagons they physically moved (linked back to their request when
|
||||
* one drove the move). Pass a `userId` to scope to one staffer; pass
|
||||
* `undefined` for the admin all-staff view. Scope is decided by the CALLER
|
||||
* (the controller passes the caller's id unless they hold the history-all
|
||||
* permission) — this method trusts its argument.
|
||||
*/
|
||||
async getHistory(userId?: string | null): Promise<TransferHistory> {
|
||||
const requests = await this.requestRepo.find({
|
||||
where: userId
|
||||
? [{ requestedByUserId: userId }, { fulfilledByUserId: userId }]
|
||||
: {},
|
||||
relations: REQUEST_RELATIONS,
|
||||
order: { createdAt: 'DESC' },
|
||||
take: HISTORY_LIMIT,
|
||||
});
|
||||
|
||||
const movements = await this.movementRepo.find({
|
||||
// Own view: moves I made. All view: every user-attributed move (skip the
|
||||
// system-written loaded/reposition legs that carry no mover).
|
||||
where: userId
|
||||
? { movedByUserId: userId }
|
||||
: { movedByUserId: Not(IsNull()) },
|
||||
relations: { wagon: true, fromYard: true, toYard: true, transferRequest: true },
|
||||
order: { occurredAt: 'DESC' },
|
||||
take: HISTORY_LIMIT,
|
||||
});
|
||||
|
||||
return { requests, movements };
|
||||
}
|
||||
|
||||
/** Withdraw a still-PENDING request. */
|
||||
async cancelRequest(id: string): Promise<WagonTransferRequest> {
|
||||
const request = await this.findById(id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Wagon } from './entities/wagon.entity';
|
||||
import { WagonMovement } from './entities/wagon-movement.entity';
|
||||
import { WagonTransferRequest } from './entities/wagon-transfer-request.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
@@ -10,7 +11,15 @@ import { WagonsService } from './wagons.service';
|
||||
import { WagonTransferRequestsService } from './wagon-transfer-requests.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Wagon, WagonTransferRequest, Train, Yard])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Wagon,
|
||||
WagonMovement,
|
||||
WagonTransferRequest,
|
||||
Train,
|
||||
Yard,
|
||||
]),
|
||||
],
|
||||
controllers: [
|
||||
WagonsController,
|
||||
TrainWagonsReorderController,
|
||||
|
||||
@@ -180,6 +180,7 @@ export class WagonsService {
|
||||
async bulkTransfer(
|
||||
dto: BulkTransferWagonsDto,
|
||||
userId?: string | null,
|
||||
opts?: { transferRequestId?: string | null },
|
||||
): Promise<{ moved: number }> {
|
||||
const { wagonIds, toYardId } = dto;
|
||||
if (!wagonIds.length) return { moved: 0 };
|
||||
@@ -215,6 +216,7 @@ export class WagonsService {
|
||||
toYardId,
|
||||
kind: WagonMovementKind.Manual,
|
||||
movedByUserId: userId ?? null,
|
||||
transferRequestId: opts?.transferRequestId ?? null,
|
||||
occurredAt: new Date(),
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user