mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
warehouse
This commit is contained in:
@@ -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/",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user