feat(reporting): split FP charts into per-finding and per-ticket donuts

Renamed the existing FP chart to "FP Finding Status" (counts findings per
workflow state) and added a new "FP Workflow Status" chart that counts
unique FP# ticket IDs per state — so 10 findings under one FP# ticket
counts as 1 ticket, not 10.

Backend: extractFPWorkflow now returns { id, state }; syncFPWorkflowCounts
builds both a finding-count map and a deduped FP# ID map, storing them in
separate columns (fp_workflow_counts_json, fp_id_counts_json). The endpoint
returns findingCounts/findingTotal and idCounts/idTotal.

Frontend: FPWorkflowDonut accepts a centerLabel prop; both donuts share the
same component fed with their respective data slices from the single fetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 12:13:13 -06:00
parent 602c75bf24
commit 7314dc16cb
2 changed files with 72 additions and 33 deletions

View File

@@ -398,7 +398,7 @@ const FP_WORKFLOW_DEFS = [
{ key: 'Unknown', label: 'Unknown', color: '#334155' },
];
function FPWorkflowDonut({ counts, total }) {
function FPWorkflowDonut({ counts, total, centerLabel = 'FP TOTAL' }) {
const SIZE = 180;
const CX = SIZE / 2;
const CY = SIZE / 2;
@@ -438,7 +438,7 @@ function FPWorkflowDonut({ counts, total }) {
{total.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
FP TOTAL
{centerLabel}
</text>
</svg>
@@ -1076,7 +1076,7 @@ export default function ReportingPage({ filterDate, filterEXC }) {
const [syncing, setSyncing] = useState(false);
const [statusCounts, setStatusCounts] = useState({ open: 0, closed: 0 });
const [countsLoading, setCountsLoading] = useState(true);
const [fpWorkflowCounts, setFPWorkflowCounts] = useState({ counts: {}, total: 0 });
const [fpCounts, setFPCounts] = useState({ findingCounts: {}, findingTotal: 0, idCounts: {}, idTotal: 0 });
const [sort, setSort] = useState({ field: 'severity', dir: 'desc' });
const [columnOrder, setColumnOrder] = useState(loadColumnOrder);
const [columnFilters, setColumnFilters] = useState(() =>
@@ -1117,7 +1117,12 @@ export default function ReportingPage({ filterDate, filterEXC }) {
try {
const res = await fetch(`${API_BASE}/ivanti/findings/fp-workflow-counts`, { credentials: 'include' });
const data = await res.json();
if (res.ok) setFPWorkflowCounts({ counts: data.counts || {}, total: data.total || 0 });
if (res.ok) setFPCounts({
findingCounts: data.findingCounts || {},
findingTotal: data.findingTotal || 0,
idCounts: data.idCounts || {},
idTotal: data.idTotal || 0,
});
} catch (e) {
console.error('Error loading FP workflow counts:', e);
}
@@ -1354,12 +1359,23 @@ export default function ReportingPage({ filterDate, filterEXC }) {
{/* Divider */}
<div style={{ width: '1px', background: 'rgba(255,255,255,0.06)', alignSelf: 'stretch', flexShrink: 0 }} />
{/* FP Workflow Status donut */}
{/* FP Finding Status donut — # of findings per FP workflow state */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
FP Finding Status
</div>
<FPWorkflowDonut counts={fpCounts.findingCounts} total={fpCounts.findingTotal} centerLabel="FINDINGS" />
</div>
{/* Divider */}
<div style={{ width: '1px', background: 'rgba(255,255,255,0.06)', alignSelf: 'stretch', flexShrink: 0 }} />
{/* FP Workflow Status donut — # of unique FP# ticket IDs per state */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
FP Workflow Status
</div>
<FPWorkflowDonut counts={fpWorkflowCounts.counts} total={fpWorkflowCounts.total} />
<FPWorkflowDonut counts={fpCounts.idCounts} total={fpCounts.idTotal} centerLabel="FP TICKETS" />
</div>
</div>
</div>