dashboard UI

This commit is contained in:
Hagernesh
2026-08-20 13:42:34 +00:00
parent 82c795999e
commit 2e28b10610

View File

@@ -1,9 +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 { Badge, Card, Center, Divider, Group, Loader, Select, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; import { Button, Card, Center, Group, Loader, Popover, Select, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core';
import { DatePickerInput } from '@mantine/dates'; import { DatePickerInput } from '@mantine/dates';
import { import {
ClipboardList, ClipboardList,
Filter,
PackageCheck, PackageCheck,
PackageOpen, PackageOpen,
PackagePlus, PackagePlus,
@@ -17,7 +18,6 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { PageContainer, PageHeader } from '@/components/page'; import { PageContainer, PageHeader } from '@/components/page';
import { getDateRangePresets } from '@/components/common/dateRangePresets';
import { import {
AccrualDashboard, AccrualDashboard,
CycleTimeCard, CycleTimeCard,
@@ -44,44 +44,42 @@ interface Metric {
icon: React.ReactNode; icon: React.ReactNode;
/** Route to navigate to when the card is clicked. */ /** Route to navigate to when the card is clicked. */
to: string; to: string;
theme: string;
} }
const ORANGE = 'rgb(241, 147, 23)';
const GREEN = '#084b21';
const METRICS: Metric[] = [ const METRICS: Metric[] = [
{ key: 'totalWarehouses', label: 'Total Warehouses', icon: <WarehouseIcon size={22} />, to: '/dashboard/warehouses', theme: ORANGE }, { key: 'totalWarehouses', label: 'Total Warehouses', icon: <WarehouseIcon size={18} />, to: '/dashboard/warehouses' },
{ key: 'totalInventory', label: 'Total Inventory', icon: <Boxes size={22} />, to: '/dashboard/warehouse-inventory', theme: GREEN }, { key: 'totalInventory', label: 'Total Inventory', icon: <Boxes size={18} />, to: '/dashboard/warehouse-inventory' },
{ key: 'received', label: 'Received Today', icon: <PackagePlus size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE }, { key: 'received', label: 'Received Today', icon: <PackagePlus size={18} />, to: '/dashboard/warehouse-inventory?status=RECEIVED' },
{ key: 'awaitingInspection', label: 'Awaiting Inspection', icon: <ClipboardList size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: GREEN }, { key: 'awaitingInspection', label: 'Awaiting Inspection', icon: <ClipboardList size={18} />, to: '/dashboard/warehouse-inventory?status=RECEIVED' },
{ key: 'emptyContainers', label: 'Empty Containers', icon: <PackageOpen size={22} />, to: '/dashboard/containers', theme: ORANGE }, { key: 'emptyContainers', label: 'Empty Containers', icon: <PackageOpen size={18} />, to: '/dashboard/containers' },
{ key: 'importTrains', label: 'Import Trains', icon: <Train size={22} />, to: '/dashboard/import-warehouse', theme: GREEN }, { key: 'importTrains', label: 'Import Trains', icon: <Train size={18} />, to: '/dashboard/import-warehouse' },
{ key: 'exportTrains', label: 'Export Trains', icon: <Train size={22} />, to: '/dashboard/export-warehouse', theme: ORANGE }, { key: 'exportTrains', label: 'Export Trains', icon: <Train size={18} />, to: '/dashboard/export-warehouse' },
{ key: 'loaded', label: 'Loaded', icon: <Truck size={22} />, to: '/dashboard/loaded-inventory', theme: GREEN }, { key: 'loaded', label: 'Loaded', icon: <Truck size={18} />, to: '/dashboard/loaded-inventory' },
{ key: 'dispatched', label: 'Dispatched', icon: <Send size={22} />, to: '/dashboard/dispatch-queue', theme: ORANGE }, { key: 'dispatched', label: 'Dispatched', icon: <Send size={18} />, to: '/dashboard/dispatch-queue' },
{ key: 'readyForPickup', label: 'Ready For Pickup', icon: <PackageSearch size={22} />, to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: GREEN }, { key: 'readyForPickup', label: 'Ready For Pickup', icon: <PackageSearch size={18} />, to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP' },
{ key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={22} />, to: '/dashboard/loading-queue', theme: ORANGE }, { key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={18} />, to: '/dashboard/loading-queue' },
{ key: 'delivered', label: 'Delivered', icon: <CircleCheck size={22} />, to: '/dashboard/warehouse-inventory?status=DELIVERED', theme: GREEN }, { key: 'delivered', label: 'Delivered', icon: <CircleCheck size={18} />, to: '/dashboard/warehouse-inventory?status=DELIVERED' },
]; ];
export default function WarehouseDashboardPage() { export default function WarehouseDashboardPage() {
const navigate = useNavigate(); const navigate = useNavigate();
// Both null → the API defaults `received` to "today", matching the page's original behaviour. // null → the API defaults `received` to "today", matching the page's original behaviour.
const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null]); const [receivedDate, setReceivedDate] = useState<string | null>(null);
const [warehouseId, setWarehouseId] = useState<string | null>(null); const [warehouseId, setWarehouseId] = useState<string | null>(null);
const [dateFrom, dateTo] = dateRange; const [filtersOpen, setFiltersOpen] = useState(false);
const hasCustomRange = Boolean(dateFrom || dateTo); const hasCustomDate = Boolean(receivedDate);
const warehousesQuery = useWarehouses(); const warehousesQuery = useWarehouses();
const warehouseOptions = useMemo( const warehouseOptions = useMemo(
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
[warehousesQuery.data], [warehousesQuery.data],
); );
const activeFilterCount = (warehouseId ? 1 : 0) + (hasCustomDate ? 1 : 0);
const { data, isError, isLoading } = useWarehouseDashboard({ const { data, isError, isLoading } = useWarehouseDashboard({
dateFrom: dateFrom ?? undefined, // Same date both ends → the one day the picker selected, inclusive.
dateTo: dateTo ?? undefined, dateFrom: receivedDate ?? undefined,
dateTo: receivedDate ?? undefined,
warehouseId: warehouseId ?? undefined, warehouseId: warehouseId ?? undefined,
}); });
@@ -89,57 +87,64 @@ export default function WarehouseDashboardPage() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Warehouse Dashboard" title="Warehouse Dashboard"
subtitle="Live overview of warehouse capacity and inventory lifecycle." subtitle="Freight import/export logistics operations overview"
action={ action={
<Group gap="sm" wrap="wrap" justify="flex-end"> <Group gap="sm" wrap="wrap" justify="flex-end">
<Select
placeholder="All warehouses"
clearable
searchable
data={warehouseOptions}
value={warehouseId}
onChange={setWarehouseId}
w={220}
/>
<DatePickerInput <DatePickerInput
type="range"
placeholder="Received: today" placeholder="Received: today"
value={dateRange} value={receivedDate}
onChange={setDateRange} onChange={setReceivedDate}
presets={getDateRangePresets()}
clearable clearable
w={230} w={180}
/> />
<Badge <Popover opened={filtersOpen} onChange={setFiltersOpen} position="bottom-end" withArrow shadow="md">
color="edr-green" <Popover.Target>
variant="light" <Button
size="lg" variant="default"
leftSection={ leftSection={<Filter size={16} />}
<span rightSection={activeFilterCount > 0 ? <Text size="xs" fw={700} c="edr-green">{activeFilterCount}</Text> : null}
style={{ onClick={() => setFiltersOpen((o) => !o)}
display: 'inline-block', >
width: 8, Filters
height: 8, </Button>
borderRadius: '50%', </Popover.Target>
background: 'var(--mantine-color-edr-green-6)', <Popover.Dropdown>
}} <Stack gap="sm" w={240}>
/> <Select
} label="Warehouse"
> placeholder="All warehouses"
Live · updates every 60s clearable
</Badge> searchable
data={warehouseOptions}
value={warehouseId}
onChange={setWarehouseId}
/>
{activeFilterCount > 0 && (
<Button
variant="subtle"
color="gray"
size="xs"
onClick={() => {
setWarehouseId(null);
setReceivedDate(null);
}}
>
Clear filters
</Button>
)}
</Stack>
</Popover.Dropdown>
</Popover>
</Group> </Group>
} }
/> />
{(warehouseId || hasCustomRange) && ( {(warehouseId || hasCustomDate) && (
<Text size="xs" c="dimmed" mt={-8}> <Text size="xs" c="dimmed" mt={-8}>
Scoped to{' '} Scoped to{' '}
{warehouseId ? warehouseOptions.find((o) => o.value === warehouseId)?.label ?? 'selected warehouse' : 'all warehouses'} {warehouseId ? warehouseOptions.find((o) => o.value === warehouseId)?.label ?? 'selected warehouse' : 'all warehouses'}
{hasCustomRange {hasCustomDate ? ` · Received counts for ${receivedDate}` : ' · Received counts: today'}
? ` · Received counts ${dateFrom ?? '…'} to ${dateTo ?? '…'}` . Status-backlog and fleet counters are always current regardless of the date filter.
: ' · Received counts: today'}
. Status-backlog and fleet counters are always current regardless of the date range.
</Text> </Text>
)} )}
@@ -152,41 +157,33 @@ export default function WarehouseDashboardPage() {
<Text c="red">Failed to load warehouse dashboard.</Text> <Text c="red">Failed to load warehouse dashboard.</Text>
</Center> </Center>
) : ( ) : (
<Stack gap="xl"> <Stack gap="lg">
{/* Needs attention — live ops counters (received today, pending {/* Needs attention — live ops counters (received today, pending
inspection, trucks on-site, items aging > 7 days). */} inspection, trucks on-site, items aging > 7 days). */}
<Stack gap="sm"> <WarehouseOpsKpiStrip />
<SectionTitle>Needs attention</SectionTitle>
<WarehouseOpsKpiStrip />
</Stack>
<Divider />
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="md"> <SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="md">
{METRICS.map((metric) => ( {METRICS.map((metric) => (
<Card <Card
key={metric.key} key={metric.key}
padding="lg" padding="md"
withBorder
radius="md"
onClick={() => navigate(metric.to)} onClick={() => navigate(metric.to)}
className="cursor-pointer transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-edr-primary!" className="cursor-pointer transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-edr-primary!"
> >
<Group justify="space-between" align="flex-start" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
<div> <ThemeIcon color="edr-green" variant="light" size={40} radius="md">
<Text size="xs" c="edr-muted" tt="uppercase" fw={700} style={{ letterSpacing: 0.4 }}>
{metric.key === 'received' && hasCustomRange ? 'Received' : metric.label}
</Text>
<Text fw={800} fz={32} mt={8} c="edr-text" lh={1.1}>
{data ? data[metric.key] : 0}
</Text>
</div>
<ThemeIcon
variant="light"
size={46}
radius="md"
style={{ backgroundColor: `${metric.theme}1a`, color: metric.theme }}
>
{metric.icon} {metric.icon}
</ThemeIcon> </ThemeIcon>
<Stack gap={0} style={{ minWidth: 0 }}>
<Text size="xs" c="edr-muted" fw={600}>
{metric.key === 'received' && hasCustomDate ? 'Received' : metric.label}
</Text>
<Text fw={700} fz={20} c="edr-text" lh={1.2}>
{data ? data[metric.key] : 0}
</Text>
</Stack>
</Group> </Group>
</Card> </Card>
))} ))}