Merge pull request #600 from Tria-plc/freight_feature/usermanagement

update booking logic for train schedules and customs contracts
This commit is contained in:
marshal
2026-07-10 15:14:27 +03:00
committed by GitHub
4 changed files with 39 additions and 40 deletions

View File

@@ -2533,21 +2533,17 @@ export class BookingBatchService implements OnModuleInit {
}
/**
* Keep schedule.max_wagons aligned with the train's real boarding limit: the
* locomotive's length-derived slot count, floored by the physical wagons in
* the train set (slots that exist on paper but not in the yard must not be
* sold — see {@link remainingBudget}).
* Keep schedule.max_wagons aligned with the train's boarding limit: the
* locomotive's length-derived slot count. The physical wagons currently in
* the train set do NOT cap this — bookings are admitted on length/weight
* alone and yard staff attach the wagons manually before departure.
*/
private async syncScheduleMaxWagons(
schedule: TrainSchedule,
locomotive: Locomotive,
): Promise<void> {
const limits = await this.capacityLimits(locomotive);
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
const maxWagons =
physicalWagons > 0
? Math.min(limits.base.wagons, physicalWagons)
: limits.base.wagons;
const maxWagons = limits.base.wagons;
if ((schedule.maxWagons ?? 0) !== maxWagons) {
await this.dataSource
.getRepository(TrainSchedule)
@@ -2664,11 +2660,10 @@ export class BookingBatchService implements OnModuleInit {
* reserved bookings already use ON THEIR OWN LEGS. A booking riding only
* Dire→Djibouti leaves the Addis→Dire edges untouched.
*
* The wagon axis is additionally capped by the PHYSICAL wagons marshalled in
* the schedule's train set. The length-derived slot count says how many wagons
* the locomotive could pull, not how many exist: a 760m/54-slot train with a
* 50-wagon set once split-offered 4 wagons that were never buildable — the
* customer paid and the wagon planner had nothing to assign.
* The wagon axis is the locomotive's length-derived slot count only — the
* physical wagons currently marshalled in the train set do NOT cap it.
* Bookings are admitted on length/weight capacity and yard staff attach
* the missing wagons manually before wagon assignment.
*/
private async remainingBudget(
schedule: TrainSchedule,
@@ -2676,12 +2671,7 @@ export class BookingBatchService implements OnModuleInit {
wagonDims: WagonDims,
): Promise<CorridorBudget> {
const stops = await this.stopsForSchedule(schedule);
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
const base =
physicalWagons > 0
? { ...limits.base, wagons: Math.min(limits.base.wagons, physicalWagons) }
: limits.base;
const budget = new CorridorBudget(stops, base, limits.tolerance);
const budget = new CorridorBudget(stops, limits.base, limits.tolerance);
const allocated = (schedule.scheduleBookings ?? [])
.map((sb) => sb.booking)
.filter((b): b is Booking => Boolean(b));

View File

@@ -241,11 +241,10 @@ export default function ContractDetailPage() {
});
// Intercity contracts are never window-gated: the shipment rides a passing
// import/export train that staff assign later, so booking is always open.
// GENERAL contracts are also not gated at creation — the booking enters the
// per-booking clearance gate first and picks its shipment day at proceed time.
// ONE_TIME and GENERAL contracts are both gated — booking is only possible
// while a window on the contract's lane is open.
const bookingWindowOpen =
contract?.tradeDirection === "DOMESTIC" ||
contract?.contractKind === "GENERAL" ||
hasOpenWindow(bookingWindows);
// Draw-down capacity per cargo line (GENERAL contracts only). The backend

View File

@@ -146,14 +146,12 @@ export default function NewShipmentPage() {
// Coarse gate: if the customer deep-links here while no booking window is
// open, show the same closed-state notice as the contract page instead of the
// form. Still allowed the moment any window isOpenNow. Intercity contracts
// are never window-gated — the shipment rides a passing train that staff
// pick at finalize time, so booking is always open. GENERAL contracts are not
// gated at creation either: the booking enters per-booking clearance first
// and picks its shipment day at proceed time.
// form. Still allowed the moment any window isOpenNow. Applies to ONE_TIME
// and GENERAL alike. Intercity contracts are never window-gated — the
// shipment rides a passing train that staff pick at finalize time, so
// booking is always open.
if (
contract.tradeDirection !== "DOMESTIC" &&
contract.contractKind !== "GENERAL" &&
!hasOpenWindow(bookingWindows)
) {
return (

View File

@@ -61,11 +61,16 @@ export default function NewShipmentRequestPage() {
const isContainer = contract.freightType === "CONTAINER";
const route = contract.routes?.[0];
// GENERAL customs contracts: GL schedules the shipment during clearance —
// the customer only states the quantity, never picks a date.
const hasCustoms =
contract.contractKind === "GENERAL" &&
(contract.serviceType?.includesCustoms ?? contract.customsClearingEnabled);
const handleSubmit = () => {
const dto: Freight.CreateBookingRequestDto = {
contractRouteId: route?.id,
scheduledDate: scheduledDate || undefined,
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
notes: notes.trim() || undefined,
};
@@ -104,19 +109,26 @@ export default function NewShipmentRequestPage() {
<Paper withBorder radius="lg" p="xl" style={{ borderColor: BORDER }}>
<Stack gap="md">
<DatePickerInput
label="Preferred shipment date"
placeholder="Pick a date"
leftSection={<CalendarDays size={16} />}
minDate={new Date().toISOString().slice(0, 10)}
value={scheduledDate || null}
onChange={(v) => setScheduledDate(v ?? "")}
radius="md"
popoverProps={{ withinPortal: true }}
/>
{!hasCustoms && (
<DatePickerInput
label="Preferred shipment date"
placeholder="Pick a date"
leftSection={<CalendarDays size={16} />}
minDate={new Date().toISOString().slice(0, 10)}
value={scheduledDate || null}
onChange={(v) => setScheduledDate(v ?? "")}
radius="md"
popoverProps={{ withinPortal: true }}
/>
)}
<NumberInput
label={isContainer ? "Number of containers" : "Cargo weight (tons)"}
description={
hasCustoms
? "Global Logistics schedules the shipment date during customs clearance — you only state the quantity."
: undefined
}
value={quantity}
onChange={setQuantity}
min={1}