mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 23:35:42 +00:00
Merge pull request #525 from Tria-plc/Truckdetantion
Container truck assignment on customer portal for import
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weights are tonnes everywhere. Warehouse / yard / zone capacity was stored in
|
||||||
|
* kg (e.g. 25000, 5000, 2500) — convert existing rows to tonnes (÷1000). Cargo
|
||||||
|
* weight (warehouse_inventory.weight ← cargo_total_weight_vgm) is already tonnes
|
||||||
|
* and is NOT touched; truck gross weight has no data yet. Runs exactly once
|
||||||
|
* (tracked by TypeORM) — re-running would divide again.
|
||||||
|
*/
|
||||||
|
export class WarehouseCapacityKgToTons2020000000000 implements MigrationInterface {
|
||||||
|
name = 'WarehouseCapacityKgToTons2020000000000';
|
||||||
|
|
||||||
|
private readonly tables = ['warehouses', 'warehouse_yards', 'warehouse_zones'];
|
||||||
|
private readonly columns = ['capacity_weight', 'current_weight', 'max_weight'];
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
for (const table of this.tables) {
|
||||||
|
for (const column of this.columns) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE freight.${table} SET ${column} = ${column} / 1000.0 WHERE ${column} IS NOT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
for (const table of this.tables) {
|
||||||
|
for (const column of this.columns) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE freight.${table} SET ${column} = ${column} * 1000.0 WHERE ${column} IS NOT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,16 +42,16 @@ export class CustomerTruckService {
|
|||||||
const booking = await this.loadBookingGuard(bookingId);
|
const booking = await this.loadBookingGuard(bookingId);
|
||||||
this.assertSelfHaulPaid(booking);
|
this.assertSelfHaulPaid(booking);
|
||||||
|
|
||||||
const isExport = booking.tradeDirection === 'EXPORT';
|
|
||||||
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
||||||
|
|
||||||
// EXPORT: the truck delivers 1–2 known containers. IMPORT: containers are
|
// Both import and export specify the containers each truck carries. Capacity
|
||||||
// not pre-specified — they are registered + weighed when the truck leaves.
|
// is size-based: a 40ft container fills the truck (max 1); two 20ft containers
|
||||||
if (isExport) {
|
// fit (max 2), no size mixing. #trucks <= #containers follows naturally since
|
||||||
if (requested.length < 1 || requested.length > 2) {
|
// each container is assigned to exactly one truck.
|
||||||
throw new BadRequestException('An export truck must carry 1 or 2 of the booking containers');
|
if (requested.length < 1) {
|
||||||
}
|
throw new BadRequestException('Select at least one container for this truck');
|
||||||
} else if (requested.length > 2) {
|
}
|
||||||
|
if (requested.length > 2) {
|
||||||
throw new BadRequestException('A truck carries at most 2 containers');
|
throw new BadRequestException('A truck carries at most 2 containers');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +68,13 @@ export class CustomerTruckService {
|
|||||||
throw new ConflictException(`Container ${n} is already loaded onto another truck`);
|
throw new ConflictException(`Container ${n} is already loaded onto another truck`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Size cap: a 40ft container fills the truck.
|
||||||
|
const sizes = await this.containerSizes(bookingId, requested);
|
||||||
|
if (sizes.some((s) => s.includes('40')) && requested.length > 1) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'A 40ft container fills the truck — assign only 1 container to this truck',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.dataSource.transaction(async (manager) => {
|
await this.dataSource.transaction(async (manager) => {
|
||||||
@@ -244,7 +251,7 @@ export class CustomerTruckService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const grossKg = await this.vgmKgForContainers(bookingId, requested);
|
const grossTons = await this.vgmTonsForContainers(bookingId, requested);
|
||||||
await this.dataSource.transaction(async (manager) => {
|
await this.dataSource.transaction(async (manager) => {
|
||||||
await manager.getRepository(CustomerTruckContainer).softDelete({ assignmentId });
|
await manager.getRepository(CustomerTruckContainer).softDelete({ assignmentId });
|
||||||
await manager.getRepository(CustomerTruckContainer).save(
|
await manager.getRepository(CustomerTruckContainer).save(
|
||||||
@@ -256,18 +263,19 @@ export class CustomerTruckService {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Provisional gross from the loaded containers' VGM — overridden by the
|
// Provisional gross (tonnes) from the loaded containers' VGM — overridden
|
||||||
// weighed gross on departure.
|
// by the weighed gross on departure. (Column is *_kg but holds tonnes.)
|
||||||
await manager.getRepository(CustomerTruckAssignment).update(assignmentId, {
|
await manager.getRepository(CustomerTruckAssignment).update(assignmentId, {
|
||||||
grossWeightKg: grossKg,
|
grossWeightKg: grossTons,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return this.listTrucks(bookingId);
|
return this.listTrucks(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async vgmKgForContainers(bookingId: string, numbers: string[]): Promise<number> {
|
/** Summed VGM (tonnes) of the given containers — provisional truck gross. */
|
||||||
const [row]: Array<{ kg: string }> = await this.dataSource.query(
|
private async vgmTonsForContainers(bookingId: string, numbers: string[]): Promise<number> {
|
||||||
`SELECT COALESCE(SUM(bcu.vgm_tons), 0) * 1000 AS kg
|
const [row]: Array<{ tons: string }> = await this.dataSource.query(
|
||||||
|
`SELECT COALESCE(SUM(bcu.vgm_tons), 0) AS tons
|
||||||
FROM freight.booking_container_units bcu
|
FROM freight.booking_container_units bcu
|
||||||
JOIN freight.booking_container bc
|
JOIN freight.booking_container bc
|
||||||
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
|
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
|
||||||
@@ -276,7 +284,7 @@ export class CustomerTruckService {
|
|||||||
AND bcu.deleted_at IS NULL`,
|
AND bcu.deleted_at IS NULL`,
|
||||||
[bookingId, numbers],
|
[bookingId, numbers],
|
||||||
);
|
);
|
||||||
return Number(row?.kg ?? 0);
|
return Number(row?.tons ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -399,4 +407,20 @@ export class CustomerTruckService {
|
|||||||
);
|
);
|
||||||
return rows.map((r) => r.containerNumber.trim().toUpperCase());
|
return rows.map((r) => r.containerNumber.trim().toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Contract container sizes (e.g. "20ft" / "40ft") for the given container numbers. */
|
||||||
|
private async containerSizes(bookingId: string, numbers: string[]): Promise<string[]> {
|
||||||
|
if (!numbers.length) return [];
|
||||||
|
const rows: Array<{ size: string | null }> = await this.dataSource.query(
|
||||||
|
`SELECT bc.container_size AS "size"
|
||||||
|
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 UPPER(bcu.container_number) = ANY($2)
|
||||||
|
AND bcu.deleted_at IS NULL`,
|
||||||
|
[bookingId, numbers],
|
||||||
|
);
|
||||||
|
return rows.map((r) => (r.size ?? '').trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class CreateWarehouseYardDto {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
capacityContainers?: number;
|
capacityContainers?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Max weight capacity (kg). Defaults to capacityWeight.' })
|
@ApiPropertyOptional({ description: 'Max weight capacity (t). Defaults to capacityWeight.' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class CreateWarehouseZoneDto {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
capacityContainers?: number;
|
capacityContainers?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Max weight capacity (kg). Defaults to capacityWeight.' })
|
@ApiPropertyOptional({ description: 'Max weight capacity (t). Defaults to capacityWeight.' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class CreateWarehouseDto {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
capacityContainers?: number;
|
capacityContainers?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Max weight capacity (kg). Defaults to capacityWeight.' })
|
@ApiPropertyOptional({ description: 'Max weight capacity (t). Defaults to capacityWeight.' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class LoadInventoryDto {
|
|||||||
@IsUUID()
|
@IsUUID()
|
||||||
wagonId!: string;
|
wagonId!: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Weight loaded onto the wagon (kg)' })
|
@ApiPropertyOptional({ description: 'Weight loaded onto the wagon (t)' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class WarehouseInspectionService {
|
|||||||
expectedWeight: expected,
|
expectedWeight: expected,
|
||||||
actualWeight: actual,
|
actualWeight: actual,
|
||||||
weightLoss,
|
weightLoss,
|
||||||
weightLossUnit: weightLoss !== null ? 'kg' : null,
|
weightLossUnit: weightLoss !== null ? 't' : null,
|
||||||
hasMissingItems: dto.hasMissingItems ?? false,
|
hasMissingItems: dto.hasMissingItems ?? false,
|
||||||
missingItemsDescription: dto.missingItemsDescription ?? null,
|
missingItemsDescription: dto.missingItemsDescription ?? null,
|
||||||
remarks: dto.remarks ?? null,
|
remarks: dto.remarks ?? null,
|
||||||
|
|||||||
@@ -2020,7 +2020,7 @@ export class WarehouseInventoryService {
|
|||||||
activityType: 'INVENTORY_RECEIVED',
|
activityType: 'INVENTORY_RECEIVED',
|
||||||
inventoryId: saved.id,
|
inventoryId: saved.id,
|
||||||
warehouseId: dto.warehouseId,
|
warehouseId: dto.warehouseId,
|
||||||
description: `GRN ${grnNumber}: received ${weight}kg via truck ${truckEntrance.truckPlateNumber}`,
|
description: `GRN ${grnNumber}: received ${weight}t via truck ${truckEntrance.truckPlateNumber}`,
|
||||||
performedBy: dto.performedBy,
|
performedBy: dto.performedBy,
|
||||||
},
|
},
|
||||||
manager,
|
manager,
|
||||||
@@ -2677,7 +2677,7 @@ export class WarehouseInventoryService {
|
|||||||
['Pickup Truck Plate', data.plateNumber],
|
['Pickup Truck Plate', data.plateNumber],
|
||||||
['Driver', data.driverName],
|
['Driver', data.driverName],
|
||||||
['Truck Type', data.truckType],
|
['Truck Type', data.truckType],
|
||||||
['Gross Weight (Loaded on Truck)', `${data.grossWeightKg.toLocaleString()} kg`],
|
['Gross Weight (Loaded on Truck)', `${data.grossWeightKg.toLocaleString()} t`],
|
||||||
['Gate-Out Time', gateOut],
|
['Gate-Out Time', gateOut],
|
||||||
['Clearance Status', 'CLEARED FOR WAREHOUSE EXIT'],
|
['Clearance Status', 'CLEARED FOR WAREHOUSE EXIT'],
|
||||||
];
|
];
|
||||||
@@ -3608,8 +3608,8 @@ export class WarehouseInventoryService {
|
|||||||
['Booking Containers', data.bookingContainerSummary],
|
['Booking Containers', data.bookingContainerSummary],
|
||||||
['Cargo / Goods Description', data.cargoDescription],
|
['Cargo / Goods Description', data.cargoDescription],
|
||||||
['Quantity', data.quantity],
|
['Quantity', data.quantity],
|
||||||
['Received Weight', `${data.weight.toLocaleString()} kg`],
|
['Received Weight', `${data.weight.toLocaleString()} t`],
|
||||||
['Booking Declared Weight', data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} kg` : null],
|
['Booking Declared Weight', data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} t` : null],
|
||||||
['Volume', data.volume == null ? null : data.volume.toLocaleString()],
|
['Volume', data.volume == null ? null : data.volume.toLocaleString()],
|
||||||
['Warehouse', data.warehouse],
|
['Warehouse', data.warehouse],
|
||||||
['Yard', data.yard],
|
['Yard', data.yard],
|
||||||
@@ -3731,7 +3731,7 @@ export class WarehouseInventoryService {
|
|||||||
`${(data.truckPlateNumber && data.truckWeightKg
|
`${(data.truckPlateNumber && data.truckWeightKg
|
||||||
? data.truckWeightKg
|
? data.truckWeightKg
|
||||||
: data.weight
|
: data.weight
|
||||||
).toLocaleString()} kg`,
|
).toLocaleString()} t`,
|
||||||
],
|
],
|
||||||
['Warehouse', data.warehouse],
|
['Warehouse', data.warehouse],
|
||||||
['Yard', data.yard],
|
['Yard', data.yard],
|
||||||
@@ -3894,8 +3894,8 @@ export class WarehouseInventoryService {
|
|||||||
['Booking Containers', data.bookingContainerSummary],
|
['Booking Containers', data.bookingContainerSummary],
|
||||||
['Cargo / Goods Description', data.cargoDescription],
|
['Cargo / Goods Description', data.cargoDescription],
|
||||||
['Quantity', data.quantity],
|
['Quantity', data.quantity],
|
||||||
['Inventory Weight', `${data.weight.toLocaleString()} kg`],
|
['Inventory Weight', `${data.weight.toLocaleString()} t`],
|
||||||
['Booking Declared Weight', data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} kg` : null],
|
['Booking Declared Weight', data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} t` : null],
|
||||||
['Warehouse', data.warehouse],
|
['Warehouse', data.warehouse],
|
||||||
['Yard', data.yard],
|
['Yard', data.yard],
|
||||||
['Zone', data.zone],
|
['Zone', data.zone],
|
||||||
@@ -3968,8 +3968,8 @@ export class WarehouseInventoryService {
|
|||||||
<tr><th>1. Goods</th><td>${esc(data.cargoDescription || data.containerNumber || data.bookingReference)}</td></tr>
|
<tr><th>1. Goods</th><td>${esc(data.cargoDescription || data.containerNumber || data.bookingReference)}</td></tr>
|
||||||
<tr><th>Container</th><td>${esc(data.containerNumber)}</td></tr>
|
<tr><th>Container</th><td>${esc(data.containerNumber)}</td></tr>
|
||||||
<tr><th>Booking Containers</th><td>${esc(data.bookingContainerSummary)}</td></tr>
|
<tr><th>Booking Containers</th><td>${esc(data.bookingContainerSummary)}</td></tr>
|
||||||
<tr><th>Inventory Weight</th><td>${esc(`${data.weight.toLocaleString()} kg`)}</td></tr>
|
<tr><th>Inventory Weight</th><td>${esc(`${data.weight.toLocaleString()} t`)}</td></tr>
|
||||||
<tr><th>Booking Declared Weight</th><td>${esc(data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} kg` : null)}</td></tr>
|
<tr><th>Booking Declared Weight</th><td>${esc(data.bookingDeclaredWeight ? `${data.bookingDeclaredWeight.toLocaleString()} t` : null)}</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="section-title">Handover Clause</div>
|
<div class="section-title">Handover Clause</div>
|
||||||
@@ -4303,9 +4303,9 @@ export class WarehouseInventoryService {
|
|||||||
dto.truckType?.trim() ? `Truck Type: ${dto.truckType.trim()}` : null,
|
dto.truckType?.trim() ? `Truck Type: ${dto.truckType.trim()}` : null,
|
||||||
dto.containerNumber?.trim() ? `Container Number: ${dto.containerNumber.trim()}` : null,
|
dto.containerNumber?.trim() ? `Container Number: ${dto.containerNumber.trim()}` : null,
|
||||||
dto.gateInTime ? `Gate In Time: ${dto.gateInTime}` : null,
|
dto.gateInTime ? `Gate In Time: ${dto.gateInTime}` : null,
|
||||||
`Tare Weight: ${tareWeight} kg`,
|
`Tare Weight: ${tareWeight} t`,
|
||||||
grossWeight == null ? null : `Gross Weight: ${grossWeight} kg`,
|
grossWeight == null ? null : `Gross Weight: ${grossWeight} t`,
|
||||||
computedNetWeight == null ? null : `Net Weight: ${computedNetWeight} kg`,
|
computedNetWeight == null ? null : `Net Weight: ${computedNetWeight} t`,
|
||||||
dto.gateOutTime ? `Gate Out Time: ${dto.gateOutTime}` : null,
|
dto.gateOutTime ? `Gate Out Time: ${dto.gateOutTime}` : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -4357,7 +4357,7 @@ export class WarehouseInventoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extractExitInspectionNumber(note: string | null | undefined, label: string): number | undefined {
|
private extractExitInspectionNumber(note: string | null | undefined, label: string): number | undefined {
|
||||||
const value = this.extractExitInspectionLine(note, label)?.replace(/\s*kg$/i, '');
|
const value = this.extractExitInspectionLine(note, label)?.replace(/\s*(kg|t)$/i, '');
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
@@ -4420,9 +4420,9 @@ export class WarehouseInventoryService {
|
|||||||
truck?.driverName ? `Driver: ${truck.driverName}` : null,
|
truck?.driverName ? `Driver: ${truck.driverName}` : null,
|
||||||
truck?.driverPhone ? `Driver Phone: ${truck.driverPhone}` : null,
|
truck?.driverPhone ? `Driver Phone: ${truck.driverPhone}` : null,
|
||||||
truck?.driverLicenseNumber ? `Driver License: ${truck.driverLicenseNumber}` : null,
|
truck?.driverLicenseNumber ? `Driver License: ${truck.driverLicenseNumber}` : null,
|
||||||
truck?.entranceTareWeightKg !== undefined ? `Entrance Tare Weight: ${Number(truck.entranceTareWeightKg)} kg` : null,
|
truck?.entranceTareWeightKg !== undefined ? `Entrance Tare Weight: ${Number(truck.entranceTareWeightKg)} t` : null,
|
||||||
truck?.weighingRequired !== undefined ? `Weighing Required: ${truck.weighingRequired ? 'Yes' : 'No'}` : null,
|
truck?.weighingRequired !== undefined ? `Weighing Required: ${truck.weighingRequired ? 'Yes' : 'No'}` : null,
|
||||||
truck?.exitTareWeightKg !== undefined ? `Exit Tare Weight: ${Number(truck.exitTareWeightKg)} kg` : null,
|
truck?.exitTareWeightKg !== undefined ? `Exit Tare Weight: ${Number(truck.exitTareWeightKg)} t` : null,
|
||||||
truck?.declarationNumber ? `Declaration / Bill of Entry: ${truck.declarationNumber}` : null,
|
truck?.declarationNumber ? `Declaration / Bill of Entry: ${truck.declarationNumber}` : null,
|
||||||
truck?.incoterms ? `Incoterms: ${truck.incoterms}` : null,
|
truck?.incoterms ? `Incoterms: ${truck.incoterms}` : null,
|
||||||
truck?.hsCodes ? `HS Codes: ${truck.hsCodes}` : null,
|
truck?.hsCodes ? `HS Codes: ${truck.hsCodes}` : null,
|
||||||
@@ -4430,8 +4430,8 @@ export class WarehouseInventoryService {
|
|||||||
truck?.itemDescription ? `Item Description: ${truck.itemDescription}` : null,
|
truck?.itemDescription ? `Item Description: ${truck.itemDescription}` : null,
|
||||||
truck?.packagingType ? `Packaging Type: ${truck.packagingType}` : null,
|
truck?.packagingType ? `Packaging Type: ${truck.packagingType}` : null,
|
||||||
truck?.unitCount !== undefined ? `Unit Count: ${Number(truck.unitCount)}` : null,
|
truck?.unitCount !== undefined ? `Unit Count: ${Number(truck.unitCount)}` : null,
|
||||||
truck?.grossWeightKg !== undefined ? `Gross Weight: ${Number(truck.grossWeightKg)} kg` : null,
|
truck?.grossWeightKg !== undefined ? `Gross Weight: ${Number(truck.grossWeightKg)} t` : null,
|
||||||
truck?.netWeightKg !== undefined ? `Net Weight: ${Number(truck.netWeightKg)} kg` : null,
|
truck?.netWeightKg !== undefined ? `Net Weight: ${Number(truck.netWeightKg)} t` : null,
|
||||||
truck?.volumeDimensions ? `Volume / Dimensions: ${truck.volumeDimensions}` : null,
|
truck?.volumeDimensions ? `Volume / Dimensions: ${truck.volumeDimensions}` : null,
|
||||||
truck?.conditionAtReceipt ? `Condition at Receipt: ${truck.conditionAtReceipt}` : null,
|
truck?.conditionAtReceipt ? `Condition at Receipt: ${truck.conditionAtReceipt}` : null,
|
||||||
truck?.damagedRejectedQuantity !== undefined ? `Damaged / Rejected Quantity: ${Number(truck.damagedRejectedQuantity)}` : null,
|
truck?.damagedRejectedQuantity !== undefined ? `Damaged / Rejected Quantity: ${Number(truck.damagedRejectedQuantity)}` : null,
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
|||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Capacity weight (kg)"
|
label="Capacity weight (t)"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
min={0}
|
min={0}
|
||||||
value={form.capacityWeight}
|
value={form.capacityWeight}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYa
|
|||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Capacity weight (kg)"
|
label="Capacity weight (t)"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
min={0}
|
min={0}
|
||||||
value={form.capacityWeight}
|
value={form.capacityWeight}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneMod
|
|||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Capacity weight (kg)"
|
label="Capacity weight (t)"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
min={0}
|
min={0}
|
||||||
value={form.capacityWeight}
|
value={form.capacityWeight}
|
||||||
|
|||||||
@@ -174,13 +174,13 @@ export function InspectionReportModal({ opened, onClose, inventoryId }: Inspecti
|
|||||||
{hasWeightLoss && (
|
{hasWeightLoss && (
|
||||||
<Group grow mt="xs">
|
<Group grow mt="xs">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Expected weight (kg)"
|
label="Expected weight (t)"
|
||||||
min={0}
|
min={0}
|
||||||
value={expectedWeight}
|
value={expectedWeight}
|
||||||
onChange={(v) => setExpectedWeight(v === '' ? '' : Number(v))}
|
onChange={(v) => setExpectedWeight(v === '' ? '' : Number(v))}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Actual weight (kg)"
|
label="Actual weight (t)"
|
||||||
min={0}
|
min={0}
|
||||||
value={actualWeight}
|
value={actualWeight}
|
||||||
onChange={(v) => setActualWeight(v === '' ? '' : Number(v))}
|
onChange={(v) => setActualWeight(v === '' ? '' : Number(v))}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function InventoryDetailModal({ opened, onClose, item }: InventoryDetailM
|
|||||||
<DetailRow label="Release reference" value={item.releaseOrderReference ?? '-'} />
|
<DetailRow label="Release reference" value={item.releaseOrderReference ?? '-'} />
|
||||||
<DetailRow label="Handover reference" value={handoverReference || '-'} />
|
<DetailRow label="Handover reference" value={handoverReference || '-'} />
|
||||||
<DetailRow label="Quantity" value={formatNumber(item.quantity)} />
|
<DetailRow label="Quantity" value={formatNumber(item.quantity)} />
|
||||||
<DetailRow label="Weight" value={`${formatNumber(item.weight)} kg`} />
|
<DetailRow label="Weight" value={`${formatNumber(item.weight)} t`} />
|
||||||
<DetailRow label="Volume" value={item.volume == null ? '-' : formatNumber(item.volume)} />
|
<DetailRow label="Volume" value={item.volume == null ? '-' : formatNumber(item.volume)} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function InventoryInquiryDetailModal({ opened, onClose, result }: Invento
|
|||||||
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||||
<DetailRow label="Item" value={itemLabel(result)} />
|
<DetailRow label="Item" value={itemLabel(result)} />
|
||||||
<DetailRow label="Quantity" value={formatNumber(result.quantity)} />
|
<DetailRow label="Quantity" value={formatNumber(result.quantity)} />
|
||||||
<DetailRow label="Weight" value={`${formatNumber(result.weight)} kg`} />
|
<DetailRow label="Weight" value={`${formatNumber(result.weight)} t`} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Divider label="Location" labelPosition="left" />
|
<Divider label="Location" labelPosition="left" />
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModal
|
|||||||
<WagonSelect label="Wagon" required value={wagonId} onChange={setWagonId} />
|
<WagonSelect label="Wagon" required value={wagonId} onChange={setWagonId} />
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Loaded weight (kg)"
|
label="Loaded weight (t)"
|
||||||
placeholder="Defaults to item weight"
|
placeholder="Defaults to item weight"
|
||||||
min={0}
|
min={0}
|
||||||
value={loadedWeight}
|
value={loadedWeight}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const STAGE_COLOR: Record<string, string> = {
|
|||||||
LOADED: 'green',
|
LOADED: 'green',
|
||||||
};
|
};
|
||||||
|
|
||||||
const weight = (w: number | null) => (w == null ? '—' : `${Number(w).toLocaleString()} kg`);
|
const weight = (w: number | null) => (w == null ? '—' : `${Number(w).toLocaleString()} t`);
|
||||||
|
|
||||||
interface BookingGroup {
|
interface BookingGroup {
|
||||||
bookingId: string | null;
|
bookingId: string | null;
|
||||||
|
|||||||
@@ -520,14 +520,14 @@ function TruckEntranceFields({
|
|||||||
{value.weighingRequired && (
|
{value.weighingRequired && (
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Gross weight (kg)"
|
label="Gross weight (t)"
|
||||||
required
|
required
|
||||||
min={0}
|
min={0}
|
||||||
value={value.grossWeightKg}
|
value={value.grossWeightKg}
|
||||||
onChange={(v) => onChange({ ...value, grossWeightKg: v === '' ? '' : Number(v) })}
|
onChange={(v) => onChange({ ...value, grossWeightKg: v === '' ? '' : Number(v) })}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Exit tare weight (kg)"
|
label="Exit tare weight (t)"
|
||||||
required
|
required
|
||||||
min={0}
|
min={0}
|
||||||
value={value.exitTareWeightKg}
|
value={value.exitTareWeightKg}
|
||||||
@@ -596,7 +596,7 @@ function TruckEntranceFields({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Net weight (kg)"
|
label="Net weight (t)"
|
||||||
min={0}
|
min={0}
|
||||||
value={value.netWeightKg}
|
value={value.netWeightKg}
|
||||||
onChange={(v) => onChange({ ...value, netWeightKg: v === '' ? '' : Number(v) })}
|
onChange={(v) => onChange({ ...value, netWeightKg: v === '' ? '' : Number(v) })}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const lineValue = (notes: string | null | undefined, label: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const lineNumber = (notes: string | null | undefined, label: string): number | '' => {
|
const lineNumber = (notes: string | null | undefined, label: string): number | '' => {
|
||||||
const value = lineValue(notes, label).replace(/\s*kg$/i, '');
|
const value = lineValue(notes, label).replace(/\s*(kg|t)$/i, '');
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isFinite(parsed) ? parsed : '';
|
return Number.isFinite(parsed) ? parsed : '';
|
||||||
@@ -397,13 +397,13 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|||||||
<TextInput label="Gate in time" type="datetime-local" value={gateInTime} onChange={(e) => setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
<TextInput label="Gate in time" type="datetime-local" value={gateInTime} onChange={(e) => setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput label="Tare weight (kg)" required min={0} value={tareWeight} onChange={(v) => setTareWeight(v === '' ? '' : Number(v))} readOnly={isEntranceLocked} />
|
<NumberInput label="Tare weight (t)" required min={0} value={tareWeight} onChange={(v) => setTareWeight(v === '' ? '' : Number(v))} readOnly={isEntranceLocked} />
|
||||||
<NumberInput label="Gross weight (kg)" required={isExitStep} min={0} value={grossWeight} onChange={(v) => setGrossWeight(v === '' ? '' : Number(v))} disabled={!isExitStep} />
|
<NumberInput label="Gross weight (t)" required={isExitStep} min={0} value={grossWeight} onChange={(v) => setGrossWeight(v === '' ? '' : Number(v))} disabled={!isExitStep} />
|
||||||
<NumberInput label="Recorded net weight (system kg)" min={0} value={systemNetWeight} readOnly />
|
<NumberInput label="Recorded net weight (system t)" min={0} value={systemNetWeight} readOnly />
|
||||||
</Group>
|
</Group>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Text size="sm" c={weightMismatch ? 'red' : 'dimmed'}>
|
<Text size="sm" c={weightMismatch ? 'red' : 'dimmed'}>
|
||||||
Computed net: <b>{computedNetWeight == null ? '-' : `${computedNetWeight.toLocaleString()} kg`}</b>
|
Computed net: <b>{computedNetWeight == null ? '-' : `${computedNetWeight.toLocaleString()} t`}</b>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput label="Gate out time" type="datetime-local" value={gateOutTime} onChange={(e) => setGateOutTime(e.currentTarget.value)} disabled={!isExitStep} />
|
<TextInput label="Gate out time" type="datetime-local" value={gateOutTime} onChange={(e) => setGateOutTime(e.currentTarget.value)} disabled={!isExitStep} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const columns: ColumnDef<Loading>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'weight',
|
id: 'weight',
|
||||||
header: 'Loaded Weight (kg)',
|
header: 'Loaded Weight (t)',
|
||||||
cell: ({ row }) => formatNumber(row.original.loadedWeight),
|
cell: ({ row }) => formatNumber(row.original.loadedWeight),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ function PendingPaymentTable({ items, onNavigate }: PendingPaymentTableProps) {
|
|||||||
},
|
},
|
||||||
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
|
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
|
||||||
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
|
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
|
||||||
{ id: 'weight', header: 'Weight (kg)', cell: ({ row }) => formatNumber(row.original.weight) },
|
{ id: 'weight', header: 'Weight (t)', cell: ({ row }) => formatNumber(row.original.weight) },
|
||||||
{
|
{
|
||||||
id: 'payment',
|
id: 'payment',
|
||||||
header: 'Payment',
|
header: 'Payment',
|
||||||
|
|||||||
@@ -73,10 +73,7 @@ export function CustomerTruckAssignmentCard({
|
|||||||
(n) => !assignedNumbers.has(n),
|
(n) => !assignedNumbers.has(n),
|
||||||
);
|
);
|
||||||
|
|
||||||
// EXPORT trucks deliver known containers (pre-selected). IMPORT trucks don't —
|
// Both import and export specify the containers each truck carries.
|
||||||
// staff register + weigh what was loaded when the truck leaves.
|
|
||||||
const isExport = booking.tradeDirection === "EXPORT";
|
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setPlateNumber("");
|
setPlateNumber("");
|
||||||
setDriverName("");
|
setDriverName("");
|
||||||
@@ -91,8 +88,7 @@ export function CustomerTruckAssignmentCard({
|
|||||||
truckPlateNumber: plateNumber.trim().toUpperCase(),
|
truckPlateNumber: plateNumber.trim().toUpperCase(),
|
||||||
driverName: driverName.trim(),
|
driverName: driverName.trim(),
|
||||||
truckType: truckType.trim(),
|
truckType: truckType.trim(),
|
||||||
// Import: containers are registered + weighed on departure, not here.
|
containerNumbers: containers,
|
||||||
containerNumbers: isExport ? containers : [],
|
|
||||||
}),
|
}),
|
||||||
onSuccess: (list) => {
|
onSuccess: (list) => {
|
||||||
queryClient.setQueryData(trucksKey, list);
|
queryClient.setQueryData(trucksKey, list);
|
||||||
@@ -123,7 +119,7 @@ export function CustomerTruckAssignmentCard({
|
|||||||
setError("Plate number, driver name and truck type are required.");
|
setError("Plate number, driver name and truck type are required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isExport && (containers.length < 1 || containers.length > 2)) {
|
if (containers.length < 1 || containers.length > 2) {
|
||||||
setError("Select 1 or 2 container numbers for this truck.");
|
setError("Select 1 or 2 container numbers for this truck.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -207,8 +203,8 @@ export function CustomerTruckAssignmentCard({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add-truck form. Export needs unassigned containers; import always allows another truck. */}
|
{/* Add-truck form — both directions assign the containers each truck carries. */}
|
||||||
{(isExport ? availableContainers.length > 0 : true) ? (
|
{availableContainers.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Divider label="Add a truck" labelPosition="center" />
|
<Divider label="Add a truck" labelPosition="center" />
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
||||||
@@ -231,19 +227,18 @@ export function CustomerTruckAssignmentCard({
|
|||||||
value={truckType || null}
|
value={truckType || null}
|
||||||
onChange={(value) => setTruckType(value ?? "")}
|
onChange={(value) => setTruckType(value ?? "")}
|
||||||
/>
|
/>
|
||||||
{isExport && (
|
<MultiSelect
|
||||||
<MultiSelect
|
label="Containers to load"
|
||||||
label="Containers to load (1–2)"
|
description="20ft: up to 2 per truck · 40ft: 1 per truck"
|
||||||
required
|
required
|
||||||
placeholder="Select container numbers"
|
placeholder="Select container numbers"
|
||||||
data={availableContainers}
|
data={availableContainers}
|
||||||
value={containers}
|
value={containers}
|
||||||
onChange={setContainers}
|
onChange={setContainers}
|
||||||
maxValues={2}
|
maxValues={2}
|
||||||
searchable
|
searchable
|
||||||
nothingFoundMessage="No unassigned containers"
|
nothingFoundMessage="No unassigned containers"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user