mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
fix vehicle
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { DateInput } from "@mantine/dates";
|
|
||||||
|
|
||||||
import { FLEET_SELECT_NONE, type FleetFormFieldDef } from "@/pages/fleet/config/resources";
|
import { FLEET_SELECT_NONE, type FleetFormFieldDef } from "@/pages/fleet/config/resources";
|
||||||
import type { FleetRecord } from "@/services/fleet/fleet.service";
|
import type { FleetRecord } from "@/services/fleet/fleet.service";
|
||||||
@@ -165,21 +164,20 @@ const FleetFormDialog = ({
|
|||||||
|
|
||||||
if (field.type === "date") {
|
if (field.type === "date") {
|
||||||
return (
|
return (
|
||||||
<DateInput
|
<TextInput
|
||||||
key={field.name}
|
key={field.name}
|
||||||
|
type="date"
|
||||||
label={field.label}
|
label={field.label}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
value={value ? new Date(value as string) : null}
|
value={typeof value === "string" ? value.slice(0, 10) : ""}
|
||||||
onChange={(date) =>
|
onChange={(e) =>
|
||||||
setValues((current) => ({
|
setValues((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[field.name]: date ? (date instanceof Date ? date.toISOString().split('T')[0] : date) : "",
|
[field.name]: e.currentTarget?.value ?? "",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
clearable
|
|
||||||
valueFormat="DD/MM/YYYY"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Alert, Button, Group, Modal, Stack, Text, Textarea, TextInput } from '@mantine/core';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { useDeliverInventory } from '@/hooks/useWarehouses';
|
||||||
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
|
import { extractErrorMessage } from './options';
|
||||||
|
|
||||||
|
interface DeliverInventoryModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
item: WarehouseInventoryItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeliverInventoryModal({ opened, onClose, item }: DeliverInventoryModalProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const deliverMutation = useDeliverInventory();
|
||||||
|
const [receiverName, setReceiverName] = useState('');
|
||||||
|
const [remarks, setRemarks] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) {
|
||||||
|
setReceiverName('');
|
||||||
|
setRemarks('');
|
||||||
|
}
|
||||||
|
}, [opened, item]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!item) return;
|
||||||
|
if (!receiverName.trim()) {
|
||||||
|
toast({ variant: 'destructive', title: 'Receiver name is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deliverMutation.mutateAsync({
|
||||||
|
id: item.id,
|
||||||
|
payload: { receiverName: receiverName.trim(), remarks: remarks.trim() || undefined },
|
||||||
|
});
|
||||||
|
toast({ title: 'Delivered — proof of delivery captured' });
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
toast({ variant: 'destructive', title: 'Delivery failed', description: extractErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal opened={opened} onClose={onClose} title="Deliver to customer (proof of delivery)" centered size="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Alert icon={<Info size={16} />} color="green" variant="light">
|
||||||
|
<Text size="sm">
|
||||||
|
A release order must already be issued. Capturing the receiver marks the goods <b>DELIVERED</b>.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
<TextInput
|
||||||
|
label="Receiver name"
|
||||||
|
required
|
||||||
|
placeholder="Who received the goods"
|
||||||
|
value={receiverName}
|
||||||
|
onChange={(e) => setReceiverName(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label="Remarks"
|
||||||
|
placeholder="Optional delivery notes"
|
||||||
|
minRows={2}
|
||||||
|
value={remarks}
|
||||||
|
onChange={(e) => setRemarks(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end" mt="sm">
|
||||||
|
<Button variant="default" onClick={onClose} disabled={deliverMutation.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="green" onClick={handleSubmit} loading={deliverMutation.isPending}>
|
||||||
|
Confirm delivery
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,14 +5,17 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
import {
|
import {
|
||||||
useDispatchInventory,
|
useDispatchInventory,
|
||||||
useMarkReadyForLoading,
|
useMarkReadyForLoading,
|
||||||
|
useMarkReadyForPickup,
|
||||||
useStoreInventory,
|
useStoreInventory,
|
||||||
} from '@/hooks/useWarehouses';
|
} from '@/hooks/useWarehouses';
|
||||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
|
import { DeliverInventoryModal } from './DeliverInventoryModal';
|
||||||
import { FeePreviewModal } from './FeePreviewModal';
|
import { FeePreviewModal } from './FeePreviewModal';
|
||||||
import { InspectionReportModal } from './InspectionReportModal';
|
import { InspectionReportModal } from './InspectionReportModal';
|
||||||
import { InventoryHistoryModal } from './InventoryHistoryModal';
|
import { InventoryHistoryModal } from './InventoryHistoryModal';
|
||||||
import { LoadInventoryModal } from './LoadInventoryModal';
|
import { LoadInventoryModal } from './LoadInventoryModal';
|
||||||
import { MoveInventoryModal } from './MoveInventoryModal';
|
import { MoveInventoryModal } from './MoveInventoryModal';
|
||||||
|
import { ReleaseOrderModal } from './ReleaseOrderModal';
|
||||||
import { ReserveInventoryModal } from './ReserveInventoryModal';
|
import { ReserveInventoryModal } from './ReserveInventoryModal';
|
||||||
import { WarehouseInventoryTable } from './WarehouseInventoryTable';
|
import { WarehouseInventoryTable } from './WarehouseInventoryTable';
|
||||||
import { extractErrorMessage } from './options';
|
import { extractErrorMessage } from './options';
|
||||||
@@ -32,9 +35,12 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
|||||||
const [historyItem, setHistoryItem] = useState<WarehouseInventoryItem | null>(null);
|
const [historyItem, setHistoryItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
const [inspectItem, setInspectItem] = useState<WarehouseInventoryItem | null>(null);
|
const [inspectItem, setInspectItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
const [feeItem, setFeeItem] = useState<WarehouseInventoryItem | null>(null);
|
const [feeItem, setFeeItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
|
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
|
const [deliverItem, setDeliverItem] = useState<WarehouseInventoryItem | null>(null);
|
||||||
|
|
||||||
const storeMutation = useStoreInventory();
|
const storeMutation = useStoreInventory();
|
||||||
const readyMutation = useMarkReadyForLoading();
|
const readyMutation = useMarkReadyForLoading();
|
||||||
|
const pickupMutation = useMarkReadyForPickup();
|
||||||
const dispatchMutation = useDispatchInventory();
|
const dispatchMutation = useDispatchInventory();
|
||||||
|
|
||||||
const runDirect = async (item: WarehouseInventoryItem, fn: () => Promise<unknown>, label: string) => {
|
const runDirect = async (item: WarehouseInventoryItem, fn: () => Promise<unknown>, label: string) => {
|
||||||
@@ -63,6 +69,14 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
|||||||
return;
|
return;
|
||||||
case 'dispatch':
|
case 'dispatch':
|
||||||
return runDirect(item, () => dispatchMutation.mutateAsync(item.id), 'Inventory dispatched');
|
return runDirect(item, () => dispatchMutation.mutateAsync(item.id), 'Inventory dispatched');
|
||||||
|
case 'ready-for-pickup':
|
||||||
|
return runDirect(item, () => pickupMutation.mutateAsync(item.id), 'Ready for pickup');
|
||||||
|
case 'release':
|
||||||
|
setReleaseItem(item);
|
||||||
|
return;
|
||||||
|
case 'deliver':
|
||||||
|
setDeliverItem(item);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,6 +124,8 @@ export function InventoryWorkbench({ items, isLoading }: InventoryWorkbenchProps
|
|||||||
onClose={() => setFeeItem(null)}
|
onClose={() => setFeeItem(null)}
|
||||||
inventoryId={feeItem?.id ?? null}
|
inventoryId={feeItem?.id ?? null}
|
||||||
/>
|
/>
|
||||||
|
<ReleaseOrderModal opened={Boolean(releaseItem)} onClose={() => setReleaseItem(null)} item={releaseItem} />
|
||||||
|
<DeliverInventoryModal opened={Boolean(deliverItem)} onClose={() => setDeliverItem(null)} item={deliverItem} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Alert, Button, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { useReleaseInventory } from '@/hooks/useWarehouses';
|
||||||
|
import type { WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
|
import { extractErrorMessage } from './options';
|
||||||
|
|
||||||
|
interface ReleaseOrderModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
item: WarehouseInventoryItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const releaseMutation = useReleaseInventory();
|
||||||
|
const [reference, setReference] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) setReference(item?.releaseOrderReference ?? '');
|
||||||
|
}, [opened, item]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!item) return;
|
||||||
|
try {
|
||||||
|
await releaseMutation.mutateAsync({ id: item.id, payload: { reference: reference.trim() || undefined } });
|
||||||
|
toast({ title: 'Release order issued' });
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
toast({ variant: 'destructive', title: 'Release failed', description: extractErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal opened={opened} onClose={onClose} title="Issue release order (DO)" centered size="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Alert icon={<Info size={16} />} color="orange" variant="light">
|
||||||
|
<Text size="sm">
|
||||||
|
Records the delivery order / release order sent to the customer. Once issued, the goods can be
|
||||||
|
picked up and delivered.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
<TextInput
|
||||||
|
label="Release order reference"
|
||||||
|
placeholder="e.g. DO-2026-001"
|
||||||
|
value={reference}
|
||||||
|
onChange={(e) => setReference(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end" mt="sm">
|
||||||
|
<Button variant="default" onClick={onClose} disabled={releaseMutation.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="orange" onClick={handleSubmit} loading={releaseMutation.isPending}>
|
||||||
|
Issue release order
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { ActionIcon, Badge, Button, Group, Table, Text, Tooltip } from '@mantine
|
|||||||
import { ArrowRightLeft, ClipboardList, Coins, History } from 'lucide-react';
|
import { ArrowRightLeft, ClipboardList, Coins, History } from 'lucide-react';
|
||||||
|
|
||||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||||
import { INVENTORY_NEXT_ACTION } from '@/types/warehouse';
|
import { getNextInventoryAction } from '@/types/warehouse';
|
||||||
import { InventoryStatusBadge } from './badges';
|
import { InventoryStatusBadge } from './badges';
|
||||||
import { formatDate, formatNumber, humanizeEnum } from './options';
|
import { formatDate, formatNumber, humanizeEnum } from './options';
|
||||||
|
|
||||||
@@ -29,6 +29,9 @@ const actionColor: Record<InventoryAction, string> = {
|
|||||||
'ready-for-loading': 'cyan',
|
'ready-for-loading': 'cyan',
|
||||||
load: 'teal',
|
load: 'teal',
|
||||||
dispatch: 'green',
|
dispatch: 'green',
|
||||||
|
'ready-for-pickup': 'orange',
|
||||||
|
release: 'yellow',
|
||||||
|
deliver: 'green',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WarehouseInventoryTable({
|
export function WarehouseInventoryTable({
|
||||||
@@ -70,7 +73,7 @@ export function WarehouseInventoryTable({
|
|||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const kind = itemKind(item);
|
const kind = itemKind(item);
|
||||||
const busy = busyId === item.id;
|
const busy = busyId === item.id;
|
||||||
const nextAction = INVENTORY_NEXT_ACTION[item.status];
|
const nextAction = getNextInventoryAction(item);
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={item.id}>
|
<Table.Tr key={item.id}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ const inventoryStatusColor: Record<InventoryStatus, string> = {
|
|||||||
READY_FOR_LOADING: 'cyan',
|
READY_FOR_LOADING: 'cyan',
|
||||||
LOADED: 'teal',
|
LOADED: 'teal',
|
||||||
DISPATCHED: 'green',
|
DISPATCHED: 'green',
|
||||||
|
READY_FOR_PICKUP: 'orange',
|
||||||
|
DELIVERED: 'green',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InventoryStatusBadge({ status }: { status: InventoryStatus }) {
|
export function InventoryStatusBadge({ status }: { status: InventoryStatus }) {
|
||||||
|
|||||||
@@ -302,6 +302,10 @@ export const URL_CONSTANTS = {
|
|||||||
MARK_READY: (id: string) => `/warehouse-inventory/${id}/ready-for-loading`,
|
MARK_READY: (id: string) => `/warehouse-inventory/${id}/ready-for-loading`,
|
||||||
LOAD: (id: string) => `/warehouse-inventory/${id}/load`,
|
LOAD: (id: string) => `/warehouse-inventory/${id}/load`,
|
||||||
DISPATCH: (id: string) => `/warehouse-inventory/${id}/dispatch`,
|
DISPATCH: (id: string) => `/warehouse-inventory/${id}/dispatch`,
|
||||||
|
// Import branch
|
||||||
|
MARK_READY_PICKUP: (id: string) => `/warehouse-inventory/${id}/ready-for-pickup`,
|
||||||
|
RELEASE: (id: string) => `/warehouse-inventory/${id}/release`,
|
||||||
|
DELIVER: (id: string) => `/warehouse-inventory/${id}/deliver`,
|
||||||
},
|
},
|
||||||
|
|
||||||
WAREHOUSE_LOADINGS: {
|
WAREHOUSE_LOADINGS: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
// API host is env-driven (set VITE_API_URL per environment, e.g. the remote
|
||||||
|
// https://edrfreightapi.triaplc.com for prod). Falls back to the local API for dev.
|
||||||
// export const API_BASE_URL = 'http://localhost:3001';
|
export const API_BASE_URL =
|
||||||
|
(import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:3001';
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
LoadInventoryPayload,
|
LoadInventoryPayload,
|
||||||
MoveInventoryPayload,
|
MoveInventoryPayload,
|
||||||
ReceiveInventoryPayload,
|
ReceiveInventoryPayload,
|
||||||
|
ReleaseOrderPayload,
|
||||||
|
DeliverInventoryPayload,
|
||||||
ReserveInventoryPayload,
|
ReserveInventoryPayload,
|
||||||
SaveWarehousePayload,
|
SaveWarehousePayload,
|
||||||
SaveYardPayload,
|
SaveYardPayload,
|
||||||
@@ -172,6 +174,18 @@ export const useMoveInventory = () =>
|
|||||||
warehouseService.move(args.id, args.payload),
|
warehouseService.move(args.id, args.payload),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Import branch (READY_FOR_PICKUP → DELIVERED) ───────────────────────────
|
||||||
|
export const useMarkReadyForPickup = () =>
|
||||||
|
useInventoryMutation((id: string) => warehouseService.markReadyForPickup(id));
|
||||||
|
export const useReleaseInventory = () =>
|
||||||
|
useInventoryMutation((args: { id: string; payload: ReleaseOrderPayload }) =>
|
||||||
|
warehouseService.release(args.id, args.payload),
|
||||||
|
);
|
||||||
|
export const useDeliverInventory = () =>
|
||||||
|
useInventoryMutation((args: { id: string; payload: DeliverInventoryPayload }) =>
|
||||||
|
warehouseService.deliver(args.id, args.payload),
|
||||||
|
);
|
||||||
|
|
||||||
// ── Loading (Batch 3) ────────────────────────────────────────────────────────
|
// ── Loading (Batch 3) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function useLoadableWagons(enabled = true) {
|
export function useLoadableWagons(enabled = true) {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export interface FormFieldDef {
|
|||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
options?: { label: string; value: string }[];
|
options?: { label: string; value: string }[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
description?: string;
|
||||||
|
disabled?: boolean;
|
||||||
/** Hide this field when another field currently equals one of these values. */
|
/** Hide this field when another field currently equals one of these values. */
|
||||||
hideWhen?: { field: string; equals: string[] };
|
hideWhen?: { field: string; equals: string[] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Card, Center, Container, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core';
|
import { Card, Center, Container, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
|
ClipboardList,
|
||||||
|
ShieldCheck,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
PackagePlus,
|
PackagePlus,
|
||||||
|
PackageSearch,
|
||||||
|
CircleCheck,
|
||||||
Send,
|
Send,
|
||||||
Truck,
|
Truck,
|
||||||
Warehouse as WarehouseIcon,
|
Warehouse as WarehouseIcon,
|
||||||
@@ -33,11 +37,15 @@ const METRICS: Metric[] = [
|
|||||||
{ key: 'totalWarehouses', label: 'Total Warehouses', icon: <WarehouseIcon size={22} />, to: '/dashboard/warehouses', theme: ORANGE },
|
{ key: 'totalWarehouses', label: 'Total Warehouses', icon: <WarehouseIcon size={22} />, to: '/dashboard/warehouses', theme: ORANGE },
|
||||||
{ key: 'totalInventory', label: 'Total Inventory', icon: <Boxes size={22} />, to: '/dashboard/warehouse-inventory', theme: GREEN },
|
{ key: 'totalInventory', label: 'Total Inventory', icon: <Boxes size={22} />, to: '/dashboard/warehouse-inventory', theme: GREEN },
|
||||||
{ key: 'receivedToday', label: 'Received Today', icon: <PackagePlus size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE },
|
{ key: 'receivedToday', label: 'Received Today', icon: <PackagePlus size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE },
|
||||||
|
{ key: 'awaitingInspection', label: 'Awaiting Inspection', icon: <ClipboardList size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: GREEN },
|
||||||
|
{ key: 'inspected', label: 'Inspected', icon: <ShieldCheck size={22} />, to: '/dashboard/warehouse-inventory', theme: ORANGE },
|
||||||
{ key: 'stored', label: 'Stored', icon: <Layers size={22} />, to: '/dashboard/warehouse-inventory?status=STORED', theme: GREEN },
|
{ key: 'stored', label: 'Stored', icon: <Layers size={22} />, to: '/dashboard/warehouse-inventory?status=STORED', theme: GREEN },
|
||||||
{ key: 'reserved', label: 'Reserved', icon: <ClipboardCheck size={22} />, to: '/dashboard/warehouse-inventory?status=RESERVED', theme: ORANGE },
|
{ key: 'reserved', label: 'Reserved', icon: <ClipboardCheck size={22} />, to: '/dashboard/warehouse-inventory?status=RESERVED', theme: ORANGE },
|
||||||
{ key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={22} />, to: '/dashboard/loading-queue', theme: GREEN },
|
{ key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={22} />, to: '/dashboard/loading-queue', theme: GREEN },
|
||||||
{ key: 'loaded', label: 'Loaded', icon: <Truck size={22} />, to: '/dashboard/loaded-inventory', theme: ORANGE },
|
{ key: 'loaded', label: 'Loaded', icon: <Truck size={22} />, to: '/dashboard/loaded-inventory', theme: ORANGE },
|
||||||
{ key: 'dispatched', label: 'Dispatched', icon: <Send size={22} />, to: '/dashboard/dispatch-queue', theme: GREEN },
|
{ key: 'dispatched', label: 'Dispatched', icon: <Send size={22} />, to: '/dashboard/dispatch-queue', theme: GREEN },
|
||||||
|
{ key: 'readyForPickup', label: 'Ready For Pickup', icon: <PackageSearch size={22} />, to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: ORANGE },
|
||||||
|
{ key: 'delivered', label: 'Delivered', icon: <CircleCheck size={22} />, to: '/dashboard/warehouse-inventory?status=DELIVERED', theme: GREEN },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function WarehouseDashboardPage() {
|
export default function WarehouseDashboardPage() {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import type {
|
|||||||
LoadInventoryPayload,
|
LoadInventoryPayload,
|
||||||
MoveInventoryPayload,
|
MoveInventoryPayload,
|
||||||
ReceiveInventoryPayload,
|
ReceiveInventoryPayload,
|
||||||
|
ReleaseOrderPayload,
|
||||||
|
DeliverInventoryPayload,
|
||||||
ReserveInventoryPayload,
|
ReserveInventoryPayload,
|
||||||
SaveWarehousePayload,
|
SaveWarehousePayload,
|
||||||
SaveYardPayload,
|
SaveYardPayload,
|
||||||
@@ -104,6 +106,14 @@ export const warehouseService = {
|
|||||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.LOAD(id), payload),
|
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.LOAD(id), payload),
|
||||||
dispatch: (id: string) =>
|
dispatch: (id: string) =>
|
||||||
apiClient.patch<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DISPATCH(id)),
|
apiClient.patch<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DISPATCH(id)),
|
||||||
|
|
||||||
|
// ── Import branch (READY_FOR_PICKUP → DELIVERED) ─────────────────────────
|
||||||
|
markReadyForPickup: (id: string) =>
|
||||||
|
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MARK_READY_PICKUP(id)),
|
||||||
|
release: (id: string, payload: ReleaseOrderPayload) =>
|
||||||
|
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.RELEASE(id), payload),
|
||||||
|
deliver: (id: string, payload: DeliverInventoryPayload) =>
|
||||||
|
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DELIVER(id), payload),
|
||||||
move: (id: string, payload: MoveInventoryPayload) =>
|
move: (id: string, payload: MoveInventoryPayload) =>
|
||||||
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVE(id), payload),
|
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVE(id), payload),
|
||||||
movements: (id: string) =>
|
movements: (id: string) =>
|
||||||
|
|||||||
@@ -29,10 +29,28 @@ export const INVENTORY_STATUSES = [
|
|||||||
'READY_FOR_LOADING',
|
'READY_FOR_LOADING',
|
||||||
'LOADED',
|
'LOADED',
|
||||||
'DISPATCHED',
|
'DISPATCHED',
|
||||||
|
// Import branch
|
||||||
|
'READY_FOR_PICKUP',
|
||||||
|
'DELIVERED',
|
||||||
] as const;
|
] as const;
|
||||||
export type InventoryStatus = (typeof INVENTORY_STATUSES)[number];
|
export type InventoryStatus = (typeof INVENTORY_STATUSES)[number];
|
||||||
|
|
||||||
/** Next allowed lifecycle action keyed by current status. */
|
export type InventoryAction =
|
||||||
|
| 'store'
|
||||||
|
| 'reserve'
|
||||||
|
| 'ready-for-loading'
|
||||||
|
| 'load'
|
||||||
|
| 'dispatch'
|
||||||
|
// Import branch
|
||||||
|
| 'ready-for-pickup'
|
||||||
|
| 'release'
|
||||||
|
| 'deliver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default next lifecycle action keyed by current status. The RECEIVED and
|
||||||
|
* READY_FOR_PICKUP rows are direction/release dependent — use
|
||||||
|
* {@link getNextInventoryAction} which resolves those at runtime.
|
||||||
|
*/
|
||||||
export const INVENTORY_NEXT_ACTION: Record<InventoryStatus, InventoryAction | null> = {
|
export const INVENTORY_NEXT_ACTION: Record<InventoryStatus, InventoryAction | null> = {
|
||||||
RECEIVED: 'store',
|
RECEIVED: 'store',
|
||||||
STORED: 'reserve',
|
STORED: 'reserve',
|
||||||
@@ -40,9 +58,34 @@ export const INVENTORY_NEXT_ACTION: Record<InventoryStatus, InventoryAction | nu
|
|||||||
READY_FOR_LOADING: 'load',
|
READY_FOR_LOADING: 'load',
|
||||||
LOADED: 'dispatch',
|
LOADED: 'dispatch',
|
||||||
DISPATCHED: null,
|
DISPATCHED: null,
|
||||||
|
READY_FOR_PICKUP: 'release',
|
||||||
|
DELIVERED: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InventoryAction = 'store' | 'reserve' | 'ready-for-loading' | 'load' | 'dispatch';
|
/**
|
||||||
|
* Resolve the next action for an inventory item, accounting for trade
|
||||||
|
* direction, the inspection gate, and whether a release order was issued.
|
||||||
|
* Returns null when no advance button should be shown (e.g. awaiting inspection).
|
||||||
|
*/
|
||||||
|
export function getNextInventoryAction(item: WarehouseInventoryItem): InventoryAction | null {
|
||||||
|
const inspected = item.inspectionStatus === 'PASSED';
|
||||||
|
const isImport = item.booking?.tradeDirection === 'IMPORT';
|
||||||
|
|
||||||
|
switch (item.status) {
|
||||||
|
case 'RECEIVED':
|
||||||
|
// Import goods skip storage; they need inspection before pickup.
|
||||||
|
if (isImport) return inspected ? 'ready-for-pickup' : null;
|
||||||
|
return 'store';
|
||||||
|
case 'RESERVED':
|
||||||
|
// Export loading is gated on a passed inspection.
|
||||||
|
return inspected ? 'ready-for-loading' : null;
|
||||||
|
case 'READY_FOR_PICKUP':
|
||||||
|
// Issue the DO / release order first, then hand over the goods.
|
||||||
|
return item.releaseDate ? 'deliver' : 'release';
|
||||||
|
default:
|
||||||
|
return INVENTORY_NEXT_ACTION[item.status];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface WarehouseZone {
|
export interface WarehouseZone {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -136,6 +179,7 @@ export interface WarehouseInventoryItem {
|
|||||||
weight: number;
|
weight: number;
|
||||||
volume: number | null;
|
volume: number | null;
|
||||||
status: InventoryStatus;
|
status: InventoryStatus;
|
||||||
|
inspectionStatus: string | null;
|
||||||
arrivedAt: string | null;
|
arrivedAt: string | null;
|
||||||
storedAt: string | null;
|
storedAt: string | null;
|
||||||
reservedAt: string | null;
|
reservedAt: string | null;
|
||||||
@@ -143,6 +187,11 @@ export interface WarehouseInventoryItem {
|
|||||||
readyForLoadingAt: string | null;
|
readyForLoadingAt: string | null;
|
||||||
loadedAt: string | null;
|
loadedAt: string | null;
|
||||||
dispatchedAt: string | null;
|
dispatchedAt: string | null;
|
||||||
|
// Import branch
|
||||||
|
readyForPickupAt: string | null;
|
||||||
|
releaseDate: string | null;
|
||||||
|
releaseOrderReference: string | null;
|
||||||
|
deliveredAt: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
warehouse?: Warehouse | null;
|
warehouse?: Warehouse | null;
|
||||||
yard?: WarehouseYard | null;
|
yard?: WarehouseYard | null;
|
||||||
@@ -156,6 +205,7 @@ export interface InventoryBookingRef {
|
|||||||
reference?: string | null;
|
reference?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
paymentStatus?: string | null;
|
paymentStatus?: string | null;
|
||||||
|
tradeDirection?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InventoryMovement {
|
export interface InventoryMovement {
|
||||||
@@ -197,11 +247,15 @@ export interface WarehouseDashboard {
|
|||||||
totalWarehouses: number;
|
totalWarehouses: number;
|
||||||
totalInventory: number;
|
totalInventory: number;
|
||||||
receivedToday: number;
|
receivedToday: number;
|
||||||
|
awaitingInspection: number;
|
||||||
|
inspected: number;
|
||||||
stored: number;
|
stored: number;
|
||||||
reserved: number;
|
reserved: number;
|
||||||
readyForLoading: number;
|
readyForLoading: number;
|
||||||
loaded: number;
|
loaded: number;
|
||||||
dispatched: number;
|
dispatched: number;
|
||||||
|
readyForPickup: number;
|
||||||
|
delivered: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loading (Batch 3) ────────────────────────────────────────────────────────
|
// ── Loading (Batch 3) ────────────────────────────────────────────────────────
|
||||||
@@ -265,6 +319,19 @@ export interface ReserveInventoryPayload {
|
|||||||
inventoryId: string;
|
inventoryId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Import branch: DO / release order sent to the customer. */
|
||||||
|
export interface ReleaseOrderPayload {
|
||||||
|
reference?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import branch: proof of delivery captured on customer pickup. */
|
||||||
|
export interface DeliverInventoryPayload {
|
||||||
|
receiverName: string;
|
||||||
|
deliveredAt?: string;
|
||||||
|
remarks?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InventoryInquiryResult {
|
export interface InventoryInquiryResult {
|
||||||
id: string;
|
id: string;
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user