import { AlertTriangle, CalendarClock, Clock3, History, Layers, Ruler, TrendingUp } from 'lucide-react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import type { QualityBadge as Badge } from '@/lib/types-analytics'; const ICON: Record = { fresh: Clock3, historical: History, sparse: Layers, 'limited-coverage': Ruler, stale: CalendarClock, flagged: AlertTriangle, forecast: TrendingUp, }; const TONE: Record = { fresh: 'border-accent/40 bg-accent-soft text-accent', historical: 'border-rule text-ink-2', sparse: 'border-warn/40 text-warn', 'limited-coverage': 'border-warn/40 text-warn', stale: 'border-warn/40 text-warn', flagged: 'border-down/40 text-down', forecast: 'border-rule text-ink-2', }; /** One data-quality badge: glyph + label always accompany the colour (never colour alone). */ export function QualityBadge({ badge, className, showHint = false }: { badge: Badge; className?: string; showHint?: boolean }) { const Icon = ICON[badge] ?? Clock3; const label = t(`quality.${badge}` as 'quality.fresh'); const hint = t(`quality.hint.${badge}` as 'quality.hint.fresh'); return ( {label} {showHint ? · {hint} : null} ); } /** Row of badges (deduplicated, stable order). Renders nothing when empty. */ export function QualityBadges({ badges, className, max = 5 }: { badges: Badge[] | null | undefined; className?: string; max?: number }) { if (!badges?.length) return null; const order: Badge[] = ['fresh', 'stale', 'historical', 'sparse', 'limited-coverage', 'flagged', 'forecast']; const list = order.filter((b) => badges.includes(b)).slice(0, max); return ( ); } /** * Compact quality strip: latest year · first year · points · coverage, plus badges. Used by provenance panel, * indicator and country pages. All fields optional; missing ones are skipped. */ export function QualityStrip({ latestYear, firstYear, points, coveragePct, missingYears, continuityPct, badges, className }: { latestYear?: number | null; firstYear?: number | null; points?: number | null; coveragePct?: number | null; missingYears?: number | null; continuityPct?: number | null; badges?: Badge[] | null; className?: string }) { const cells: Array<[string, string]> = []; if (firstYear != null && latestYear != null) cells.push([t('prov.years'), `${firstYear}–${latestYear}`]); else if (latestYear != null) cells.push([t('quality.latestYear'), String(latestYear)]); if (points != null) cells.push([t('quality.points'), String(points)]); if (missingYears != null) cells.push([t('quality.missing'), String(missingYears)]); if (continuityPct != null) cells.push([t('quality.continuity'), `${Math.round(continuityPct)} %`]); if (coveragePct != null) cells.push([t('quality.coverage'), `${Math.round(coveragePct)} %`]); if (!cells.length && !badges?.length) return null; return (
{cells.length ? (
{cells.map(([k, v]) => (
{k}
{v}
))}
) : null}
); } /** * Derive badges client-side from a series when the API quality endpoints are not available: * fresh / stale from the latest year vs reference, historical from the first year, sparse from the point count. */ export function deriveBadges(opts: { firstYear?: number | null; latestYear?: number | null; points?: number | null; referenceYear?: number | null; hasForecast?: boolean; flaggedShare?: number | null; coveragePct?: number | null }): Badge[] { const out: Badge[] = []; const ref = opts.referenceYear ?? new Date().getUTCFullYear() - 1; if (opts.latestYear != null) { if (opts.latestYear >= ref - 1) out.push('fresh'); else if (opts.latestYear <= ref - 3) out.push('stale'); } if (opts.firstYear != null && opts.firstYear <= 1970) out.push('historical'); if (opts.points != null && opts.points < 10) out.push('sparse'); if (opts.coveragePct != null && opts.coveragePct < 50) out.push('limited-coverage'); if (opts.flaggedShare != null && opts.flaggedShare > 5) out.push('flagged'); if (opts.hasForecast) out.push('forecast'); return out; }