mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +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>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import { Card, Skeleton, Text } from "@mantine/core";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface KpiItem {
|
|
label: string;
|
|
value: ReactNode;
|
|
/** Optional leading icon rendered in a tinted chip. */
|
|
icon?: LucideIcon;
|
|
/** Secondary line under the label (e.g. a unit or comparison). */
|
|
hint?: string;
|
|
/**
|
|
* Mantine color name for the icon chip (e.g. "edr-green", "red", "yellow").
|
|
* Defaults to the brand green so a strip reads as uniform unless a page opts
|
|
* into semantic tints.
|
|
*/
|
|
color?: string;
|
|
/**
|
|
* Optional change vs a prior period, rendered as a ▲/▼ chip next to the value
|
|
* (green up, red down, muted zero). E.g. today's count minus yesterday's.
|
|
*/
|
|
delta?: number;
|
|
}
|
|
|
|
export interface KpiStripProps {
|
|
items: KpiItem[];
|
|
/** Show skeletons in place of values while data loads. */
|
|
loading?: boolean;
|
|
}
|
|
|
|
/**
|
|
* A single bordered card divided into up to five KPI cells:
|
|
* `[ kpi | kpi | kpi ]`. Hairline dividers separate cells (vertical on wide
|
|
* screens, horizontal when they wrap). Surface, border and shadow all come from
|
|
* the theme — no per-cell backgrounds, gradients or custom shadows.
|
|
*/
|
|
export function KpiStrip({ items, loading = false }: KpiStripProps) {
|
|
// The spec caps a strip at five cells; extra items are dropped rather than
|
|
// silently overflowing into an unreadable row.
|
|
const cells = items.slice(0, 5);
|
|
|
|
return (
|
|
<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;
|
|
const color = item.color ?? "edr-green";
|
|
return (
|
|
<div
|
|
key={item.label}
|
|
className={cn(
|
|
"flex flex-1 items-center gap-3 px-5 py-4",
|
|
index > 0 &&
|
|
"border-t border-edr-border sm:border-l sm:border-t-0",
|
|
)}
|
|
>
|
|
{Icon ? (
|
|
<div
|
|
className="flex size-10 shrink-0 items-center justify-center rounded-lg"
|
|
style={{
|
|
background: `var(--mantine-color-${color}-1)`,
|
|
color: `var(--mantine-color-${color}-7)`,
|
|
}}
|
|
>
|
|
<Icon size={20} strokeWidth={2} />
|
|
</div>
|
|
) : null}
|
|
|
|
<div style={{ minWidth: 0 }}>
|
|
{loading ? (
|
|
<Skeleton height={26} width={72} radius="sm" my={2} />
|
|
) : (
|
|
<div className="flex items-baseline gap-2">
|
|
<Text
|
|
fw={800}
|
|
fz={24}
|
|
lh={1.05}
|
|
c="edr-text"
|
|
style={{ letterSpacing: "-0.02em" }}
|
|
truncate
|
|
>
|
|
{item.value}
|
|
</Text>
|
|
{item.delta != null && item.delta !== 0 ? (
|
|
<Text
|
|
component="span"
|
|
fz="xs"
|
|
fw={700}
|
|
c={item.delta > 0 ? "edr-green" : "red"}
|
|
style={{ whiteSpace: "nowrap" }}
|
|
>
|
|
{item.delta > 0 ? "▲" : "▼"}
|
|
{Math.abs(item.delta)}
|
|
</Text>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
<Text size="xs" fw={600} c="edr-muted" truncate>
|
|
{item.label}
|
|
{item.hint ? ` · ${item.hint}` : ""}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default KpiStrip;
|