[FEAT] Modulo Controlli periodici (control monitoring §9.1/A.8.16) — v1.23.0

Registro controlli ricorrenti (owner/frequenza/metodo/controllo SoA-NIS2 collegato)
+ registro esecuzioni come EVIDENZA (data/esito/note) con avanzamento automatico
della scadenza; da esito non conforme generazione NC o NC+azione correttiva
(riuso non_conformities/capa_actions, nuova source 'monitoring'); report
scheda+storico stampabile; scadenze nel Calendario unico.

mig.057: periodic_controls + periodic_control_executions (FK CASCADE, UNIQUE
(organization_id, code)). PeriodicControlController (slug periodic-controls) +
controlli-periodici.html + CalendarController.srcPeriodicControls.

Fix da flotta di verifica avversariale (9 finding confermati):
- advance(): correzione overflow strtotime '+1 month' (31/01 -> 28/02, no salto
  mese) via DateTimeImmutable con clamp all'ultimo giorno valido (mensile/
  trimestrale/semestrale/annuale); giornaliero/settimanale/custom invariati.
- create(): retry-on-duplicate del codice CTL-NNN con UNIQUE (org_id, code).
- update(): array_key_exists per azzerare owner_role_id / next_due_date.
- delete(): rimossa cleanup morta su review_schedule (il calendario legge diretto).
- CalendarController.srcReviewSchedule: esclude internal_audit/stakeholder_activity
  (gia' con sorgente diretta) -> fix doppia comparsa nel calendario.
- calendario.html: TYPE_META 'periodic_control' (chip filtro + label + colore).

Additivo. Cache-buster ?v= bump (sidebar) + sw cache nis2-shell-v1.23.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-17 09:21:03 +02:00
co-authored by Claude Opus 4.8
parent 0738504fdd
commit 4fdeae7cee
52 changed files with 1043 additions and 169 deletions
+26 -2
View File
@@ -36,7 +36,7 @@ class CalendarController extends BaseController
'policy_review', 'risk_treatment', 'control_review',
'nc_target_close', 'capa_action', 'training_due',
'stakeholder_activity', 'review_schedule',
'internal_audit', 'management_review_decision',
'internal_audit', 'management_review_decision', 'periodic_control',
];
// ─────────────────────────────────────────────────────────────────────────
@@ -131,6 +131,7 @@ class CalendarController extends BaseController
'review_schedule' => fn() => $this->srcReviewSchedule($orgId, $from, $to),
'internal_audits' => fn() => $this->srcInternalAudits($orgId, $from, $to),
'mgmt_reviews' => fn() => $this->srcManagementReviewDecisions($orgId, $from, $to),
'periodic_controls'=> fn() => $this->srcPeriodicControls($orgId, $from, $to),
];
foreach ($sources as $rows) {
try {
@@ -328,10 +329,14 @@ class CalendarController extends BaseController
/** Scadenziario revisioni periodiche (A4 4.4): review_schedule.next_review_date. */
private function srcReviewSchedule(int $orgId, string $from, string $to): array
{
// Esclude i tipi che hanno già una sorgente diretta dedicata (internal_audit via
// srcInternalAudits, stakeholder_activity via srcStkActivities): altrimenti
// comparirebbero due volte nel calendario. Restano gli scheduler generici.
$rows = Database::fetchAll(
'SELECT id, title, entity_type, next_review_date, last_reviewed_at
FROM review_schedule
WHERE organization_id = ? AND next_review_date IS NOT NULL',
WHERE organization_id = ? AND next_review_date IS NOT NULL
AND entity_type NOT IN ("internal_audit", "stakeholder_activity")',
[$orgId]
);
$out = [];
@@ -363,6 +368,25 @@ class CalendarController extends BaseController
return array_values(array_filter($out));
}
/** Controlli periodici (mig.057, opzionale): periodic_controls.next_due_date. */
private function srcPeriodicControls(int $orgId, string $from, string $to): array
{
$rows = Database::fetchAll(
"SELECT id, code, title, next_due_date, status
FROM periodic_controls
WHERE organization_id = ? AND next_due_date IS NOT NULL AND status = 'active'",
[$orgId]
);
$out = [];
foreach ($rows as $r) {
$title = trim((string) $r['code'] . ' ' . (string) $r['title']);
$out[] = $this->makeEvent('periodic_controls', 'periodic_control',
'Controllo periodico: ' . $title, $r['next_due_date'],
'medium', 'periodic_control', (int) $r['id'], '/controlli-periodici.html', $from, $to, false);
}
return array_values(array_filter($out));
}
/**
* Decisioni del riesame di direzione (Modulo B, opzionale):
* management_review_decisions.due_date JOIN management_reviews per filtrare per org.