mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 09:35:44 +00:00
Recieved invontory detals
This commit is contained in:
@@ -49,6 +49,7 @@ import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
||||
import WarehouseDetailPage from "./pages/warehouses/WarehouseDetailPage";
|
||||
import WarehouseInventoryPage from "./pages/warehouses/WarehouseInventoryPage";
|
||||
import InventoryInquiryPage from "./pages/warehouses/InventoryInquiryPage";
|
||||
import WarehouseDashboardPage from "./pages/warehouses/WarehouseDashboardPage";
|
||||
|
||||
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
{
|
||||
@@ -121,6 +122,11 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
{
|
||||
title: "Warehouse Management",
|
||||
items: [
|
||||
{
|
||||
label: "Warehouse Dashboard",
|
||||
href: "/dashboard/warehouse-dashboard",
|
||||
icon: <LayoutDashboard />,
|
||||
},
|
||||
{
|
||||
label: "Warehouses",
|
||||
href: "/dashboard/warehouses",
|
||||
@@ -284,6 +290,7 @@ const App = () => {
|
||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
||||
<Route path="warehouse-inventory" element={<WarehouseInventoryPage />} />
|
||||
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} />
|
||||
<Route path="warehouse-dashboard" element={<WarehouseDashboardPage />} />
|
||||
|
||||
<Route path="user-management" element={<UserManagementPage />} />
|
||||
<Route path="user-management/users" element={<UsersPage />} />
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Center, Loader, Text, Timeline } from '@mantine/core';
|
||||
import {
|
||||
ArrowRightLeft,
|
||||
ClipboardCheck,
|
||||
PackageCheck,
|
||||
PackagePlus,
|
||||
Send,
|
||||
Truck,
|
||||
Warehouse,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useInventoryActivity } from '@/hooks/useWarehouses';
|
||||
import type { ActivityType } from '@/types/warehouse';
|
||||
import { formatDate, humanizeEnum } from './options';
|
||||
|
||||
const activityIcon: Record<ActivityType, React.ReactNode> = {
|
||||
INVENTORY_RECEIVED: <PackagePlus size={14} />,
|
||||
INVENTORY_STORED: <Warehouse size={14} />,
|
||||
INVENTORY_MOVED: <ArrowRightLeft size={14} />,
|
||||
INVENTORY_RESERVED: <ClipboardCheck size={14} />,
|
||||
READY_FOR_LOADING: <PackageCheck size={14} />,
|
||||
INVENTORY_LOADED: <Truck size={14} />,
|
||||
INVENTORY_DISPATCHED: <Send size={14} />,
|
||||
};
|
||||
|
||||
export function ActivityTimeline({ inventoryId }: { inventoryId: string }) {
|
||||
const { data, isLoading } = useInventoryActivity(inventoryId);
|
||||
const items = data ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py="lg">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="md" size="sm">
|
||||
No activity recorded yet.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Timeline active={items.length} bulletSize={24} lineWidth={2}>
|
||||
{items.map((log) => (
|
||||
<Timeline.Item key={log.id} bullet={activityIcon[log.activityType]} title={humanizeEnum(log.activityType)}>
|
||||
{log.description && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{log.description}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs" mt={4} c="dimmed">
|
||||
{log.performedBy ?? 'system'} · {formatDate(log.createdAt)}
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Select } from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { bookingsService } from '@/services/bookings.service';
|
||||
|
||||
interface BookingSelectProps {
|
||||
value: string;
|
||||
onChange: (bookingId: string) => void;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
/** Comma-separated statuses to restrict the list (e.g. "PAID" for reservations). */
|
||||
statuses?: string;
|
||||
}
|
||||
|
||||
/** Searchable booking picker — shows the human reference (e.g. BKG-BULK-002), submits the UUID. */
|
||||
export function BookingSelect({ value, onChange, label = 'Booking', required, statuses }: BookingSelectProps) {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['bookings', 'options', statuses ?? 'all'],
|
||||
queryFn: () =>
|
||||
bookingsService.list({ pageSize: 200, ...(statuses ? { statuses } : {}) }).then((r) => r.items),
|
||||
});
|
||||
|
||||
const options = (data ?? []).map((b) => ({
|
||||
value: b.id,
|
||||
label: b.status ? `${b.reference} · ${b.status}` : b.reference,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
required={required}
|
||||
searchable
|
||||
clearable
|
||||
data={options}
|
||||
value={value || null}
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
placeholder={isLoading ? 'Loading bookings…' : 'Search booking reference'}
|
||||
nothingFoundMessage="No bookings found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ interface FormState {
|
||||
locationName: string;
|
||||
capacityWeight: number | '';
|
||||
capacityContainers: number | '';
|
||||
maxVolume: number | '';
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ const emptyForm = (): FormState => ({
|
||||
locationName: '',
|
||||
capacityWeight: '',
|
||||
capacityContainers: '',
|
||||
maxVolume: '',
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
|
||||
@@ -58,6 +60,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
||||
locationName: warehouse.locationName ?? '',
|
||||
capacityWeight: warehouse.capacityWeight ?? '',
|
||||
capacityContainers: warehouse.capacityContainers ?? '',
|
||||
maxVolume: warehouse.maxVolume ?? '',
|
||||
status: warehouse.status,
|
||||
}
|
||||
: emptyForm(),
|
||||
@@ -80,6 +83,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
||||
locationName: form.locationName.trim() || undefined,
|
||||
capacityWeight: form.capacityWeight === '' ? undefined : Number(form.capacityWeight),
|
||||
capacityContainers: form.capacityContainers === '' ? undefined : Number(form.capacityContainers),
|
||||
maxVolume: form.maxVolume === '' ? undefined : Number(form.maxVolume),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -105,14 +109,14 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
||||
placeholder="Modjo Open Warehouse"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, name: v })); }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Code"
|
||||
placeholder="MODJO-OW"
|
||||
required
|
||||
value={form.code}
|
||||
onChange={(e) => setForm((f) => ({ ...f, code: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, code: v })); }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -139,7 +143,7 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
||||
label="Location name"
|
||||
placeholder="Modjo, Oromia"
|
||||
value={form.locationName}
|
||||
onChange={(e) => setForm((f) => ({ ...f, locationName: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, locationName: v })); }}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
@@ -157,6 +161,13 @@ export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWareh
|
||||
value={form.capacityContainers}
|
||||
onChange={(value) => setForm((f) => ({ ...f, capacityContainers: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max volume (m³)"
|
||||
placeholder="Optional"
|
||||
min={0}
|
||||
value={form.maxVolume}
|
||||
onChange={(value) => setForm((f) => ({ ...f, maxVolume: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
|
||||
@@ -19,6 +19,7 @@ interface FormState {
|
||||
type: WarehouseYardType;
|
||||
capacityWeight: number | '';
|
||||
capacityContainers: number | '';
|
||||
maxVolume: number | '';
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ const emptyForm = (): FormState => ({
|
||||
type: 'CONTAINER_YARD',
|
||||
capacityWeight: '',
|
||||
capacityContainers: '',
|
||||
maxVolume: '',
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
|
||||
@@ -48,6 +50,7 @@ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYa
|
||||
type: yard.type,
|
||||
capacityWeight: yard.capacityWeight ?? '',
|
||||
capacityContainers: yard.capacityContainers ?? '',
|
||||
maxVolume: yard.maxVolume ?? '',
|
||||
status: yard.status,
|
||||
}
|
||||
: emptyForm(),
|
||||
@@ -69,6 +72,7 @@ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYa
|
||||
type: form.type,
|
||||
capacityWeight: form.capacityWeight === '' ? undefined : Number(form.capacityWeight),
|
||||
capacityContainers: form.capacityContainers === '' ? undefined : Number(form.capacityContainers),
|
||||
maxVolume: form.maxVolume === '' ? undefined : Number(form.maxVolume),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -94,14 +98,14 @@ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYa
|
||||
placeholder="Container Yard A"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, name: v })); }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Code"
|
||||
placeholder="CY-A"
|
||||
required
|
||||
value={form.code}
|
||||
onChange={(e) => setForm((f) => ({ ...f, code: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, code: v })); }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -139,6 +143,13 @@ export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYa
|
||||
value={form.capacityContainers}
|
||||
onChange={(value) => setForm((f) => ({ ...f, capacityContainers: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max volume (m³)"
|
||||
placeholder="Optional"
|
||||
min={0}
|
||||
value={form.maxVolume}
|
||||
onChange={(value) => setForm((f) => ({ ...f, maxVolume: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
|
||||
@@ -19,6 +19,7 @@ interface FormState {
|
||||
type: WarehouseZoneType;
|
||||
capacityWeight: number | '';
|
||||
capacityContainers: number | '';
|
||||
maxVolume: number | '';
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ const emptyForm = (): FormState => ({
|
||||
type: 'CONTAINER_ZONE',
|
||||
capacityWeight: '',
|
||||
capacityContainers: '',
|
||||
maxVolume: '',
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
|
||||
@@ -48,6 +50,7 @@ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneMod
|
||||
type: zone.type,
|
||||
capacityWeight: zone.capacityWeight ?? '',
|
||||
capacityContainers: zone.capacityContainers ?? '',
|
||||
maxVolume: zone.maxVolume ?? '',
|
||||
status: zone.status,
|
||||
}
|
||||
: emptyForm(),
|
||||
@@ -69,6 +72,7 @@ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneMod
|
||||
type: form.type,
|
||||
capacityWeight: form.capacityWeight === '' ? undefined : Number(form.capacityWeight),
|
||||
capacityContainers: form.capacityContainers === '' ? undefined : Number(form.capacityContainers),
|
||||
maxVolume: form.maxVolume === '' ? undefined : Number(form.maxVolume),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -94,14 +98,14 @@ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneMod
|
||||
placeholder="Zone A-01"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, name: v })); }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Code"
|
||||
placeholder="A-01"
|
||||
required
|
||||
value={form.code}
|
||||
onChange={(e) => setForm((f) => ({ ...f, code: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, code: v })); }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -139,6 +143,13 @@ export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneMod
|
||||
value={form.capacityContainers}
|
||||
onChange={(value) => setForm((f) => ({ ...f, capacityContainers: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max volume (m³)"
|
||||
placeholder="Optional"
|
||||
min={0}
|
||||
value={form.maxVolume}
|
||||
onChange={(value) => setForm((f) => ({ ...f, maxVolume: value === '' ? '' : Number(value) }))}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Modal, Tabs } from '@mantine/core';
|
||||
import { ArrowRightLeft, ListChecks } from 'lucide-react';
|
||||
|
||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { ActivityTimeline } from './ActivityTimeline';
|
||||
import { InventoryMovementHistoryTable } from './InventoryMovementHistoryTable';
|
||||
|
||||
interface InventoryHistoryModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
item: WarehouseInventoryItem | null;
|
||||
}
|
||||
|
||||
export function InventoryHistoryModal({ opened, onClose, item }: InventoryHistoryModalProps) {
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Inventory history" centered size="xl">
|
||||
{item && (
|
||||
<Tabs defaultValue="activity">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="activity" leftSection={<ListChecks size={16} />}>
|
||||
Activity
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="movements" leftSection={<ArrowRightLeft size={16} />}>
|
||||
Movements
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="activity" pt="md">
|
||||
<ActivityTimeline inventoryId={item.id} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="movements" pt="md">
|
||||
<InventoryMovementHistoryTable inventoryId={item.id} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Center, Loader, Table, Text } from '@mantine/core';
|
||||
|
||||
import { useInventoryMovements } from '@/hooks/useWarehouses';
|
||||
import { formatDate } from './options';
|
||||
|
||||
const shortId = (id?: string | null) => (id ? `${id.slice(0, 8)}…` : '—');
|
||||
|
||||
export function InventoryMovementHistoryTable({ inventoryId }: { inventoryId: string }) {
|
||||
const { data, isLoading } = useInventoryMovements(inventoryId);
|
||||
const movements = data ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py="lg">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (movements.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="md" size="sm">
|
||||
No movements recorded for this item.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={640}>
|
||||
<Table verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>From (W / Y / Z)</Table.Th>
|
||||
<Table.Th>To (W / Y / Z)</Table.Th>
|
||||
<Table.Th>Remarks</Table.Th>
|
||||
<Table.Th>By</Table.Th>
|
||||
<Table.Th>When</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{movements.map((m) => (
|
||||
<Table.Tr key={m.id}>
|
||||
<Table.Td>
|
||||
<Text size="xs">
|
||||
{shortId(m.fromWarehouseId)} / {shortId(m.fromYardId)} / {shortId(m.fromZoneId)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">
|
||||
{shortId(m.toWarehouseId)} / {shortId(m.toYardId)} / {shortId(m.toZoneId)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{m.remarks ?? '—'}</Table.Td>
|
||||
<Table.Td>{m.movedBy ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(m.movedAt)}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react';
|
||||
import { Center, Loader } from '@mantine/core';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
useDispatchInventory,
|
||||
useLoadInventory,
|
||||
useMarkReadyForLoading,
|
||||
useStoreInventory,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { InventoryHistoryModal } from './InventoryHistoryModal';
|
||||
import { MoveInventoryModal } from './MoveInventoryModal';
|
||||
import { ReserveInventoryModal } from './ReserveInventoryModal';
|
||||
import { WarehouseInventoryTable } from './WarehouseInventoryTable';
|
||||
import { extractErrorMessage } from './options';
|
||||
|
||||
interface InventoryWorkbenchProps {
|
||||
items: WarehouseInventoryItem[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
/** Inventory table + all lifecycle actions (advance / move / reserve / history). */
|
||||
export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps) {
|
||||
const { toast } = useToast();
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [moveItem, setMoveItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [reserveItem, setReserveItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
const [historyItem, setHistoryItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
|
||||
const storeMutation = useStoreInventory();
|
||||
const readyMutation = useMarkReadyForLoading();
|
||||
const loadMutation = useLoadInventory();
|
||||
const dispatchMutation = useDispatchInventory();
|
||||
|
||||
const runDirect = async (item: WarehouseInventoryItem, fn: () => Promise<unknown>, label: string) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await fn();
|
||||
toast({ title: label });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const advance = (item: WarehouseInventoryItem, action: InventoryAction) => {
|
||||
switch (action) {
|
||||
case 'store':
|
||||
return runDirect(item, () => storeMutation.mutateAsync(item.id), 'Inventory stored');
|
||||
case 'reserve':
|
||||
setReserveItem(item);
|
||||
return;
|
||||
case 'ready-for-loading':
|
||||
return runDirect(item, () => readyMutation.mutateAsync(item.id), 'Ready for loading');
|
||||
case 'load':
|
||||
return runDirect(item, () => loadMutation.mutateAsync(item.id), 'Inventory loaded');
|
||||
case 'dispatch':
|
||||
return runDirect(item, () => dispatchMutation.mutateAsync(item.id), 'Inventory dispatched');
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WarehouseInventoryTable
|
||||
items={items}
|
||||
busyId={busyId}
|
||||
onAdvance={advance}
|
||||
onMove={setMoveItem}
|
||||
onHistory={setHistoryItem}
|
||||
/>
|
||||
|
||||
<MoveInventoryModal opened={Boolean(moveItem)} onClose={() => setMoveItem(null)} item={moveItem} />
|
||||
<ReserveInventoryModal
|
||||
opened={Boolean(reserveItem)}
|
||||
onClose={() => setReserveItem(null)}
|
||||
item={reserveItem}
|
||||
/>
|
||||
<InventoryHistoryModal
|
||||
opened={Boolean(historyItem)}
|
||||
onClose={() => setHistoryItem(null)}
|
||||
item={historyItem}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Group, Modal, Select, Stack, Textarea } from '@mantine/core';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useMoveInventory, useWarehouseYards, useWarehouseZones, useWarehouses } from '@/hooks/useWarehouses';
|
||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { extractErrorMessage } from './options';
|
||||
|
||||
interface MoveInventoryModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
item: WarehouseInventoryItem | null;
|
||||
}
|
||||
|
||||
export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModalProps) {
|
||||
const { toast } = useToast();
|
||||
const moveMutation = useMoveInventory();
|
||||
const [warehouseId, setWarehouseId] = useState('');
|
||||
const [yardId, setYardId] = useState('');
|
||||
const [zoneId, setZoneId] = useState('');
|
||||
const [remarks, setRemarks] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
setWarehouseId('');
|
||||
setYardId('');
|
||||
setZoneId('');
|
||||
setRemarks('');
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const warehousesQuery = useWarehouses({ status: 'ACTIVE' });
|
||||
const yardsQuery = useWarehouseYards(warehouseId || undefined);
|
||||
const zonesQuery = useWarehouseZones(yardId || undefined);
|
||||
|
||||
const warehouseOptions = useMemo(
|
||||
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
||||
[warehousesQuery.data],
|
||||
);
|
||||
const yardOptions = useMemo(
|
||||
() => (yardsQuery.data ?? []).filter((y) => y.status === 'ACTIVE').map((y) => ({ value: y.id, label: `${y.name} (${y.code})` })),
|
||||
[yardsQuery.data],
|
||||
);
|
||||
const zoneOptions = useMemo(
|
||||
() => (zonesQuery.data ?? []).filter((z) => z.status === 'ACTIVE').map((z) => ({ value: z.id, label: `${z.name} (${z.code})` })),
|
||||
[zonesQuery.data],
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!item) return;
|
||||
if (!warehouseId || !yardId || !zoneId) {
|
||||
toast({ variant: 'destructive', title: 'Select destination warehouse, yard and zone' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await moveMutation.mutateAsync({
|
||||
id: item.id,
|
||||
payload: { warehouseId, yardId, zoneId, remarks: remarks.trim() || undefined },
|
||||
});
|
||||
toast({ title: 'Inventory moved' });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Move failed', description: extractErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Move inventory" centered size="lg">
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Destination warehouse"
|
||||
placeholder="Select warehouse"
|
||||
required
|
||||
searchable
|
||||
data={warehouseOptions}
|
||||
value={warehouseId || null}
|
||||
onChange={(v) => {
|
||||
setWarehouseId(v ?? '');
|
||||
setYardId('');
|
||||
setZoneId('');
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Destination yard"
|
||||
placeholder={!warehouseId ? 'Select a warehouse first' : 'Select yard'}
|
||||
required
|
||||
searchable
|
||||
disabled={!warehouseId}
|
||||
data={yardOptions}
|
||||
value={yardId || null}
|
||||
onChange={(v) => {
|
||||
setYardId(v ?? '');
|
||||
setZoneId('');
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Destination zone"
|
||||
placeholder={!yardId ? 'Select a yard first' : 'Select zone'}
|
||||
required
|
||||
searchable
|
||||
disabled={!yardId}
|
||||
data={zoneOptions}
|
||||
value={zoneId || null}
|
||||
onChange={(v) => setZoneId(v ?? '')}
|
||||
/>
|
||||
<Textarea
|
||||
label="Remarks"
|
||||
placeholder="Reason for the move"
|
||||
autosize
|
||||
minRows={2}
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.currentTarget.value)}
|
||||
/>
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={moveMutation.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={moveMutation.isPending}>
|
||||
Move inventory
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useWarehouses,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import type { ReceiveInventoryPayload } from '@/types/warehouse';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { extractErrorMessage } from './options';
|
||||
|
||||
interface ReceiveInventoryModalProps {
|
||||
@@ -125,12 +126,10 @@ export function ReceiveInventoryModal({
|
||||
{bookingId ? (
|
||||
<TextInput label="Booking" value={bookingLabel ?? bookingId} readOnly />
|
||||
) : (
|
||||
<TextInput
|
||||
label="Booking ID"
|
||||
placeholder="Booking UUID"
|
||||
required
|
||||
<BookingSelect
|
||||
label="Booking"
|
||||
value={form.bookingId}
|
||||
onChange={(e) => setForm((f) => ({ ...f, bookingId: e.currentTarget.value }))}
|
||||
onChange={(v) => setForm((f) => ({ ...f, bookingId: v }))}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -198,7 +197,7 @@ export function ReceiveInventoryModal({
|
||||
autosize
|
||||
minRows={2}
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm((f) => ({ ...f, notes: e.currentTarget.value }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setForm((f) => ({ ...f, notes: v })); }}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Group, Modal, Stack, Text } from '@mantine/core';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useReserveInventory } from '@/hooks/useWarehouses';
|
||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { extractErrorMessage } from './options';
|
||||
|
||||
interface ReserveInventoryModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
item: WarehouseInventoryItem | null;
|
||||
}
|
||||
|
||||
export function ReserveInventoryModal({ opened, onClose, item }: ReserveInventoryModalProps) {
|
||||
const { toast } = useToast();
|
||||
const reserveMutation = useReserveInventory();
|
||||
const [bookingId, setBookingId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (opened) setBookingId(item?.bookingId ?? '');
|
||||
}, [opened, item]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!item) return;
|
||||
if (!bookingId.trim()) {
|
||||
toast({ variant: 'destructive', title: 'Booking is required' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await reserveMutation.mutateAsync({ inventoryId: item.id, bookingId: bookingId.trim() });
|
||||
toast({ title: 'Inventory reserved' });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Reserve failed', description: extractErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Reserve inventory" centered size="md">
|
||||
<Stack gap="md">
|
||||
<Alert icon={<Info size={16} />} color="blue" variant="light">
|
||||
<Text size="sm">The booking must be in <b>PAID</b> status and the inventory must be <b>STORED</b>.</Text>
|
||||
</Alert>
|
||||
<BookingSelect label="Booking (PAID)" required statuses="PAID" value={bookingId} onChange={setBookingId} />
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={reserveMutation.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={reserveMutation.isPending}>
|
||||
Reserve
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Badge, Button, Group, Table, Text, Tooltip } from '@mantine/core';
|
||||
import { ClipboardCheck, PackageCheck } from 'lucide-react';
|
||||
import { ActionIcon, Badge, Button, Group, Table, Text, Tooltip } from '@mantine/core';
|
||||
import { ArrowRightLeft, History } from 'lucide-react';
|
||||
|
||||
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { INVENTORY_NEXT_ACTION } from '@/types/warehouse';
|
||||
import { InventoryStatusBadge } from './badges';
|
||||
import { formatDate, formatNumber } from './options';
|
||||
import { formatDate, formatNumber, humanizeEnum } from './options';
|
||||
|
||||
interface WarehouseInventoryTableProps {
|
||||
items: WarehouseInventoryItem[];
|
||||
onInspect: (item: WarehouseInventoryItem) => void;
|
||||
onReadyForLoading: (item: WarehouseInventoryItem) => void;
|
||||
busyId?: string | null;
|
||||
onAdvance: (item: WarehouseInventoryItem, action: InventoryAction) => void;
|
||||
onMove: (item: WarehouseInventoryItem) => void;
|
||||
onHistory: (item: WarehouseInventoryItem) => void;
|
||||
}
|
||||
|
||||
const itemKind = (item: WarehouseInventoryItem) => {
|
||||
@@ -19,11 +21,20 @@ const itemKind = (item: WarehouseInventoryItem) => {
|
||||
return { label: '—', color: 'gray' };
|
||||
};
|
||||
|
||||
const actionColor: Record<InventoryAction, string> = {
|
||||
store: 'blue',
|
||||
reserve: 'grape',
|
||||
'ready-for-loading': 'cyan',
|
||||
load: 'teal',
|
||||
dispatch: 'green',
|
||||
};
|
||||
|
||||
export function WarehouseInventoryTable({
|
||||
items,
|
||||
onInspect,
|
||||
onReadyForLoading,
|
||||
busyId,
|
||||
onAdvance,
|
||||
onMove,
|
||||
onHistory,
|
||||
}: WarehouseInventoryTableProps) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
@@ -34,7 +45,7 @@ export function WarehouseInventoryTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={1100}>
|
||||
<Table.ScrollContainer minWidth={1150}>
|
||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
@@ -47,7 +58,6 @@ export function WarehouseInventoryTable({
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Arrived</Table.Th>
|
||||
<Table.Th>Ready</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
@@ -55,14 +65,21 @@ export function WarehouseInventoryTable({
|
||||
{items.map((item) => {
|
||||
const kind = itemKind(item);
|
||||
const busy = busyId === item.id;
|
||||
const nextAction = INVENTORY_NEXT_ACTION[item.status];
|
||||
return (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>
|
||||
<Tooltip label={item.bookingId} withArrow>
|
||||
<Text size="sm" fw={600}>
|
||||
{item.bookingId.slice(0, 8)}…
|
||||
{item.bookingId ? (
|
||||
<Tooltip label={item.bookingId} withArrow>
|
||||
<Text size="sm" fw={600}>
|
||||
{item.bookingId.slice(0, 8)}…
|
||||
</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
—
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.warehouse?.code ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.yard?.code ?? '—'}</Table.Td>
|
||||
@@ -80,33 +97,31 @@ export function WarehouseInventoryTable({
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(item.arrivedAt)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(item.readyForLoadingAt)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="cyan"
|
||||
leftSection={<ClipboardCheck size={14} />}
|
||||
disabled={item.status !== 'ARRIVED_AT_WAREHOUSE' || busy}
|
||||
loading={busy}
|
||||
onClick={() => onInspect(item)}
|
||||
>
|
||||
Inspect
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<PackageCheck size={14} />}
|
||||
disabled={item.status !== 'UNDER_INSPECTION' || busy}
|
||||
loading={busy}
|
||||
onClick={() => onReadyForLoading(item)}
|
||||
>
|
||||
Ready
|
||||
</Button>
|
||||
{nextAction && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color={actionColor[nextAction]}
|
||||
loading={busy}
|
||||
onClick={() => onAdvance(item, nextAction)}
|
||||
>
|
||||
{humanizeEnum(nextAction.replace(/-/g, '_'))}
|
||||
</Button>
|
||||
)}
|
||||
{item.status !== 'DISPATCHED' && (
|
||||
<Tooltip label="Move" withArrow>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onMove(item)}>
|
||||
<ArrowRightLeft size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="History" withArrow>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onHistory(item)}>
|
||||
<History size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -34,9 +34,12 @@ export function WarehouseStatusBadge({ status }: { status: WarehouseStatus }) {
|
||||
}
|
||||
|
||||
const inventoryStatusColor: Record<InventoryStatus, string> = {
|
||||
ARRIVED_AT_WAREHOUSE: 'yellow',
|
||||
UNDER_INSPECTION: 'cyan',
|
||||
READY_FOR_LOADING: 'green',
|
||||
RECEIVED: 'yellow',
|
||||
STORED: 'blue',
|
||||
RESERVED: 'grape',
|
||||
READY_FOR_LOADING: 'cyan',
|
||||
LOADED: 'teal',
|
||||
DISPATCHED: 'green',
|
||||
};
|
||||
|
||||
export function InventoryStatusBadge({ status }: { status: InventoryStatus }) {
|
||||
|
||||
@@ -11,3 +11,10 @@ export { CreateYardModal } from './CreateYardModal';
|
||||
export { CreateZoneModal } from './CreateZoneModal';
|
||||
export { ReceiveInventoryModal } from './ReceiveInventoryModal';
|
||||
export { WarehouseInfoCard } from './WarehouseInfoCard';
|
||||
export { MoveInventoryModal } from './MoveInventoryModal';
|
||||
export { ReserveInventoryModal } from './ReserveInventoryModal';
|
||||
export { InventoryMovementHistoryTable } from './InventoryMovementHistoryTable';
|
||||
export { ActivityTimeline } from './ActivityTimeline';
|
||||
export { InventoryHistoryModal } from './InventoryHistoryModal';
|
||||
export { InventoryWorkbench } from './InventoryWorkbench';
|
||||
export { BookingSelect } from './BookingSelect';
|
||||
|
||||
@@ -186,6 +186,7 @@ export const URL_CONSTANTS = {
|
||||
|
||||
WAREHOUSES: {
|
||||
BASE: '/warehouses',
|
||||
DASHBOARD: '/warehouses/dashboard',
|
||||
BY_ID: (id: string) => `/warehouses/${id}`,
|
||||
YARDS: (warehouseId: string) => `/warehouses/${warehouseId}/yards`,
|
||||
},
|
||||
@@ -202,9 +203,15 @@ export const URL_CONSTANTS = {
|
||||
WAREHOUSE_INVENTORY: {
|
||||
BASE: '/warehouse-inventory',
|
||||
RECEIVE: '/warehouse-inventory/receive',
|
||||
RESERVE: '/warehouse-inventory/reserve',
|
||||
READY_FOR_LOADING: '/warehouse-inventory/ready-for-loading',
|
||||
INQUIRY: '/warehouse-inventory/inquiry',
|
||||
INSPECT: (id: string) => `/warehouse-inventory/${id}/inspect`,
|
||||
MOVE: (id: string) => `/warehouse-inventory/${id}/move`,
|
||||
MOVEMENTS: (id: string) => `/warehouse-inventory/${id}/movements`,
|
||||
ACTIVITY: (id: string) => `/warehouse-inventory/${id}/activity`,
|
||||
STORE: (id: string) => `/warehouse-inventory/${id}/store`,
|
||||
MARK_READY: (id: string) => `/warehouse-inventory/${id}/ready-for-loading`,
|
||||
LOAD: (id: string) => `/warehouse-inventory/${id}/load`,
|
||||
DISPATCH: (id: string) => `/warehouse-inventory/${id}/dispatch`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@ import { warehouseService } from '@/services/warehouse.service';
|
||||
import type {
|
||||
InventoryFilter,
|
||||
InventoryInquiryFilter,
|
||||
MoveInventoryPayload,
|
||||
ReceiveInventoryPayload,
|
||||
ReserveInventoryPayload,
|
||||
SaveWarehousePayload,
|
||||
SaveYardPayload,
|
||||
SaveZonePayload,
|
||||
@@ -137,19 +139,49 @@ export function useReceiveInventory() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useInspectInventory() {
|
||||
function useInventoryMutation<TArgs>(fn: (args: TArgs) => Promise<unknown>) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.inspectInventory(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }),
|
||||
mutationFn: fn,
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['warehouse-inventory'] });
|
||||
qc.invalidateQueries({ queryKey: warehouseKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarkReadyForLoading() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => warehouseService.markReadyForLoading(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['warehouse-inventory'] }),
|
||||
export const useStoreInventory = () => useInventoryMutation((id: string) => warehouseService.store(id));
|
||||
export const useReserveInventory = () =>
|
||||
useInventoryMutation((payload: ReserveInventoryPayload) => warehouseService.reserve(payload));
|
||||
export const useMarkReadyForLoading = () =>
|
||||
useInventoryMutation((id: string) => warehouseService.markReadyForLoading(id));
|
||||
export const useLoadInventory = () => useInventoryMutation((id: string) => warehouseService.load(id));
|
||||
export const useDispatchInventory = () => useInventoryMutation((id: string) => warehouseService.dispatch(id));
|
||||
export const useMoveInventory = () =>
|
||||
useInventoryMutation((args: { id: string; payload: MoveInventoryPayload }) =>
|
||||
warehouseService.move(args.id, args.payload),
|
||||
);
|
||||
|
||||
export function useInventoryMovements(id?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['warehouse-inventory', id, 'movements'],
|
||||
queryFn: () => warehouseService.movements(id as string).then((r) => r.data),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
|
||||
export function useInventoryActivity(id?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['warehouse-inventory', id, 'activity'],
|
||||
queryFn: () => warehouseService.activity(id as string).then((r) => r.data),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
|
||||
export function useWarehouseDashboard() {
|
||||
return useQuery({
|
||||
queryKey: ['warehouses', 'dashboard'],
|
||||
queryFn: () => warehouseService.dashboard().then((r) => r.data),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,21 +61,21 @@ export default function InventoryInquiryPage() {
|
||||
label="Booking number"
|
||||
placeholder="e.g. BKG-00123"
|
||||
value={draft.bookingNumber ?? ''}
|
||||
onChange={(e) => setDraft((f) => ({ ...f, bookingNumber: e.currentTarget.value || undefined }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
|
||||
w={200}
|
||||
/>
|
||||
<TextInput
|
||||
label="Container number"
|
||||
placeholder="e.g. MSKU1234567"
|
||||
value={draft.containerNumber ?? ''}
|
||||
onChange={(e) => setDraft((f) => ({ ...f, containerNumber: e.currentTarget.value || undefined }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
|
||||
w={200}
|
||||
/>
|
||||
<TextInput
|
||||
label="Goods name"
|
||||
placeholder="e.g. Coffee"
|
||||
value={draft.goodsName ?? ''}
|
||||
onChange={(e) => setDraft((f) => ({ ...f, goodsName: e.currentTarget.value || undefined }))}
|
||||
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, goodsName: v || undefined })); }}
|
||||
w={180}
|
||||
/>
|
||||
<Select
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Card, Center, Container, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon, Title } from '@mantine/core';
|
||||
import {
|
||||
ClipboardCheck,
|
||||
PackageCheck,
|
||||
PackagePlus,
|
||||
Send,
|
||||
Truck,
|
||||
Warehouse as WarehouseIcon,
|
||||
Boxes,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { useWarehouseDashboard } from '@/hooks/useWarehouses';
|
||||
import type { WarehouseDashboard } from '@/types/warehouse';
|
||||
|
||||
interface Metric {
|
||||
key: keyof WarehouseDashboard;
|
||||
label: string;
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
const METRICS: Metric[] = [
|
||||
{ key: 'totalWarehouses', label: 'Total Warehouses', color: 'indigo', icon: <WarehouseIcon size={20} /> },
|
||||
{ key: 'totalInventory', label: 'Total Inventory', color: 'gray', icon: <Boxes size={20} /> },
|
||||
{ key: 'receivedToday', label: 'Received Today', color: 'yellow', icon: <PackagePlus size={20} /> },
|
||||
{ key: 'stored', label: 'Stored', color: 'blue', icon: <Layers size={20} /> },
|
||||
{ key: 'reserved', label: 'Reserved', color: 'grape', icon: <ClipboardCheck size={20} /> },
|
||||
{ key: 'readyForLoading', label: 'Ready For Loading', color: 'cyan', icon: <PackageCheck size={20} /> },
|
||||
{ key: 'loaded', label: 'Loaded', color: 'teal', icon: <Truck size={20} /> },
|
||||
{ key: 'dispatched', label: 'Dispatched', color: 'green', icon: <Send size={20} /> },
|
||||
];
|
||||
|
||||
export default function WarehouseDashboardPage() {
|
||||
const { data, isLoading } = useWarehouseDashboard();
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs items={[{ label: 'Warehouse dashboard' }]} />
|
||||
|
||||
<Stack gap="lg" mt="sm">
|
||||
<div>
|
||||
<Title order={2}>Warehouse Dashboard</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Live overview of warehouse capacity and inventory lifecycle.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="md">
|
||||
{METRICS.map((metric) => (
|
||||
<Card key={metric.key} withBorder radius="md" padding="lg">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{metric.label}
|
||||
</Text>
|
||||
<Text fw={700} size="28px" mt={6}>
|
||||
{data ? data[metric.key] : 0}
|
||||
</Text>
|
||||
</div>
|
||||
<ThemeIcon variant="light" color={metric.color} size="lg" radius="md">
|
||||
{metric.icon}
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -19,26 +19,22 @@ import {
|
||||
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus } from 'lucide-react';
|
||||
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
CreateYardModal,
|
||||
CreateZoneModal,
|
||||
WarehouseInventoryTable,
|
||||
InventoryWorkbench,
|
||||
WarehouseStatusBadge,
|
||||
WarehouseTypeBadge,
|
||||
formatCapacity,
|
||||
humanizeEnum,
|
||||
} from '@/components/warehouses';
|
||||
import {
|
||||
useInspectInventory,
|
||||
useMarkReadyForLoading,
|
||||
useWarehouse,
|
||||
useWarehouseInventory,
|
||||
useWarehouseYards,
|
||||
useWarehouseZones,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||
import type { WarehouseInventoryItem, WarehouseYard, WarehouseZone } from '@/types/warehouse';
|
||||
import type { WarehouseYard, WarehouseZone } from '@/types/warehouse';
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
@@ -56,7 +52,6 @@ function StatCard({ label, value }: { label: string; value: string }) {
|
||||
export default function WarehouseDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: warehouse, isLoading } = useWarehouse(id);
|
||||
const yardsQuery = useWarehouseYards(id);
|
||||
@@ -71,9 +66,6 @@ export default function WarehouseDetailPage() {
|
||||
const zonesQuery = useWarehouseZones(selectedYardId ?? undefined);
|
||||
|
||||
const inventoryQuery = useWarehouseInventory(id ? { warehouseId: id } : undefined);
|
||||
const inspectMutation = useInspectInventory();
|
||||
const readyMutation = useMarkReadyForLoading();
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const yards = yardsQuery.data ?? [];
|
||||
const yardOptions = useMemo(
|
||||
@@ -81,30 +73,6 @@ export default function WarehouseDetailPage() {
|
||||
[yards],
|
||||
);
|
||||
|
||||
const handleInspect = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await inspectMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory under inspection' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReady = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await readyMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory ready for loading' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center mih="60vh">
|
||||
@@ -337,18 +305,7 @@ export default function WarehouseDetailPage() {
|
||||
{/* INVENTORY */}
|
||||
<Tabs.Panel value="inventory" pt="lg">
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
{inventoryQuery.isLoading ? (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
) : (
|
||||
<WarehouseInventoryTable
|
||||
items={inventoryQuery.data ?? []}
|
||||
onInspect={handleInspect}
|
||||
onReadyForLoading={handleReady}
|
||||
busyId={busyId}
|
||||
/>
|
||||
)}
|
||||
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
|
||||
</Card>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Button, Card, Center, Container, Group, Loader, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Button, Card, Container, Group, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { PackagePlus, Search } from 'lucide-react';
|
||||
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
InventoryWorkbench,
|
||||
ReceiveInventoryModal,
|
||||
WarehouseInventoryTable,
|
||||
inventoryStatusOptions,
|
||||
} from '@/components/warehouses';
|
||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||
import {
|
||||
useInspectInventory,
|
||||
useMarkReadyForLoading,
|
||||
useWarehouseInventory,
|
||||
useWarehouseYards,
|
||||
useWarehouseZones,
|
||||
useWarehouses,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import type { InventoryFilter, InventoryStatus, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import type { InventoryFilter, InventoryStatus } from '@/types/warehouse';
|
||||
|
||||
export default function WarehouseInventoryPage() {
|
||||
const { toast } = useToast();
|
||||
const [filter, setFilter] = useState<InventoryFilter>({});
|
||||
const [search, setSearch] = useState('');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300);
|
||||
const queryFilter = useMemo<InventoryFilter>(
|
||||
@@ -39,9 +33,6 @@ export default function WarehouseInventoryPage() {
|
||||
const zonesQuery = useWarehouseZones(filter.yardId);
|
||||
const inventoryQuery = useWarehouseInventory(queryFilter);
|
||||
|
||||
const inspectMutation = useInspectInventory();
|
||||
const readyMutation = useMarkReadyForLoading();
|
||||
|
||||
const warehouseOptions = useMemo(
|
||||
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
|
||||
[warehousesQuery.data],
|
||||
@@ -55,30 +46,6 @@ export default function WarehouseInventoryPage() {
|
||||
[zonesQuery.data],
|
||||
);
|
||||
|
||||
const handleInspect = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await inspectMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory under inspection' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReady = async (item: WarehouseInventoryItem) => {
|
||||
setBusyId(item.id);
|
||||
try {
|
||||
await readyMutation.mutateAsync(item.id);
|
||||
toast({ title: 'Inventory ready for loading' });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs items={[{ label: 'Warehouse inventory' }]} />
|
||||
@@ -88,7 +55,7 @@ export default function WarehouseInventoryPage() {
|
||||
<div>
|
||||
<Title order={2}>Warehouse Inventory</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Track received items and move them through inspection to loading.
|
||||
Track received items through the storage, reservation, loading and dispatch lifecycle.
|
||||
</Text>
|
||||
</div>
|
||||
<Button leftSection={<PackagePlus size={16} />} onClick={() => setModalOpen(true)}>
|
||||
@@ -147,18 +114,7 @@ export default function WarehouseInventoryPage() {
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{inventoryQuery.isLoading ? (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
) : (
|
||||
<WarehouseInventoryTable
|
||||
items={inventoryQuery.data ?? []}
|
||||
onInspect={handleInspect}
|
||||
onReadyForLoading={handleReady}
|
||||
busyId={busyId}
|
||||
/>
|
||||
)}
|
||||
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
@@ -5,11 +5,16 @@ import type {
|
||||
InventoryFilter,
|
||||
InventoryInquiryFilter,
|
||||
InventoryInquiryResult,
|
||||
InventoryMovement,
|
||||
MoveInventoryPayload,
|
||||
ReceiveInventoryPayload,
|
||||
ReserveInventoryPayload,
|
||||
SaveWarehousePayload,
|
||||
SaveYardPayload,
|
||||
SaveZonePayload,
|
||||
Warehouse,
|
||||
WarehouseActivityLog,
|
||||
WarehouseDashboard,
|
||||
WarehouseFilter,
|
||||
WarehouseInventoryItem,
|
||||
WarehouseYard,
|
||||
@@ -27,6 +32,7 @@ export const warehouseService = {
|
||||
apiClient.get<Warehouse[]>(URL_CONSTANTS.WAREHOUSES.BASE, {
|
||||
params: cleanParams(filter ?? {}),
|
||||
}),
|
||||
dashboard: () => apiClient.get<WarehouseDashboard>(URL_CONSTANTS.WAREHOUSES.DASHBOARD),
|
||||
getById: (id: string) => apiClient.get<Warehouse>(URL_CONSTANTS.WAREHOUSES.BY_ID(id)),
|
||||
create: (payload: SaveWarehousePayload) =>
|
||||
apiClient.post<Warehouse>(URL_CONSTANTS.WAREHOUSES.BASE, payload),
|
||||
@@ -58,10 +64,6 @@ export const warehouseService = {
|
||||
}),
|
||||
receiveInventory: (payload: ReceiveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RECEIVE, 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)),
|
||||
listReadyForLoading: (filter?: InventoryFilter) =>
|
||||
apiClient.get<WarehouseInventoryItem[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.READY_FOR_LOADING, {
|
||||
params: cleanParams(filter ?? {}),
|
||||
@@ -70,4 +72,22 @@ export const warehouseService = {
|
||||
apiClient.get<InventoryInquiryResult[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.INQUIRY, {
|
||||
params: cleanParams(filter ?? {}),
|
||||
}),
|
||||
|
||||
// ── Lifecycle (Batch 2) ──────────────────────────────────────────────────
|
||||
store: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.STORE(id)),
|
||||
reserve: (payload: ReserveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RESERVE, payload),
|
||||
markReadyForLoading: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MARK_READY(id)),
|
||||
load: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.LOAD(id)),
|
||||
dispatch: (id: string) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DISPATCH(id)),
|
||||
move: (id: string, payload: MoveInventoryPayload) =>
|
||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVE(id), payload),
|
||||
movements: (id: string) =>
|
||||
apiClient.get<InventoryMovement[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVEMENTS(id)),
|
||||
activity: (id: string) =>
|
||||
apiClient.get<WarehouseActivityLog[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.ACTIVITY(id)),
|
||||
};
|
||||
|
||||
@@ -23,12 +23,27 @@ export const WAREHOUSE_ZONE_TYPES = [
|
||||
export type WarehouseZoneType = (typeof WAREHOUSE_ZONE_TYPES)[number];
|
||||
|
||||
export const INVENTORY_STATUSES = [
|
||||
'ARRIVED_AT_WAREHOUSE',
|
||||
'UNDER_INSPECTION',
|
||||
'RECEIVED',
|
||||
'STORED',
|
||||
'RESERVED',
|
||||
'READY_FOR_LOADING',
|
||||
'LOADED',
|
||||
'DISPATCHED',
|
||||
] as const;
|
||||
export type InventoryStatus = (typeof INVENTORY_STATUSES)[number];
|
||||
|
||||
/** Next allowed lifecycle action keyed by current status. */
|
||||
export const INVENTORY_NEXT_ACTION: Record<InventoryStatus, InventoryAction | null> = {
|
||||
RECEIVED: 'store',
|
||||
STORED: 'reserve',
|
||||
RESERVED: 'ready-for-loading',
|
||||
READY_FOR_LOADING: 'load',
|
||||
LOADED: 'dispatch',
|
||||
DISPATCHED: null,
|
||||
};
|
||||
|
||||
export type InventoryAction = 'store' | 'reserve' | 'ready-for-loading' | 'load' | 'dispatch';
|
||||
|
||||
export interface WarehouseZone {
|
||||
id: string;
|
||||
yardId: string;
|
||||
@@ -37,8 +52,11 @@ export interface WarehouseZone {
|
||||
type: WarehouseZoneType;
|
||||
capacityWeight: number | null;
|
||||
capacityContainers: number | null;
|
||||
maxWeight: number | null;
|
||||
maxVolume: number | null;
|
||||
currentWeight: number;
|
||||
currentContainers: number;
|
||||
currentVolume: number;
|
||||
status: WarehouseStatus;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -51,8 +69,11 @@ export interface WarehouseYard {
|
||||
type: WarehouseYardType;
|
||||
capacityWeight: number | null;
|
||||
capacityContainers: number | null;
|
||||
maxWeight: number | null;
|
||||
maxVolume: number | null;
|
||||
currentWeight: number;
|
||||
currentContainers: number;
|
||||
currentVolume: number;
|
||||
status: WarehouseStatus;
|
||||
isActive: boolean;
|
||||
zones?: WarehouseZone[];
|
||||
@@ -67,8 +88,11 @@ export interface Warehouse {
|
||||
locationName: string | null;
|
||||
capacityWeight: number | null;
|
||||
capacityContainers: number | null;
|
||||
maxWeight: number | null;
|
||||
maxVolume: number | null;
|
||||
currentWeight: number;
|
||||
currentContainers: number;
|
||||
currentVolume: number;
|
||||
status: WarehouseStatus;
|
||||
isActive: boolean;
|
||||
yards?: WarehouseYard[];
|
||||
@@ -81,7 +105,7 @@ export interface WarehouseInventoryItem {
|
||||
warehouseId: string;
|
||||
yardId: string;
|
||||
zoneId: string;
|
||||
bookingId: string;
|
||||
bookingId: string | null;
|
||||
cargoId: string | null;
|
||||
containerId: string | null;
|
||||
goodsId: string | null;
|
||||
@@ -90,14 +114,76 @@ export interface WarehouseInventoryItem {
|
||||
volume: number | null;
|
||||
status: InventoryStatus;
|
||||
arrivedAt: string | null;
|
||||
storedAt: string | null;
|
||||
reservedAt: string | null;
|
||||
inspectedAt: string | null;
|
||||
readyForLoadingAt: string | null;
|
||||
loadedAt: string | null;
|
||||
dispatchedAt: string | null;
|
||||
notes: string | null;
|
||||
warehouse?: Warehouse | null;
|
||||
yard?: WarehouseYard | null;
|
||||
zone?: WarehouseZone | null;
|
||||
}
|
||||
|
||||
export interface InventoryMovement {
|
||||
id: string;
|
||||
inventoryId: string;
|
||||
fromWarehouseId: string;
|
||||
fromYardId: string;
|
||||
fromZoneId: string;
|
||||
toWarehouseId: string;
|
||||
toYardId: string;
|
||||
toZoneId: string;
|
||||
remarks: string | null;
|
||||
movedBy: string | null;
|
||||
movedAt: string;
|
||||
}
|
||||
|
||||
export const ACTIVITY_TYPES = [
|
||||
'INVENTORY_RECEIVED',
|
||||
'INVENTORY_STORED',
|
||||
'INVENTORY_MOVED',
|
||||
'INVENTORY_RESERVED',
|
||||
'READY_FOR_LOADING',
|
||||
'INVENTORY_LOADED',
|
||||
'INVENTORY_DISPATCHED',
|
||||
] as const;
|
||||
export type ActivityType = (typeof ACTIVITY_TYPES)[number];
|
||||
|
||||
export interface WarehouseActivityLog {
|
||||
id: string;
|
||||
inventoryId: string | null;
|
||||
warehouseId: string | null;
|
||||
activityType: ActivityType;
|
||||
description: string | null;
|
||||
performedBy: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WarehouseDashboard {
|
||||
totalWarehouses: number;
|
||||
totalInventory: number;
|
||||
receivedToday: number;
|
||||
stored: number;
|
||||
reserved: number;
|
||||
readyForLoading: number;
|
||||
loaded: number;
|
||||
dispatched: number;
|
||||
}
|
||||
|
||||
export interface MoveInventoryPayload {
|
||||
warehouseId: string;
|
||||
yardId: string;
|
||||
zoneId: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
export interface ReserveInventoryPayload {
|
||||
bookingId: string;
|
||||
inventoryId: string;
|
||||
}
|
||||
|
||||
export interface InventoryInquiryResult {
|
||||
id: string;
|
||||
bookingId: string;
|
||||
@@ -127,6 +213,8 @@ export interface SaveWarehousePayload {
|
||||
locationName?: string;
|
||||
capacityWeight?: number;
|
||||
capacityContainers?: number;
|
||||
maxWeight?: number;
|
||||
maxVolume?: number;
|
||||
status?: WarehouseStatus;
|
||||
}
|
||||
|
||||
@@ -136,6 +224,8 @@ export interface SaveYardPayload {
|
||||
type: WarehouseYardType;
|
||||
capacityWeight?: number;
|
||||
capacityContainers?: number;
|
||||
maxWeight?: number;
|
||||
maxVolume?: number;
|
||||
status?: WarehouseStatus;
|
||||
}
|
||||
|
||||
@@ -145,6 +235,8 @@ export interface SaveZonePayload {
|
||||
type: WarehouseZoneType;
|
||||
capacityWeight?: number;
|
||||
capacityContainers?: number;
|
||||
maxWeight?: number;
|
||||
maxVolume?: number;
|
||||
status?: WarehouseStatus;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user