[FIX] P2/P3: aggiunti i metodi+route realmente mancanti (commit precedenti incompleti)
I commit 56ce97d/1a5db30/14c06c8 contenevano migrazioni+HTML ma gli Edit dei metodi controller e delle route erano falliti silenziosamente (ancore errate). Ora presenti e testati E2E in produzione: - DashboardController::sectorBenchmark (era 501) - SupplyChainController: sendQuestionnaire/publicQuestionnaire/submitPublicQuestionnaire/questionnaireStatus/resolveQuestionnaire + route 'supply-chain' (era 404) - PolicyController: attest/attestations/versions/diff/pendingAttestations + snapshot in approve + route (era 404) Test: benchmark 200, supplier flow send->submit(score 61)->dedup 409->DB risk_score=39, policy approve->attest(coverage 50%)->bump v2.0->diff(+2/-1)->pending ricompare. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c15d8db510
commit
31b8a4572c
@@ -150,6 +150,90 @@ class DashboardController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/sectorBenchmark
|
||||
* Benchmark settoriale ANONIMIZZATO (P2): confronta lo score di compliance
|
||||
* dell'organizzazione con la media/distribuzione del proprio settore sulla
|
||||
* rete multi-tenant. k-anonymity: aggregati solo se >= MIN_PEERS organizzazioni.
|
||||
*/
|
||||
public function sectorBenchmark(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$MIN_PEERS = 3;
|
||||
|
||||
$org = Database::fetchOne('SELECT sector FROM organizations WHERE id = ?', [$orgId]);
|
||||
if (!$org) {
|
||||
$this->jsonError('Organizzazione non trovata', 404, 'NOT_FOUND');
|
||||
}
|
||||
$sector = $org['sector'];
|
||||
|
||||
$mine = Database::fetchOne(
|
||||
"SELECT overall_score FROM assessments WHERE organization_id = ? AND status='completed'
|
||||
ORDER BY completed_at DESC LIMIT 1",
|
||||
[$orgId]
|
||||
);
|
||||
$myScore = $mine ? (float) $mine['overall_score'] : null;
|
||||
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT a.overall_score
|
||||
FROM assessments a
|
||||
JOIN (
|
||||
SELECT organization_id, MAX(completed_at) mx
|
||||
FROM assessments WHERE status='completed'
|
||||
GROUP BY organization_id
|
||||
) last ON last.organization_id = a.organization_id AND last.mx = a.completed_at
|
||||
JOIN organizations o ON o.id = a.organization_id
|
||||
WHERE o.is_active = 1 AND o.sector = ? AND a.status='completed' AND a.overall_score IS NOT NULL",
|
||||
[$sector]
|
||||
);
|
||||
$scores = array_map(fn($r) => (float) $r['overall_score'], $rows);
|
||||
$n = count($scores);
|
||||
|
||||
if ($n < $MIN_PEERS) {
|
||||
$this->jsonSuccess([
|
||||
'sector' => $sector,
|
||||
'sufficient_data' => false,
|
||||
'min_peers_required' => $MIN_PEERS,
|
||||
'peers_available' => $n,
|
||||
'my_score' => $myScore,
|
||||
'message' => 'Dati insufficienti per un benchmark anonimo (servono almeno ' . $MIN_PEERS . ' organizzazioni nel settore).',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
sort($scores);
|
||||
$avg = array_sum($scores) / $n;
|
||||
$median = $n % 2 ? $scores[intdiv($n, 2)] : ($scores[$n / 2 - 1] + $scores[$n / 2]) / 2;
|
||||
$p25 = $scores[(int) floor(0.25 * ($n - 1))];
|
||||
$p75 = $scores[(int) floor(0.75 * ($n - 1))];
|
||||
|
||||
$percentile = null; $position = null;
|
||||
if ($myScore !== null) {
|
||||
$below = count(array_filter($scores, fn($s) => $s < $myScore));
|
||||
$percentile = (int) round($below / $n * 100);
|
||||
$position = $myScore >= $p75 ? 'top_quartile'
|
||||
: ($myScore >= $median ? 'above_median'
|
||||
: ($myScore >= $p25 ? 'below_median' : 'bottom_quartile'));
|
||||
}
|
||||
|
||||
$this->jsonSuccess([
|
||||
'sector' => $sector,
|
||||
'sufficient_data' => true,
|
||||
'peers' => $n,
|
||||
'my_score' => $myScore !== null ? round($myScore, 1) : null,
|
||||
'sector_average' => round($avg, 1),
|
||||
'sector_median' => round($median, 1),
|
||||
'sector_min' => round($scores[0], 1),
|
||||
'sector_max' => round($scores[$n - 1], 1),
|
||||
'sector_p25' => round($p25, 1),
|
||||
'sector_p75' => round($p75, 1),
|
||||
'my_percentile' => $percentile,
|
||||
'my_position' => $position,
|
||||
'delta_vs_average' => $myScore !== null ? round($myScore - $avg, 1) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/upcoming-deadlines
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user