feat: enhance train scheduling and contract management features

- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage.
- Implemented API endpoints for recording station work and managing wagon detach requests.
- Updated contract templates to include Ethiopian customs handling options.
- Enhanced shipment forms to collect customs clearing agent details for without-customs bookings.
- Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts.
- Improved validation for customs clearing agent information in shipment forms.
- Updated various components and services to accommodate new features and ensure data integrity.
This commit is contained in:
Marshal
2026-08-25 21:44:21 +00:00
parent d5a5085d6d
commit b926a3116e
67 changed files with 2998 additions and 255 deletions

View File

@@ -114,6 +114,43 @@ export function bookingCargoTons(booking: {
);
}
/**
* Customer-requested wagon count of a NUMBER_OF_WAGONS bulk booking; 0 when
* the booking carries none (every other cargo unit). The request was validated
* against wagon capacity at booking creation, so sizing code honours it
* verbatim instead of deriving a count from tonnage.
*/
export function requestedBulkWagons(booking: {
bulkRequestedWagons?: number | string | null;
}): number {
const n = Math.floor(num(booking.bulkRequestedWagons));
return n > 0 ? n : 0;
}
/**
* Booking-aware {@link bulkTonsPerWagon}: a NUMBER_OF_WAGONS booking fixed its
* wagon count, so each wagon carries tons ÷ requested (the even spread the
* customer asked for), never more. Rounded UP to 3 decimals so
* ceil(tons / perWagon) lands exactly on the requested count instead of one
* over on float error. Other bookings get the cargo-type figure unchanged.
*/
export function bulkTonsPerWagonFor(
booking: Parameters<typeof bookingCargoTons>[0] & {
bulkRequestedWagons?: number | string | null;
},
cargoType: ItemFitCargoType | undefined,
wagonTypeId: string | null | undefined,
capacityTons: number | string | null | undefined,
): number {
const base = bulkTonsPerWagon(cargoType, wagonTypeId, capacityTons);
const requested = requestedBulkWagons(booking);
if (!requested) return base;
const tons = bookingCargoTons(booking);
if (!(tons > 0)) return base;
const evenShare = Math.ceil((tons / requested) * 1000) / 1000;
return base > 0 ? Math.min(base, evenShare) : evenShare;
}
/**
* Wagons a break-bulk (PER_ITEM) bulk booking needs. Items are indivisible, so
* floor how many whole items fit one wagon, then ceil the wagon count:
@@ -184,11 +221,16 @@ export function bulkTonsPerWagon(
* usable per-wagon figure, so callers can fall back as before.
*/
export function bulkTonWagonsRequired(
booking: Parameters<typeof bookingCargoTons>[0],
booking: Parameters<typeof bookingCargoTons>[0] & {
bulkRequestedWagons?: number | string | null;
},
cargoType: ItemFitCargoType | undefined,
wagonTypeId: string | null | undefined,
capacityTons: number | string | null | undefined,
): number {
// NUMBER_OF_WAGONS: the customer fixed the count — honour it verbatim.
const requested = requestedBulkWagons(booking);
if (requested) return requested;
const perWagon = bulkTonsPerWagon(cargoType, wagonTypeId, capacityTons);
const tons = bookingCargoTons(booking);
if (!(perWagon > 0) || !(tons > 0)) return 0;