implement contract cancellation feature and update contract statuses

- Added functionality to cancel contracts, allowing users to provide a reason for cancellation.
- Updated contract statuses to include SUSPENDED and changed CLOSED to COMPLETED.
- Enhanced the UI to reflect the new cancellation option and updated messaging for contract statuses.
- Refactored contract booking actions to accommodate changes in booking logic for ONE_TIME and GENERAL contracts.
- Removed clearance document management from the contract detail page, as it is now handled per booking.
- Introduced a SQL script to reset bookings and train schedules for development purposes.
This commit is contained in:
Marshal
2026-07-28 05:02:58 +00:00
parent 481878e553
commit 2429f6b629
83 changed files with 3083 additions and 3729 deletions

View File

@@ -5,7 +5,7 @@ import {
consistViolations,
deriveTrainCapacityFromLocomotive,
grossWagonWeightTons,
minLocomotiveLimits,
combinedLocomotiveLimits,
sizePartialOfferWagons,
trainSetLocomotiveLimits,
} from './train-capacity.util';
@@ -197,42 +197,75 @@ describe('train-capacity.util', () => {
expect(bookingTrainLengthMeters('BULK', 3, { container: 14, bulk: 18 })).toBe(54);
});
it('takes the weakest locomotive across a multi-locomotive set', () => {
const limits = minLocomotiveLimits([
{ maxPullWeightTons: 3500, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
{ maxPullWeightTons: 4000, maxTrainLengthMeters: 760, overageToleranceTons: 20 },
it('SUMS pull weight and weight tolerance across a multi-locomotive set', () => {
// Two units haul together: 1750 + 1750 = 3500T base, 90 + 90 = 180T overage.
const limits = combinedLocomotiveLimits([
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
]);
expect(limits?.maxPullWeightTons).toBe(3500);
expect(limits?.overageToleranceTons).toBe(20);
expect(limits?.overageToleranceTons).toBe(180);
// A single locomotive is just its own limit — no doubling, no halving.
expect(
combinedLocomotiveLimits([
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
])?.maxPullWeightTons,
).toBe(1750);
});
it('takes the MINIMUM train length — a second locomotive does not lengthen the siding', () => {
const limits = combinedLocomotiveLimits([
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceMeters: 20 },
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 700, overageToleranceMeters: 5 },
]);
expect(limits?.maxTrainLengthMeters).toBe(700);
expect(limits?.overageToleranceMeters).toBe(5);
});
it('ignores unconfigured (null) tolerances instead of zeroing the set (S-2026-00024)', () => {
// LOCO-019 had 90T tolerance, LOCO-020 had none configured: the set must
// keep the 90, not collapse to 0 and reject 3547.6T on a 3500T train.
const limits = minLocomotiveLimits([
{ maxPullWeightTons: 3500, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
{ maxPullWeightTons: 3500, maxTrainLengthMeters: 760, overageToleranceTons: null },
// keep the 90 rather than collapse to 0 — an unset value abstains.
const limits = combinedLocomotiveLimits([
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: 90 },
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: null },
]);
expect(limits?.overageToleranceTons).toBe(90);
// All unconfigured → no tolerance.
const none = minLocomotiveLimits([
const none = combinedLocomotiveLimits([
{ maxPullWeightTons: 3500, maxTrainLengthMeters: 760 },
]);
expect(none?.overageToleranceTons).toBe(0);
});
it('reports no pull limit when NO locomotive has one configured', () => {
// Summing must not turn "unset" into 0 and strand every booking; an
// all-unset set keeps the old "no opinion" behaviour.
const limits = combinedLocomotiveLimits([
{ maxPullWeightTons: 0, maxTrainLengthMeters: 760 },
{ maxPullWeightTons: 0, maxTrainLengthMeters: 760 },
]);
expect(limits?.maxPullWeightTons).toBe(Infinity);
// One configured, one not → only the configured one contributes.
expect(
combinedLocomotiveLimits([
{ maxPullWeightTons: 1750, maxTrainLengthMeters: 760 },
{ maxPullWeightTons: 0, maxTrainLengthMeters: 760 },
])?.maxPullWeightTons,
).toBe(1750);
});
it('trainSetLocomotiveLimits prefers link rows and falls back to the legacy single loco', () => {
const l1 = { maxPullWeightTons: 3500, maxTrainLengthMeters: 760, overageToleranceTons: 90 };
const l2 = { maxPullWeightTons: 3600, maxTrainLengthMeters: 700, overageToleranceTons: null };
const l1 = { maxPullWeightTons: 1750, maxTrainLengthMeters: 760, overageToleranceTons: 90 };
const l2 = { maxPullWeightTons: 1800, maxTrainLengthMeters: 700, overageToleranceTons: null };
expect(
trainSetLocomotiveLimits({ locomotive: null, locomotives: [{ locomotive: l1 }, { locomotive: l2 }] }),
).toEqual({
maxPullWeightTons: 3500,
maxPullWeightTons: 3550,
maxTrainLengthMeters: 700,
overageToleranceTons: 90,
overageToleranceMeters: 0,
});
expect(trainSetLocomotiveLimits({ locomotive: l1 })?.maxPullWeightTons).toBe(3500);
expect(trainSetLocomotiveLimits({ locomotive: l1 })?.maxPullWeightTons).toBe(1750);
expect(trainSetLocomotiveLimits(null)).toBeNull();
expect(trainSetLocomotiveLimits({ locomotive: null, locomotives: [] })).toBeNull();
});