requireAuth(); $user = $this->getCurrentUser() ?? []; $uid = (int) $this->getCurrentUserId(); // Aziende visibili all'utente (stessa regola di OrganizationController::list): // super_admin = tutte; consulente = clienti del proprio studio (consulting_firm_id) // UNION le proprie membership; altri = solo le proprie membership. $firmId = (int) ($user['consulting_firm_id'] ?? 0); if (($user['role'] ?? '') === 'super_admin') { $orgs = Database::fetchAll( "SELECT id, name, entity_type, voluntary_compliance, sector FROM organizations WHERE is_active = 1 ORDER BY name LIMIT 200" ); } elseif (($user['role'] ?? '') === 'consultant' && $firmId > 0) { $orgs = Database::fetchAll( "SELECT DISTINCT o.id, o.name, o.entity_type, o.voluntary_compliance, o.sector FROM organizations o LEFT JOIN user_organizations uo ON uo.organization_id = o.id AND uo.user_id = ? WHERE o.is_active = 1 AND (o.consulting_firm_id = ? OR uo.user_id IS NOT NULL) ORDER BY o.name LIMIT 200", [$uid, $firmId] ); } else { $orgs = Database::fetchAll( "SELECT o.id, o.name, o.entity_type, o.voluntary_compliance, o.sector FROM organizations o JOIN user_organizations uo ON uo.organization_id = o.id WHERE uo.user_id = ? AND o.is_active = 1 ORDER BY o.name LIMIT 200", [$uid] ); } $now = date('Y-m-d H:i:s'); $companies = []; $allDeadlines = []; $sumScore = 0; $nScore = 0; $overdueTotal = 0; foreach ($orgs as $o) { $oid = (int) $o['id']; $asmt = Database::fetchOne( "SELECT overall_score FROM assessments WHERE organization_id = ? AND status = 'completed' ORDER BY completed_at DESC LIMIT 1", [$oid]); $score = ($asmt && $asmt['overall_score'] !== null) ? (int) round((float) $asmt['overall_score']) : null; $risks = (int) (Database::fetchOne( "SELECT COUNT(*) c FROM risks WHERE organization_id = ? AND status <> 'closed'", [$oid])['c'] ?? 0); $incs = (int) (Database::fetchOne( "SELECT COUNT(*) c FROM incidents WHERE organization_id = ? AND status NOT IN ('closed','post_mortem')", [$oid])['c'] ?? 0); $dls = $this->collectDeadlines($oid); $overdue = 0; foreach ($dls as $d) { $d['org_id'] = $oid; $d['org_name'] = $o['name']; $d['overdue'] = ($d['due_date'] < $now); if ($d['overdue']) { $overdue++; } $allDeadlines[] = $d; } if ($score !== null) { $sumScore += $score; $nScore++; } $overdueTotal += $overdue; $companies[] = [ 'id' => $oid, 'name' => $o['name'], 'entity_type' => $o['entity_type'], 'voluntary_compliance' => (int) $o['voluntary_compliance'], 'sector' => $o['sector'], 'compliance_score' => $score, 'open_risks' => $risks, 'open_incidents' => $incs, 'deadlines_overdue' => $overdue, 'deadlines_upcoming' => count($dls) - $overdue, ]; } usort($allDeadlines, fn($a, $b) => strcmp((string) $a['due_date'], (string) $b['due_date'])); $this->jsonSuccess([ 'companies' => $companies, 'deadlines' => $allDeadlines, 'kpis' => [ 'total_companies' => count($companies), 'avg_compliance' => $nScore ? (int) round($sumScore / $nScore) : null, 'overdue_total' => $overdueTotal, 'upcoming_total' => count($allDeadlines) - $overdueTotal, ], ]); } /** * Scadenze "aperte" di una org (incidenti CSIRT, policy, trattamenti rischio, * formazione) con orizzonte a +30 giorni ma includendo anche gli scaduti (past-due), * così il cruscotto distingue "in ritardo" da "in arrivo". Fonti coerenti con * DashboardController::deadlines(). */ private function collectDeadlines(int $orgId): array { $out = []; $horizon = "DATE_ADD(NOW(), INTERVAL 30 DAY)"; $inc = Database::fetchAll( "SELECT id, title, early_warning_due, early_warning_sent_at, notification_due, notification_sent_at, final_report_due, final_report_sent_at FROM incidents WHERE organization_id = ? AND is_significant = 1 AND status NOT IN ('closed','post_mortem')", [$orgId]); foreach ($inc as $i) { if ($i['early_warning_due'] && !$i['early_warning_sent_at']) $out[] = ['type'=>'incident_early_warning','title'=>"Early Warning: {$i['title']}",'due_date'=>$i['early_warning_due'],'severity'=>'critical','entity_type'=>'incident','entity_id'=>(int)$i['id']]; if ($i['notification_due'] && !$i['notification_sent_at']) $out[] = ['type'=>'incident_notification','title'=>"Notifica CSIRT: {$i['title']}",'due_date'=>$i['notification_due'],'severity'=>'high','entity_type'=>'incident','entity_id'=>(int)$i['id']]; if ($i['final_report_due'] && !$i['final_report_sent_at']) $out[] = ['type'=>'incident_final_report','title'=>"Report finale: {$i['title']}",'due_date'=>$i['final_report_due'],'severity'=>'medium','entity_type'=>'incident','entity_id'=>(int)$i['id']]; } foreach (Database::fetchAll( "SELECT id, title, next_review_date FROM policies WHERE organization_id = ? AND next_review_date IS NOT NULL AND next_review_date <= $horizon AND status NOT IN ('archived')", [$orgId]) as $p) { $out[] = ['type'=>'policy_review','title'=>"Revisione policy: {$p['title']}",'due_date'=>$p['next_review_date'],'severity'=>'medium','entity_type'=>'policy','entity_id'=>(int)$p['id']]; } foreach (Database::fetchAll( "SELECT rt.id, rt.due_date, r.title AS risk_title FROM risk_treatments rt JOIN risks r ON r.id = rt.risk_id WHERE r.organization_id = ? AND rt.status IN ('planned','in_progress') AND rt.due_date IS NOT NULL AND rt.due_date <= $horizon", [$orgId]) as $t) { $out[] = ['type'=>'risk_treatment','title'=>"Trattamento rischio: {$t['risk_title']}",'due_date'=>$t['due_date'],'severity'=>'medium','entity_type'=>'risk_treatment','entity_id'=>(int)$t['id']]; } foreach (Database::fetchAll( "SELECT ta.id, tc.title, ta.due_date FROM training_assignments ta JOIN training_courses tc ON tc.id = ta.course_id WHERE ta.organization_id = ? AND ta.status IN ('assigned','in_progress') AND ta.due_date IS NOT NULL AND ta.due_date <= $horizon", [$orgId]) as $tr) { $out[] = ['type'=>'training_due','title'=>"Formazione: {$tr['title']}",'due_date'=>$tr['due_date'],'severity'=>'low','entity_type'=>'training','entity_id'=>(int)$tr['id']]; } return $out; } }