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) : 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; 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); };