/* QueueHeatmap — open tickets by queue × priority. * * Geometric grid; each cell is a square tinted with the brand blue. * Intensity scales linearly to the most-loaded cell (so the heaviest * cell renders at full saturation). Per-row totals on the right; the * column headers carry priority labels. * * Empty cells render as a thin dashed outline rather than nothing — * preserves the grid alignment and makes the absence visible. */ 'use client'; import { priorityBadge } from '@/lib/status-registry'; import { cn } from '@/lib/utils'; interface HeatmapRow { queueId: number; queueLabel: string; total: number; byPriority: Record; } interface QueueHeatmapProps { data: HeatmapRow[]; /** Priority IDs in column order. Defaults to Critical→Very Low. */ priorities?: number[]; } const DEFAULT_PRIORITIES = [2, 3, 4, 6, 7]; export function QueueHeatmap({ data, priorities = DEFAULT_PRIORITIES }: QueueHeatmapProps) { const max = data.reduce( (m, row) => Math.max(m, ...priorities.map((p) => row.byPriority[p] ?? 0)), 1, ); if (data.length === 0) { return (

No open tickets.

); } return (
{priorities.map((p) => { const badge = priorityBadge(p); return ( ); })} {data.map((row) => ( {priorities.map((p) => ( ))} ))}
Queue {badge.label.slice(0, 3)} Total
{row.queueLabel} {row.total}
); } function Cell({ value, max, priorityLabel, queueLabel, }: { value: number; max: number; priorityLabel: string; queueLabel: string; }) { if (value === 0) { return (
); } // Normalize 0..1 then floor into one of 6 opacity steps; smallest cell still // reads, largest is at 0.85. const ratio = Math.min(value / max, 1); const opacity = Math.max(0.18, ratio * 0.85); return (
0.55 ? 'var(--primary-foreground)' : 'var(--foreground)', }} title={`${queueLabel} · ${priorityLabel}: ${value}`} aria-label={`${queueLabel}, ${priorityLabel}: ${value}`} > {value}
); }