mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
warehouse
This commit is contained in:
@@ -42,8 +42,9 @@ import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedul
|
||||
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
|
||||
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
||||
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
||||
import RoutesPage from "./pages/fleet/RoutesPage";
|
||||
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
||||
import RoutesPage from "./pages/fleet/RoutesPage";
|
||||
import WarehouseDashboardPage from "./pages/warehouses/WarehouseDashboardPage";
|
||||
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
||||
import WarehouseDetailPage from "./pages/warehouses/WarehouseDetailPage";
|
||||
import WarehouseInventoryPage from "./pages/warehouses/WarehouseInventoryPage";
|
||||
import InventoryInquiryPage from "./pages/warehouses/InventoryInquiryPage";
|
||||
@@ -122,13 +123,18 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Warehouse Management",
|
||||
items: [
|
||||
{
|
||||
label: "Warehouses",
|
||||
href: "/dashboard/warehouses",
|
||||
icon: <Container />,
|
||||
},
|
||||
title: "Warehouse Management",
|
||||
items: [
|
||||
{
|
||||
label: "Warehouse dashboard",
|
||||
href: "/dashboard/warehouses",
|
||||
icon: <LayoutDashboard />,
|
||||
},
|
||||
{
|
||||
label: "Warehouses",
|
||||
href: "/dashboard/warehouses/list",
|
||||
icon: <Container />,
|
||||
},
|
||||
{
|
||||
label: "Inventory",
|
||||
href: "/dashboard/warehouse-inventory",
|
||||
@@ -296,8 +302,10 @@ const App = () => {
|
||||
<Route path="containers" element={<FleetResourcePage />} />
|
||||
<Route path="cargoes" element={<FleetResourcePage />} />
|
||||
|
||||
<Route path="warehouses" element={<WarehouseListPage />} />
|
||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
||||
<Route path="warehouse-dashboard" element={<Navigate to="/dashboard/warehouses" replace />} />
|
||||
<Route path="warehouses" element={<WarehouseDashboardPage />} />
|
||||
<Route path="warehouses/list" element={<WarehouseListPage />} />
|
||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
||||
<Route path="warehouse-inventory" element={<WarehouseInventoryPage />} />
|
||||
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} />
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -244,10 +244,15 @@ export const URL_CONSTANTS = {
|
||||
WAREHOUSE_INVENTORY: {
|
||||
BASE: '/warehouse-inventory',
|
||||
RECEIVE: '/warehouse-inventory/receive',
|
||||
DASHBOARD_SUMMARY: '/warehouse-inventory/dashboard/summary',
|
||||
READY_FOR_LOADING: '/warehouse-inventory/ready-for-loading',
|
||||
INQUIRY: '/warehouse-inventory/inquiry',
|
||||
STORE: (id: string) => `/warehouse-inventory/${id}/store`,
|
||||
RESERVE: (id: string) => `/warehouse-inventory/${id}/reserve`,
|
||||
INSPECT: (id: string) => `/warehouse-inventory/${id}/inspect`,
|
||||
MARK_READY: (id: string) => `/warehouse-inventory/${id}/ready-for-loading`,
|
||||
LOAD: (id: string) => `/warehouse-inventory/${id}/load`,
|
||||
DISPATCH: (id: string) => `/warehouse-inventory/${id}/dispatch`,
|
||||
MOVE: (id: string) => `/warehouse-inventory/${id}/move`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
InventoryInquiryFilter,
|
||||
MoveInventoryPayload,
|
||||
ReceiveInventoryPayload,
|
||||
ReserveInventoryPayload,
|
||||
SaveWarehousePayload,
|
||||
SaveYardPayload,
|
||||
SaveZonePayload,
|
||||
@@ -15,10 +16,12 @@ import type {
|
||||
export const warehouseKeys = {
|
||||
all: ['warehouses'] as const,
|
||||
list: (filter?: WarehouseFilter) => ['warehouses', 'list', filter ?? {}] as const,
|
||||
facilities: () => ['warehouses', 'facilities'] as const,
|
||||
detail: (id: string) => ['warehouses', 'detail', id] as const,
|
||||
yards: (warehouseId: string) => ['warehouses', warehouseId, 'yards'] as const,
|
||||
zones: (yardId: string) => ['warehouse-yards', yardId, 'zones'] as const,
|
||||
inventory: (filter?: InventoryFilter) => ['warehouse-inventory', 'list', filter ?? {}] as const,
|
||||
dashboardSummary: (filter?: InventoryFilter) => ['warehouse-dashboard', 'summary', filter ?? {}] as const,
|
||||
inquiry: (filter: InventoryInquiryFilter) => ['warehouse-inventory', 'inquiry', filter] as const,
|
||||
};
|
||||
|
||||
@@ -39,6 +42,13 @@ export function useWarehouse(id?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useWarehouseFacilities() {
|
||||
return useQuery({
|
||||
queryKey: warehouseKeys.facilities(),
|
||||
queryFn: () => warehouseService.listFacilities().then((r) => r.data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWarehouse() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
@@ -127,6 +137,13 @@ export function useWarehouseInventory(filter?: InventoryFilter) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useWarehouseDashboardSummary(filter?: InventoryFilter) {
|
||||
return useQuery({
|
||||
queryKey: warehouseKeys.dashboardSummary(filter),
|
||||
queryFn: () => warehouseService.getDashboardSummary(filter).then((r) => r.data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useReceiveInventory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
@@ -134,6 +151,30 @@ export function useReceiveInventory() {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: warehouseKeys.all });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useStoreInventory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.storeInventory(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useReserveInventory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: string; payload: ReserveInventoryPayload }) =>
|
||||
warehouseService.reserveInventory(id, payload),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -150,7 +191,32 @@ export function useMarkReadyForLoading() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.markReadyForLoading(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useLoadInventory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.loadInventory(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDispatchInventory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.dispatchInventory(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-dashboard'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
ClipboardCheck,
|
||||
Container as ContainerIcon,
|
||||
Layers,
|
||||
Package,
|
||||
PackagePlus,
|
||||
Send,
|
||||
Truck,
|
||||
Warehouse as WarehouseIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import {
|
||||
useWarehouseDashboardSummary,
|
||||
useWarehouseFacilities,
|
||||
useWarehouses,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import type { InventoryFilter } from '@/types/warehouse';
|
||||
|
||||
export default function WarehouseDashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const [filter, setFilter] = useState<InventoryFilter>({});
|
||||
const facilitiesQuery = useWarehouseFacilities();
|
||||
const warehousesQuery = useWarehouses(filter.facilityId ? { stationId: filter.facilityId } : undefined);
|
||||
const summaryQuery = useWarehouseDashboardSummary(filter);
|
||||
|
||||
const counts = summaryQuery.data ?? {
|
||||
totalWarehouses: 0,
|
||||
totalInventory: 0,
|
||||
receivedToday: 0,
|
||||
stored: 0,
|
||||
reserved: 0,
|
||||
readyForLoading: 0,
|
||||
loaded: 0,
|
||||
dispatched: 0,
|
||||
};
|
||||
|
||||
const facilityOptions = useMemo(
|
||||
() =>
|
||||
(facilitiesQuery.data ?? []).map((facility) => ({
|
||||
value: facility.id,
|
||||
label: `${facility.label ?? facility.name ?? facility.code} (${facility.code})`,
|
||||
})),
|
||||
[facilitiesQuery.data],
|
||||
);
|
||||
const warehouseOptions = useMemo(
|
||||
() => (warehousesQuery.data ?? []).map((warehouse) => ({ value: warehouse.id, label: `${warehouse.name} (${warehouse.code})` })),
|
||||
[warehousesQuery.data],
|
||||
);
|
||||
|
||||
const loading = summaryQuery.isLoading;
|
||||
const hasError = summaryQuery.isError;
|
||||
|
||||
const cards: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
icon: typeof WarehouseIcon;
|
||||
color: string;
|
||||
href: string;
|
||||
disabled?: boolean;
|
||||
}> = [
|
||||
{
|
||||
label: 'Total warehouses',
|
||||
value: counts.totalWarehouses,
|
||||
icon: WarehouseIcon,
|
||||
color: 'indigo',
|
||||
href: '/dashboard/warehouses/list',
|
||||
},
|
||||
{
|
||||
label: 'Total inventory',
|
||||
value: counts.totalInventory,
|
||||
icon: Package,
|
||||
color: 'gray',
|
||||
href: '/dashboard/warehouse-inventory',
|
||||
},
|
||||
{
|
||||
label: 'Received today',
|
||||
value: counts.receivedToday,
|
||||
icon: PackagePlus,
|
||||
color: 'orange',
|
||||
href: '/dashboard/warehouse-inventory',
|
||||
},
|
||||
{
|
||||
label: 'Stored',
|
||||
value: counts.stored,
|
||||
icon: Layers,
|
||||
color: 'blue',
|
||||
href: '/dashboard/warehouse-inventory?status=STORED',
|
||||
},
|
||||
{
|
||||
label: 'Reserved',
|
||||
value: counts.reserved,
|
||||
icon: ClipboardCheck,
|
||||
color: 'grape',
|
||||
href: '/dashboard/warehouse-inventory?status=RESERVED',
|
||||
},
|
||||
{
|
||||
label: 'Ready for loading',
|
||||
value: counts.readyForLoading,
|
||||
icon: ContainerIcon,
|
||||
color: 'cyan',
|
||||
href: '/dashboard/warehouse-inventory?status=READY_FOR_LOADING',
|
||||
},
|
||||
{
|
||||
label: 'Loaded',
|
||||
value: counts.loaded,
|
||||
icon: Truck,
|
||||
color: 'teal',
|
||||
href: '/dashboard/warehouse-inventory?status=LOADED',
|
||||
},
|
||||
{
|
||||
label: 'Dispatched',
|
||||
value: counts.dispatched,
|
||||
icon: Send,
|
||||
color: 'green',
|
||||
href: '/dashboard/warehouse-inventory?status=DISPATCHED',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs items={[{ label: 'Warehouse dashboard' }]} />
|
||||
|
||||
<Stack gap="xl" mt="sm">
|
||||
<Card withBorder radius="md" padding="xl" bg="gray.0">
|
||||
<Group justify="space-between" align="center" gap="xl">
|
||||
<div>
|
||||
<Title order={1}>Warehouse Dashboard</Title>
|
||||
<Text c="dimmed" size="lg" mt={6}>
|
||||
Live overview of warehouse capacity and inventory lifecycle.
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs" visibleFrom="sm">
|
||||
<WarehouseIcon size={58} color="var(--mantine-color-green-6)" />
|
||||
<Truck size={58} color="var(--mantine-color-orange-6)" />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{hasError && (
|
||||
<Alert color="yellow" variant="light" title="Some dashboard metrics could not be loaded">
|
||||
Available cards still show data from the APIs that responded.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<Select
|
||||
placeholder="All facilities"
|
||||
clearable
|
||||
searchable
|
||||
data={facilityOptions}
|
||||
value={filter.facilityId ?? null}
|
||||
onChange={(value) =>
|
||||
setFilter((current) => ({ ...current, facilityId: value ?? undefined, warehouseId: undefined }))
|
||||
}
|
||||
w={260}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All warehouses"
|
||||
clearable
|
||||
searchable
|
||||
data={warehouseOptions}
|
||||
value={filter.warehouseId ?? null}
|
||||
onChange={(value) => setFilter((current) => ({ ...current, warehouseId: value ?? undefined }))}
|
||||
w={260}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<Card
|
||||
key={card.label}
|
||||
withBorder
|
||||
radius="md"
|
||||
padding="xl"
|
||||
component="button"
|
||||
type="button"
|
||||
disabled={card.disabled}
|
||||
onClick={() => navigate(card.href)}
|
||||
style={{
|
||||
cursor: card.disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: card.disabled ? 0.65 : 1,
|
||||
textAlign: 'left',
|
||||
transition: 'transform 120ms ease, box-shadow 120ms ease',
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Stack gap={10}>
|
||||
<Text tt="uppercase" fw={700} c="dimmed" size="sm">
|
||||
{card.label}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<Skeleton height={38} width={76} radius="sm" />
|
||||
) : (
|
||||
<Text fw={800} size="36px" lh={1}>
|
||||
{card.value.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<ThemeIcon color={card.color} variant="light" size={52} radius="md">
|
||||
<Icon size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export default function WarehouseDetailPage() {
|
||||
<Container size="sm" py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<Text fw={700}>Warehouse not found</Text>
|
||||
<Button variant="default" leftSection={<ArrowLeft size={16} />} onClick={() => navigate('/dashboard/warehouses')}>
|
||||
<Button variant="default" leftSection={<ArrowLeft size={16} />} onClick={() => navigate('/dashboard/warehouses/list')}>
|
||||
Back to warehouses
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -130,7 +130,8 @@ export default function WarehouseDetailPage() {
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Warehouses', href: '/dashboard/warehouses' },
|
||||
{ label: 'Warehouse dashboard', href: '/dashboard/warehouses' },
|
||||
{ label: 'Warehouses', href: '/dashboard/warehouses/list' },
|
||||
{ label: warehouse.name },
|
||||
]}
|
||||
/>
|
||||
@@ -138,7 +139,7 @@ export default function WarehouseDetailPage() {
|
||||
<Stack gap="lg" mt="sm">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Group gap="md" align="center">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => navigate('/dashboard/warehouses')}>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => navigate('/dashboard/warehouses/list')}>
|
||||
<ArrowLeft size={18} />
|
||||
</ActionIcon>
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
@@ -26,9 +28,13 @@ import {
|
||||
} from '@/components/warehouses';
|
||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||
import {
|
||||
useDispatchInventory,
|
||||
useInspectInventory,
|
||||
useLoadInventory,
|
||||
useMarkReadyForLoading,
|
||||
useMoveInventory,
|
||||
useReserveInventory,
|
||||
useStoreInventory,
|
||||
useWarehouseInventory,
|
||||
useWarehouseYards,
|
||||
useWarehouseZones,
|
||||
@@ -38,8 +44,15 @@ import type { InventoryFilter, InventoryStatus, WarehouseInventoryItem } from '@
|
||||
|
||||
export default function WarehouseInventoryPage() {
|
||||
const { toast } = useToast();
|
||||
const [filter, setFilter] = useState<InventoryFilter>({});
|
||||
const [search, setSearch] = useState('');
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialStatus = searchParams.get('status') as InventoryStatus | null;
|
||||
const [filter, setFilter] = useState<InventoryFilter>({
|
||||
status: initialStatus ?? undefined,
|
||||
warehouseId: searchParams.get('warehouseId') ?? undefined,
|
||||
yardId: searchParams.get('yardId') ?? undefined,
|
||||
zoneId: searchParams.get('zoneId') ?? undefined,
|
||||
});
|
||||
const [search, setSearch] = useState(searchParams.get('search') ?? '');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [moveItem, setMoveItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [moveDraft, setMoveDraft] = useState<{
|
||||
@@ -64,7 +77,11 @@ export default function WarehouseInventoryPage() {
|
||||
const inventoryQuery = useWarehouseInventory(queryFilter);
|
||||
|
||||
const inspectMutation = useInspectInventory();
|
||||
const storeMutation = useStoreInventory();
|
||||
const reserveMutation = useReserveInventory();
|
||||
const readyMutation = useMarkReadyForLoading();
|
||||
const loadMutation = useLoadInventory();
|
||||
const dispatchMutation = useDispatchInventory();
|
||||
const moveMutation = useMoveInventory();
|
||||
|
||||
const warehouseOptions = useMemo(
|
||||
@@ -127,6 +144,82 @@ export default function WarehouseInventoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStore = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await storeMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory stored' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Store failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReserve = async (item: WarehouseInventoryItem) => {
|
||||
if (item.status !== 'STORED') {
|
||||
toast({ variant: 'destructive', title: 'Only STORED inventory can be reserved.' });
|
||||
return;
|
||||
}
|
||||
if (item.booking?.status !== 'PAID' && item.booking?.paymentStatus !== 'PAID') {
|
||||
toast({ variant: 'destructive', title: 'Only PAID bookings can reserve stored inventory.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await reserveMutation.mutateAsync({ id: item.id, payload: { bookingId: item.bookingId } });
|
||||
toast({ title: 'Inventory reserved' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Reserve failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoad = async (item: WarehouseInventoryItem) => {
|
||||
if (item.booking?.status !== 'PAID' && item.booking?.paymentStatus !== 'PAID') {
|
||||
toast({ variant: 'destructive', title: 'Only PAID bookings can be loaded.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await loadMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory loaded' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDispatch = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await dispatchMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory dispatched' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Dispatch failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const inventory = inventoryQuery.data ?? [];
|
||||
const readyToLoad = inventory.filter(
|
||||
(item) =>
|
||||
item.status === 'READY_FOR_LOADING' &&
|
||||
(item.booking?.status === 'PAID' || item.booking?.paymentStatus === 'PAID'),
|
||||
);
|
||||
const pendingPayment = inventory.filter(
|
||||
(item) =>
|
||||
item.status === 'READY_FOR_LOADING' &&
|
||||
item.booking?.status !== 'PAID' &&
|
||||
item.booking?.paymentStatus !== 'PAID',
|
||||
);
|
||||
const loadedInventory = inventory.filter((item) => item.status === 'LOADED');
|
||||
|
||||
const handleMove = async () => {
|
||||
if (!moveItem || !moveDraft.warehouseId || !moveDraft.yardId || !moveDraft.zoneId) return;
|
||||
setBusyId(moveItem.id);
|
||||
@@ -223,15 +316,72 @@ export default function WarehouseInventoryPage() {
|
||||
</Center>
|
||||
) : (
|
||||
<WarehouseInventoryTable
|
||||
items={inventoryQuery.data ?? []}
|
||||
items={inventory}
|
||||
onInspect={handleInspect}
|
||||
onStore={handleStore}
|
||||
onReserve={handleReserve}
|
||||
onReadyForLoading={handleReady}
|
||||
onLoad={handleLoad}
|
||||
onDispatch={handleDispatch}
|
||||
onMove={openMoveModal}
|
||||
busyId={busyId}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Tabs defaultValue="ready">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="ready">Ready to Load</Tabs.Tab>
|
||||
<Tabs.Tab value="pending">Pending Payment</Tabs.Tab>
|
||||
<Tabs.Tab value="loaded">Loaded Inventory</Tabs.Tab>
|
||||
<Tabs.Tab value="dispatch">Dispatch Queue</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="ready" pt="md">
|
||||
<WarehouseInventoryTable
|
||||
items={readyToLoad}
|
||||
onInspect={handleInspect}
|
||||
onStore={handleStore}
|
||||
onReserve={handleReserve}
|
||||
onReadyForLoading={handleReady}
|
||||
onLoad={handleLoad}
|
||||
onDispatch={handleDispatch}
|
||||
onMove={openMoveModal}
|
||||
busyId={busyId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="pending" pt="md">
|
||||
<WarehouseInventoryTable
|
||||
items={pendingPayment}
|
||||
onInspect={handleInspect}
|
||||
onReadyForLoading={handleReady}
|
||||
onMove={openMoveModal}
|
||||
busyId={busyId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="loaded" pt="md">
|
||||
<WarehouseInventoryTable
|
||||
items={loadedInventory}
|
||||
onInspect={handleInspect}
|
||||
onReadyForLoading={handleReady}
|
||||
onDispatch={handleDispatch}
|
||||
onMove={openMoveModal}
|
||||
busyId={busyId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="dispatch" pt="md">
|
||||
<WarehouseInventoryTable
|
||||
items={loadedInventory}
|
||||
onInspect={handleInspect}
|
||||
onReadyForLoading={handleReady}
|
||||
onDispatch={handleDispatch}
|
||||
busyId={busyId}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<ReceiveInventoryModal opened={modalOpen} onClose={() => setModalOpen(false)} />
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function WarehouseListPage() {
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs items={[{ label: 'Warehouses' }]} />
|
||||
<Breadcrumbs items={[{ label: 'Warehouse dashboard', href: '/dashboard/warehouses' }, { label: 'Warehouses' }]} />
|
||||
|
||||
<Stack gap="lg" mt="sm">
|
||||
<Group justify="space-between" align="flex-end">
|
||||
|
||||
@@ -7,10 +7,13 @@ import type {
|
||||
InventoryInquiryResult,
|
||||
MoveInventoryPayload,
|
||||
ReceiveInventoryPayload,
|
||||
ReserveInventoryPayload,
|
||||
SaveWarehousePayload,
|
||||
SaveYardPayload,
|
||||
SaveZonePayload,
|
||||
Warehouse,
|
||||
WarehouseDashboardSummary,
|
||||
WarehouseFacility,
|
||||
WarehouseFilter,
|
||||
WarehouseInventoryItem,
|
||||
WarehouseYard,
|
||||
@@ -33,6 +36,7 @@ export const warehouseService = {
|
||||
apiClient.post<Warehouse>(URL_CONSTANTS.WAREHOUSES.BASE, payload),
|
||||
update: (id: string, payload: Partial<SaveWarehousePayload>) =>
|
||||
apiClient.patch<Warehouse>(URL_CONSTANTS.WAREHOUSES.BY_ID(id), payload),
|
||||
listFacilities: () => apiClient.get<WarehouseFacility[]>(URL_CONSTANTS.RULE_ENGINE.YARDS),
|
||||
|
||||
// ── Yards ────────────────────────────────────────────────────────────────
|
||||
listYards: (warehouseId: string) =>
|
||||
@@ -59,10 +63,22 @@ export const warehouseService = {
|
||||
}),
|
||||
receiveInventory: (payload: ReceiveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RECEIVE, payload),
|
||||
getDashboardSummary: (filter?: InventoryFilter) =>
|
||||
apiClient.get<WarehouseDashboardSummary>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DASHBOARD_SUMMARY, {
|
||||
params: cleanParams(filter ?? {}),
|
||||
}),
|
||||
storeInventory: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.STORE(id)),
|
||||
reserveInventory: (id: string, payload: ReserveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RESERVE(id), payload),
|
||||
inspectInventory: (id: string) =>
|
||||
apiClient.patch<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.INSPECT(id)),
|
||||
markReadyForLoading: (id: string) =>
|
||||
apiClient.patch<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MARK_READY(id)),
|
||||
loadInventory: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.LOAD(id)),
|
||||
dispatchInventory: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DISPATCH(id)),
|
||||
moveInventory: (id: string, payload: MoveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVE(id), payload),
|
||||
listReadyForLoading: (filter?: InventoryFilter) =>
|
||||
|
||||
@@ -23,12 +23,25 @@ export const WAREHOUSE_ZONE_TYPES = [
|
||||
export type WarehouseZoneType = (typeof WAREHOUSE_ZONE_TYPES)[number];
|
||||
|
||||
export const INVENTORY_STATUSES = [
|
||||
'RECEIVED',
|
||||
'STORED',
|
||||
'RESERVED',
|
||||
'ARRIVED_AT_WAREHOUSE',
|
||||
'UNDER_INSPECTION',
|
||||
'READY_FOR_LOADING',
|
||||
'LOADED',
|
||||
'DISPATCHED',
|
||||
] as const;
|
||||
export type InventoryStatus = (typeof INVENTORY_STATUSES)[number];
|
||||
|
||||
export interface WarehouseFacility {
|
||||
id: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
code: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface WarehouseZone {
|
||||
id: string;
|
||||
yardId: string;
|
||||
@@ -64,6 +77,7 @@ export interface Warehouse {
|
||||
code: string;
|
||||
type: WarehouseType;
|
||||
stationId: string | null;
|
||||
facility?: WarehouseFacility | null;
|
||||
locationName: string | null;
|
||||
capacityWeight: number | null;
|
||||
capacityContainers: number | null;
|
||||
@@ -93,6 +107,12 @@ export interface WarehouseInventoryItem {
|
||||
inspectedAt: string | null;
|
||||
readyForLoadingAt: string | null;
|
||||
notes: string | null;
|
||||
booking?: {
|
||||
id: string;
|
||||
reference: string;
|
||||
status: string;
|
||||
paymentStatus: string;
|
||||
} | null;
|
||||
warehouse?: Warehouse | null;
|
||||
yard?: WarehouseYard | null;
|
||||
zone?: WarehouseZone | null;
|
||||
@@ -124,6 +144,7 @@ export interface SaveWarehousePayload {
|
||||
code: string;
|
||||
type: WarehouseType;
|
||||
stationId?: string;
|
||||
facilityId?: string;
|
||||
locationName?: string;
|
||||
capacityWeight?: number;
|
||||
capacityContainers?: number;
|
||||
@@ -169,6 +190,21 @@ export interface MoveInventoryPayload {
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
export interface ReserveInventoryPayload {
|
||||
bookingId: string;
|
||||
}
|
||||
|
||||
export interface WarehouseDashboardSummary {
|
||||
totalWarehouses: number;
|
||||
totalInventory: number;
|
||||
receivedToday: number;
|
||||
stored: number;
|
||||
reserved: number;
|
||||
readyForLoading: number;
|
||||
loaded: number;
|
||||
dispatched: number;
|
||||
}
|
||||
|
||||
export interface WarehouseFilter {
|
||||
search?: string;
|
||||
type?: WarehouseType;
|
||||
@@ -177,6 +213,7 @@ export interface WarehouseFilter {
|
||||
}
|
||||
|
||||
export interface InventoryFilter {
|
||||
facilityId?: string;
|
||||
warehouseId?: string;
|
||||
yardId?: string;
|
||||
zoneId?: string;
|
||||
@@ -186,6 +223,8 @@ export interface InventoryFilter {
|
||||
goodsId?: string;
|
||||
status?: InventoryStatus;
|
||||
search?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}
|
||||
|
||||
export interface InventoryInquiryFilter {
|
||||
|
||||
Reference in New Issue
Block a user