mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 10:45:44 +00:00
feat(train-scheduling): implement container movement between wagons
- Added functionality to move containers between wagons in the train scheduling system. - Introduced API endpoint and service method to handle container movement. - Updated component to support drag-and-drop for rearranging containers. - Enhanced to allow moving containers to other wagons via a context menu. - Implemented UI feedback for container movement actions, including loading states and success/error notifications. - Updated relevant types and constants to accommodate new container movement logic. - Added tests for the rule engine to ensure proper handling of hazardous bookings.
This commit is contained in:
@@ -77,6 +77,7 @@ import {
|
||||
TrainScheduleFreightType,
|
||||
} from './dto/list-train-schedules-query.dto';
|
||||
import { PinWagonsDto } from './dto/pin-wagons.dto';
|
||||
import { MoveContainerItemDto } from './dto/move-container-item.dto';
|
||||
import { UpdateContainerItemDto } from './dto/update-container-item.dto';
|
||||
import { UpdateImportLoadingStatusDto } from './dto/update-import-loading-status.dto';
|
||||
import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto';
|
||||
@@ -122,7 +123,7 @@ import {
|
||||
sumWagonsRequired,
|
||||
type TrainLimitConfig,
|
||||
validateContainerPlacements,
|
||||
validateMixedTrainLimits,
|
||||
validateMixedTrainLimitsPerEdge,
|
||||
type ContainerPlacementInput,
|
||||
type WagonPlanSlot,
|
||||
} from './wagon-plan.util';
|
||||
@@ -1538,8 +1539,21 @@ export class TrainSchedulingService {
|
||||
}
|
||||
}
|
||||
|
||||
// The rebuild below deletes EVERY schedule↔booking link row and recreates
|
||||
// only what makes the new plan. Ride-along (intercity) bookings are linked
|
||||
// OUTSIDE this flow — by acceptIntercity/allocate — and never appear in the
|
||||
// workspace's picked ids, so planning from dto.bookingIds alone silently
|
||||
// orphans them: PAID + SCHEDULED with no link and no wagon, invisible in
|
||||
// every list. Every (re)assignment therefore re-plans the WHOLE train:
|
||||
// the requested ids plus everything currently linked.
|
||||
const linkedRows =
|
||||
await this.trainScheduleBookingsRepository.findByScheduleId(scheduleId);
|
||||
const allBookingIds = [
|
||||
...new Set([...dto.bookingIds, ...linkedRows.map((row) => row.bookingId)]),
|
||||
];
|
||||
|
||||
const previewDto = {
|
||||
bookingIds: dto.bookingIds,
|
||||
bookingIds: allBookingIds,
|
||||
scheduleDate: schedule.scheduledDepartureDate.toISOString(),
|
||||
originStationId: schedule.originStationId,
|
||||
destinationStationId: schedule.destinationStationId,
|
||||
@@ -1562,8 +1576,13 @@ export class TrainSchedulingService {
|
||||
// preview the wagon plan first, then lay containers into the plan's slots.
|
||||
// Without this the placement validator rejects container bookings outright
|
||||
// ("Container placements are required for container bookings").
|
||||
// Callers hand-pick placements only for the bookings they know about; the
|
||||
// union above may have folded in linked ride-alongs those placements never
|
||||
// covered. Auto-fill whatever units are missing (all of them when no
|
||||
// placements were sent at all) so the placement validator doesn't reject
|
||||
// container bookings the caller couldn't have placed.
|
||||
let containerPlacements = dto.containerPlacements;
|
||||
if (!containerPlacements?.length) {
|
||||
{
|
||||
const preview = await this.validateBookingsForScheduling(
|
||||
previewDto,
|
||||
freightType ?? null,
|
||||
@@ -1578,18 +1597,28 @@ export class TrainSchedulingService {
|
||||
);
|
||||
if (containerBookings.length) {
|
||||
const units = expandBookingContainerUnits(containerBookings);
|
||||
const slots = getContainerSlotSequenceNos(preview.wagonPlan);
|
||||
const generated = autoFillPlacements(units, slots);
|
||||
const missing = findMissingContainerNumberIssues(units, generated);
|
||||
if (missing.length) {
|
||||
throw new BadRequestException({
|
||||
message: `Booking validation failed: ${missing
|
||||
.map((m) => m.issue)
|
||||
.join('; ')}`,
|
||||
violations: missing.map((m) => m.issue),
|
||||
});
|
||||
const providedKeys = new Set(
|
||||
(containerPlacements ?? []).map(
|
||||
(p) => `${p.bookingContainerId}:${p.unitIndex}`,
|
||||
),
|
||||
);
|
||||
const unplacedUnits = units.filter(
|
||||
(u) => !providedKeys.has(`${u.bookingContainerId}:${u.unitIndex}`),
|
||||
);
|
||||
if (unplacedUnits.length) {
|
||||
const slots = getContainerSlotSequenceNos(preview.wagonPlan);
|
||||
const generated = autoFillPlacements(unplacedUnits, slots);
|
||||
const missing = findMissingContainerNumberIssues(unplacedUnits, generated);
|
||||
if (missing.length) {
|
||||
throw new BadRequestException({
|
||||
message: `Booking validation failed: ${missing
|
||||
.map((m) => m.issue)
|
||||
.join('; ')}`,
|
||||
violations: missing.map((m) => m.issue),
|
||||
});
|
||||
}
|
||||
containerPlacements = [...(containerPlacements ?? []), ...generated];
|
||||
}
|
||||
containerPlacements = generated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1632,8 +1661,10 @@ export class TrainSchedulingService {
|
||||
// NW5 free) — the caller saw HTTP 200 and a green toast over a no-op.
|
||||
// A stock shortage is a physical impossibility, so forceAssign cannot
|
||||
// override it either.
|
||||
// Linked ride-alongs count as requested too: silently dropping one here is
|
||||
// exactly the delete-and-recreate orphan this method must never produce.
|
||||
const plannedIds = new Set(validation.bookings.map((b) => b.id));
|
||||
const droppedRequested = dto.bookingIds.filter((id) => !plannedIds.has(id));
|
||||
const droppedRequested = allBookingIds.filter((id) => !plannedIds.has(id));
|
||||
if (droppedRequested.length) {
|
||||
const reasonById = new Map(
|
||||
validation.deferredBookings.map((d) => [d.id, `${d.reference}: ${d.reason}`]),
|
||||
@@ -3852,25 +3883,24 @@ export class TrainSchedulingService {
|
||||
);
|
||||
}
|
||||
|
||||
// Corridor-aware: a booking belongs on this train when its origin and
|
||||
// destination lie on the schedule's stop list in order — sub-corridor
|
||||
// bookings (Dire→Djibouti on an Addis→…→Djibouti train) are valid. The
|
||||
// stop list is also what makes the wagon plan leg-aware below.
|
||||
let stops = [dto.originStationId, dto.destinationStationId];
|
||||
if (targetScheduleId) {
|
||||
const target = await this.trainSchedulesRepository.findById(targetScheduleId);
|
||||
if (target) stops = await this.stopYardsForSchedule(target);
|
||||
}
|
||||
if (
|
||||
await (async () => {
|
||||
// Corridor-aware: a booking belongs on this train when its origin and
|
||||
// destination lie on the schedule's stop list in order — sub-corridor
|
||||
// bookings (Dire→Djibouti on an Addis→…→Djibouti train) are valid.
|
||||
let stops = [dto.originStationId, dto.destinationStationId];
|
||||
if (targetScheduleId) {
|
||||
const target = await this.trainSchedulesRepository.findById(targetScheduleId);
|
||||
if (target) stops = await this.stopYardsForSchedule(target);
|
||||
bookings.some((b) => {
|
||||
if (targetScheduleId && b.trainScheduleId === targetScheduleId) {
|
||||
return false;
|
||||
}
|
||||
return bookings.some((b) => {
|
||||
if (targetScheduleId && b.trainScheduleId === targetScheduleId) {
|
||||
return false;
|
||||
}
|
||||
const fromIdx = stops.indexOf(b.originYardId);
|
||||
const toIdx = stops.indexOf(b.destinationYardId);
|
||||
return fromIdx < 0 || toIdx < 0 || fromIdx >= toIdx;
|
||||
});
|
||||
})()
|
||||
const fromIdx = stops.indexOf(b.originYardId);
|
||||
const toIdx = stops.indexOf(b.destinationYardId);
|
||||
return fromIdx < 0 || toIdx < 0 || fromIdx >= toIdx;
|
||||
})
|
||||
) {
|
||||
violations.push('Selected bookings must lie on the schedule route (origin before destination)');
|
||||
}
|
||||
@@ -3956,7 +3986,22 @@ export class TrainSchedulingService {
|
||||
stock = { mode: 'YARD', remainingByTypeId, codesByTypeId };
|
||||
}
|
||||
|
||||
const planned = planWagonsWithStock({ bookings, allowed, stock });
|
||||
// Leg-aware stock: each booking consumes wagons only on the edges it rides,
|
||||
// so a ride-along on an empty leg never competes with cargo on a full one.
|
||||
const legByBookingId = new Map(
|
||||
bookings.flatMap((b) => {
|
||||
const from = stops.indexOf(b.originYardId);
|
||||
const to = stops.indexOf(b.destinationYardId);
|
||||
return from >= 0 && to > from ? [[b.id, { from, to }] as const] : [];
|
||||
}),
|
||||
);
|
||||
const planned = planWagonsWithStock({
|
||||
bookings,
|
||||
allowed,
|
||||
stock,
|
||||
legs: legByBookingId,
|
||||
edgeCount: Math.max(1, stops.length - 1),
|
||||
});
|
||||
violations.push(...planned.configIssues);
|
||||
const fittingBookings = planned.fitting;
|
||||
const deferredBookings: DeferredBookingRow[] = planned.deferred;
|
||||
@@ -4018,10 +4063,11 @@ export class TrainSchedulingService {
|
||||
).values(),
|
||||
];
|
||||
pushLimit(
|
||||
validateMixedTrainLimits(
|
||||
validateMixedTrainLimitsPerEdge(
|
||||
wagonPlan,
|
||||
plannedWagonTypes.length ? plannedWagonTypes : [{ lengthMeters: 14 }],
|
||||
trainLimits,
|
||||
stops,
|
||||
),
|
||||
);
|
||||
if (requireContainerPlacements && resolvedMode !== 'BULK') {
|
||||
@@ -6810,12 +6856,33 @@ export class TrainSchedulingService {
|
||||
status: sb.booking?.status ?? null,
|
||||
schedulingStatus: sb.booking?.schedulingStatus ?? null,
|
||||
freightType: sb.booking?.freightType ?? null,
|
||||
// Which leg of the corridor this booking rides — the workspace can't
|
||||
// tell a ride-along (intercity) or sub-corridor booking from through
|
||||
// cargo without it.
|
||||
tradeDirection: sb.booking?.tradeDirection ?? null,
|
||||
originYardId: sb.booking?.originYardId ?? null,
|
||||
destinationYardId: sb.booking?.destinationYardId ?? null,
|
||||
origin:
|
||||
sb.booking?.originYard?.label ?? sb.booking?.originYard?.code ?? null,
|
||||
destination:
|
||||
sb.booking?.destinationYard?.label ??
|
||||
sb.booking?.destinationYard?.code ??
|
||||
null,
|
||||
wagonsRequired:
|
||||
sb.booking?.wagonsRequired != null
|
||||
? Number(sb.booking.wagonsRequired)
|
||||
: null,
|
||||
loadedAt: sb.booking?.loadedAt?.toISOString() ?? null,
|
||||
arrivedAt: sb.booking?.arrivedAt?.toISOString() ?? null,
|
||||
// Loaded/unloaded is tracked on the schedule↔booking link, not the
|
||||
// booking itself — staff flip it per booking in the workspace before
|
||||
// dispatch. Defaults UNLOADED for links written before the column.
|
||||
loadingStatus: sb.loadingStatus ?? LoadingStatus.Unloaded,
|
||||
wagonAssigned: allocatedBookingIds.has(sb.booking?.id ?? sb.bookingId),
|
||||
})) ?? [],
|
||||
// Ordered corridor stops (route milestones; falls back to the two
|
||||
// endpoints) — lets the UI draw per-segment occupancy and label legs.
|
||||
stops: this.mapScheduleStops(schedule),
|
||||
// True when the wagon plan above is served from the frozen snapshot (schedule
|
||||
// is dispatched/arrived/cancelled) rather than the live joins — the UI can badge
|
||||
// it "historical" and skip re-pin affordances.
|
||||
@@ -6824,6 +6891,42 @@ export class TrainSchedulingService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Ordered corridor stops with labels, from the loaded route graph (no extra query). */
|
||||
private mapScheduleStops(
|
||||
schedule: TrainSchedule,
|
||||
): Array<{ yardId: string; label: string }> {
|
||||
const milestones = [...(schedule.route?.milestones ?? [])].sort(
|
||||
(a, b) => a.sequenceNo - b.sequenceNo,
|
||||
);
|
||||
const raw = milestones.length >= 2
|
||||
? milestones.map((m) => ({
|
||||
yardId: m.yardId,
|
||||
label: m.yard?.label ?? m.yard?.code ?? m.yardId,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
yardId: schedule.originStationId,
|
||||
label:
|
||||
schedule.originStation?.label ??
|
||||
schedule.originStation?.code ??
|
||||
schedule.originStationId,
|
||||
},
|
||||
{
|
||||
yardId: schedule.destinationStationId,
|
||||
label:
|
||||
schedule.destinationStation?.label ??
|
||||
schedule.destinationStation?.code ??
|
||||
schedule.destinationStationId,
|
||||
},
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
return raw.filter((stop) => {
|
||||
if (!stop.yardId || seen.has(stop.yardId)) return false;
|
||||
seen.add(stop.yardId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private isHoldActive(booking: Booking): boolean {
|
||||
if (!booking.holdExpiresAt) return false;
|
||||
return booking.holdExpiresAt.getTime() > Date.now();
|
||||
@@ -7232,6 +7335,232 @@ export class TrainSchedulingService {
|
||||
return { id: itemId, containerNumber: dto.containerNumber ?? null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Staff rearrange: move one container to another wagon of the same train, or
|
||||
* swap two containers (cross-wagon, or same-wagon to exchange slot positions).
|
||||
* Capacity is re-validated here — one wagon holds 2 TEU (one 40ft or two
|
||||
* 20ft) and the wagon's rated payload is never exceeded — so a drag on the
|
||||
* consist can't silently overload a wagon. Allocation rows follow the items:
|
||||
* the booking gets an allocation on the target wagon (created if missing),
|
||||
* weights shift with the container, and an allocation left with no items is
|
||||
* deleted.
|
||||
*/
|
||||
async moveContainerItem(
|
||||
scheduleId: string,
|
||||
itemId: string,
|
||||
dto: MoveContainerItemDto,
|
||||
): Promise<any> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (['DISPATCHED', 'ARRIVED'].includes(schedule.status)) {
|
||||
throw new BadRequestException('Cannot rearrange containers on a dispatched train');
|
||||
}
|
||||
|
||||
const wagonById = new Map((schedule.trainSet?.wagons ?? []).map((w) => [w.id, w]));
|
||||
const itemRepo = this.dataSource.getRepository(WagonAllocationContainerItem);
|
||||
const allocRepo = this.dataSource.getRepository(WagonBookingAllocation);
|
||||
|
||||
const item = await itemRepo.findOne({
|
||||
where: { id: itemId },
|
||||
relations: { allocation: true, containerType: true },
|
||||
});
|
||||
const sourceWagon = item?.allocation
|
||||
? wagonById.get(item.allocation.trainSetWagonId)
|
||||
: undefined;
|
||||
if (!item?.allocation || !sourceWagon) {
|
||||
throw new NotFoundException(`Container item ${itemId} not found on this schedule`);
|
||||
}
|
||||
const targetWagon = wagonById.get(dto.targetTrainSetWagonId);
|
||||
if (!targetWagon) {
|
||||
throw new NotFoundException('Target wagon is not part of this schedule');
|
||||
}
|
||||
|
||||
const loadAllocations = (trainSetWagonId: string) =>
|
||||
allocRepo.find({
|
||||
where: { trainSetWagonId },
|
||||
relations: { containerItems: { containerType: true } },
|
||||
});
|
||||
const [sourceAllocs, targetAllocs] = await Promise.all([
|
||||
loadAllocations(sourceWagon.id),
|
||||
loadAllocations(targetWagon.id),
|
||||
]);
|
||||
if (
|
||||
targetAllocs.some((a) => (a.loadType ?? '').toUpperCase().includes('BULK'))
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Wagon #${targetWagon.sequenceNo} carries a bulk load — containers cannot ride it`,
|
||||
);
|
||||
}
|
||||
|
||||
const swapItem = dto.swapWithItemId
|
||||
? targetAllocs
|
||||
.flatMap((a) => a.containerItems ?? [])
|
||||
.find((it) => it.id === dto.swapWithItemId)
|
||||
: undefined;
|
||||
if (dto.swapWithItemId && !swapItem) {
|
||||
throw new BadRequestException('The container to swap with is not on the target wagon');
|
||||
}
|
||||
if (swapItem?.id === item.id) {
|
||||
throw new BadRequestException('Cannot swap a container with itself');
|
||||
}
|
||||
if (sourceWagon.id === targetWagon.id && !swapItem) {
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
// TEU per container: 40ft fills a wagon (2), 20ft takes half (1). One
|
||||
// wagon never exceeds 2 TEU — the same rule the auto-allocation packs by.
|
||||
const MAX_TEU_PER_WAGON = 2;
|
||||
const teuOf = (it: { containerType?: { sizeFt?: number | null } | null }) =>
|
||||
(it.containerType?.sizeFt ?? 20) >= 40 ? 2 : 1;
|
||||
const itemsOf = (allocs: WagonBookingAllocation[]) =>
|
||||
allocs.flatMap((a) => a.containerItems ?? []);
|
||||
// Weight a container carries into the move: its own gross when recorded,
|
||||
// otherwise an even share of its allocation's weight.
|
||||
const weightOf = (
|
||||
it: WagonAllocationContainerItem,
|
||||
alloc: WagonBookingAllocation,
|
||||
siblings: number,
|
||||
) =>
|
||||
Number(it.grossWeightTons) ||
|
||||
Number(alloc.allocatedWeightTons) / Math.max(1, siblings);
|
||||
|
||||
const sourceAlloc = sourceAllocs.find((a) => a.id === item.wagonBookingAllocationId);
|
||||
if (!sourceAlloc) {
|
||||
throw new NotFoundException(`Container item ${itemId} not found on this schedule`);
|
||||
}
|
||||
const itemWeight = weightOf(item, sourceAlloc, (sourceAlloc.containerItems ?? []).length);
|
||||
const swapAlloc = swapItem
|
||||
? targetAllocs.find((a) => a.id === swapItem.wagonBookingAllocationId)
|
||||
: undefined;
|
||||
const swapWeight =
|
||||
swapItem && swapAlloc
|
||||
? weightOf(swapItem, swapAlloc, (swapAlloc.containerItems ?? []).length)
|
||||
: 0;
|
||||
|
||||
if (sourceWagon.id !== targetWagon.id) {
|
||||
const targetTeu = itemsOf(targetAllocs)
|
||||
.filter((it) => it.id !== swapItem?.id)
|
||||
.reduce((sum, it) => sum + teuOf(it), 0);
|
||||
if (targetTeu + teuOf(item) > MAX_TEU_PER_WAGON) {
|
||||
throw new BadRequestException(
|
||||
`Wagon #${targetWagon.sequenceNo} has no room — a wagon holds one 40ft or two 20ft containers`,
|
||||
);
|
||||
}
|
||||
if (swapItem) {
|
||||
const sourceTeu = itemsOf(sourceAllocs)
|
||||
.filter((it) => it.id !== item.id)
|
||||
.reduce((sum, it) => sum + teuOf(it), 0);
|
||||
if (sourceTeu + teuOf(swapItem) > MAX_TEU_PER_WAGON) {
|
||||
throw new BadRequestException(
|
||||
`Wagon #${sourceWagon.sequenceNo} has no room for the swapped container — a wagon holds one 40ft or two 20ft containers`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cargoOn = (allocs: WagonBookingAllocation[]) =>
|
||||
allocs.reduce((sum, a) => sum + Number(a.allocatedWeightTons || 0), 0);
|
||||
const checkPayload = (
|
||||
wagon: { sequenceNo: number; capacityTons?: number | null },
|
||||
cargoAfter: number,
|
||||
) => {
|
||||
const capacity = Number(wagon.capacityTons ?? 0);
|
||||
if (capacity > 0 && cargoAfter > capacity + 0.001) {
|
||||
throw new BadRequestException(
|
||||
`Wagon #${wagon.sequenceNo} would carry ${roundTons(cargoAfter)}T — over its ${capacity}T payload`,
|
||||
);
|
||||
}
|
||||
};
|
||||
checkPayload(targetWagon, cargoOn(targetAllocs) - swapWeight + itemWeight);
|
||||
if (swapItem) {
|
||||
checkPayload(sourceWagon, cargoOn(sourceAllocs) - itemWeight + swapWeight);
|
||||
}
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const items = manager.getRepository(WagonAllocationContainerItem);
|
||||
const allocs = manager.getRepository(WagonBookingAllocation);
|
||||
|
||||
// Same-wagon swap: the containers only trade slot positions.
|
||||
if (sourceWagon.id === targetWagon.id && swapItem) {
|
||||
const a = item.positionOnWagon ?? null;
|
||||
const b = swapItem.positionOnWagon ?? null;
|
||||
await items.update(item.id, { positionOnWagon: b });
|
||||
await items.update(swapItem.id, { positionOnWagon: a });
|
||||
return;
|
||||
}
|
||||
|
||||
const moveOne = async (
|
||||
moving: WagonAllocationContainerItem,
|
||||
toWagonId: string,
|
||||
weight: number,
|
||||
) => {
|
||||
// Re-read the source allocation — the other leg of a swap may have
|
||||
// already shifted weight on it within this transaction.
|
||||
const from = await allocs.findOne({
|
||||
where: { id: moving.wagonBookingAllocationId },
|
||||
});
|
||||
if (!from) return;
|
||||
let to = await allocs.findOne({
|
||||
where: { trainSetWagonId: toWagonId, bookingId: from.bookingId },
|
||||
});
|
||||
if (!to) {
|
||||
to = await allocs.save(
|
||||
allocs.create({
|
||||
trainSetWagonId: toWagonId,
|
||||
bookingId: from.bookingId,
|
||||
allocatedWeightTons: 0,
|
||||
loadType: from.loadType ?? 'CONTAINER',
|
||||
status: from.status ?? 'PLANNED',
|
||||
}),
|
||||
);
|
||||
}
|
||||
await items.update(moving.id, { wagonBookingAllocationId: to.id });
|
||||
await allocs.update(to.id, {
|
||||
allocatedWeightTons: roundTons(Number(to.allocatedWeightTons) + weight),
|
||||
});
|
||||
const remaining = await items.count({
|
||||
where: { wagonBookingAllocationId: from.id },
|
||||
});
|
||||
if (remaining === 0) {
|
||||
await allocs.delete(from.id);
|
||||
} else {
|
||||
await allocs.update(from.id, {
|
||||
allocatedWeightTons: roundTons(
|
||||
Math.max(0, Number(from.allocatedWeightTons) - weight),
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await moveOne(item, targetWagon.id, itemWeight);
|
||||
if (swapItem) {
|
||||
await moveOne(swapItem, sourceWagon.id, swapWeight);
|
||||
}
|
||||
|
||||
// Keep slot positions dense (1..n) on both touched wagons.
|
||||
const renumber = async (trainSetWagonId: string) => {
|
||||
const wagonAllocs = await allocs.find({
|
||||
where: { trainSetWagonId },
|
||||
relations: { containerItems: true },
|
||||
});
|
||||
const wagonItems = wagonAllocs
|
||||
.flatMap((a) => a.containerItems ?? [])
|
||||
.sort((x, y) => (x.positionOnWagon ?? 99) - (y.positionOnWagon ?? 99));
|
||||
for (let i = 0; i < wagonItems.length; i += 1) {
|
||||
if (wagonItems[i].positionOnWagon !== i + 1) {
|
||||
await items.update(wagonItems[i].id, { positionOnWagon: i + 1 });
|
||||
}
|
||||
}
|
||||
};
|
||||
await renumber(sourceWagon.id);
|
||||
await renumber(targetWagon.id);
|
||||
});
|
||||
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
async getUnassignedBookings(scheduleId: string): Promise<UnassignedBookingsResponse> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
|
||||
Reference in New Issue
Block a user