[200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries

This commit is contained in:
Marshal
2026-08-20 15:45:42 +00:00
parent 8d7551bb8e
commit 9e1d5ee9f2
58 changed files with 3324 additions and 810 deletions

View File

@@ -0,0 +1,30 @@
import {
defaultPlannedWagonYards,
misalignedWagons,
scheduleYardOf,
} from './planned-wagon-yards.util';
const w = (id: string, currentYardId: string | null) => ({ id, currentYardId });
describe('planned-wagon-yards.util', () => {
it('scheduleYardOf prefers the plan and falls back to the physical yard', () => {
expect(scheduleYardOf({ w1: 'B' }, w('w1', 'C'))).toBe('B');
expect(scheduleYardOf({ w1: 'B' }, w('w2', 'C'))).toBe('C');
expect(scheduleYardOf(null, w('w2', null))).toBeNull();
});
it('defaultPlannedWagonYards snapshots on-route yards and rehomes the rest to origin', () => {
const { plan, rehomed } = defaultPlannedWagonYards(
[w('a', 'A'), w('b', 'B'), w('x', 'X'), w('n', null)],
new Set(['A', 'B', 'C']),
'A',
);
expect(plan).toEqual({ a: 'A', b: 'B', x: 'A', n: 'A' });
expect(rehomed.map((r) => r.id)).toEqual(['x', 'n']);
});
it('misalignedWagons lists only planned wagons standing elsewhere', () => {
const out = misalignedWagons({ a: 'A', b: 'B' }, [w('a', 'A'), w('b', 'C'), w('z', 'Z')]);
expect(out.map((r) => r.id)).toEqual(['b']);
});
});

View File

@@ -0,0 +1,51 @@
/**
* Per-schedule wagon yard plan: `{ wagonId: yardId }` — where THIS departure
* boards each consist wagon, independent of where the steel physically stands
* (`wagons.current_yard_id`, one fact shared by every schedule of the train).
*/
export type PlannedWagonYards = Record<string, string>;
type YardedWagon = { id: string; currentYardId: string | null };
/** Yard a schedule boards a wagon from: its own plan first, the physical yard otherwise. */
export function scheduleYardOf(
plan: PlannedWagonYards | null | undefined,
wagon: YardedWagon,
): string | null {
return plan?.[wagon.id] ?? wagon.currentYardId;
}
/**
* Default plan when a schedule is created from a built train: snapshot every
* wagon's physical yard (so later physical moves never shift this departure's
* capacity); a wagon standing off the route's pickup stops — or nowhere — is
* planned at the origin instead, and reported back so staff can redistribute.
*/
export function defaultPlannedWagonYards(
wagons: readonly YardedWagon[],
pickupYardIds: ReadonlySet<string>,
originYardId: string,
): { plan: PlannedWagonYards; rehomed: YardedWagon[] } {
const plan: PlannedWagonYards = {};
const rehomed: YardedWagon[] = [];
for (const wagon of wagons) {
if (wagon.currentYardId && pickupYardIds.has(wagon.currentYardId)) {
plan[wagon.id] = wagon.currentYardId;
} else {
plan[wagon.id] = originYardId;
rehomed.push(wagon);
}
}
return { plan, rehomed };
}
/** Wagons whose planned yard disagrees with where they physically stand. */
export function misalignedWagons<T extends YardedWagon>(
plan: PlannedWagonYards | null | undefined,
wagons: readonly T[],
): T[] {
return wagons.filter((w) => {
const planned = plan?.[w.id];
return planned != null && planned !== w.currentYardId;
});
}