diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 0e6f1fd8c..d51108043 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -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 { 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 { 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)); diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx index 784fa6bf5..2fed7e7a0 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx @@ -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 diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx index 699b143fb..06cb017c9 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx @@ -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 ( diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentRequestPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentRequestPage.tsx index 38924251e..23cfdfe0d 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentRequestPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentRequestPage.tsx @@ -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() { - } - minDate={new Date().toISOString().slice(0, 10)} - value={scheduledDate || null} - onChange={(v) => setScheduledDate(v ?? "")} - radius="md" - popoverProps={{ withinPortal: true }} - /> + {!hasCustoms && ( + } + minDate={new Date().toISOString().slice(0, 10)} + value={scheduledDate || null} + onChange={(v) => setScheduledDate(v ?? "")} + radius="md" + popoverProps={{ withinPortal: true }} + /> + )}