Merge branch 'dev' into freight/feat/fixes-v1

This commit is contained in:
Nathnael
2026-07-09 08:51:50 +00:00
181 changed files with 7344 additions and 1613 deletions

View File

@@ -102,6 +102,7 @@ import type {
LoadInventoryPayload,
LoadPassedExportResult,
MoveInventoryPayload,
StoreInventoryPayload,
PayInvoicePayload,
ReadyToLoadRow,
ReceiveInventoryPayload,
@@ -1054,10 +1055,13 @@ export const api = {
() => [["warehouse-inventory"], ["warehouses"]],
),
store: endpoint<string, WarehouseInventoryItem>(
store: endpoint<
{ id: string; payload?: StoreInventoryPayload },
WarehouseInventoryItem
>(
"warehouse-inventory",
"store",
(id) => warehouseService.store(id).then((r) => r.data),
({ id, payload }) => warehouseService.store(id, payload).then((r) => r.data),
undefined,
() => INVENTORY_INVALIDATIONS,
),

View File

@@ -482,10 +482,25 @@ export const contractsService = {
postContract<Freight.IContract>(C.OPS_CLEARANCE_FINALIZE(id)),
// ── Booking under contract (GL ET — Path B) ──
createBookingUnderContract: (
// The API returns { booking, warnings } (CreateBookingUnderContractResult) —
// unwrap to the booking itself so callers can use its id directly.
createBookingUnderContract: async (
id: string,
payload: Freight.CreateBookingUnderContractDto,
) => postContract<{ id: string; reference: string }>(C.BOOKINGS(id), payload),
): Promise<{ id: string; reference: string; warnings?: string[] }> => {
const result = await postContract<{
booking?: { id: string; reference: string };
id?: string;
reference?: string;
warnings?: string[];
}>(C.BOOKINGS(id), payload);
const booking = result.booking ?? result;
return {
id: booking.id ?? "",
reference: booking.reference ?? "",
warnings: result.warnings,
};
},
/**
* Pre-create validation + authoritative price preview: the same

View File

@@ -27,6 +27,10 @@ export interface Locomotive {
currentYard?: { id: string; label?: string; code?: string } | null;
maxPullWeightTons: number;
maxTrainLengthMeters: number;
/** Tons a train may exceed maxPullWeightTons by before scheduling blocks it. */
overageToleranceTons?: number | null;
/** Metres a train may exceed maxTrainLengthMeters by before scheduling blocks it. */
overageToleranceMeters?: number | null;
powerKw?: number | null;
tractionForceKn?: number | null;
maxSpeedKmh?: number | null;

View File

@@ -39,11 +39,16 @@ export interface SaveRoutePayload {
status?: RouteStatus;
}
/**
* Human-readable route label: yard names, not yard codes. Staff read
* "Addis Ababa → Dire Dawa", not "ADDIS_ABABA → DIRE_DAWA". Falls back to the
* code only when a yard has no label.
*/
export function formatRouteLabel(route: RouteRecord): string {
const origin =
route.originYard?.code ?? route.originYard?.label ?? 'Origin';
route.originYard?.label ?? route.originYard?.code ?? 'Origin';
const dest =
route.destinationYard?.code ?? route.destinationYard?.label ?? 'Destination';
route.destinationYard?.label ?? route.destinationYard?.code ?? 'Destination';
return `${origin}${dest}`;
}

View File

@@ -8,7 +8,8 @@ export interface WagonType {
name: string;
capacityTons: number;
lengthMeters: number;
maxWagonsPerTrain?: number | null;
/** Empty wagon weight; required, since pull limits apply to tare + cargo. */
tareWeightTons: number;
supportedLoadTypes: string[];
isActive: boolean;
}

View File

@@ -15,14 +15,16 @@ export interface Wagon {
label: string;
country?: string;
} | null;
/** Owns this wagon's spec — tare, capacity, length are read from here, never off the wagon. */
wagonType?: {
id: string;
code: string;
name: string;
supportedLoadTypes?: string[];
tareWeightTons?: number;
capacityTons?: number;
lengthMeters?: number;
} | null;
tareWeight: number;
maxPayloadWeight: number;
status: Freight.WagonStatus;
currentYardId: string | null;
currentYard?: { id: string; label?: string; code?: string } | null;

View File

@@ -30,6 +30,7 @@ import type {
LoadableWagon,
LoadInventoryPayload,
MoveInventoryPayload,
StoreInventoryPayload,
ReceiveInventoryPayload,
ReleaseOrderPayload,
DeliverInventoryPayload,
@@ -63,7 +64,7 @@ import type {
WarehouseZone,
} from '@/types/warehouse';
export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'LOADED' | 'LEFT' | 'DELIVERED';
export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
export interface ContainerItem {
containerNumber: string;
@@ -74,9 +75,12 @@ export interface ContainerItem {
truckPlate: string | null;
truckArrived: boolean;
truckLeft: boolean;
/** Operator has loaded this container onto the truck (customer assignment alone is not "loaded"). */
loaded: boolean;
bookingReference: string | null;
contractId: string | null;
hasLastMile: boolean;
handoverSigned: boolean;
}
/** A pre-dispatch EXPORT train that has inventory waiting to be loaded. */
@@ -135,6 +139,26 @@ export const warehouseService = {
return data?.data ?? data ?? [];
},
/** Ask the customer to sign the booking's handover (creates one if none, then notifies). */
requestHandoverSignature: async (
bookingId: string,
): Promise<{ notified: boolean; reference: string | null; alreadySigned: boolean }> => {
const { data } = await apiClient.post(
`/warehouse-inventory/bookings/${bookingId}/request-handover-signature`,
);
return data?.data ?? data;
},
/** A booking's containers with VGM cargo weight (tonnes) for exit weighing. */
getContainerWeights: async (
bookingId: string,
): Promise<Array<{ containerNumber: string; weightTons: number }>> => {
const { data } = await apiClient.get(
`/warehouse-inventory/bookings/${bookingId}/container-weights`,
);
return data?.data ?? data ?? [];
},
/** Booking container numbers not yet loaded onto any truck. */
getLoadableContainers: async (bookingId: string): Promise<string[]> => {
const { data } = await apiClient.get(
@@ -235,8 +259,8 @@ export const warehouseService = {
}),
// ── Lifecycle (Batch 2) ──────────────────────────────────────────────────
store: (id: string) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.STORE(id)),
store: (id: string, payload?: StoreInventoryPayload) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.STORE(id), payload),
reserve: (payload: ReserveInventoryPayload) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RESERVE, payload),
markReadyForLoading: (id: string) =>