feat(last-mile): wire multi-container assignment, bulk drawdown + EDR exit papers

Frontend for the per-truck EDR backend:

- Assign modal takes a container LOAD per truck (MultiSelect, max 2) instead of a
  single container, so a truck can carry one 40ft or two 20ft. Sends
  containerNumbers[]; the API enforces the size rule.
- requiredVehicles is now size-aware: a 40ft fills a truck (1 each), two 20ft
  share one. It previously assumed ceil(containers / 2) regardless of size, so a
  2x40ft booking under-counted the trucks needed.
- Bulk bookings show the drawdown in the assign modal — remaining vs total
  tonnage, what's been hauled, and a "fully hauled" state when it reaches 0.
- New "Truck exit papers" row action opens a per-truck list (plate, containers,
  arrival, exit, net weight) with a per-truck exit-paper download.
- last-mile findAll/findById now load each assignment's containers so the UI can
  hydrate a truck's load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-16 09:03:04 +00:00
parent ff49554cc2
commit d5182c5d85
5 changed files with 267 additions and 38 deletions

View File

@@ -67,8 +67,16 @@ export interface LastMileRecord {
vehicleAssignments?: Array<{
id: string;
vehicleId: string;
/** @deprecated Legacy single container — `containers` is authoritative. */
containerNumber?: string | null;
/** Containers riding this truck: one 40ft, or up to two 20ft. */
containers?: Array<{ id: string; containerNumber: string }>;
distanceKm?: number | null;
/** Per-truck arrival / exit, stamped by the warehouse weighing steps. */
arrivedAt?: string | null;
departedAt?: string | null;
grossWeightTons?: number | null;
netWeightTons?: number | null;
vehicle?: LastMileVehicle | null;
}>;
/** Present only when an invoice has actually been generated (not on distance). */
@@ -99,8 +107,13 @@ export const lastMileService = {
api.delete<void>(LM.BY_ID(id)),
setVehicles: (
id: string,
vehicles: Array<{ vehicleId: string; containerNumber?: string | null }>,
vehicles: Array<{ vehicleId: string; containerNumbers?: string[] }>,
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/vehicles`, { vehicles }),
/** Bulk drawdown: tonnage still to be hauled on this booking. */
remainingTons: (bookingId: string) =>
api.get<{ totalTons: number; hauledTons: number; remainingTons: number; complete: boolean }>(
`${LM.BASE}/booking/${bookingId}/remaining-tons`,
),
setDistances: (
id: string,
distances: Array<{ vehicleId: string; distanceKm: number }>,

View File

@@ -323,6 +323,11 @@ export const warehouseService = {
apiClient.get<Blob>(`/warehouse-inventory/customer-truck-exit-paper/${assignmentId}`, {
responseType: 'blob',
}),
/** Per-truck exit paper PDF for an EDR last-mile truck. */
downloadEdrTruckExitPaper: (assignmentId: string) =>
apiClient.get<Blob>(`/warehouse-inventory/edr-truck-exit-paper/${assignmentId}`, {
responseType: 'blob',
}),
deliver: (id: string, payload: DeliverInventoryPayload) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DELIVER(id), payload),