style: finish revamp of the warehouse page

This commit is contained in:
Nathnael
2026-06-20 10:45:22 +00:00
parent b7b03db945
commit 3dfb8d99ba
8 changed files with 631 additions and 552 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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"
/>
);
}

View File

@@ -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)} />

View File

@@ -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">