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:
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user