diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 0f3d282..142f4dc 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -385,6 +385,101 @@ function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) { ); } +// --------------------------------------------------------------------------- +// SVG Donut Chart — FP Workflow Status distribution +// --------------------------------------------------------------------------- +const FP_WORKFLOW_DEFS = [ + { key: 'Actionable', label: 'Actionable', color: '#F59E0B' }, + { key: 'Requested', label: 'Requested', color: '#0EA5E9' }, + { key: 'Reworked', label: 'Reworked', color: '#A855F7' }, + { key: 'Approved', label: 'Approved', color: '#22C55E' }, + { key: 'Rejected', label: 'Rejected', color: '#EF4444' }, + { key: 'Expired', label: 'Expired', color: '#64748B' }, + { key: 'Unknown', label: 'Unknown', color: '#334155' }, +]; + +function FPWorkflowDonut({ findings }) { + const SIZE = 180; + const CX = SIZE / 2; + const CY = SIZE / 2; + const OUTER = 72; + const INNER = 48; + + const counts = useMemo(() => { + const map = {}; + FP_WORKFLOW_DEFS.forEach(d => { map[d.key] = 0; }); + findings.forEach((f) => { + if (!f.workflow) return; + const state = f.workflow.state || ''; + const def = FP_WORKFLOW_DEFS.find(d => d.key.toLowerCase() === state.toLowerCase()); + const key = def ? def.key : 'Unknown'; + map[key] = (map[key] || 0) + 1; + }); + return map; + }, [findings]); + + const total = Object.values(counts).reduce((a, b) => a + b, 0); + + if (total === 0) { + return ( +
+

No FP workflows — click Sync to load

+
+ ); + } + + let cursor = 0; + const segments = FP_WORKFLOW_DEFS.map((def) => { + const count = counts[def.key] || 0; + const start = cursor; + const end = count > 0 ? cursor + (count / total) * 360 : cursor; + if (count > 0) cursor = end; + return { ...def, count, start, end }; + }).filter(s => s.count > 0); + + return ( +
+ + + {segments.map((seg) => ( + + ))} + + {total.toLocaleString()} + + + FP 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' @@ -1257,6 +1352,17 @@ export default function ReportingPage({ filterDate, filterEXC }) { }} />
+ + {/* Divider */} +
+ + {/* FP Workflow Status donut */} +
+
+ FP Workflow Status +
+ +