mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 12:43:43 +00:00
Adds the operational-performance layer to the warehouse cockpit:
- Dwell time & aging: dwellStats() (avg + 0-3/4-7/8-14/15+ buckets over
in-warehouse items) → DwellAgingCard histogram.
- Cycle time & on-time: cycleStats() (arrived→ready→loaded→dispatched stage
averages + dock-to-dispatch) and onTimeDispatchStats() (share of items that
left before their storage free-days expired, resolved via the fee engine's
own rule matching) → CycleTimeCard.
- Live tiles: opsStats() now returns receivedYesterday; KpiStrip renders an
optional ▲/▼ delta, and the ops strip shows received-today vs yesterday.
New endpoints: GET /warehouse-inventory/{dwell-stats,cycle-stats} and
/warehouse-fees/on-time-dispatch (all guarded). New "Performance" section on
WarehouseDashboardPage. On-time reads N/A when there is no sample / no active
storage rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import { Card, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core';
|
|
import { Gauge } from 'lucide-react';
|
|
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
|
|
import { useOnTimeDispatch, useWarehouseCycleStats } from '@/hooks/useWarehouses';
|
|
import { formatDays } from './options';
|
|
|
|
function onTimeColor(pct: number | null | undefined): string {
|
|
if (pct == null) return 'edr-text';
|
|
if (pct >= 80) return 'teal';
|
|
if (pct >= 50) return 'orange';
|
|
return 'red';
|
|
}
|
|
|
|
/**
|
|
* Warehouse performance: on-time dispatch rate (items that left before their
|
|
* storage free-days expired), average dock-to-dispatch, and the per-stage
|
|
* cycle times that make it up.
|
|
*/
|
|
export function CycleTimeCard() {
|
|
const { data: cycle, isLoading: cycleLoading } = useWarehouseCycleStats();
|
|
const { data: onTime, isLoading: onTimeLoading } = useOnTimeDispatch();
|
|
const isLoading = cycleLoading || onTimeLoading;
|
|
|
|
const hasStages = (cycle?.sampleSize ?? 0) > 0;
|
|
const stages = cycle?.stages ?? [];
|
|
|
|
return (
|
|
<Card withBorder radius="lg" padding="lg" h="100%">
|
|
<Group gap="sm" mb="md">
|
|
<ThemeIcon size="lg" radius="md" color="teal" variant="light">
|
|
<Gauge size={20} />
|
|
</ThemeIcon>
|
|
<div>
|
|
<Text fw={700}>Cycle time & on-time</Text>
|
|
<Text size="xs" c="dimmed">
|
|
Dispatch performance over the last 90 days
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
|
|
{isLoading ? (
|
|
<Group justify="center" h={220}>
|
|
<Loader />
|
|
</Group>
|
|
) : (
|
|
<Stack gap="md">
|
|
<SimpleGrid cols={2} spacing="sm">
|
|
<Stack gap={2}>
|
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
|
On-time dispatch
|
|
</Text>
|
|
<Text fw={800} fz={30} lh={1.1} c={onTimeColor(onTime?.onTimePct)}>
|
|
{onTime?.onTimePct == null ? 'N/A' : `${onTime.onTimePct}%`}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{onTime?.onTimePct == null
|
|
? 'No storage rule / sample'
|
|
: `${onTime.onTimeCount}/${onTime.sampleSize} left before free-days`}
|
|
</Text>
|
|
</Stack>
|
|
<Stack gap={2}>
|
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
|
Dock → dispatch
|
|
</Text>
|
|
<Text fw={800} fz={30} lh={1.1}>
|
|
{hasStages ? formatDays(cycle?.avgDockToDispatchDays) : '—'}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
avg over {cycle?.sampleSize ?? 0} dispatched
|
|
</Text>
|
|
</Stack>
|
|
</SimpleGrid>
|
|
|
|
{hasStages ? (
|
|
<ResponsiveContainer width="100%" height={170}>
|
|
<BarChart
|
|
data={stages}
|
|
layout="vertical"
|
|
margin={{ top: 4, right: 16, left: 8, bottom: 0 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" horizontal={false} stroke="var(--mantine-color-gray-2)" />
|
|
<XAxis type="number" tick={{ fontSize: 12 }} unit="d" />
|
|
<YAxis type="category" dataKey="label" width={130} tick={{ fontSize: 11 }} />
|
|
<Tooltip
|
|
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
|
|
formatter={(value) => [`${value} days`, 'Avg'] as [string, string]}
|
|
/>
|
|
<Bar dataKey="avgDays" name="Avg days" fill="#12b886" radius={[0, 6, 6, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<Group justify="center" align="center" h={170}>
|
|
<Text c="dimmed" size="sm">
|
|
Not enough dispatched items yet to chart stage times.
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|