warehouse

This commit is contained in:
hagiye
2026-06-19 17:13:17 +03:00
parent 63f177e6d0
commit 484ac187a9
22 changed files with 904 additions and 45 deletions

View File

@@ -64,6 +64,34 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
subtitle: "Manage route definitions built from freight yards",
},
},
{
prefix: "/dashboard/warehouses/list",
meta: {
title: "Warehouses",
subtitle: "Manage warehouses, yards, and zones",
},
},
{
prefix: "/dashboard/warehouse-inventory",
meta: {
title: "Warehouse inventory",
subtitle: "Track received items through inspection and loading",
},
},
{
prefix: "/dashboard/inventory-inquiry",
meta: {
title: "Inventory inquiry",
subtitle: "Locate cargo, containers, and goods inside the warehouse network",
},
},
{
prefix: "/dashboard/warehouses",
meta: {
title: "Warehouse dashboard",
subtitle: "Live overview of warehouse capacity and inventory lifecycle",
},
},
...getFleetRouteMeta(),
{
prefix: "/dashboard/trains/",

View File

@@ -10,7 +10,7 @@ import {
} from '@mantine/core';
import { useToast } from '@/hooks/use-toast';
import { useCreateWarehouse, useUpdateWarehouse } from '@/hooks/useWarehouses';
import { useCreateWarehouse, useUpdateWarehouse, useWarehouseFacilities } from '@/hooks/useWarehouses';
import type { SaveWarehousePayload, Warehouse, WarehouseType } from '@/types/warehouse';
import { extractErrorMessage, statusOptions, warehouseTypeOptions } from './options';
@@ -24,6 +24,7 @@ interface FormState {
name: string;
code: string;
type: WarehouseType;
stationId: string;
locationName: string;
capacityWeight: number | '';
capacityContainers: number | '';
@@ -34,6 +35,7 @@ const emptyForm = (): FormState => ({
name: '',
code: '',
type: 'OPEN_WAREHOUSE',
stationId: '',
locationName: '',
capacityWeight: '',
capacityContainers: '',
@@ -45,8 +47,14 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
const { toast } = useToast();
const createMutation = useCreateWarehouse();
const updateMutation = useUpdateWarehouse();
const facilitiesQuery = useWarehouseFacilities();
const [form, setForm] = useState<FormState>(emptyForm());
const facilityOptions = (facilitiesQuery.data ?? []).map((facility) => ({
value: facility.id,
label: `${facility.label ?? facility.name ?? facility.code} (${facility.code})`,
}));
useEffect(() => {
if (opened) {
setForm(
@@ -55,6 +63,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
name: warehouse.name,
code: warehouse.code,
type: warehouse.type,
stationId: warehouse.stationId ?? '',
locationName: warehouse.locationName ?? '',
capacityWeight: warehouse.capacityWeight ?? '',
capacityContainers: warehouse.capacityContainers ?? '',
@@ -77,6 +86,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
name: form.name.trim(),
code: form.code.trim(),
type: form.type,
stationId: form.stationId || undefined,
locationName: form.locationName.trim() || undefined,
capacityWeight: form.capacityWeight === '' ? undefined : Number(form.capacityWeight),
capacityContainers: form.capacityContainers === '' ? undefined : Number(form.capacityContainers),
@@ -135,6 +145,16 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
)}
</Group>
<Select
label="Facility / Port"
placeholder="Select facility"
clearable
searchable
data={facilityOptions}
value={form.stationId || null}
onChange={(value) => setForm((f) => ({ ...f, stationId: value ?? '' }))}
/>
<TextInput
label="Location name"
placeholder="Modjo, Oromia"

View File

@@ -39,6 +39,12 @@ export function WarehouseCardView({ warehouses, onView, onEdit }: WarehouseCardV
<WarehouseTypeBadge type={warehouse.type} />
</Group>
<Text size="sm" c="dimmed">
Facility: {warehouse.facility
? `${warehouse.facility.label ?? warehouse.facility.name ?? warehouse.facility.code} (${warehouse.facility.code})`
: '—'}
</Text>
{warehouse.locationName && (
<Group gap={6} c="dimmed">
<MapPin size={14} />

View File

@@ -1,5 +1,5 @@
import { Badge, Button, Group, Table, Text, Tooltip } from '@mantine/core';
import { ArrowLeftRight, ClipboardCheck, PackageCheck } from 'lucide-react';
import { Badge, Button, Group, Stack, Table, Text, Tooltip } from '@mantine/core';
import { ArrowLeftRight, ClipboardCheck, PackageCheck, Send, Truck, Warehouse } from 'lucide-react';
import type { WarehouseInventoryItem } from '@/types/warehouse';
import { InventoryStatusBadge } from './badges';
@@ -8,7 +8,11 @@ import { formatDate, formatNumber } from './options';
interface WarehouseInventoryTableProps {
items: WarehouseInventoryItem[];
onInspect: (item: WarehouseInventoryItem) => void;
onStore?: (item: WarehouseInventoryItem) => void;
onReserve?: (item: WarehouseInventoryItem) => void;
onReadyForLoading: (item: WarehouseInventoryItem) => void;
onLoad?: (item: WarehouseInventoryItem) => void;
onDispatch?: (item: WarehouseInventoryItem) => void;
onMove?: (item: WarehouseInventoryItem) => void;
busyId?: string | null;
}
@@ -17,13 +21,20 @@ const itemKind = (item: WarehouseInventoryItem) => {
if (item.containerId) return { label: 'Container', color: 'blue' };
if (item.cargoId) return { label: 'Cargo', color: 'grape' };
if (item.goodsId) return { label: 'Goods', color: 'orange' };
return { label: '', color: 'gray' };
return { label: '-', color: 'gray' };
};
const isPaidBooking = (item: WarehouseInventoryItem) =>
item.booking?.status === 'PAID' || item.booking?.paymentStatus === 'PAID';
export function WarehouseInventoryTable({
items,
onInspect,
onStore,
onReserve,
onReadyForLoading,
onLoad,
onDispatch,
onMove,
busyId,
}: WarehouseInventoryTableProps) {
@@ -36,14 +47,12 @@ export function WarehouseInventoryTable({
}
return (
<Table.ScrollContainer minWidth={1100}>
<Table.ScrollContainer minWidth={1280}>
<Table highlightOnHover verticalSpacing="sm" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Booking</Table.Th>
<Table.Th>Warehouse</Table.Th>
<Table.Th>Yard</Table.Th>
<Table.Th>Zone</Table.Th>
<Table.Th>Location Path</Table.Th>
<Table.Th>Item</Table.Th>
<Table.Th>Qty</Table.Th>
<Table.Th>Weight</Table.Th>
@@ -57,18 +66,38 @@ export function WarehouseInventoryTable({
{items.map((item) => {
const kind = itemKind(item);
const busy = busyId === item.id;
const paid = isPaidBooking(item);
const facility =
item.warehouse?.facility?.label ??
item.warehouse?.facility?.name ??
item.warehouse?.facility?.code ??
'No facility';
const locationPath = [
facility,
item.warehouse?.code ?? '-',
item.yard?.code ?? '-',
item.zone?.code ?? '-',
].join(' -> ');
return (
<Table.Tr key={item.id}>
<Table.Td>
<Tooltip label={item.bookingId} withArrow>
<Text size="sm" fw={600}>
{item.bookingId.slice(0, 8)}
</Text>
<Stack gap={2}>
<Text size="sm" fw={600}>
{item.booking?.reference ?? `${item.bookingId.slice(0, 8)}...`}
</Text>
<Text size="xs" c={paid ? 'green' : 'dimmed'}>
{item.booking?.paymentStatus ?? item.booking?.status ?? 'Unknown payment'}
</Text>
</Stack>
</Tooltip>
</Table.Td>
<Table.Td>{item.warehouse?.code ?? '—'}</Table.Td>
<Table.Td>{item.yard?.code ?? '—'}</Table.Td>
<Table.Td>{item.zone?.code ?? '—'}</Table.Td>
<Table.Td>
<Text size="sm" fw={500}>
{locationPath}
</Text>
</Table.Td>
<Table.Td>
<Badge color={kind.color} variant="light" size="sm" radius="md">
{kind.label}
@@ -109,17 +138,61 @@ export function WarehouseInventoryTable({
>
Inspect
</Button>
<Button
size="compact-xs"
variant="light"
color="blue"
leftSection={<Warehouse size={14} />}
disabled={!onStore || !['RECEIVED', 'ARRIVED_AT_WAREHOUSE'].includes(item.status) || busy}
loading={busy}
onClick={() => onStore?.(item)}
>
Store
</Button>
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<ClipboardCheck size={14} />}
disabled={!onReserve || item.status !== 'STORED' || busy}
loading={busy}
onClick={() => onReserve?.(item)}
>
Reserve
</Button>
<Button
size="compact-xs"
variant="light"
color="green"
leftSection={<PackageCheck size={14} />}
disabled={item.status !== 'UNDER_INSPECTION' || busy}
disabled={item.status !== 'RESERVED' || busy}
loading={busy}
onClick={() => onReadyForLoading(item)}
>
Ready
</Button>
<Button
size="compact-xs"
variant="light"
color="teal"
leftSection={<Truck size={14} />}
disabled={!onLoad || item.status !== 'READY_FOR_LOADING' || !paid || busy}
loading={busy}
onClick={() => onLoad?.(item)}
>
Loaded
</Button>
<Button
size="compact-xs"
variant="light"
color="orange"
leftSection={<Send size={14} />}
disabled={!onDispatch || item.status !== 'LOADED' || busy}
loading={busy}
onClick={() => onDispatch?.(item)}
>
Dispatch
</Button>
</Group>
</Table.Td>
</Table.Tr>

View File

@@ -27,6 +27,7 @@ export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTablePro
<Table.Tr>
<Table.Th>Code</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Facility / Port</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Location</Table.Th>
<Table.Th>Weight (cur / cap)</Table.Th>
@@ -44,6 +45,11 @@ export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTablePro
</Anchor>
</Table.Td>
<Table.Td>{warehouse.name}</Table.Td>
<Table.Td>
{warehouse.facility
? `${warehouse.facility.label ?? warehouse.facility.name ?? warehouse.facility.code} (${warehouse.facility.code})`
: '—'}
</Table.Td>
<Table.Td>
<WarehouseTypeBadge type={warehouse.type} />
</Table.Td>

View File

@@ -34,9 +34,14 @@ export function WarehouseStatusBadge({ status }: { status: WarehouseStatus }) {
}
const inventoryStatusColor: Record<InventoryStatus, string> = {
RECEIVED: 'yellow',
STORED: 'blue',
RESERVED: 'grape',
ARRIVED_AT_WAREHOUSE: 'yellow',
UNDER_INSPECTION: 'cyan',
READY_FOR_LOADING: 'green',
LOADED: 'teal',
DISPATCHED: 'gray',
};
export function InventoryStatusBadge({ status }: { status: InventoryStatus }) {