mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 07:15:45 +00:00
dashboard UI
This commit is contained in:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user