[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:
co-authored by
Claude Opus 4.8
parent
0738504fdd
commit
4fdeae7cee
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user