style: warehouse revamp

This commit is contained in:
Nathnael
2026-06-20 10:30:31 +00:00
parent 138ac5b47d
commit b7b03db945
10 changed files with 353 additions and 414 deletions

View File

@@ -37,7 +37,7 @@ export function KpiStrip({ items, loading = false }: KpiStripProps) {
const cells = items.slice(0, 5);
return (
<Card withBorder shadow="sm" radius="lg" p={0} className="overflow-hidden">
<Card withBorder shadow="sm" p={0} className="overflow-hidden">
<div className="flex flex-col sm:flex-row">
{cells.map((item, index) => {
const Icon = item.icon;

View File

@@ -4,20 +4,17 @@ import {
Badge,
Button,
Card,
Container,
Group,
Loader,
Stack,
Table,
Text,
} from '@mantine/core';
import { ClipboardList, Eye, PackageOpen, Truck } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { PageContainer, PageHeader } from '@/components/page';
import {
InspectionReportModal,
VisualEmptyState,
WarehouseHero,
formatDate,
} from '@/components/warehouses';
import { useArrivalQueue, useAutoUnloadArrived, useUnloadBooking } from '@/hooks/useWarehouses';
@@ -64,31 +61,27 @@ export default function ArrivalQueuePage() {
};
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Arrival queue' }]} />
<PageContainer>
<PageHeader
title="Arrival / Unloading Queue"
subtitle="Arrived bookings ready to unload, store and inspect."
action={
<Button
leftSection={<PackageOpen size={16} />}
loading={autoUnload.isPending}
onClick={handleAutoUnload}
>
Auto Unload Arrived Bookings
</Button>
}
/>
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="container"
secondaryVariant="warehouse"
title="Arrival / Unloading Queue"
subtitle="Arrived bookings ready to unload, store and inspect."
/>
<Card>
<Text fw={600} mb="md">
{items.length} arrived booking(s)
</Text>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" mb="md">
<Text fw={600}>{items.length} arrived booking(s)</Text>
<Button
color="orange"
leftSection={<PackageOpen size={16} />}
loading={autoUnload.isPending}
onClick={handleAutoUnload}
>
Auto Unload Arrived Bookings
</Button>
</Group>
{isLoading ? (
{isLoading ? (
<Group justify="center" py="xl">
<Loader />
</Group>
@@ -187,14 +180,13 @@ export default function ArrivalQueuePage() {
</Table>
</Table.ScrollContainer>
)}
</Card>
</Stack>
</Card>
<InspectionReportModal
opened={Boolean(inspectInventoryId)}
onClose={() => setInspectInventoryId(null)}
inventoryId={inspectInventoryId}
/>
</Container>
</PageContainer>
);
}

View File

@@ -1,7 +1,7 @@
import { Card, Container, Stack } from '@mantine/core';
import { Card } from '@mantine/core';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { InventoryWorkbench, VisualEmptyState, WarehouseHero } from '@/components/warehouses';
import { PageContainer, PageHeader } from '@/components/page';
import { InventoryWorkbench, VisualEmptyState } from '@/components/warehouses';
import { useWarehouseInventory } from '@/hooks/useWarehouses';
/** Items that are LOADED and awaiting dispatch (train departure). */
@@ -10,29 +10,23 @@ export default function DispatchQueuePage() {
const items = data ?? [];
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Dispatch queue' }]} />
<PageContainer>
<PageHeader
title="Dispatch Queue"
subtitle="Loaded inventory awaiting train departure. Mark items dispatched once they leave."
/>
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="train"
secondaryVariant="route"
title="Dispatch Queue"
subtitle="Loaded inventory awaiting train departure. Mark items dispatched once they leave."
/>
<Card withBorder radius="md" padding="lg">
{!isLoading && items.length === 0 ? (
<VisualEmptyState
variant="train"
title="Nothing to dispatch"
description="Loaded items appear here, ready to mark as dispatched."
/>
) : (
<InventoryWorkbench items={items} isLoading={isLoading} />
)}
</Card>
</Stack>
</Container>
<Card>
{!isLoading && items.length === 0 ? (
<VisualEmptyState
variant="train"
title="Nothing to dispatch"
description="Loaded items appear here, ready to mark as dispatched."
/>
) : (
<InventoryWorkbench items={items} isLoading={isLoading} />
)}
</Card>
</PageContainer>
);
}

View File

@@ -1,8 +1,8 @@
import { useMemo, useState } from 'react';
import { Button, Card, Center, Container, Group, Loader, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Button, Card, Center, Group, Loader, Select, Stack, TextInput } from '@mantine/core';
import { Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { PageContainer, PageHeader } from '@/components/page';
import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
import {
useInventoryInquiry,
@@ -43,113 +43,107 @@ export default function InventoryInquiryPage() {
};
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Inventory inquiry' }]} />
<PageContainer>
<PageHeader
title="Inventory Inquiry"
subtitle="Locate any cargo, container or goods inside the warehouse network."
/>
<Stack gap="lg" mt="sm">
<div>
<Title order={2}>Inventory Inquiry</Title>
<Text c="dimmed" size="sm">
Locate any cargo, container or goods inside the warehouse network.
</Text>
</div>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
label="Booking number"
placeholder="e.g. BKG-00123"
value={draft.bookingNumber ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
w={200}
/>
<TextInput
label="Container number"
placeholder="e.g. MSKU1234567"
value={draft.containerNumber ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
w={200}
/>
<TextInput
label="Goods name"
placeholder="e.g. Coffee"
value={draft.goodsName ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, goodsName: v || undefined })); }}
w={180}
/>
<Select
label="Warehouse"
placeholder="Any"
clearable
searchable
data={warehouseOptions}
value={draft.warehouseId ?? null}
onChange={(value) =>
setDraft((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={200}
/>
<Select
label="Yard"
placeholder="Any"
clearable
searchable
disabled={!draft.warehouseId}
data={yardOptions}
value={draft.yardId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={180}
/>
<Select
label="Zone"
placeholder="Any"
clearable
searchable
disabled={!draft.yardId}
data={zoneOptions}
value={draft.zoneId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, zoneId: value ?? undefined }))}
w={180}
/>
<Select
label="Status"
placeholder="Any"
clearable
data={inventoryStatusOptions}
value={draft.status ?? null}
onChange={(value) => setDraft((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={180}
/>
</Group>
<Group>
<Button leftSection={<Search size={16} />} onClick={runSearch}>
Search
</Button>
<Button variant="default" onClick={reset}>
Reset
</Button>
</Group>
</Stack>
</Card>
<Card withBorder radius="md" padding="lg">
{isFetching ? (
<Center py="xl">
<Loader />
</Center>
) : results.length === 0 ? (
<VisualEmptyState
variant="container"
title="No items found"
description="Adjust your filters and search to locate cargo, containers or goods across the warehouse network."
<Card>
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
label="Booking number"
placeholder="e.g. BKG-00123"
value={draft.bookingNumber ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
w={200}
/>
) : (
<WarehouseInquiryTable results={results} />
)}
</Card>
</Stack>
</Container>
<TextInput
label="Container number"
placeholder="e.g. MSKU1234567"
value={draft.containerNumber ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
w={200}
/>
<TextInput
label="Goods name"
placeholder="e.g. Coffee"
value={draft.goodsName ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, goodsName: v || undefined })); }}
w={180}
/>
<Select
label="Warehouse"
placeholder="Any"
clearable
searchable
data={warehouseOptions}
value={draft.warehouseId ?? null}
onChange={(value) =>
setDraft((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={200}
/>
<Select
label="Yard"
placeholder="Any"
clearable
searchable
disabled={!draft.warehouseId}
data={yardOptions}
value={draft.yardId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={180}
/>
<Select
label="Zone"
placeholder="Any"
clearable
searchable
disabled={!draft.yardId}
data={zoneOptions}
value={draft.zoneId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, zoneId: value ?? undefined }))}
w={180}
/>
<Select
label="Status"
placeholder="Any"
clearable
data={inventoryStatusOptions}
value={draft.status ?? null}
onChange={(value) => setDraft((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={180}
/>
</Group>
<Group>
<Button leftSection={<Search size={16} />} onClick={runSearch}>
Search
</Button>
<Button variant="default" onClick={reset}>
Reset
</Button>
</Group>
</Stack>
</Card>
<Card>
{isFetching ? (
<Center py="xl">
<Loader />
</Center>
) : results.length === 0 ? (
<VisualEmptyState
variant="container"
title="No items found"
description="Adjust your filters and search to locate cargo, containers or goods across the warehouse network."
/>
) : (
<WarehouseInquiryTable results={results} />
)}
</Card>
</PageContainer>
);
}

View File

@@ -1,7 +1,7 @@
import { Badge, Card, Container, Group, Loader, Stack, Table, Text } from '@mantine/core';
import { Badge, Card, Group, Loader, Table, Text } from '@mantine/core';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { FreightVisual, VisualEmptyState, WarehouseHero, formatDate, formatNumber } from '@/components/warehouses';
import { PageContainer, PageHeader } from '@/components/page';
import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
import { useWarehouseLoadings } from '@/hooks/useWarehouses';
/** Record of every inventory item loaded onto a wagon. */
@@ -10,77 +10,71 @@ export default function LoadedInventoryPage() {
const loadings = data ?? [];
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Loaded inventory' }]} />
<PageContainer>
<PageHeader
title="Loaded Inventory"
subtitle="Items loaded onto wagons, with their loading records."
/>
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="container"
secondaryVariant="wagon"
title="Loaded Inventory"
subtitle="Items loaded onto wagons, with their loading records."
/>
<Card withBorder radius="md" padding="lg">
{isLoading ? (
<Group justify="center" py="xl">
<Loader />
</Group>
) : 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>
<Card>
{isLoading ? (
<Group justify="center" py="xl">
<Loader />
</Group>
) : 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.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>
)}
</Card>
</Stack>
</Container>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</Card>
</PageContainer>
);
}

View File

@@ -1,13 +1,12 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Badge, Button, Card, Container, Group, Stack, Table, Tabs, Text } from '@mantine/core';
import { Badge, Button, Card, Group, Table, Tabs, Text } from '@mantine/core';
import { CreditCard, Eye, Truck } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { PageContainer, PageHeader } from '@/components/page';
import {
InventoryWorkbench,
VisualEmptyState,
WarehouseHero,
formatNumber,
} from '@/components/warehouses';
import { useAutoLoadReady, useWarehouseInventory } from '@/hooks/useWarehouses';
@@ -53,30 +52,23 @@ export default function LoadingQueuePage() {
const unpaidItems = useMemo(() => readyItems.filter((i) => !isPaid(i)), [readyItems]);
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Loading queue' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="wagon"
secondaryVariant="cargo"
title="Loading Queue"
subtitle="Manage bookings and inventory through the loading workflow."
/>
<Group justify="flex-end">
<PageContainer>
<PageHeader
title="Loading Queue"
subtitle="Manage bookings and inventory through the loading workflow."
action={
<Button
color="green"
leftSection={<Truck size={16} />}
loading={autoLoad.isPending}
onClick={handleAutoLoad}
>
Auto Load Ready Items
</Button>
</Group>
}
/>
<Card withBorder radius="md" padding="lg">
<Tabs defaultValue="ready">
<Card>
<Tabs defaultValue="ready">
<Tabs.List>
<Tabs.Tab
value="ready"
@@ -171,10 +163,9 @@ export default function LoadingQueuePage() {
<InventoryWorkbench items={loadedItems} isLoading={loadedLoading} />
)}
</Tabs.Panel>
</Tabs>
</Card>
</Stack>
</Container>
</Tabs>
</Card>
</PageContainer>
);
}

View File

@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button, Card, Container, Group, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Button, Card, Group, Select, Stack, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { PackagePlus, Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { PageContainer, PageHeader } from '@/components/page';
import {
InventoryWorkbench,
ReceiveInventoryModal,
@@ -52,79 +52,73 @@ export default function WarehouseInventoryPage() {
);
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouse inventory' }]} />
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>Warehouse Inventory</Title>
<Text c="dimmed" size="sm">
Track received items through the storage, reservation, loading and dispatch lifecycle.
</Text>
</div>
<PageContainer>
<PageHeader
title="Warehouse Inventory"
subtitle="Track received items through the storage, reservation, loading and dispatch lifecycle."
action={
<Button leftSection={<PackagePlus size={16} />} onClick={() => setModalOpen(true)}>
Receive Inventory
</Button>
</Group>
}
/>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
placeholder="Search notes"
leftSection={<Search size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={220}
/>
<Select
placeholder="All warehouses"
clearable
searchable
data={warehouseOptions}
value={filter.warehouseId ?? null}
onChange={(value) =>
setFilter((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={220}
/>
<Select
placeholder="All yards"
clearable
searchable
disabled={!filter.warehouseId}
data={yardOptions}
value={filter.yardId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={200}
/>
<Select
placeholder="All zones"
clearable
searchable
disabled={!filter.yardId}
data={zoneOptions}
value={filter.zoneId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, zoneId: value ?? undefined }))}
w={200}
/>
<Select
placeholder="All statuses"
clearable
data={inventoryStatusOptions}
value={filter.status ?? null}
onChange={(value) => setFilter((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={200}
/>
</Group>
<Card>
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
placeholder="Search notes"
leftSection={<Search size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={220}
/>
<Select
placeholder="All warehouses"
clearable
searchable
data={warehouseOptions}
value={filter.warehouseId ?? null}
onChange={(value) =>
setFilter((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={220}
/>
<Select
placeholder="All yards"
clearable
searchable
disabled={!filter.warehouseId}
data={yardOptions}
value={filter.yardId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={200}
/>
<Select
placeholder="All zones"
clearable
searchable
disabled={!filter.yardId}
data={zoneOptions}
value={filter.zoneId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, zoneId: value ?? undefined }))}
w={200}
/>
<Select
placeholder="All statuses"
clearable
data={inventoryStatusOptions}
value={filter.status ?? null}
onChange={(value) => setFilter((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={200}
/>
</Group>
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
</Stack>
</Card>
</Stack>
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
</Stack>
</Card>
<ReceiveInventoryModal opened={modalOpen} onClose={() => setModalOpen(false)} />
</Container>
</PageContainer>
);
}

View File

@@ -4,7 +4,6 @@ import {
Badge,
Button,
Card,
Container,
Divider,
Group,
Loader,
@@ -18,8 +17,7 @@ import {
} from '@mantine/core';
import { Ban, CreditCard, Eye, Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { WarehouseHero } from '@/components/warehouses';
import { PageContainer, PageHeader } from '@/components/page';
import { useToast } from '@/hooks/use-toast';
import {
useCancelInvoice,
@@ -59,18 +57,14 @@ export default function WarehouseInvoicesPage() {
}, [invoices, search]);
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouse fee invoices' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="container"
secondaryVariant="warehouse"
title="Warehouse Fee Invoices"
subtitle="Demurrage & storage invoices generated from warehouse fee rules."
/>
<PageContainer>
<PageHeader
title="Warehouse Fee Invoices"
subtitle="Demurrage & storage invoices generated from warehouse fee rules."
/>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" mb="md" wrap="wrap">
<Card>
<Group justify="space-between" mb="md" wrap="wrap">
<TextInput
placeholder="Search invoice no / booking / customer"
leftSection={<Search size={16} />}
@@ -123,11 +117,10 @@ export default function WarehouseInvoicesPage() {
</Table>
</Table.ScrollContainer>
)}
</Card>
</Stack>
</Card>
<InvoiceDetailModal id={detailId} onClose={() => setDetailId(null)} />
</Container>
</PageContainer>
);
}
@@ -217,7 +210,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
onChange={(v) => setPayAmount(v === '' ? '' : Number(v))}
style={{ flex: 1 }}
/>
<Button color="green" leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
Pay
</Button>
</Group>

View File

@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Card, Center, Container, Group, Loader, Stack, Text, Title } from '@mantine/core';
import { Button, Card, Center, Loader, Stack, Text } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { Plus } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { PageContainer, PageHeader } from '@/components/page';
import {
CreateWarehouseModal,
WarehouseCardView,
@@ -42,44 +42,38 @@ export default function WarehouseListPage() {
const openDetail = (warehouse: Warehouse) => navigate(`/dashboard/warehouses/${warehouse.id}`);
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouses' }]} />
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>Warehouses</Title>
<Text c="dimmed" size="sm">
Manage warehouses, yards and zones.
</Text>
</div>
<PageContainer>
<PageHeader
title="Warehouses"
subtitle="Manage warehouses, yards and zones."
action={
<Button leftSection={<Plus size={16} />} onClick={openCreate}>
Create Warehouse
</Button>
</Group>
}
/>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<WarehouseFilters filter={filter} onChange={setFilter} view={view} onViewChange={setView} />
<Card>
<Stack gap="md">
<WarehouseFilters filter={filter} onChange={setFilter} view={view} onViewChange={setView} />
{isLoading ? (
<Center py="xl">
<Loader />
</Center>
) : isError ? (
<Text c="red" ta="center" py="xl">
Failed to load warehouses.
</Text>
) : view === 'table' ? (
<WarehouseTable warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
) : (
<WarehouseCardView warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
)}
</Stack>
</Card>
</Stack>
{isLoading ? (
<Center py="xl">
<Loader />
</Center>
) : isError ? (
<Text c="red" ta="center" py="xl">
Failed to load warehouses.
</Text>
) : view === 'table' ? (
<WarehouseTable warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
) : (
<WarehouseCardView warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
)}
</Stack>
</Card>
<CreateWarehouseModal opened={modalOpen} onClose={() => setModalOpen(false)} warehouse={editing} />
</Container>
</PageContainer>
);
}

View File

@@ -4,7 +4,6 @@ import {
Badge,
Button,
Card,
Container,
Group,
Loader,
Modal,
@@ -18,8 +17,7 @@ import {
} from '@mantine/core';
import { Plus, Trash2 } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { WarehouseHero } from '@/components/warehouses';
import { PageContainer, PageHeader } from '@/components/page';
import { useToast } from '@/hooks/use-toast';
import {
useAllocationRules,
@@ -45,31 +43,26 @@ const clean = (s: string) => s.trim() || undefined;
export default function WarehouseRulesPage() {
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouse rules' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="warehouse"
secondaryVariant="container"
title="Allocation & Fee Rules"
subtitle="Configure deterministic yard allocation and storage / demurrage free time and rates."
/>
<Card withBorder radius="md" padding="lg">
<Tabs defaultValue="allocation">
<Tabs.List>
<Tabs.Tab value="allocation">Allocation Rules</Tabs.Tab>
<Tabs.Tab value="fees">Storage / Demurrage Fees</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="allocation" pt="md">
<AllocationRules />
</Tabs.Panel>
<Tabs.Panel value="fees" pt="md">
<FeeRules />
</Tabs.Panel>
</Tabs>
</Card>
</Stack>
</Container>
<PageContainer>
<PageHeader
title="Allocation & Fee Rules"
subtitle="Configure deterministic yard allocation and storage / demurrage free time and rates."
/>
<Card>
<Tabs defaultValue="allocation">
<Tabs.List>
<Tabs.Tab value="allocation">Allocation Rules</Tabs.Tab>
<Tabs.Tab value="fees">Storage / Demurrage Fees</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="allocation" pt="md">
<AllocationRules />
</Tabs.Panel>
<Tabs.Panel value="fees" pt="md">
<FeeRules />
</Tabs.Panel>
</Tabs>
</Card>
</PageContainer>
);
}
@@ -116,7 +109,7 @@ function AllocationRules() {
<>
<Group justify="space-between" mb="sm">
<Text c="dimmed" size="sm">{rules.length} rule(s) matched by ascending priority</Text>
<Button color="orange" leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New allocation rule</Button>
<Button leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New allocation rule</Button>
</Group>
{isLoading ? (
<Group justify="center" py="xl"><Loader /></Group>
@@ -172,7 +165,7 @@ function AllocationRules() {
</Group>
<Group justify="flex-end" mt="sm">
<Button variant="default" onClick={() => setOpen(false)}>Cancel</Button>
<Button color="orange" loading={create.isPending} onClick={submit}>Create</Button>
<Button loading={create.isPending} onClick={submit}>Create</Button>
</Group>
</Stack>
</Modal>
@@ -222,7 +215,7 @@ function FeeRules() {
<>
<Group justify="space-between" mb="sm">
<Text c="dimmed" size="sm">{rules.length} rule(s) most specific match applies</Text>
<Button color="teal" leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New fee rule</Button>
<Button leftSection={<Plus size={16} />} onClick={() => setOpen(true)}>New fee rule</Button>
</Group>
{isLoading ? (
<Group justify="center" py="xl"><Loader /></Group>
@@ -276,7 +269,7 @@ function FeeRules() {
</Group>
<Group justify="flex-end" mt="sm">
<Button variant="default" onClick={() => setOpen(false)}>Cancel</Button>
<Button color="teal" loading={create.isPending} onClick={submit}>Create</Button>
<Button loading={create.isPending} onClick={submit}>Create</Button>
</Group>
</Stack>
</Modal>