mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: warehouse revamp
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user