mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Nullable freight_type on the warehouse, null meaning it takes both. No backfill: every existing warehouse is unrestricted today and writing a value would narrow allocation behind the operator's back. Reuses FREIGHT_TYPES from the booking entity rather than a third copy of the same two values.
175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
import {
|
|
WAREHOUSE_FREIGHT_TYPES,
|
|
WAREHOUSE_TYPES,
|
|
WAREHOUSE_YARD_TYPES,
|
|
WAREHOUSE_ZONE_TYPES,
|
|
WAREHOUSE_STATUSES,
|
|
INVENTORY_STATUSES,
|
|
type ImportUnloadedItem,
|
|
type Warehouse,
|
|
type WarehouseInventoryItem,
|
|
type WarehouseYard,
|
|
} from '@/types/warehouse';
|
|
|
|
export const humanizeEnum = (value: string) =>
|
|
value
|
|
.toLowerCase()
|
|
.split('_')
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(' ');
|
|
|
|
const toOptions = (values: readonly string[]) =>
|
|
values.map((value) => ({ value, label: humanizeEnum(value) }));
|
|
|
|
/**
|
|
* Warehouses actually located at a train's station — e.g. a train destined for
|
|
* Indode should only offer Indode's own warehouse, not Sebeta's or Modjo's.
|
|
* Falls back to every warehouse when the station is unmapped (no `stationId`
|
|
* match anywhere), so unusual/legacy data never blocks the unload flow entirely.
|
|
*/
|
|
export const warehousesAtStation = (warehouses: Warehouse[], stationId: string | null | undefined) => {
|
|
if (!stationId) return warehouses;
|
|
const atStation = warehouses.filter((w) => w.stationId === stationId);
|
|
return atStation.length ? atStation : warehouses;
|
|
};
|
|
|
|
/**
|
|
* Yards at ONE warehouse eligible to receive a booking, given what it actually
|
|
* is — e.g. at Indode: container import always narrows to Yard 5, export to
|
|
* Yard 6; a Wheat booking narrows to Yard 4 (Dry Bulk), not Break Bulk or
|
|
* Coffee/Tea. Mirrors `warehousesAtStation`'s fallback philosophy: an
|
|
* unconfigured yard (no cargo types set) stays open rather than disappearing,
|
|
* but a yard that IS configured for other cargo never shows for a mismatch.
|
|
*
|
|
* Container yards are the one case with no such fallback: a CONTAINER_YARD
|
|
* left at direction BOTH/null (Indode's Yard 10 service yard, Yard 11
|
|
* equipment yard) is a service/equipment yard, not a customer cargo yard, and
|
|
* must never be offered just because the exact-direction stack is missing.
|
|
*/
|
|
export const yardsForBooking = (
|
|
yards: WarehouseYard[],
|
|
params: {
|
|
warehouseId: string | null | undefined;
|
|
freightType: string | null | undefined;
|
|
tradeDirection: string | null | undefined;
|
|
cargoTypeCode: string | null | undefined;
|
|
},
|
|
): WarehouseYard[] => {
|
|
const atWarehouse = yards.filter((y) => y.warehouseId === params.warehouseId && y.isActive);
|
|
const isContainer = (params.freightType ?? '').toUpperCase() === 'CONTAINER';
|
|
|
|
if (isContainer) {
|
|
const direction = (params.tradeDirection ?? '').toUpperCase();
|
|
return atWarehouse.filter((y) => y.type === 'CONTAINER_YARD' && y.direction === direction);
|
|
}
|
|
|
|
const nonContainer = atWarehouse.filter((y) => y.type !== 'CONTAINER_YARD');
|
|
if (!params.cargoTypeCode) return nonContainer;
|
|
|
|
const cargoMatched = nonContainer.filter((y) => {
|
|
const codes = (y.cargoTypes ?? []).map((c) => c.code);
|
|
return codes.length === 0 || codes.includes(params.cargoTypeCode as string);
|
|
});
|
|
return cargoMatched.length ? cargoMatched : nonContainer;
|
|
};
|
|
|
|
export const warehouseTypeOptions = toOptions(WAREHOUSE_TYPES);
|
|
export const warehouseFreightTypeOptions = toOptions(WAREHOUSE_FREIGHT_TYPES);
|
|
export const yardTypeOptions = toOptions(WAREHOUSE_YARD_TYPES);
|
|
export const zoneTypeOptions = toOptions(WAREHOUSE_ZONE_TYPES);
|
|
export const statusOptions = toOptions(WAREHOUSE_STATUSES);
|
|
export const inventoryStatusOptions = toOptions(INVENTORY_STATUSES);
|
|
|
|
export const formatNumber = (value: number | null | undefined) => {
|
|
if (value === null || value === undefined) return '—';
|
|
const num = Number(value);
|
|
if (Number.isNaN(num)) return '—';
|
|
return num.toLocaleString(undefined, { maximumFractionDigits: 3 });
|
|
};
|
|
|
|
export const formatCapacity = (current: number, capacity: number | null | undefined) => {
|
|
const cur = formatNumber(current);
|
|
if (capacity === null || capacity === undefined) return cur;
|
|
return `${cur} / ${formatNumber(capacity)}`;
|
|
};
|
|
|
|
/** A day count as a short, human duration: "0.2d" / "3.5 days" / "—". */
|
|
export const formatDays = (value: number | null | undefined) => {
|
|
if (value === null || value === undefined || Number.isNaN(Number(value))) return '—';
|
|
const num = Number(value);
|
|
const rounded = Math.round(num * 10) / 10;
|
|
return `${rounded} ${rounded === 1 ? 'day' : 'days'}`;
|
|
};
|
|
|
|
// Despite the name, this has always rendered date + time — hence formatDateTime.
|
|
export { formatDateTime as formatDate } from '@/lib/format';
|
|
|
|
// Name fields (warehouse / fee rule / allocation rule) accept letters and spaces only — no numbers.
|
|
export const lettersOnly = (value: string) => value.replace(/[^A-Za-z\s]/g, '');
|
|
|
|
export const extractErrorMessage = (error: unknown, fallback = 'Something went wrong') => {
|
|
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
|
|
const data = responseData && typeof responseData === 'object' ? (responseData as Record<string, unknown>) : undefined;
|
|
const rawMessage = data?.message ?? data?.error ?? (error as Error)?.message;
|
|
return Array.isArray(rawMessage) ? rawMessage.join(', ') : rawMessage ? String(rawMessage) : fallback;
|
|
};
|
|
|
|
/**
|
|
* An unloaded-queue row seen as the inventory item `ReleaseOrderModal` expects.
|
|
* Both truck-arrival openers (last mile, import trucks) work off queue rows.
|
|
*/
|
|
export const toReleaseInventoryItem = (row: ImportUnloadedItem): WarehouseInventoryItem =>
|
|
({
|
|
id: row.id,
|
|
bookingId: row.bookingId,
|
|
quantity: 1,
|
|
weight: Number(row.weight) || 0,
|
|
grnNumber: row.grnNumber,
|
|
status: row.currentStatus,
|
|
arrivedAt: row.arrivalTime,
|
|
unloadedAt: row.arrivalTime,
|
|
inspectionStatus: row.inspectionStatus,
|
|
releaseDate: row.releaseDate,
|
|
releaseOrderReference: row.releaseOrderReference,
|
|
handoverDocumentReference: row.handoverDocumentReference,
|
|
handoverDocumentDate: row.handoverDocumentDate,
|
|
deliveredAt: row.deliveredAt,
|
|
// Carries the saved [Exit Inspection] block so truck-leaving prefills the
|
|
// details captured at arrival (plate, driver, tare, gate-in).
|
|
notes: row.notes,
|
|
booking: row.bookingId
|
|
? {
|
|
id: row.bookingId,
|
|
reference: row.bookingReference ?? row.bookingId,
|
|
tradeDirection: 'IMPORT',
|
|
lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null,
|
|
customerTruckPlateNumber: row.customerTruckPlateNumber,
|
|
customerTruckDriverName: row.customerTruckDriverName,
|
|
customerTruckType: row.customerTruckType,
|
|
customerTruckContainerNumber: row.customerTruckContainerNumber,
|
|
customerTruckAssignedAt: row.customerTruckAssignedAt,
|
|
}
|
|
: null,
|
|
}) as unknown as WarehouseInventoryItem;
|
|
|
|
/**
|
|
* Error extractor for blob-download requests. When `responseType: 'blob'`, axios
|
|
* delivers the JSON error body as a Blob, so `extractErrorMessage` can't read
|
|
* `.message`. Decode the Blob to text, parse it, then fall back to the sync path.
|
|
*/
|
|
export const extractDownloadErrorMessage = async (error: unknown, fallback = 'Something went wrong') => {
|
|
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
|
|
if (responseData instanceof Blob) {
|
|
try {
|
|
const text = await responseData.text();
|
|
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
const raw = parsed?.message ?? parsed?.error;
|
|
if (Array.isArray(raw)) return raw.join(', ');
|
|
if (raw) return String(raw);
|
|
} catch {
|
|
/* not JSON — fall through */
|
|
}
|
|
}
|
|
return extractErrorMessage(error, fallback);
|
|
};
|