mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: finish revamp of the warehouse page
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Stack, Table, Text } from '@mantine/core';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import type { InventoryInquiryResult } from '@/types/warehouse';
|
||||
import { InventoryStatusBadge } from './badges';
|
||||
@@ -16,70 +17,61 @@ const itemDescriptor = (result: InventoryInquiryResult) => {
|
||||
return '—';
|
||||
};
|
||||
|
||||
export function WarehouseInquiryTable({ results }: WarehouseInquiryTableProps) {
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No matching items. Adjust your search to locate cargo, containers or goods.
|
||||
const columns: ColumnDef<InventoryInquiryResult>[] = [
|
||||
{
|
||||
id: 'booking',
|
||||
header: 'Booking',
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" fw={600}>
|
||||
{row.original.bookingNumber ?? row.original.bookingId.slice(0, 8)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
{ id: 'customer', header: 'Customer', cell: ({ row }) => row.original.customerName ?? '—' },
|
||||
{ id: 'item', header: 'Item', cell: ({ row }) => itemDescriptor(row.original) },
|
||||
{
|
||||
id: 'warehouse',
|
||||
header: 'Warehouse',
|
||||
cell: ({ row }) => (
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{row.original.warehouse?.name ?? '—'}</Text>
|
||||
{row.original.warehouse?.code && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{row.original.warehouse.code}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: 'yard', header: 'Yard', cell: ({ row }) => row.original.yard?.name ?? '—' },
|
||||
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.name ?? '—' },
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => <InventoryStatusBadge status={row.original.status} />,
|
||||
},
|
||||
{ id: 'qty', header: 'Qty', cell: ({ row }) => formatNumber(row.original.quantity) },
|
||||
{ id: 'weight', header: 'Weight', cell: ({ row }) => formatNumber(row.original.weight) },
|
||||
{
|
||||
id: 'arrived',
|
||||
header: 'Arrived',
|
||||
cell: ({ row }) => <Text size="xs">{formatDate(row.original.arrivedAt)}</Text>,
|
||||
},
|
||||
{
|
||||
id: 'ready',
|
||||
header: 'Ready',
|
||||
cell: ({ row }) => <Text size="xs">{formatDate(row.original.readyForLoadingAt)}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
export function WarehouseInquiryTable({ results }: WarehouseInquiryTableProps) {
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={1100}>
|
||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Item</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Arrived</Table.Th>
|
||||
<Table.Th>Ready</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{results.map((result) => (
|
||||
<Table.Tr key={result.id}>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>
|
||||
{result.bookingNumber ?? result.bookingId.slice(0, 8)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{result.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{itemDescriptor(result)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{result.warehouse?.name ?? '—'}</Text>
|
||||
{result.warehouse?.code && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{result.warehouse.code}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>{result.yard?.name ?? '—'}</Table.Td>
|
||||
<Table.Td>{result.zone?.name ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<InventoryStatusBadge status={result.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>{formatNumber(result.quantity)}</Table.Td>
|
||||
<Table.Td>{formatNumber(result.weight)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(result.arrivedAt)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(result.readyForLoadingAt)}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={results}
|
||||
status="success"
|
||||
emptyMessage="No matching items. Adjust your search to locate cargo, containers or goods."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ActionIcon, Badge, Button, Group, Table, Text, Tooltip } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { ActionIcon, Badge, Button, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { ArrowRightLeft, ClipboardList, Coins, History } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import { INVENTORY_NEXT_ACTION } from '@/types/warehouse';
|
||||
@@ -40,115 +42,117 @@ export function WarehouseInventoryTable({
|
||||
onInspect,
|
||||
onFeePreview,
|
||||
}: WarehouseInventoryTableProps) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No inventory items found.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
const columns = useMemo<ColumnDef<WarehouseInventoryItem>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'booking',
|
||||
header: 'Booking',
|
||||
cell: ({ row }) =>
|
||||
row.original.bookingId ? (
|
||||
<Tooltip label={row.original.bookingId} withArrow>
|
||||
<Text size="sm" fw={600}>
|
||||
{row.original.bookingId.slice(0, 8)}…
|
||||
</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
—
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'facility',
|
||||
header: 'Facility',
|
||||
cell: ({ row }) => row.original.warehouse?.facility?.name ?? '—',
|
||||
},
|
||||
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
|
||||
{ id: 'yard', header: 'Yard', cell: ({ row }) => row.original.yard?.code ?? '—' },
|
||||
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
|
||||
{
|
||||
id: 'item',
|
||||
header: 'Item',
|
||||
cell: ({ row }) => {
|
||||
const kind = itemKind(row.original);
|
||||
return (
|
||||
<Badge color={kind.color} variant="light" size="sm" radius="md">
|
||||
{kind.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ id: 'qty', header: 'Qty', cell: ({ row }) => formatNumber(row.original.quantity) },
|
||||
{ id: 'weight', header: 'Weight', cell: ({ row }) => formatNumber(row.original.weight) },
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => <InventoryStatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
id: 'arrived',
|
||||
header: 'Arrived',
|
||||
cell: ({ row }) => <Text size="xs">{formatDate(row.original.arrivedAt)}</Text>,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
const busy = busyId === item.id;
|
||||
const nextAction = INVENTORY_NEXT_ACTION[item.status];
|
||||
return (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||
{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>
|
||||
)}
|
||||
{onInspect && (
|
||||
<Tooltip label="Inspection / Report" withArrow>
|
||||
<ActionIcon variant="subtle" color="orange" onClick={() => onInspect(item)}>
|
||||
<ClipboardList size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onFeePreview && (
|
||||
<Tooltip label="Storage / Demurrage preview" withArrow>
|
||||
<ActionIcon variant="subtle" color="teal" onClick={() => onFeePreview(item)}>
|
||||
<Coins size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="History" withArrow>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onHistory(item)}>
|
||||
<History size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[busyId, onAdvance, onMove, onHistory, onInspect, onFeePreview],
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={1150}>
|
||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking</Table.Th>
|
||||
<Table.Th>Facility</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Item</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Arrived</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{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>
|
||||
{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>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.warehouse?.facility?.name ?? '—'}</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>
|
||||
<Badge color={kind.color} variant="light" size="sm" radius="md">
|
||||
{kind.label}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{formatNumber(item.quantity)}</Table.Td>
|
||||
<Table.Td>{formatNumber(item.weight)}</Table.Td>
|
||||
<Table.Td>
|
||||
<InventoryStatusBadge status={item.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(item.arrivedAt)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
{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>
|
||||
)}
|
||||
{onInspect && (
|
||||
<Tooltip label="Inspection / Report" withArrow>
|
||||
<ActionIcon variant="subtle" color="orange" onClick={() => onInspect(item)}>
|
||||
<ClipboardList size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onFeePreview && (
|
||||
<Tooltip label="Storage / Demurrage preview" withArrow>
|
||||
<ActionIcon variant="subtle" color="teal" onClick={() => onFeePreview(item)}>
|
||||
<Coins 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>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
status="success"
|
||||
emptyMessage="No inventory items found."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ActionIcon, Anchor, Group, Table, Text } from '@mantine/core';
|
||||
import { ActionIcon, Group, Text } from '@mantine/core';
|
||||
import { Eye, Pencil } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { useStations } from '@/hooks/useStations';
|
||||
import type { Warehouse } from '@/types/warehouse';
|
||||
@@ -20,73 +21,91 @@ export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTablePro
|
||||
[stations],
|
||||
);
|
||||
|
||||
if (warehouses.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No warehouses found.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
const columns: ColumnDef<Warehouse>[] = [
|
||||
{
|
||||
id: 'code',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => (
|
||||
<Text
|
||||
fw={600}
|
||||
size="sm"
|
||||
c="edr-green.7"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => onView(row.original)}
|
||||
>
|
||||
{row.original.code}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
|
||||
{
|
||||
id: 'facility',
|
||||
header: 'Facility',
|
||||
cell: ({ row }) => {
|
||||
const name = row.original.stationId
|
||||
? stationNameById.get(row.original.stationId)
|
||||
: undefined;
|
||||
return name ? (
|
||||
<Text size="sm" fw={500}>
|
||||
{name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
—
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => <WarehouseTypeBadge type={row.original.type} />,
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
header: 'Location',
|
||||
cell: ({ row }) => row.original.locationName ?? '—',
|
||||
},
|
||||
{
|
||||
id: 'weight',
|
||||
header: 'Weight (cur / cap)',
|
||||
cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
|
||||
},
|
||||
{
|
||||
id: 'containers',
|
||||
header: 'Containers (cur / cap)',
|
||||
cell: ({ row }) =>
|
||||
formatCapacity(row.original.currentContainers, row.original.capacityContainers),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => <WarehouseStatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onView(row.original)} title="View">
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onEdit(row.original)} title="Edit">
|
||||
<Pencil size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={900}>
|
||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Code</Table.Th>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>Facility</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Location</Table.Th>
|
||||
<Table.Th>Weight (cur / cap)</Table.Th>
|
||||
<Table.Th>Containers (cur / cap)</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{warehouses.map((warehouse) => (
|
||||
<Table.Tr key={warehouse.id}>
|
||||
<Table.Td>
|
||||
<Anchor fw={600} size="sm" onClick={() => onView(warehouse)}>
|
||||
{warehouse.code}
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
<Table.Td>{warehouse.name}</Table.Td>
|
||||
<Table.Td>
|
||||
{warehouse.stationId && stationNameById.get(warehouse.stationId) ? (
|
||||
<Text size="sm" fw={500}>
|
||||
{stationNameById.get(warehouse.stationId)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
—
|
||||
</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<WarehouseTypeBadge type={warehouse.type} />
|
||||
</Table.Td>
|
||||
<Table.Td>{warehouse.locationName ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatCapacity(warehouse.currentWeight, warehouse.capacityWeight)}</Table.Td>
|
||||
<Table.Td>{formatCapacity(warehouse.currentContainers, warehouse.capacityContainers)}</Table.Td>
|
||||
<Table.Td>
|
||||
<WarehouseStatusBadge status={warehouse.status} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onView(warehouse)} title="View">
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => onEdit(warehouse)} title="Edit">
|
||||
<Pencil size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={warehouses}
|
||||
status="success"
|
||||
onRowClick={(warehouse) => onView(warehouse)}
|
||||
emptyMessage="No warehouses found."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Table,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { ClipboardList, Eye, PackageOpen, Truck } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import {
|
||||
@@ -60,6 +59,96 @@ export default function ArrivalQueuePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<ArrivalQueueItem>[] = [
|
||||
{
|
||||
id: 'booking',
|
||||
header: 'Booking',
|
||||
cell: ({ row }) => (
|
||||
<Text fw={600} size="sm">
|
||||
{row.original.bookingReference}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{ id: 'customer', header: 'Customer', cell: ({ row }) => row.original.customer ?? '—' },
|
||||
{
|
||||
id: 'cargo',
|
||||
header: 'Cargo / Container',
|
||||
cell: ({ row }) => row.original.container ?? row.original.cargo ?? '—',
|
||||
},
|
||||
{
|
||||
id: 'arrival',
|
||||
header: 'Arrival',
|
||||
cell: ({ row }) => <Text size="xs">{formatDate(row.original.arrivalDate)}</Text>,
|
||||
},
|
||||
{ id: 'facility', header: 'Facility', cell: ({ row }) => row.original.facility ?? '—' },
|
||||
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse ?? '—' },
|
||||
{ id: 'yard', header: 'Yard', cell: ({ row }) => row.original.yard ?? '—' },
|
||||
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone ?? '—' },
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) =>
|
||||
row.original.unloaded ? (
|
||||
<Badge variant="light" color="green" size="sm">
|
||||
{row.original.currentStatus ?? 'RECEIVED'}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="light" color="orange" size="sm">
|
||||
Not unloaded
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'inspection',
|
||||
header: 'Inspection',
|
||||
cell: ({ row }) => inspectionBadge(row.original.inspectionStatus),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||
{!item.unloaded && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={unloadOne.isPending}
|
||||
onClick={() => handleUnloadOne(item)}
|
||||
>
|
||||
Unload
|
||||
</Button>
|
||||
)}
|
||||
{item.inventoryId && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<ClipboardList size={14} />}
|
||||
onClick={() => setInspectInventoryId(item.inventoryId)}
|
||||
>
|
||||
Inspect
|
||||
</Button>
|
||||
)}
|
||||
{item.inventoryId && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
leftSection={<Eye size={14} />}
|
||||
onClick={() => navigate('/dashboard/warehouse-inventory')}
|
||||
>
|
||||
Inventory
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
@@ -81,105 +170,20 @@ export default function ArrivalQueuePage() {
|
||||
{items.length} arrived booking(s)
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : items.length === 0 ? (
|
||||
<VisualEmptyState
|
||||
variant="container"
|
||||
title="No arrived bookings"
|
||||
description="Bookings in transit that arrive appear here for unloading and inspection."
|
||||
/>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1100}>
|
||||
<Table verticalSpacing="sm" highlightOnHover striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Cargo / Container</Table.Th>
|
||||
<Table.Th>Arrival</Table.Th>
|
||||
<Table.Th>Facility</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Inspection</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((item) => (
|
||||
<Table.Tr key={item.bookingId}>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">{item.bookingReference}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{item.customer ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.container ?? item.cargo ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(item.arrivalDate)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{item.facility ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.warehouse ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.yard ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.zone ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
{item.unloaded ? (
|
||||
<Badge variant="light" color="green" size="sm">
|
||||
{item.currentStatus ?? 'RECEIVED'}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="light" color="orange" size="sm">
|
||||
Not unloaded
|
||||
</Badge>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>{inspectionBadge(item.inspectionStatus)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
{!item.unloaded && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={unloadOne.isPending}
|
||||
onClick={() => handleUnloadOne(item)}
|
||||
>
|
||||
Unload
|
||||
</Button>
|
||||
)}
|
||||
{item.inventoryId && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<ClipboardList size={14} />}
|
||||
onClick={() => setInspectInventoryId(item.inventoryId)}
|
||||
>
|
||||
Inspect
|
||||
</Button>
|
||||
)}
|
||||
{item.inventoryId && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
leftSection={<Eye size={14} />}
|
||||
onClick={() => navigate('/dashboard/warehouse-inventory')}
|
||||
>
|
||||
Inventory
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
{!isLoading && items.length === 0 ? (
|
||||
<VisualEmptyState
|
||||
variant="container"
|
||||
title="No arrived bookings"
|
||||
description="Bookings in transit that arrive appear here for unloading and inspection."
|
||||
/>
|
||||
) : (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
status={isLoading ? 'loading' : 'success'}
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<InspectionReportModal
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
import { Badge, Card, Group, Loader, Table, Text } from '@mantine/core';
|
||||
import { Badge, Card, Group, Text } from '@mantine/core';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
|
||||
import { useWarehouseLoadings } from '@/hooks/useWarehouses';
|
||||
|
||||
type Loading = NonNullable<ReturnType<typeof useWarehouseLoadings>['data']>[number];
|
||||
|
||||
const columns: ColumnDef<Loading>[] = [
|
||||
{
|
||||
id: 'wagon',
|
||||
header: 'Wagon',
|
||||
cell: ({ row }) => (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<FreightVisual variant="wagon" size={22} />
|
||||
<Text fw={600} size="sm">
|
||||
{row.original.wagonNumber ?? row.original.wagonId.slice(0, 8)}
|
||||
</Text>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'warehouse',
|
||||
header: 'Warehouse',
|
||||
cell: ({ row }) =>
|
||||
row.original.inventory?.warehouse
|
||||
? `${row.original.inventory.warehouse.name} (${row.original.inventory.warehouse.code})`
|
||||
: '—',
|
||||
},
|
||||
{
|
||||
id: 'zone',
|
||||
header: 'Zone',
|
||||
cell: ({ row }) => (row.original.inventory?.zone ? row.original.inventory.zone.name : '—'),
|
||||
},
|
||||
{
|
||||
id: 'weight',
|
||||
header: 'Loaded Weight (kg)',
|
||||
cell: ({ row }) => formatNumber(row.original.loadedWeight),
|
||||
},
|
||||
{
|
||||
id: 'loadedAt',
|
||||
header: 'Loaded At',
|
||||
cell: ({ row }) => formatDate(row.original.loadedAt),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
color={row.original.inventory?.status === 'DISPATCHED' ? 'green' : 'teal'}
|
||||
size="sm"
|
||||
>
|
||||
{row.original.inventory?.status ?? 'LOADED'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/** Record of every inventory item loaded onto a wagon. */
|
||||
export default function LoadedInventoryPage() {
|
||||
const { data, isLoading } = useWarehouseLoadings();
|
||||
@@ -17,62 +71,19 @@ export default function LoadedInventoryPage() {
|
||||
/>
|
||||
|
||||
<Card>
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : loadings.length === 0 ? (
|
||||
{!isLoading && loadings.length === 0 ? (
|
||||
<VisualEmptyState
|
||||
variant="container"
|
||||
title="No loaded inventory yet"
|
||||
description="Once items are loaded onto a wagon, their records show here."
|
||||
/>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={760}>
|
||||
<Table verticalSpacing="sm" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Wagon</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Loaded Weight (kg)</Table.Th>
|
||||
<Table.Th>Loaded At</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{loadings.map((l) => (
|
||||
<Table.Tr key={l.id}>
|
||||
<Table.Td>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<FreightVisual variant="wagon" size={22} />
|
||||
<Text fw={600} size="sm">
|
||||
{l.wagonNumber ?? l.wagonId.slice(0, 8)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{l.inventory?.warehouse
|
||||
? `${l.inventory.warehouse.name} (${l.inventory.warehouse.code})`
|
||||
: '—'}
|
||||
</Table.Td>
|
||||
<Table.Td>{l.inventory?.zone ? l.inventory.zone.name : '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(l.loadedWeight)}</Table.Td>
|
||||
<Table.Td>{formatDate(l.loadedAt)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={l.inventory?.status === 'DISPATCHED' ? 'green' : 'teal'}
|
||||
size="sm"
|
||||
>
|
||||
{l.inventory?.status ?? 'LOADED'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={loadings}
|
||||
status={isLoading ? 'loading' : 'success'}
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</PageContainer>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Badge, Button, Card, Group, Table, Tabs, Text } from '@mantine/core';
|
||||
import { Badge, Button, Card, Group, Tabs, Text } from '@mantine/core';
|
||||
import { CreditCard, Eye, Truck } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import {
|
||||
@@ -176,63 +177,67 @@ interface PendingPaymentTableProps {
|
||||
|
||||
/** Read-only view of unpaid ready-for-loading items. No Mark-as-Loaded action. */
|
||||
function PendingPaymentTable({ items, onNavigate }: PendingPaymentTableProps) {
|
||||
const columns: ColumnDef<WarehouseInventoryItem>[] = [
|
||||
{
|
||||
id: 'booking',
|
||||
header: 'Booking',
|
||||
cell: ({ row }) => (
|
||||
<Text fw={600} size="sm">
|
||||
{row.original.booking?.reference ?? row.original.bookingId?.slice(0, 8) ?? '—'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{ id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' },
|
||||
{ id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' },
|
||||
{ id: 'weight', header: 'Weight (kg)', cell: ({ row }) => formatNumber(row.original.weight) },
|
||||
{
|
||||
id: 'payment',
|
||||
header: 'Payment',
|
||||
cell: ({ row }) => (
|
||||
<Badge color="orange" variant="light" size="sm">
|
||||
{row.original.booking?.status ?? row.original.booking?.paymentStatus ?? 'UNPAID'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="gray"
|
||||
leftSection={<Eye size={14} />}
|
||||
disabled={!item.bookingId}
|
||||
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
||||
>
|
||||
Booking
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<CreditCard size={14} />}
|
||||
disabled={!item.bookingId}
|
||||
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
||||
>
|
||||
Payment
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={820}>
|
||||
<Table verticalSpacing="sm" highlightOnHover striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Weight (kg)</Table.Th>
|
||||
<Table.Th>Payment</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{item.booking?.reference ?? item.bookingId?.slice(0, 8) ?? '—'}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{item.warehouse?.code ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.zone?.code ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(item.weight)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="orange" variant="light" size="sm">
|
||||
{item.booking?.status ?? item.booking?.paymentStatus ?? 'UNPAID'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="gray"
|
||||
leftSection={<Eye size={14} />}
|
||||
disabled={!item.bookingId}
|
||||
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
||||
>
|
||||
Booking
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
leftSection={<CreditCard size={14} />}
|
||||
disabled={!item.bookingId}
|
||||
onClick={() => item.bookingId && onNavigate(`/dashboard/booking-requests/${item.bookingId}`)}
|
||||
>
|
||||
Payment
|
||||
</Button>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
status="success"
|
||||
emptyMessage="No unpaid items."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { Ban, CreditCard, Eye, Search } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -56,6 +57,56 @@ export default function WarehouseInvoicesPage() {
|
||||
return invoices.filter((i) => [i.invoiceNumber, i.bookingId, i.customerId].join(' ').toLowerCase().includes(q));
|
||||
}, [invoices, search]);
|
||||
|
||||
const invoiceColumns: ColumnDef<WarehouseFeeInvoice>[] = [
|
||||
{
|
||||
id: 'invoiceNumber',
|
||||
header: 'Invoice No',
|
||||
cell: ({ row }) => (
|
||||
<Text fw={600} size="sm">
|
||||
{row.original.invoiceNumber}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{ id: 'type', header: 'Type', cell: ({ row }) => row.original.invoiceType.replace(/_/g, ' ') },
|
||||
{ id: 'total', header: 'Total', cell: ({ row }) => fmt(row.original.totalAmount, row.original.currency) },
|
||||
{ id: 'paid', header: 'Paid', cell: ({ row }) => fmt(row.original.paidAmount, row.original.currency) },
|
||||
{
|
||||
id: 'balance',
|
||||
header: 'Balance',
|
||||
cell: ({ row }) => fmt(row.original.balanceAmount, row.original.currency),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="light" color={STATUS_COLOR[row.original.status]}>
|
||||
{row.original.status.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'issued',
|
||||
header: 'Issued',
|
||||
cell: ({ row }) => <Text size="xs">{fmtDate(row.original.issuedAt)}</Text>,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => (
|
||||
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => setDetailId(row.original.id)}
|
||||
title="View"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
@@ -82,41 +133,13 @@ export default function WarehouseInvoicesPage() {
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : filtered.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">No invoices found.</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1000}>
|
||||
<Table striped highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Invoice No</Table.Th><Table.Th>Type</Table.Th><Table.Th>Total</Table.Th>
|
||||
<Table.Th>Paid</Table.Th><Table.Th>Balance</Table.Th><Table.Th>Status</Table.Th>
|
||||
<Table.Th>Issued</Table.Th><Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{filtered.map((inv) => (
|
||||
<Table.Tr key={inv.id}>
|
||||
<Table.Td><Text fw={600} size="sm">{inv.invoiceNumber}</Text></Table.Td>
|
||||
<Table.Td>{inv.invoiceType.replace(/_/g, ' ')}</Table.Td>
|
||||
<Table.Td>{fmt(inv.totalAmount, inv.currency)}</Table.Td>
|
||||
<Table.Td>{fmt(inv.paidAmount, inv.currency)}</Table.Td>
|
||||
<Table.Td>{fmt(inv.balanceAmount, inv.currency)}</Table.Td>
|
||||
<Table.Td><Badge variant="light" color={STATUS_COLOR[inv.status]}>{inv.status.replace(/_/g, ' ')}</Badge></Table.Td>
|
||||
<Table.Td><Text size="xs">{fmtDate(inv.issuedAt)}</Text></Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => setDetailId(inv.id)} title="View">
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
<DataTable
|
||||
columns={invoiceColumns}
|
||||
data={filtered}
|
||||
status={isLoading ? 'loading' : 'success'}
|
||||
emptyMessage="No invoices found."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<InvoiceDetailModal id={detailId} onClose={() => setDetailId(null)} />
|
||||
|
||||
@@ -5,17 +5,16 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Tabs,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -105,45 +104,52 @@ function AllocationRules() {
|
||||
setForm({ name: '', priority: 100, freightType: '', tradeDirection: '', cargoTypeCode: '', containerStatus: '', targetYardCode: '', storageType: '' });
|
||||
};
|
||||
|
||||
const columns: ColumnDef<(typeof rules)[number]>[] = [
|
||||
{ id: 'priority', header: 'Priority', cell: ({ row }) => row.original.priority },
|
||||
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
|
||||
{ id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? '—' },
|
||||
{ id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? '—' },
|
||||
{ id: 'cargo', header: 'Cargo code', cell: ({ row }) => row.original.cargoTypeCode ?? '—' },
|
||||
{
|
||||
id: 'targetYard',
|
||||
header: 'Target yard',
|
||||
cell: ({ row }) => <Badge variant="light">{row.original.targetYardCode}</Badge>,
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
header: 'Active',
|
||||
cell: ({ row }) => (
|
||||
<Badge color={row.original.isActive ? 'green' : 'gray'} variant="light">
|
||||
{row.original.isActive ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => (
|
||||
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => remove.mutate(row.original.id)} title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text c="dimmed" size="sm">{rules.length} rule(s) — matched by ascending priority</Text>
|
||||
<Button leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New allocation rule</Button>
|
||||
</Group>
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={900}>
|
||||
<Table striped highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Priority</Table.Th><Table.Th>Name</Table.Th><Table.Th>Freight</Table.Th>
|
||||
<Table.Th>Trade</Table.Th><Table.Th>Cargo code</Table.Th><Table.Th>Target yard</Table.Th>
|
||||
<Table.Th>Active</Table.Th><Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rules.map((r) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Table.Td>{r.priority}</Table.Td>
|
||||
<Table.Td>{r.name}</Table.Td>
|
||||
<Table.Td>{r.freightType ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.tradeDirection ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.cargoTypeCode ?? '—'}</Table.Td>
|
||||
<Table.Td><Badge variant="light">{r.targetYardCode}</Badge></Table.Td>
|
||||
<Table.Td><Badge color={r.isActive ? 'green' : 'gray'} variant="light">{r.isActive ? 'Yes' : 'No'}</Badge></Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => remove.mutate(r.id)} title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rules}
|
||||
status={isLoading ? 'loading' : 'success'}
|
||||
emptyMessage="No allocation rules yet."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title="New allocation rule" centered size="lg">
|
||||
<Stack gap="sm">
|
||||
@@ -211,45 +217,60 @@ function FeeRules() {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<(typeof rules)[number]>[] = [
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => (
|
||||
<Badge color={row.original.ruleType === 'DEMURRAGE_FEE' ? 'orange' : 'teal'} variant="light">
|
||||
{row.original.ruleType === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
|
||||
{ id: 'freight', header: 'Freight', cell: ({ row }) => row.original.freightType ?? '—' },
|
||||
{ id: 'trade', header: 'Trade', cell: ({ row }) => row.original.tradeDirection ?? '—' },
|
||||
{ id: 'freeDays', header: 'Free days', cell: ({ row }) => row.original.freeDays },
|
||||
{
|
||||
id: 'rate',
|
||||
header: 'Rate / day',
|
||||
cell: ({ row }) => `${Number(row.original.ratePerDay).toLocaleString()} ${row.original.currency}`,
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
header: 'Active',
|
||||
cell: ({ row }) => (
|
||||
<Badge color={row.original.isActive ? 'green' : 'gray'} variant="light">
|
||||
{row.original.isActive ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => (
|
||||
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => remove.mutate(row.original.id)} title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text c="dimmed" size="sm">{rules.length} rule(s) — most specific match applies</Text>
|
||||
<Button leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New fee rule</Button>
|
||||
</Group>
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={900}>
|
||||
<Table striped highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Type</Table.Th><Table.Th>Name</Table.Th><Table.Th>Freight</Table.Th>
|
||||
<Table.Th>Trade</Table.Th><Table.Th>Free days</Table.Th><Table.Th>Rate / day</Table.Th>
|
||||
<Table.Th>Active</Table.Th><Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rules.map((r) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Table.Td><Badge color={r.ruleType === 'DEMURRAGE_FEE' ? 'orange' : 'teal'} variant="light">{r.ruleType === 'DEMURRAGE_FEE' ? 'Demurrage' : 'Storage'}</Badge></Table.Td>
|
||||
<Table.Td>{r.name}</Table.Td>
|
||||
<Table.Td>{r.freightType ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.tradeDirection ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.freeDays}</Table.Td>
|
||||
<Table.Td>{Number(r.ratePerDay).toLocaleString()} {r.currency}</Table.Td>
|
||||
<Table.Td><Badge color={r.isActive ? 'green' : 'gray'} variant="light">{r.isActive ? 'Yes' : 'No'}</Badge></Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => remove.mutate(r.id)} title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rules}
|
||||
status={isLoading ? 'loading' : 'success'}
|
||||
emptyMessage="No fee rules yet."
|
||||
containerClassName="border-0 shadow-none"
|
||||
/>
|
||||
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title="New fee rule" centered size="lg">
|
||||
<Stack gap="sm">
|
||||
|
||||
Reference in New Issue
Block a user