Add sub-team level display to CCP Metrics vertical drill-down

Backend: restructured /vertical/:code/metrics endpoint to return metrics
with nested sub_teams arrays. Each metric now has the ALL: rollup as the
primary row and individual team breakdowns (ACCESS-OPS, STEAM, etc.) as
sub_teams. Also returns a teams array for the filter UI.

Frontend: VerticalDetailView now supports two interaction modes:
- Expand/collapse: click the arrow on any metric row to reveal sub-team
  breakdown inline (teal-highlighted rows beneath the parent)
- Team filter: click a team button to filter the entire table to show
  only that team's numbers per metric

Both modes avoid double-counting by using the ALL: rollup for totals
and only showing sub-team data as supplementary detail.
This commit is contained in:
Jordan Ramos
2026-05-14 12:27:46 -06:00
parent ebaf4cd18c
commit 61d7e00d4f
2 changed files with 201 additions and 29 deletions

View File

@@ -652,6 +652,7 @@ function createVCLMultiVerticalRouter(upload) {
/**
* GET /vertical/:code/metrics
* Returns per-metric breakdown for a specific vertical from the latest upload's summary data.
* Metrics are the "ALL:" rollup rows; each includes a sub_teams array with per-team numbers.
*
* @method GET
* @route /vertical/:code/metrics
@@ -671,7 +672,14 @@ function createVCLMultiVerticalRouter(upload) {
* total: number,
* compliance_pct: number,
* target: number,
* status: string
* status: string,
* sub_teams: Array<{
* team: string,
* non_compliant: number,
* compliant: number,
* total: number,
* compliance_pct: number
* }>
* }>,
* categories: Array<{
* category: string,
@@ -679,7 +687,8 @@ function createVCLMultiVerticalRouter(upload) {
* compliant: number,
* total: number,
* compliance_pct: number
* }>
* }>,
* teams: string[]
* }
* @response 400 { error: string } — invalid vertical code
* @response 500 { error: string }
@@ -695,24 +704,74 @@ function createVCLMultiVerticalRouter(upload) {
[vertical]
);
if (latestUpload.length === 0) return res.json({ vertical, metrics: [], categories: [] });
if (latestUpload.length === 0) return res.json({ vertical, metrics: [], categories: [], teams: [] });
const uploadId = latestUpload[0].id;
// Get per-metric summary data
const { rows: metrics } = await pool.query(
// Get per-metric summary data (all rows including sub-teams)
const { rows: allRows } = await pool.query(
`SELECT metric_id, metric_desc, category, team, priority,
non_compliant, compliant, total, compliance_pct, target, status
FROM vcl_multi_vertical_summary
WHERE upload_id = $1 AND vertical = $2
ORDER BY category, metric_id`,
ORDER BY category, metric_id, team`,
[uploadId, vertical]
);
// Aggregate by category — only use "ALL:" rollup rows to avoid double-counting
// Separate into rollup rows (ALL:) and sub-team rows
// metrics = rollup rows only (one per metric — used for the primary table)
// Each metric gets a sub_teams array with the team-level breakdown
const metricMap = {};
const teamSet = new Set();
for (const row of allRows) {
const isRollup = row.team && row.team.startsWith('ALL:');
const isOther = row.team === '(Other)';
if (isRollup) {
// Primary metric row
metricMap[row.metric_id] = {
metric_id: row.metric_id,
metric_desc: row.metric_desc,
category: row.category,
priority: row.priority,
non_compliant: row.non_compliant,
compliant: row.compliant,
total: row.total,
compliance_pct: row.compliance_pct,
target: row.target,
status: row.status,
team: row.team,
sub_teams: [],
};
} else if (!isOther) {
// Sub-team row (skip "(Other)" — it's a catch-all already in the rollup)
teamSet.add(row.team);
}
}
// Second pass: attach sub-team rows to their parent metric
for (const row of allRows) {
const isRollup = row.team && row.team.startsWith('ALL:');
const isOther = row.team === '(Other)';
if (isRollup || isOther) continue;
if (metricMap[row.metric_id]) {
metricMap[row.metric_id].sub_teams.push({
team: row.team,
non_compliant: row.non_compliant,
compliant: row.compliant,
total: row.total,
compliance_pct: row.compliance_pct,
});
}
}
const metrics = Object.values(metricMap);
// Aggregate by category — only use rollup rows to avoid double-counting
const categoryMap = {};
for (const m of metrics) {
if (!m.team || !m.team.startsWith('ALL:')) continue;
const cat = m.category || 'Other';
if (!categoryMap[cat]) categoryMap[cat] = { category: cat, non_compliant: 0, compliant: 0, total: 0 };
categoryMap[cat].non_compliant += m.non_compliant;
@@ -724,7 +783,10 @@ function createVCLMultiVerticalRouter(upload) {
compliance_pct: c.total > 0 ? Math.round((c.compliant / c.total) * 100 * 10) / 10 : 0,
}));
res.json({ vertical, metrics, categories });
// Return distinct team names for the vertical (useful for filtering)
const teams = [...teamSet].sort();
res.json({ vertical, metrics, categories, teams });
} catch (err) {
console.error('[VCL Multi] GET /vertical/:code/metrics error:', err.message);
res.status(500).json({ error: 'Database error' });