mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #600 from Tria-plc/freight_feature/usermanagement
update booking logic for train schedules and customs contracts
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user