mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 02:30:55 +00:00
199 lines
5.0 KiB
TypeScript
199 lines
5.0 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Box, Button, Group, Paper, Stack, Text } from "@mantine/core";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
Clock,
|
|
LayoutList,
|
|
Plus,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
|
|
import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
|
|
import { ACCENT_CHIP_BG } from "@/components/overview/OverviewKpiCard";
|
|
import {
|
|
overviewAccentGradients,
|
|
type OverviewAccent,
|
|
} from "@/components/overview/overview.styles";
|
|
import type {
|
|
BookingListSummaryMetrics,
|
|
BookingListSummaryTabs,
|
|
} from "@/services/bookings.service";
|
|
|
|
const CARD_STYLE = {
|
|
background: "var(--mantine-color-gray-0)",
|
|
border: "1px solid var(--mantine-color-gray-2)",
|
|
} as const;
|
|
|
|
export interface BookingRequestsHeaderProps {
|
|
metrics?: BookingListSummaryMetrics;
|
|
tabs?: BookingListSummaryTabs;
|
|
loading?: boolean;
|
|
isFetching?: boolean;
|
|
onCreate: () => void;
|
|
onRefresh: () => void;
|
|
}
|
|
|
|
export function BookingRequestsHeader({
|
|
metrics,
|
|
tabs,
|
|
loading,
|
|
isFetching,
|
|
onCreate,
|
|
onRefresh,
|
|
}: BookingRequestsHeaderProps) {
|
|
const val = (n?: number) => (loading ? "—" : (n ?? 0));
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<Group justify="flex-end" gap="sm">
|
|
<Button
|
|
color="edr-green"
|
|
radius="lg"
|
|
leftSection={<Plus size={18} />}
|
|
onClick={onCreate}
|
|
>
|
|
Create booking
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
radius="lg"
|
|
leftSection={<RefreshCw size={16} />}
|
|
loading={isFetching}
|
|
onClick={onRefresh}
|
|
>
|
|
Refresh
|
|
</Button>
|
|
</Group>
|
|
|
|
<Group grow gap="md" align="stretch" wrap="wrap">
|
|
<HeroStat
|
|
icon={LayoutList}
|
|
label="In queue"
|
|
value={val(metrics?.inQueue)}
|
|
hint="Matching current filter"
|
|
accent="gold"
|
|
variant="area"
|
|
/>
|
|
<HeroStat
|
|
icon={Clock}
|
|
label="Needs action"
|
|
value={val(metrics?.needsAction)}
|
|
hint="Submitted or pending"
|
|
ratio={
|
|
metrics?.inQueue ? (metrics.needsAction ?? 0) / metrics.inQueue : 0
|
|
}
|
|
accent="orange"
|
|
/>
|
|
<HeroStat
|
|
icon={AlertTriangle}
|
|
label="Urgent"
|
|
value={val(metrics?.urgent)}
|
|
hint="High priority score"
|
|
ratio={metrics?.inQueue ? (metrics.urgent ?? 0) / metrics.inQueue : 0}
|
|
accent="rose"
|
|
/>
|
|
<HeroStat
|
|
icon={CheckCircle2}
|
|
label="Completed"
|
|
value={val(tabs?.completed)}
|
|
hint="Fully executed"
|
|
accent="gold"
|
|
variant="line"
|
|
/>
|
|
</Group>
|
|
|
|
{/* {tabs ? <PipelineBar tabs={tabs} /> : null} */}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* KPI card matching the overview style: icon chip + value + label, with a mini
|
|
* graph at the bottom. Ratio cards show a ring; the rest show an area/line trend.
|
|
*/
|
|
function HeroStat({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
hint,
|
|
ratio,
|
|
accent = "emerald",
|
|
variant = "area",
|
|
}: {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
value: ReactNode;
|
|
hint?: string;
|
|
ratio?: number;
|
|
accent?: OverviewAccent;
|
|
variant?: "area" | "line";
|
|
}) {
|
|
const [, accentDeep] =
|
|
overviewAccentGradients[accent] ?? overviewAccentGradients.default;
|
|
const chipBg = ACCENT_CHIP_BG[accent] ?? ACCENT_CHIP_BG.default;
|
|
const pct =
|
|
ratio != null ? Math.round(Math.min(1, Math.max(0, ratio)) * 100) : null;
|
|
|
|
return (
|
|
<Paper
|
|
p="md"
|
|
radius="lg"
|
|
style={{ flex: "1 1 180px", minWidth: 160, ...CARD_STYLE }}
|
|
>
|
|
<Stack gap={8}>
|
|
<Group gap="sm" wrap="nowrap" align="center">
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 11,
|
|
background: chipBg,
|
|
color: accentDeep,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Icon size={20} strokeWidth={2} />
|
|
</Box>
|
|
<Stack gap={1} style={{ minWidth: 0, flex: 1 }}>
|
|
<Text
|
|
fw={800}
|
|
size="24px"
|
|
lh={1.05}
|
|
style={{ color: "#0f172a" }}
|
|
truncate
|
|
>
|
|
{value}
|
|
</Text>
|
|
<Text size="xs" fw={600} c="dimmed" truncate>
|
|
{label}
|
|
{hint ? ` · ${pct != null ? `${pct}% of queue` : hint}` : ""}
|
|
</Text>
|
|
</Stack>
|
|
{pct != null ? (
|
|
<MiniRing pct={pct} accent={accent} size={44} stroke={5}>
|
|
<Text size="10px" fw={800} style={{ color: accentDeep }}>
|
|
{pct}%
|
|
</Text>
|
|
</MiniRing>
|
|
) : null}
|
|
</Group>
|
|
|
|
{pct == null ? (
|
|
<MiniSparkline
|
|
variant={variant}
|
|
accent={accent}
|
|
baseline={0.5}
|
|
seed={label}
|
|
height={22}
|
|
/>
|
|
) : null}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|