import React from 'react'; /** * Headline figures for AI Insight cards. * Matches NEW dashboard gray card patterns (white + border-gray-100). */ export type InsightStatus = 'ok' | 'warning' | 'critical' | 'unknown'; export const INSIGHT_STATUS_META: Record< InsightStatus, { cls: string; glyph: string; label: string; bg: string; text: string; containerCls: string } > = { ok: { cls: 'bg-green-50 text-green-800 border-green-200', glyph: '✓', label: 'Normal', bg: 'bg-green-50', text: 'text-green-800', containerCls: 'bg-green-50/40 border-green-200', }, warning: { cls: 'bg-amber-50 text-amber-800 border-amber-200', glyph: '!', label: 'Perhatian', bg: 'bg-amber-50', text: 'text-amber-800', containerCls: 'bg-amber-50/40 border-amber-200', }, critical: { cls: 'bg-red-50 text-red-800 border-red-200', glyph: '⚠', label: 'Kritis', bg: 'bg-red-50', text: 'text-red-800', containerCls: 'bg-red-50/40 border-red-200', }, unknown: { cls: 'bg-gray-50 text-gray-600 border-gray-200', glyph: '–', label: 'Belum Dinilai', bg: 'bg-gray-50', text: 'text-gray-600', containerCls: 'bg-gray-50/60 border-gray-200', }, }; export const normalizeInsightStatus = (status: unknown): InsightStatus => { if (!status || typeof status !== 'string') return 'unknown'; const lower = status.toLowerCase(); if (lower === 'ok' || lower === 'healthy' || lower === 'normal' || lower === 'aman') return 'ok'; if (lower === 'warning' || lower === 'perhatian' || lower === 'waspada') return 'warning'; if (lower === 'critical' || lower === 'kritis' || lower === 'bahaya') return 'critical'; return 'unknown'; }; export interface InsightStatusBadgeProps { status: InsightStatus | string; customLabel?: string; className?: string; size?: 'xs' | 'sm' | 'md'; } export const InsightStatusBadge: React.FC = ({ status, customLabel, className = '', size = 'sm', }) => { const norm = normalizeInsightStatus(status); const meta = INSIGHT_STATUS_META[norm]; const sizeClasses = size === 'xs' ? 'px-2 py-0.5 text-[10px]' : size === 'md' ? 'px-3 py-1 text-xs' : 'px-2.5 py-0.5 text-[11px]'; return ( {customLabel || meta.label} ); }; interface DeltaChipProps { status: InsightStatus; label: string; } export const DeltaChip: React.FC = ({ status, label }) => { const chip = INSIGHT_STATUS_META[status] ?? INSIGHT_STATUS_META.unknown; return ( {label} ); }; /** Axis-less sparkline — shape only; the tile number carries the value. */ export const Sparkline: React.FC<{ values: number[]; className?: string }> = ({ values, className, }) => { const clean = values.filter((v) => typeof v === 'number' && Number.isFinite(v)); if (clean.length < 2) return null; const width = 88; const height = 28; const min = Math.min(...clean); const max = Math.max(...clean); const span = max - min || 1; const step = width / (clean.length - 1); const points = clean .map((value, index) => { const x = index * step; const y = height - 2 - ((value - min) / span) * (height - 4); return `${x.toFixed(1)},${y.toFixed(1)}`; }) .join(' '); return ( ); }; interface HeroFigureProps { label: string; value: string; unit?: string; context?: string | null; status?: InsightStatus; deltaLabel?: string | null; sparkline?: number[]; } export const HeroFigure: React.FC = ({ label, value, unit, context, status = 'unknown', deltaLabel, sparkline, }) => (
{label}
{value} {unit ? {unit} : null}
{deltaLabel ? : null} {context ? {context} : null}
{sparkline && sparkline.length > 1 ? ( ) : null}
); interface StatTileProps { label: string; value: string; unit?: string; hint?: string | null; status?: InsightStatus; deltaLabel?: string | null; sparkline?: number[]; } export const StatTile: React.FC = ({ label, value, unit, hint, status = 'unknown', deltaLabel, sparkline, }) => (
{label} {hint ? {hint} : null}
{value} {unit ? {unit} : null}
{deltaLabel ? (
) : null} {sparkline && sparkline.length > 1 ? ( ) : null}
); /** Secondary figures: subordinate to hero / tiles. */ export const MutedStatRow: React.FC<{ label: string; hint?: string | null; value: string }> = ({ label, hint, value, }) => (
{label} {hint ? {hint} : null} {value}
);