Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx
2026-06-23 15:02:25 +03:00

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>
);
}