mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
feat(clearance): preview charge documents before and after upload
This commit is contained in:
@@ -93,6 +93,12 @@ type OpenSlot = {
|
||||
legKey: string;
|
||||
/** Contiguous stop-index span this wagon physically rides (union of its cargo legs). */
|
||||
covered: { from: number; to: number };
|
||||
/**
|
||||
* Boarding-yard pool this wagon was opened from — the yard the consist plans
|
||||
* it at (`''` when the consist is not split across yards). A Mojo wagon
|
||||
* cannot later be stretched back to board at Gelan.
|
||||
*/
|
||||
pool: string;
|
||||
};
|
||||
|
||||
/** Stop-index range a booking occupies: edges `from..to-1` of the corridor. */
|
||||
@@ -197,9 +203,19 @@ export function planWagonsWithStock(params: {
|
||||
*/
|
||||
legs?: Map<string, BookingLeg>;
|
||||
edgeCount?: number;
|
||||
/**
|
||||
* Ordered corridor stop ids, parallel to the edges. Required for a consist
|
||||
* split across yards (`stock.byYardId`): a booking then draws ONLY from the
|
||||
* wagons planned at the yard it boards from (`stops[leg.from]`) — the
|
||||
* whole-train count would happily plan 17 Mojo wagons on a train that has
|
||||
* 15 there and 31 in Gelan, and the physical pin then fails after the
|
||||
* customer has paid.
|
||||
*/
|
||||
stops?: readonly string[];
|
||||
}): FlexPlanResult {
|
||||
const { bookings, allowed, stock, legs } = params;
|
||||
const edgeCount = Math.max(1, params.edgeCount ?? 1);
|
||||
const stops = params.stops ?? [];
|
||||
const openSlots: OpenSlot[] = [];
|
||||
const fitting: Booking[] = [];
|
||||
const deferred: DeferredBookingRow[] = [];
|
||||
@@ -214,32 +230,45 @@ export function planWagonsWithStock(params: {
|
||||
};
|
||||
const legKeyOf = (leg: BookingLeg) => `${leg.from}-${leg.to}`;
|
||||
|
||||
// Wagons of a type in use per corridor edge. A type is available for a leg
|
||||
// when its busiest edge WITHIN that leg still has stock spare — the max over
|
||||
// edges is the number of physical wagons the type needs simultaneously.
|
||||
// Split consist: each boarding yard is its own pool of steel (mirrors
|
||||
// WagonStockLedger). Single-yard consist / loose yard pool: one pool ''.
|
||||
const poolOf = (leg: BookingLeg): string =>
|
||||
stock.byYardId ? (stops[leg.from] ?? '') : '';
|
||||
const rowKeyFor = (wagonTypeId: string, pool: string): string =>
|
||||
pool ? `${pool}\u0000${wagonTypeId}` : wagonTypeId;
|
||||
const totalFor = (wagonTypeId: string, pool: string): number =>
|
||||
pool
|
||||
? (stock.byYardId?.get(pool)?.get(wagonTypeId) ?? 0)
|
||||
: (stock.remainingByTypeId.get(wagonTypeId) ?? 0);
|
||||
|
||||
// Wagons of a type in use per corridor edge, per pool. A type is available
|
||||
// for a leg when its busiest edge WITHIN that leg still has stock spare — the
|
||||
// max over edges is the number of physical wagons the type needs simultaneously.
|
||||
const usedPerEdge = new Map<string, number[]>();
|
||||
const usedRow = (wagonTypeId: string): number[] => {
|
||||
let row = usedPerEdge.get(wagonTypeId);
|
||||
const usedRow = (key: string): number[] => {
|
||||
let row = usedPerEdge.get(key);
|
||||
if (!row) {
|
||||
row = new Array<number>(edgeCount).fill(0);
|
||||
usedPerEdge.set(wagonTypeId, row);
|
||||
usedPerEdge.set(key, row);
|
||||
}
|
||||
return row;
|
||||
};
|
||||
const availableFor = (wagonTypeId: string, leg: BookingLeg): number => {
|
||||
const total = stock.remainingByTypeId.get(wagonTypeId) ?? 0;
|
||||
const row = usedPerEdge.get(wagonTypeId);
|
||||
const pool = poolOf(leg);
|
||||
const total = totalFor(wagonTypeId, pool);
|
||||
const row = usedPerEdge.get(rowKeyFor(wagonTypeId, pool));
|
||||
if (!row) return total;
|
||||
let busiest = 0;
|
||||
for (let e = leg.from; e < leg.to; e += 1) busiest = Math.max(busiest, row[e] ?? 0);
|
||||
return total - busiest;
|
||||
};
|
||||
|
||||
const noStockMessage = (candidates: WagonType[]): string => {
|
||||
const noStockMessage = (candidates: WagonType[], leg: BookingLeg): string => {
|
||||
const codes = candidates.map((wt) => wt.code).join('/');
|
||||
return stock.mode === 'TRAIN'
|
||||
? `Train has no free ${codes} wagon left`
|
||||
: `No available ${codes} wagon at the yard`;
|
||||
if (stock.mode !== 'TRAIN') return `No available ${codes} wagon at the yard`;
|
||||
return poolOf(leg)
|
||||
? `Train has no free ${codes} wagon planned at the boarding yard`
|
||||
: `Train has no free ${codes} wagon left`;
|
||||
};
|
||||
|
||||
/** Open a new wagon of one of the candidate types, consuming stock on the leg's edges. */
|
||||
@@ -251,7 +280,7 @@ export function planWagonsWithStock(params: {
|
||||
): OpenSlot | PlacementProblem => {
|
||||
const inStock = candidates.filter((wt) => availableFor(wt.id, leg) > 0);
|
||||
if (!inStock.length) {
|
||||
return { kind: 'stock', message: noStockMessage(candidates), candidates };
|
||||
return { kind: 'stock', message: noStockMessage(candidates, leg), candidates };
|
||||
}
|
||||
// Bulk favors the largest wagon (fewest wagons for the tonnage); containers
|
||||
// favor the deepest stock so the consist drains evenly. Ties keep config order.
|
||||
@@ -261,7 +290,8 @@ export function planWagonsWithStock(params: {
|
||||
availableFor(b.id, leg) - availableFor(a.id, leg)
|
||||
: availableFor(b.id, leg) - availableFor(a.id, leg),
|
||||
)[0];
|
||||
const row = usedRow(chosen.id);
|
||||
const pool = poolOf(leg);
|
||||
const row = usedRow(rowKeyFor(chosen.id, pool));
|
||||
for (let e = leg.from; e < leg.to; e += 1) row[e] = (row[e] ?? 0) + 1;
|
||||
const open: OpenSlot = {
|
||||
slot: slotFromWagonType(chosen, kind),
|
||||
@@ -271,6 +301,7 @@ export function planWagonsWithStock(params: {
|
||||
freeCapacityTons: Number(chosen.capacityTons),
|
||||
legKey: legKeyOf(leg),
|
||||
covered: { ...leg },
|
||||
pool,
|
||||
};
|
||||
openSlots.push(open);
|
||||
return open;
|
||||
@@ -290,8 +321,11 @@ export function planWagonsWithStock(params: {
|
||||
* slot's type spare — extending the span puts this wagon on those edges.
|
||||
*/
|
||||
const canExtendSpan = (open: OpenSlot, leg: BookingLeg): boolean => {
|
||||
const total = stock.remainingByTypeId.get(open.slot.wagonTypeId) ?? 0;
|
||||
const row = usedPerEdge.get(open.slot.wagonTypeId);
|
||||
// A pooled wagon boards where its yard is; it cannot be stretched back to
|
||||
// an EARLIER stop (the steel is not there), only ridden further.
|
||||
if (open.pool && leg.from < open.covered.from) return false;
|
||||
const total = totalFor(open.slot.wagonTypeId, open.pool);
|
||||
const row = usedPerEdge.get(rowKeyFor(open.slot.wagonTypeId, open.pool));
|
||||
const from = Math.min(open.covered.from, leg.from);
|
||||
const to = Math.max(open.covered.to, leg.to);
|
||||
for (let e = from; e < to; e += 1) {
|
||||
@@ -303,7 +337,7 @@ export function planWagonsWithStock(params: {
|
||||
|
||||
/** Grow the slot's span onto the leg's new edges, consuming stock there. */
|
||||
const extendSpan = (open: OpenSlot, leg: BookingLeg): void => {
|
||||
const row = usedRow(open.slot.wagonTypeId);
|
||||
const row = usedRow(rowKeyFor(open.slot.wagonTypeId, open.pool));
|
||||
const from = Math.min(open.covered.from, leg.from);
|
||||
const to = Math.max(open.covered.to, leg.to);
|
||||
for (let e = from; e < to; e += 1) {
|
||||
|
||||
Reference in New Issue
Block a user