import Link from 'next/link'; import type { ReactNode } from 'react'; import { cn } from '@/lib/format'; export const ADMIN_NAV = [ { href: '/admin', label: 'Overview' }, { href: '/admin/connectors', label: 'Connectors' }, { href: '/admin/coverage', label: 'Coverage' }, { href: '/admin/data-quality', label: 'Data quality' }, { href: '/admin/costs', label: 'Costs' }, { href: '/admin/taxonomy', label: 'Taxonomy' }, { href: '/admin/audit', label: 'Audit log' }, { href: '/admin/events', label: 'Events' }, ]; export function AdminShell({ children, title, subtitle, actions, current }: { children: ReactNode; title: string; subtitle?: ReactNode; actions?: ReactNode; current: string }) { return (

{title}

{subtitle ?

{subtitle}

: null}
{actions}
{children}
); } export function StatusPill({ status }: { status: string | null | undefined }) { const s = status ?? 'unknown'; const tone: Record = { maintenance: 'bg-index-bg text-index', completed: 'bg-gain-bg text-gain', healthy: 'bg-gain-bg text-gain', active: 'bg-gain-bg text-gain', success: 'bg-gain-bg text-gain', valid: 'bg-gain-bg text-gain', degraded: 'bg-alert-bg text-alert', partial: 'bg-alert-bg text-alert', paused: 'bg-alert-bg text-alert', flagged: 'bg-alert-bg text-alert', pending: 'bg-alert-bg text-alert', running: 'bg-index-bg text-index', failing: 'bg-loss-bg text-loss', failed: 'bg-loss-bg text-loss', excluded: 'bg-loss-bg text-loss', disabled: 'bg-inset text-muted', unknown: 'bg-inset text-muted' }; return {s}; } export function ActionButton({ label, tone = 'neutral', small = true }: { label: string; tone?: 'neutral' | 'danger' | 'primary'; small?: boolean }) { return ( ); } export function Kpi({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone?: 'gain' | 'loss' | 'alert' }) { return (

{label}

{value}

{sub ?

{sub}

: null}
); } /** Single-series bar chart (one hue, hover tooltips via , no legend needed). */ export function Bars({ points, format, className }: { points: Array<{ label: string; value: number }>; format?: (v: number) => string; className?: string }) { if (!points.length) return <p className="text-xs text-subtle">No data in this window.</p>; const max = Math.max(...points.map((p) => p.value), 0) || 1; const w = 600; const h = 120; const gap = 2; const bw = Math.max(2, (w - gap * points.length) / points.length); const fmt = format ?? ((v: number) => String(Math.round(v * 100) / 100)); return ( <svg viewBox={`0 0 ${w} ${h + 16}`} className={cn('h-auto w-full', className)} role="img" aria-label="Daily values"> <line x1={0} x2={w} y1={h} y2={h} stroke="var(--ri-border)" /> {points.map((p, i) => { const bh = Math.max(p.value > 0 ? 1 : 0, (p.value / max) * (h - 8)); return ( <g key={p.label}> <rect x={i * (bw + gap)} y={h - bh} width={bw} height={bh} rx={bw > 6 ? 2 : 0} fill="var(--ri-index)" opacity={0.85}> <title>{`${p.label}: ${fmt(p.value)}`} {points.length <= 31 && (i === 0 || i === points.length - 1) ? ( {p.label.slice(5)} ) : null} ); })} ); } export function fmtTs(v: unknown): string { if (!v) return '—'; const d = v instanceof Date ? v : new Date(String(v)); if (Number.isNaN(d.getTime())) return String(v); return d.toISOString().replace('T', ' ').slice(0, 16) + 'Z'; } export function n(v: unknown, digits = 0): string { const x = Number(v); return Number.isFinite(x) ? x.toLocaleString('en-US', { maximumFractionDigits: digits }) : '—'; }