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,29 +61,25 @@ export default function ArrivalQueuePage() {
};
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Arrival queue' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="container"
secondaryVariant="warehouse"
<PageContainer>
<PageHeader
title="Arrival / Unloading Queue"
subtitle="Arrived bookings ready to unload, store and inspect."
/>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" mb="md">
<Text fw={600}>{items.length} arrived booking(s)</Text>
action={
<Button
color="orange"
leftSection={<PackageOpen size={16} />}
loading={autoUnload.isPending}
onClick={handleAutoUnload}
>
Auto Unload Arrived Bookings
</Button>
</Group>
}
/>
<Card>
<Text fw={600} mb="md">
{items.length} arrived booking(s)
</Text>
{isLoading ? (
<Group justify="center" py="xl">
@@ -188,13 +181,12 @@ export default function ArrivalQueuePage() {
</Table.ScrollContainer>
)}
</Card>
</Stack>
<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,18 +10,13 @@ export default function DispatchQueuePage() {
const items = data ?? [];
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Dispatch queue' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="train"
secondaryVariant="route"
<PageContainer>
<PageHeader
title="Dispatch Queue"
subtitle="Loaded inventory awaiting train departure. Mark items dispatched once they leave."
/>
<Card withBorder radius="md" padding="lg">
<Card>
{!isLoading && items.length === 0 ? (
<VisualEmptyState
variant="train"
@@ -32,7 +27,6 @@ export default function DispatchQueuePage() {
<InventoryWorkbench items={items} isLoading={isLoading} />
)}
</Card>
</Stack>
</Container>
</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,18 +43,13 @@ 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">
<Card>
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
@@ -134,7 +129,7 @@ export default function InventoryInquiryPage() {
</Stack>
</Card>
<Card withBorder radius="md" padding="lg">
<Card>
{isFetching ? (
<Center py="xl">
<Loader />
@@ -149,7 +144,6 @@ export default function InventoryInquiryPage() {
<WarehouseInquiryTable results={results} />
)}
</Card>
</Stack>
</Container>
</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,18 +10,13 @@ export default function LoadedInventoryPage() {
const loadings = data ?? [];
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Loaded inventory' }]} />
<Stack gap="lg" mt="sm">
<WarehouseHero
variant="container"
secondaryVariant="wagon"
<PageContainer>
<PageHeader
title="Loaded Inventory"
subtitle="Items loaded onto wagons, with their loading records."
/>
<Card withBorder radius="md" padding="lg">
<Card>
{isLoading ? (
<Group justify="center" py="xl">
<Loader />
@@ -80,7 +75,6 @@ export default function LoadedInventoryPage() {
</Table.ScrollContainer>
)}
</Card>
</Stack>
</Container>
</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,29 +52,22 @@ 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"
<PageContainer>
<PageHeader
title="Loading Queue"
subtitle="Manage bookings and inventory through the loading workflow."
/>
<Group justify="flex-end">
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">
<Card>
<Tabs defaultValue="ready">
<Tabs.List>
<Tabs.Tab
@@ -173,8 +165,7 @@ export default function LoadingQueuePage() {
</Tabs.Panel>
</Tabs>
</Card>
</Stack>
</Container>
</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,23 +52,18 @@ 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">
<Card>
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
@@ -122,9 +117,8 @@ export default function WarehouseInventoryPage() {
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
</Stack>
</Card>
</Stack>
<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,17 +57,13 @@ 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"
<PageContainer>
<PageHeader
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">
<TextInput
placeholder="Search invoice no / booking / customer"
@@ -124,10 +118,9 @@ export default function WarehouseInvoicesPage() {
</Table.ScrollContainer>
)}
</Card>
</Stack>
<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,23 +42,18 @@ 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">
<Card>
<Stack gap="md">
<WarehouseFilters filter={filter} onChange={setFilter} view={view} onViewChange={setView} />
@@ -77,9 +72,8 @@ export default function WarehouseListPage() {
)}
</Stack>
</Card>
</Stack>
<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,16 +43,12 @@ 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"
<PageContainer>
<PageHeader
title="Allocation & Fee Rules"
subtitle="Configure deterministic yard allocation and storage / demurrage free time and rates."
/>
<Card withBorder radius="md" padding="lg">
<Card>
<Tabs defaultValue="allocation">
<Tabs.List>
<Tabs.Tab value="allocation">Allocation Rules</Tabs.Tab>
@@ -68,8 +62,7 @@ export default function WarehouseRulesPage() {
</Tabs.Panel>
</Tabs>
</Card>
</Stack>
</Container>
</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>