From a9404ff82a50fb0bc345f4823220ab3792bc2a0a Mon Sep 17 00:00:00 2001 From: jramos Date: Fri, 13 Mar 2026 12:50:15 -0600 Subject: [PATCH] feat(reporting): add FP# workflow status donut chart to Metrics panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second SVG donut chart showing the distribution of FP# workflow states (Expired, Rejected, Reworked, Actionable, Requested, Approved, No FP#) computed from the already-loaded findings array — no new API calls or backend changes required. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/pages/ReportingPage.js | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 69d1e4f..e956ab4 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -264,6 +264,101 @@ function StatusDonut({ open, closed, loading }) { ); } +// --------------------------------------------------------------------------- +// SVG Donut Chart — FP# workflow state distribution +// --------------------------------------------------------------------------- +const WF_STATE_DEFS = [ + { key: 'expired', label: 'Expired', color: '#EF4444' }, + { key: 'rejected', label: 'Rejected', color: '#F87171' }, + { key: 'reworked', label: 'Reworked', color: '#F59E0B' }, + { key: 'actionable', label: 'Actionable', color: '#FCD34D' }, + { key: 'requested', label: 'Requested', color: '#0EA5E9' }, + { key: 'approved', label: 'Approved', color: '#10B981' }, + { key: 'none', label: 'No FP#', color: '#334155' }, +]; + +function WorkflowDonut({ findings }) { + const SIZE = 180; + const CX = SIZE / 2; + const CY = SIZE / 2; + const OUTER = 72; + const INNER = 48; + + const counts = useMemo(() => { + const map = Object.fromEntries(WF_STATE_DEFS.map((d) => [d.key, 0])); + findings.forEach((f) => { + const state = (f.workflow?.state || '').toLowerCase(); + if (state && state in map) map[state]++; + else map.none++; + }); + return map; + }, [findings]); + + const total = Object.values(counts).reduce((a, b) => a + b, 0); + + if (total === 0) { + return ( +
+

No data — click Sync to load

+
+ ); + } + + let cursor = 0; + const segments = WF_STATE_DEFS + .map((def) => { + const count = counts[def.key]; + if (!count) return null; + const start = cursor; + const end = cursor + (count / total) * 360; + cursor = end; + return { ...def, count, start, end }; + }) + .filter(Boolean); + + return ( +
+ + + {segments.map((seg) => ( + + ))} + + {total.toLocaleString()} + + + TOTAL + + + + {/* Legend */} +
+ {segments.map((seg) => ( +
+
+
+ + {seg.label} + + + {seg.count} + + + ({((seg.count / total) * 100).toFixed(0)}%) + +
+
+ ))} +
+
+ ); +} + function SortIcon({ colKey, sort }) { if (sort.field !== colKey) return ; return sort.dir === 'asc' @@ -962,7 +1057,7 @@ export default function ReportingPage({ filterDate }) { Metric Graphs
-
+
{/* Open vs Closed donut */}
@@ -974,6 +1069,17 @@ export default function ReportingPage({ filterDate }) { loading={countsLoading} />
+ + {/* Divider */} +
+ + {/* FP# Workflow state donut */} +
+
+ FP# Workflow Status +
+ +