Make Non-Compliant stat clickable — reveals metric breakdown buttons
Clicking the Non-Compliant card on the CCP Metrics overview now toggles a panel of metric buttons below it, each showing the metric ID, category, non-compliant count, and compliance % vs target. Styled like the compliance page's MetricHealthCard pattern. Backend: added metric_breakdown to the /stats response — aggregated cross-vertical metric totals (ALL: rows only, grouped by metric_id). Also updated tech steering file to document the single-port Express architecture and the requirement to run npm run build after frontend changes.
This commit is contained in:
@@ -57,21 +57,39 @@ const TD_STYLE = {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stats Bar
|
||||
// ---------------------------------------------------------------------------
|
||||
function StatsBar({ stats }) {
|
||||
function StatsBar({ stats, onNonCompliantClick, ncExpanded }) {
|
||||
if (!stats) return null;
|
||||
const items = [
|
||||
{ label: 'Total Devices', value: stats.total_devices.toLocaleString(), color: '#94A3B8' },
|
||||
{ label: 'Compliant', value: stats.compliant.toLocaleString(), color: '#10B981' },
|
||||
{ label: 'Non-Compliant', value: stats.non_compliant.toLocaleString(), color: '#EF4444' },
|
||||
{ label: 'Non-Compliant', value: stats.non_compliant.toLocaleString(), color: '#EF4444', clickable: true },
|
||||
{ label: 'Current %', value: `${stats.compliance_pct}%`, color: stats.compliance_pct >= stats.target_pct ? '#10B981' : '#F59E0B' },
|
||||
{ label: 'Target %', value: `${stats.target_pct}%`, color: PURPLE },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
|
||||
{items.map(({ label, value, color }) => (
|
||||
<div key={label} style={STAT_CARD_STYLE}>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '0.5rem' }}>{label}</div>
|
||||
{items.map(({ label, value, color, clickable }) => (
|
||||
<div
|
||||
key={label}
|
||||
onClick={clickable ? onNonCompliantClick : undefined}
|
||||
style={{
|
||||
...STAT_CARD_STYLE,
|
||||
cursor: clickable ? 'pointer' : 'default',
|
||||
border: clickable && ncExpanded
|
||||
? '1px solid rgba(239, 68, 68, 0.6)'
|
||||
: STAT_CARD_STYLE.border,
|
||||
background: clickable && ncExpanded
|
||||
? 'rgba(239, 68, 68, 0.08)'
|
||||
: STAT_CARD_STYLE.background,
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={clickable ? e => { e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.5)'; } : undefined}
|
||||
onMouseLeave={clickable ? e => { if (!ncExpanded) e.currentTarget.style.borderColor = 'rgba(167, 139, 250, 0.2)'; } : undefined}
|
||||
>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '0.5rem' }}>
|
||||
{label}{clickable && <span style={{ marginLeft: '0.4rem', fontSize: '0.6rem', color: '#64748B' }}>{ncExpanded ? '▾' : '▸'}</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: '700', color }}>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -79,6 +97,65 @@ function StatsBar({ stats }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metric Breakdown Panel (shown when Non-Compliant is clicked)
|
||||
// ---------------------------------------------------------------------------
|
||||
function MetricBreakdownPanel({ metrics, onSelectMetric }) {
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
|
||||
// Only show metrics with non_compliant > 0
|
||||
const ncMetrics = metrics.filter(m => m.non_compliant > 0);
|
||||
if (ncMetrics.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ ...CARD_STYLE, marginBottom: '1.5rem' }}>
|
||||
<div style={{ fontSize: '0.7rem', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '1rem' }}>
|
||||
Non-Compliant by Metric
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.625rem', flexWrap: 'wrap' }}>
|
||||
{ncMetrics.map(m => {
|
||||
const pct = m.total > 0 ? (m.compliant / m.total) : 0;
|
||||
const target = Number(m.target || 0);
|
||||
const color = pct >= target ? '#10B981' : pct >= target * 0.85 ? '#F59E0B' : '#EF4444';
|
||||
return (
|
||||
<button
|
||||
key={m.metric_id}
|
||||
onClick={() => onSelectMetric(m.metric_id)}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: `1.5px solid ${color}40`,
|
||||
borderRadius: '0.5rem',
|
||||
padding: '0.75rem 1rem',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
transition: 'all 0.15s',
|
||||
minWidth: '140px',
|
||||
flex: '1 1 0',
|
||||
maxWidth: '200px',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = color + '80'; e.currentTarget.style.background = `${color}10`; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = color + '40'; e.currentTarget.style.background = 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)'; }}
|
||||
>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', marginBottom: '0.2rem' }}>
|
||||
{m.metric_id}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.6rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '0.5rem' }}>
|
||||
{m.category}
|
||||
</div>
|
||||
<div style={{ fontSize: '1.1rem', fontWeight: '700', color: '#EF4444', marginBottom: '0.2rem' }}>
|
||||
{m.non_compliant.toLocaleString()}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.6rem', color: '#64748B', fontFamily: 'monospace' }}>
|
||||
{(pct * 100).toFixed(0)}% / target {(target * 100).toFixed(0)}%
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Donut Chart
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -816,6 +893,7 @@ export default function CCPMetricsPage() {
|
||||
const [error, setError] = useState(null);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showManage, setShowManage] = useState(false);
|
||||
const [showMetricBreakdown, setShowMetricBreakdown] = useState(false);
|
||||
|
||||
// Drill-down state
|
||||
const [selectedVertical, setSelectedVertical] = useState(null);
|
||||
@@ -966,7 +1044,23 @@ export default function CCPMetricsPage() {
|
||||
{!loading && !error && stats && (
|
||||
<>
|
||||
{/* Stats bar */}
|
||||
<StatsBar stats={stats.stats} />
|
||||
<StatsBar
|
||||
stats={stats.stats}
|
||||
onNonCompliantClick={() => setShowMetricBreakdown(!showMetricBreakdown)}
|
||||
ncExpanded={showMetricBreakdown}
|
||||
/>
|
||||
|
||||
{/* Metric breakdown (revealed when Non-Compliant is clicked) */}
|
||||
{showMetricBreakdown && (
|
||||
<MetricBreakdownPanel
|
||||
metrics={stats.metric_breakdown}
|
||||
onSelectMetric={(metricId) => {
|
||||
// Find the first vertical that has this metric with non-compliant > 0
|
||||
// and navigate to it
|
||||
setShowMetricBreakdown(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Charts row */}
|
||||
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||
|
||||
Reference in New Issue
Block a user