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); const cells = items.slice(0, 5);
return ( 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"> <div className="flex flex-col sm:flex-row">
{cells.map((item, index) => { {cells.map((item, index) => {
const Icon = item.icon; const Icon = item.icon;

View File

@@ -4,20 +4,17 @@ import {
Badge, Badge,
Button, Button,
Card, Card,
Container,
Group, Group,
Loader, Loader,
Stack,
Table, Table,
Text, Text,
} from '@mantine/core'; } from '@mantine/core';
import { ClipboardList, Eye, PackageOpen, Truck } from 'lucide-react'; import { ClipboardList, Eye, PackageOpen, Truck } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs'; import { PageContainer, PageHeader } from '@/components/page';
import { import {
InspectionReportModal, InspectionReportModal,
VisualEmptyState, VisualEmptyState,
WarehouseHero,
formatDate, formatDate,
} from '@/components/warehouses'; } from '@/components/warehouses';
import { useArrivalQueue, useAutoUnloadArrived, useUnloadBooking } from '@/hooks/useWarehouses'; import { useArrivalQueue, useAutoUnloadArrived, useUnloadBooking } from '@/hooks/useWarehouses';
@@ -64,31 +61,27 @@ export default function ArrivalQueuePage() {
}; };
return ( return (
<Container size="xxl" py="lg"> <PageContainer>
<Breadcrumbs items={[{ label: 'Arrival queue' }]} /> <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"> <Card>
<WarehouseHero <Text fw={600} mb="md">
variant="container" {items.length} arrived booking(s)
secondaryVariant="warehouse" </Text>
title="Arrival / Unloading Queue"
subtitle="Arrived bookings ready to unload, store and inspect."
/>
<Card withBorder radius="md" padding="lg"> {isLoading ? (
<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 ? (
<Group justify="center" py="xl"> <Group justify="center" py="xl">
<Loader /> <Loader />
</Group> </Group>
@@ -187,14 +180,13 @@ export default function ArrivalQueuePage() {
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
)} )}
</Card> </Card>
</Stack>
<InspectionReportModal <InspectionReportModal
opened={Boolean(inspectInventoryId)} opened={Boolean(inspectInventoryId)}
onClose={() => setInspectInventoryId(null)} onClose={() => setInspectInventoryId(null)}
inventoryId={inspectInventoryId} 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 { PageContainer, PageHeader } from '@/components/page';
import { InventoryWorkbench, VisualEmptyState, WarehouseHero } from '@/components/warehouses'; import { InventoryWorkbench, VisualEmptyState } from '@/components/warehouses';
import { useWarehouseInventory } from '@/hooks/useWarehouses'; import { useWarehouseInventory } from '@/hooks/useWarehouses';
/** Items that are LOADED and awaiting dispatch (train departure). */ /** Items that are LOADED and awaiting dispatch (train departure). */
@@ -10,29 +10,23 @@ export default function DispatchQueuePage() {
const items = data ?? []; const items = data ?? [];
return ( return (
<Container size="xxl" py="lg"> <PageContainer>
<Breadcrumbs items={[{ label: 'Dispatch queue' }]} /> <PageHeader
title="Dispatch Queue"
subtitle="Loaded inventory awaiting train departure. Mark items dispatched once they leave."
/>
<Stack gap="lg" mt="sm"> <Card>
<WarehouseHero {!isLoading && items.length === 0 ? (
variant="train" <VisualEmptyState
secondaryVariant="route" variant="train"
title="Dispatch Queue" title="Nothing to dispatch"
subtitle="Loaded inventory awaiting train departure. Mark items dispatched once they leave." description="Loaded items appear here, ready to mark as dispatched."
/> />
) : (
<Card withBorder radius="md" padding="lg"> <InventoryWorkbench items={items} isLoading={isLoading} />
{!isLoading && items.length === 0 ? ( )}
<VisualEmptyState </Card>
variant="train" </PageContainer>
title="Nothing to dispatch"
description="Loaded items appear here, ready to mark as dispatched."
/>
) : (
<InventoryWorkbench items={items} isLoading={isLoading} />
)}
</Card>
</Stack>
</Container>
); );
} }

View File

@@ -1,8 +1,8 @@
import { useMemo, useState } from 'react'; 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 { Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs'; import { PageContainer, PageHeader } from '@/components/page';
import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses'; import { VisualEmptyState, WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
import { import {
useInventoryInquiry, useInventoryInquiry,
@@ -43,113 +43,107 @@ export default function InventoryInquiryPage() {
}; };
return ( return (
<Container size="xxl" py="lg"> <PageContainer>
<Breadcrumbs items={[{ label: 'Inventory inquiry' }]} /> <PageHeader
title="Inventory Inquiry"
subtitle="Locate any cargo, container or goods inside the warehouse network."
/>
<Stack gap="lg" mt="sm"> <Card>
<div> <Stack gap="md">
<Title order={2}>Inventory Inquiry</Title> <Group gap="sm" wrap="wrap">
<Text c="dimmed" size="sm"> <TextInput
Locate any cargo, container or goods inside the warehouse network. label="Booking number"
</Text> placeholder="e.g. BKG-00123"
</div> value={draft.bookingNumber ?? ''}
onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, bookingNumber: v || undefined })); }}
<Card withBorder radius="md" padding="lg"> w={200}
<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."
/> />
) : ( <TextInput
<WarehouseInquiryTable results={results} /> label="Container number"
)} placeholder="e.g. MSKU1234567"
</Card> value={draft.containerNumber ?? ''}
</Stack> onChange={(e) => { const v = e.currentTarget.value; setDraft((f) => ({ ...f, containerNumber: v || undefined })); }}
</Container> 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 { PageContainer, PageHeader } from '@/components/page';
import { FreightVisual, VisualEmptyState, WarehouseHero, formatDate, formatNumber } from '@/components/warehouses'; import { FreightVisual, VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
import { useWarehouseLoadings } from '@/hooks/useWarehouses'; import { useWarehouseLoadings } from '@/hooks/useWarehouses';
/** Record of every inventory item loaded onto a wagon. */ /** Record of every inventory item loaded onto a wagon. */
@@ -10,77 +10,71 @@ export default function LoadedInventoryPage() {
const loadings = data ?? []; const loadings = data ?? [];
return ( return (
<Container size="xxl" py="lg"> <PageContainer>
<Breadcrumbs items={[{ label: 'Loaded inventory' }]} /> <PageHeader
title="Loaded Inventory"
subtitle="Items loaded onto wagons, with their loading records."
/>
<Stack gap="lg" mt="sm"> <Card>
<WarehouseHero {isLoading ? (
variant="container" <Group justify="center" py="xl">
secondaryVariant="wagon" <Loader />
title="Loaded Inventory" </Group>
subtitle="Items loaded onto wagons, with their loading records." ) : loadings.length === 0 ? (
/> <VisualEmptyState
variant="container"
<Card withBorder radius="md" padding="lg"> title="No loaded inventory yet"
{isLoading ? ( description="Once items are loaded onto a wagon, their records show here."
<Group justify="center" py="xl"> />
<Loader /> ) : (
</Group> <Table.ScrollContainer minWidth={760}>
) : loadings.length === 0 ? ( <Table verticalSpacing="sm" highlightOnHover>
<VisualEmptyState <Table.Thead>
variant="container" <Table.Tr>
title="No loaded inventory yet" <Table.Th>Wagon</Table.Th>
description="Once items are loaded onto a wagon, their records show here." <Table.Th>Warehouse</Table.Th>
/> <Table.Th>Zone</Table.Th>
) : ( <Table.Th>Loaded Weight (kg)</Table.Th>
<Table.ScrollContainer minWidth={760}> <Table.Th>Loaded At</Table.Th>
<Table verticalSpacing="sm" highlightOnHover> <Table.Th>Status</Table.Th>
<Table.Thead> </Table.Tr>
<Table.Tr> </Table.Thead>
<Table.Th>Wagon</Table.Th> <Table.Tbody>
<Table.Th>Warehouse</Table.Th> {loadings.map((l) => (
<Table.Th>Zone</Table.Th> <Table.Tr key={l.id}>
<Table.Th>Loaded Weight (kg)</Table.Th> <Table.Td>
<Table.Th>Loaded At</Table.Th> <Group gap={6} wrap="nowrap">
<Table.Th>Status</Table.Th> <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.Tr>
</Table.Thead> ))}
<Table.Tbody> </Table.Tbody>
{loadings.map((l) => ( </Table>
<Table.Tr key={l.id}> </Table.ScrollContainer>
<Table.Td> )}
<Group gap={6} wrap="nowrap"> </Card>
<FreightVisual variant="wagon" size={22} /> </PageContainer>
<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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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