diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 8f7a71fc1..281d3f620 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -1597,6 +1597,8 @@ export class WarehouseInventoryService { status: 'UNLOADED', unloadedAt: now, arrivedAt: existing.arrivedAt ?? now, + // Import GRN is issued automatically at train unload. + ...(existing.grnNumber ? {} : { grnNumber: this.generateGrnNumber('IMPORT', booking.id, now) }), }); await this.activityLog.record({ activityType: 'INVENTORY_UNLOADED', @@ -1630,6 +1632,7 @@ export class WarehouseInventoryService { quantity: 1, weight: Number(booking.weight) || 0, status: 'UNLOADED', + grnNumber: this.generateGrnNumber('IMPORT', booking.id, now), arrivedAt: now, unloadedAt: now, notes: allocated?.rule ? `Unloaded → ${allocated.path}` : 'Unloaded from arrived import train', @@ -2757,11 +2760,12 @@ export class WarehouseInventoryService { const rows: Array<{ containerNumber: string; weightTons: string }> = await this.dataSource.query( `SELECT bcu.container_number AS "containerNumber", - COALESCE(bcu.vgm_tons, 0) AS "weightTons" + MAX(COALESCE(bcu.vgm_tons, 0)) AS "weightTons" FROM freight.booking_container_units bcu JOIN freight.booking_container bc ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL + GROUP BY bcu.container_number ORDER BY bcu.container_number`, [bookingId], ); diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx index cfa271076..6833167f7 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx @@ -202,8 +202,11 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea ]; // Only trucks actually assigned to THIS booking (last-mile prefill or customer // portal) are selectable. No global fleet list — if nothing is assigned, the - // operator types the plate manually in the field below. - const truckSelectOptions = assignedTruckOptions; + // operator types the plate manually in the field below. Deduped by plate: + // duplicate option values crash Mantine's Select. + const truckSelectOptions = [ + ...new Map(assignedTruckOptions.map((t) => [t.value, t])).values(), + ]; // Neither a last-mile truck nor a customer truck has been assigned yet. const noTruckAssigned = assignedTruckOptions.length === 0 && !isCustomerAssignedTruck; const isTruckIdentityLocked = isEntranceLocked || isCustomerAssignedTruck || hasLastMileTruckPrefill; @@ -216,10 +219,19 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea const containerWeightByNumber = new Map( containerWeights.map((c) => [c.containerNumber.toUpperCase(), Number(c.weightTons) || 0]), ); - const containerSelectData = containerWeights.map((c) => ({ - value: c.containerNumber, - label: `${c.containerNumber} · ${(Number(c.weightTons) || 0).toLocaleString()} t`, - })); + // Mantine Selects throw on duplicate option values — legacy bookings can carry + // the same container number on two lines, so dedupe defensively. + const containerSelectData = [ + ...new Map( + containerWeights.map((c) => [ + c.containerNumber, + { + value: c.containerNumber, + label: `${c.containerNumber} · ${(Number(c.weightTons) || 0).toLocaleString()} t`, + }, + ]), + ).values(), + ]; const selectedContainerNumbers = containerNumbers.map((n) => n.trim()).filter(Boolean); const selectedCargoWeight = Number( selectedContainerNumbers