mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
The dashboard counters were static — "Trucks on-site" showed a number with no way to open the list behind it, same for the others. KpiStrip now takes an optional href per item: a cell with one becomes a link (pointer, hover tint, aria-label) and a cell without one stays exactly as before, so every existing strip is unaffected. The warehouse ops strip wires each card to its detail: trucks on-site to the Trucks on Site page, received-today and pending-inspection to the inventory board filtered to RECEIVED, items-aging to the inventory board. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import { Card, Skeleton, Text } from "@mantine/core";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import type { ElementType, ReactNode } from "react";
|
|
import { Link } from "react-router-dom";
|
|
|
|
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;
|
|
/**
|
|
* Optional route the cell links to — its detail view. When set the cell
|
|
* becomes clickable (pointer, hover tint); when absent it stays static.
|
|
*/
|
|
href?: string;
|
|
}
|
|
|
|
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";
|
|
// A cell with an href becomes a link to its detail; without one it
|
|
// stays a plain div. Same layout classes either way.
|
|
const Cell: ElementType = item.href ? Link : "div";
|
|
const linkProps = item.href
|
|
? { to: item.href, "aria-label": `${item.label} — view detail` }
|
|
: {};
|
|
return (
|
|
<Cell
|
|
key={item.label}
|
|
{...(linkProps as Record<string, unknown>)}
|
|
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",
|
|
item.href &&
|
|
"cursor-pointer no-underline transition-colors hover:bg-gray-50 focus-visible:bg-gray-50",
|
|
)}
|
|
>
|
|
{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>
|
|
</Cell>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default KpiStrip;
|