[FEAT] A4 Fase 4.3 — Matrice RACI + link m2m: raci_assignments + procedure_inventory/procedure_risk/inventory_risk/risk_measure (mig.043), RaciController org-scoped (anti-IDOR, validazione object/measure), raci.html+raci.js (Bootstrap Italia/AGID), routing+sidebar+api+help+i18n. Build+review adversariale via workflow (0 finding critical/major). Cache-buster ?v=20260619.
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
05dfba6fcd
commit
b4d47d58a2
@@ -0,0 +1,498 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* NIS2 Agile - Matrice RACI + link m2m (A4 Fase 4.3)
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Hub relazionale che collega i RUOLI dell'organigramma (mig.041, org_roles)
|
||||||
|
* agli OGGETTI di compliance con una responsabilita RACI (R/A/C/I), e gestisce i
|
||||||
|
* collegamenti molti-a-molti fra procedure (policies), inventario (assets),
|
||||||
|
* rischi (risks) e misure ACN (catalogo JSON).
|
||||||
|
*
|
||||||
|
* Multi-tenancy ancorata a getCurrentOrgId(): OGNI query filtra organization_id.
|
||||||
|
* Scritture riservate a org_admin/compliance_manager (super_admin bypassa; il
|
||||||
|
* demo guard gestisce il read-only). Anti-IDOR: ogni id referenziato (ruolo,
|
||||||
|
* asset, policy, risk, supplier) e verificato come appartenente all'org corrente
|
||||||
|
* prima di insert (fetchRoleOrFail / assertObjectExists, org-scoped).
|
||||||
|
*
|
||||||
|
* Ancoraggio normativo (docs/DESIGN_A4_RELATIONAL.md):
|
||||||
|
* - GV.RR-02 (NIST CSF 2.0): ruoli, responsabilita e poteri stabiliti/comunicati
|
||||||
|
* - ID.AM (asset management): inventario; GV.PO (governance): procedure
|
||||||
|
* - art. 24 D.Lgs. 138/2024 + ID.RA (risk assessment): rischi
|
||||||
|
*
|
||||||
|
* NOTE strutturali (verificate sul codice reale):
|
||||||
|
* - 'inventario' = `assets`; 'procedure' = `policies`; rischi = `risks`;
|
||||||
|
* fornitori = `suppliers`. Label: assets.name, policies.title, risks.title,
|
||||||
|
* suppliers.name, org_roles.role_name.
|
||||||
|
* - misure ACN = application/data/acn_measures.json (campo 'code'); NON e una
|
||||||
|
* tabella DB -> risk_measure.measure_code e una stringa validata sul JSON.
|
||||||
|
* - raci_assignments.object_id e POLIMORFICO -> niente FK; esistenza verificata
|
||||||
|
* in PHP. I nomi tabella/colonna provengono SEMPRE dalle const-map qui sotto
|
||||||
|
* dopo validazione enum (mai da stringhe grezze della request).
|
||||||
|
* - Le AZIONI sono capa_actions (figlie di non_conformities): 4.3 non le usa.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/BaseController.php';
|
||||||
|
|
||||||
|
class RaciController extends BaseController
|
||||||
|
{
|
||||||
|
private const MANAGE_ROLES = ['org_admin', 'compliance_manager'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* object_type => [tabella, colonna_label, ha_soft_delete]
|
||||||
|
* I nomi sono hard-coded: dopo il gate enum sono sicuri da interpolare.
|
||||||
|
*/
|
||||||
|
private const OBJECT_MAP = [
|
||||||
|
'inventory' => ['table' => 'assets', 'label' => 'name', 'soft_delete' => false],
|
||||||
|
'procedure' => ['table' => 'policies', 'label' => 'title', 'soft_delete' => true],
|
||||||
|
'risk' => ['table' => 'risks', 'label' => 'title', 'soft_delete' => true],
|
||||||
|
'supplier' => ['table' => 'suppliers', 'label' => 'name', 'soft_delete' => true],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* link_type => [tabella, a_col, a_table, a_label, b_col, b_table, b_label]
|
||||||
|
* Per risk_measure il lato b e un codice ACN (stringa): b_table=null.
|
||||||
|
*/
|
||||||
|
private const LINK_MAP = [
|
||||||
|
'procedure_inventory' => [
|
||||||
|
'table' => 'procedure_inventory',
|
||||||
|
'a_col' => 'policy_id', 'a_table' => 'policies', 'a_label' => 'title', 'a_soft' => true,
|
||||||
|
'b_col' => 'asset_id', 'b_table' => 'assets', 'b_label' => 'name', 'b_soft' => false,
|
||||||
|
],
|
||||||
|
'procedure_risk' => [
|
||||||
|
'table' => 'procedure_risk',
|
||||||
|
'a_col' => 'policy_id', 'a_table' => 'policies', 'a_label' => 'title', 'a_soft' => true,
|
||||||
|
'b_col' => 'risk_id', 'b_table' => 'risks', 'b_label' => 'title', 'b_soft' => true,
|
||||||
|
],
|
||||||
|
'inventory_risk' => [
|
||||||
|
'table' => 'inventory_risk',
|
||||||
|
'a_col' => 'asset_id', 'a_table' => 'assets', 'a_label' => 'name', 'a_soft' => false,
|
||||||
|
'b_col' => 'risk_id', 'b_table' => 'risks', 'b_label' => 'title', 'b_soft' => true,
|
||||||
|
],
|
||||||
|
'risk_measure' => [
|
||||||
|
'table' => 'risk_measure',
|
||||||
|
'a_col' => 'risk_id', 'a_table' => 'risks', 'a_label' => 'title', 'a_soft' => true,
|
||||||
|
'b_col' => 'measure_code', 'b_table' => null, 'b_label' => null, 'b_soft' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<string,array>|null cache del catalogo misure ACN */
|
||||||
|
private static $measuresCache = null;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// MATRICE RACI
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/raci/matrix
|
||||||
|
* { roles:[{role_id, role_name, is_governance_body, holder_user_id}],
|
||||||
|
* objects:[{type, id, label}], assignments:[{role_id, object_type, object_id, raci}] }
|
||||||
|
*/
|
||||||
|
public function matrix(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
|
||||||
|
$roleRows = Database::fetchAll(
|
||||||
|
'SELECT id, role_name, is_governance_body, holder_user_id
|
||||||
|
FROM org_roles WHERE organization_id = ?
|
||||||
|
ORDER BY sort_order, role_name',
|
||||||
|
[$orgId]
|
||||||
|
);
|
||||||
|
$roles = array_map(fn($r) => [
|
||||||
|
'role_id' => (int) $r['id'],
|
||||||
|
'role_name' => $r['role_name'],
|
||||||
|
'is_governance_body' => (bool) $r['is_governance_body'],
|
||||||
|
'holder_user_id' => $r['holder_user_id'] !== null ? (int) $r['holder_user_id'] : null,
|
||||||
|
], $roleRows);
|
||||||
|
|
||||||
|
// Oggetti linkabili: union dei 4 tipi, org-scoped, ordinati per tipo+label.
|
||||||
|
$objects = [];
|
||||||
|
foreach (self::OBJECT_MAP as $type => $cfg) {
|
||||||
|
foreach ($this->fetchObjectsForType($type, $orgId) as $o) {
|
||||||
|
$objects[] = ['type' => $type, 'id' => $o['id'], 'label' => $o['label']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignRows = Database::fetchAll(
|
||||||
|
'SELECT role_id, object_type, object_id, raci
|
||||||
|
FROM raci_assignments WHERE organization_id = ?',
|
||||||
|
[$orgId]
|
||||||
|
);
|
||||||
|
$assignments = array_map(fn($a) => [
|
||||||
|
'role_id' => (int) $a['role_id'],
|
||||||
|
'object_type' => $a['object_type'],
|
||||||
|
'object_id' => (int) $a['object_id'],
|
||||||
|
'raci' => $a['raci'],
|
||||||
|
], $assignRows);
|
||||||
|
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'roles' => $roles,
|
||||||
|
'objects' => $objects,
|
||||||
|
'assignments' => $assignments,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/raci/assign — {role_id, object_type, object_id, raci}
|
||||||
|
* Upsert: una sola RACI per (role_id, object_type, object_id) nell'org.
|
||||||
|
*/
|
||||||
|
public function assign(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||||
|
$this->validateRequired(['role_id', 'object_type', 'object_id', 'raci']);
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
|
||||||
|
$roleId = (int) $this->getParam('role_id');
|
||||||
|
$type = $this->validateObjectType($this->getParam('object_type'));
|
||||||
|
$objId = (int) $this->getParam('object_id');
|
||||||
|
$raci = $this->validateRaci($this->getParam('raci'));
|
||||||
|
|
||||||
|
$this->fetchRoleOrFail($roleId);
|
||||||
|
$this->assertObjectExists($type, $objId);
|
||||||
|
|
||||||
|
$existing = Database::fetchOne(
|
||||||
|
'SELECT id FROM raci_assignments
|
||||||
|
WHERE organization_id = ? AND role_id = ? AND object_type = ? AND object_id = ?',
|
||||||
|
[$orgId, $roleId, $type, $objId]
|
||||||
|
);
|
||||||
|
if ($existing) {
|
||||||
|
$id = (int) $existing['id'];
|
||||||
|
Database::update('raci_assignments', ['raci' => $raci], 'id = ?', [$id]);
|
||||||
|
$created = false;
|
||||||
|
} else {
|
||||||
|
$id = Database::insert('raci_assignments', [
|
||||||
|
'organization_id' => $orgId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'object_type' => $type,
|
||||||
|
'object_id' => $objId,
|
||||||
|
'raci' => $raci,
|
||||||
|
'created_by' => $this->getCurrentUserId(),
|
||||||
|
]);
|
||||||
|
$created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logAudit('raci_assigned', 'raci_assignment', $id, [
|
||||||
|
'role_id' => $roleId, 'object_type' => $type, 'object_id' => $objId, 'raci' => $raci,
|
||||||
|
]);
|
||||||
|
$this->jsonSuccess(['id' => $id, 'raci' => $raci], $created ? 'RACI assegnata' : 'RACI aggiornata', $created ? 201 : 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/raci/assign?role_id=&object_type=&object_id=
|
||||||
|
* (parametri via query string: il DELETE di api.js non porta body.)
|
||||||
|
*/
|
||||||
|
public function unassign(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||||
|
$this->validateRequired(['role_id', 'object_type', 'object_id']);
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
|
||||||
|
$roleId = (int) $this->getParam('role_id');
|
||||||
|
$type = $this->validateObjectType($this->getParam('object_type'));
|
||||||
|
$objId = (int) $this->getParam('object_id');
|
||||||
|
|
||||||
|
$count = Database::count(
|
||||||
|
'raci_assignments',
|
||||||
|
'organization_id = ? AND role_id = ? AND object_type = ? AND object_id = ?',
|
||||||
|
[$orgId, $roleId, $type, $objId]
|
||||||
|
);
|
||||||
|
if ($count === 0) {
|
||||||
|
$this->jsonError('Assegnazione RACI non trovata', 404, 'RACI_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::delete(
|
||||||
|
'raci_assignments',
|
||||||
|
'organization_id = ? AND role_id = ? AND object_type = ? AND object_id = ?',
|
||||||
|
[$orgId, $roleId, $type, $objId]
|
||||||
|
);
|
||||||
|
$this->logAudit('raci_unassigned', 'raci_assignment', null, [
|
||||||
|
'role_id' => $roleId, 'object_type' => $type, 'object_id' => $objId,
|
||||||
|
]);
|
||||||
|
$this->jsonSuccess(null, 'RACI rimossa');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// OGGETTI LINKABILI (select)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/raci/objects?type=
|
||||||
|
* type in {inventory,procedure,risk,supplier} -> [{id, label}] dell'org;
|
||||||
|
* type=measure -> misure ACN dal catalogo JSON [{code, label}].
|
||||||
|
*/
|
||||||
|
public function objects(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$type = (string) $this->getParam('type', '');
|
||||||
|
|
||||||
|
if ($type === 'measure') {
|
||||||
|
$out = [];
|
||||||
|
foreach ($this->loadMeasures() as $m) {
|
||||||
|
$code = (string) ($m['code'] ?? '');
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = ['code' => $code, 'label' => $code . ' — ' . ($m['title'] ?? '')];
|
||||||
|
}
|
||||||
|
$this->jsonSuccess($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $this->validateObjectType($type);
|
||||||
|
$this->jsonSuccess($this->fetchObjectsForType($type, $this->getCurrentOrgId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// LINK MOLTI-A-MOLTI
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/raci/links?link_type=&id=
|
||||||
|
* $id = id del lato "a" (policy/asset/risk). Ritorna i lati "b" collegati con label.
|
||||||
|
*/
|
||||||
|
public function links(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$linkType = $this->validateLinkType($this->getParam('link_type'));
|
||||||
|
$aId = (int) $this->getParam('id');
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
$cfg = self::LINK_MAP[$linkType];
|
||||||
|
|
||||||
|
if ($linkType === 'risk_measure') {
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
'SELECT id AS link_id, measure_code FROM risk_measure
|
||||||
|
WHERE organization_id = ? AND risk_id = ? ORDER BY measure_code',
|
||||||
|
[$orgId, $aId]
|
||||||
|
);
|
||||||
|
$out = array_map(fn($r) => [
|
||||||
|
'link_id' => (int) $r['link_id'],
|
||||||
|
'measure_code' => $r['measure_code'],
|
||||||
|
'measure_label' => $this->measureLabel($r['measure_code']),
|
||||||
|
], $rows);
|
||||||
|
$this->jsonSuccess($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join sul lato b per la label. Nomi tabella/colonna dalla const-map (sicuri).
|
||||||
|
$bTable = $cfg['b_table'];
|
||||||
|
$bCol = $cfg['b_col'];
|
||||||
|
$bLabel = $cfg['b_label'];
|
||||||
|
$linkTbl = $cfg['table'];
|
||||||
|
$aCol = $cfg['a_col'];
|
||||||
|
|
||||||
|
$bSoftClause = $cfg['b_soft'] ? ' AND b.deleted_at IS NULL' : '';
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT l.id AS link_id, l.{$bCol} AS b_id, b.{$bLabel} AS b_label
|
||||||
|
FROM {$linkTbl} l
|
||||||
|
JOIN {$bTable} b ON b.id = l.{$bCol}
|
||||||
|
WHERE l.organization_id = ? AND l.{$aCol} = ?{$bSoftClause}
|
||||||
|
ORDER BY b.{$bLabel}",
|
||||||
|
[$orgId, $aId]
|
||||||
|
);
|
||||||
|
$out = array_map(fn($r) => [
|
||||||
|
'link_id' => (int) $r['link_id'],
|
||||||
|
'b_id' => (int) $r['b_id'],
|
||||||
|
'b_label' => $r['b_label'],
|
||||||
|
], $rows);
|
||||||
|
$this->jsonSuccess($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/raci/link — {link_type, a_id, b_id}
|
||||||
|
* Idempotente: se il link esiste gia ritorna 200 {existed:true}.
|
||||||
|
* Per risk_measure il lato b e il codice misura (stringa, validato sul JSON).
|
||||||
|
*/
|
||||||
|
public function link(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||||
|
$this->validateRequired(['link_type', 'a_id', 'b_id']);
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
|
||||||
|
$linkType = $this->validateLinkType($this->getParam('link_type'));
|
||||||
|
$cfg = self::LINK_MAP[$linkType];
|
||||||
|
$aId = (int) $this->getParam('a_id');
|
||||||
|
|
||||||
|
// Lato a: sempre un oggetto org-scoped.
|
||||||
|
$this->assertRowExists($cfg['a_table'], $aId, $orgId, $cfg['a_soft']);
|
||||||
|
|
||||||
|
if ($linkType === 'risk_measure') {
|
||||||
|
$code = trim((string) $this->getParam('b_id'));
|
||||||
|
$this->assertMeasureExists($code);
|
||||||
|
$existing = Database::fetchOne(
|
||||||
|
'SELECT id FROM risk_measure WHERE organization_id = ? AND risk_id = ? AND measure_code = ?',
|
||||||
|
[$orgId, $aId, $code]
|
||||||
|
);
|
||||||
|
if ($existing) {
|
||||||
|
$this->jsonSuccess(['id' => (int) $existing['id'], 'existed' => true], 'Collegamento gia presente', 200);
|
||||||
|
}
|
||||||
|
$id = Database::insert('risk_measure', [
|
||||||
|
'organization_id' => $orgId,
|
||||||
|
'risk_id' => $aId,
|
||||||
|
'measure_code' => $code,
|
||||||
|
'created_by' => $this->getCurrentUserId(),
|
||||||
|
]);
|
||||||
|
$this->logAudit('raci_link_created', 'risk_measure', $id, ['risk_id' => $aId, 'measure_code' => $code]);
|
||||||
|
$this->jsonSuccess(['id' => $id, 'existed' => false], 'Collegamento creato', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lato b numerico (asset/risk), org-scoped.
|
||||||
|
$bId = (int) $this->getParam('b_id');
|
||||||
|
$this->assertRowExists($cfg['b_table'], $bId, $orgId, $cfg['b_soft']);
|
||||||
|
|
||||||
|
$existing = Database::fetchOne(
|
||||||
|
"SELECT id FROM {$cfg['table']} WHERE organization_id = ? AND {$cfg['a_col']} = ? AND {$cfg['b_col']} = ?",
|
||||||
|
[$orgId, $aId, $bId]
|
||||||
|
);
|
||||||
|
if ($existing) {
|
||||||
|
$this->jsonSuccess(['id' => (int) $existing['id'], 'existed' => true], 'Collegamento gia presente', 200);
|
||||||
|
}
|
||||||
|
$id = Database::insert($cfg['table'], [
|
||||||
|
'organization_id' => $orgId,
|
||||||
|
$cfg['a_col'] => $aId,
|
||||||
|
$cfg['b_col'] => $bId,
|
||||||
|
'created_by' => $this->getCurrentUserId(),
|
||||||
|
]);
|
||||||
|
$this->logAudit('raci_link_created', $cfg['table'], $id, [$cfg['a_col'] => $aId, $cfg['b_col'] => $bId]);
|
||||||
|
$this->jsonSuccess(['id' => $id, 'existed' => false], 'Collegamento creato', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/raci/link?link_type=&a_id=&b_id=
|
||||||
|
* (parametri via query string.) Per risk_measure b_id = codice misura.
|
||||||
|
*/
|
||||||
|
public function unlink(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||||
|
$this->validateRequired(['link_type', 'a_id', 'b_id']);
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
|
||||||
|
$linkType = $this->validateLinkType($this->getParam('link_type'));
|
||||||
|
$cfg = self::LINK_MAP[$linkType];
|
||||||
|
$aId = (int) $this->getParam('a_id');
|
||||||
|
|
||||||
|
if ($linkType === 'risk_measure') {
|
||||||
|
$code = trim((string) $this->getParam('b_id'));
|
||||||
|
$where = 'organization_id = ? AND risk_id = ? AND measure_code = ?';
|
||||||
|
$params = [$orgId, $aId, $code];
|
||||||
|
} else {
|
||||||
|
$bId = (int) $this->getParam('b_id');
|
||||||
|
$where = "organization_id = ? AND {$cfg['a_col']} = ? AND {$cfg['b_col']} = ?";
|
||||||
|
$params = [$orgId, $aId, $bId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Database::count($cfg['table'], $where, $params) === 0) {
|
||||||
|
$this->jsonError('Collegamento non trovato', 404, 'LINK_NOT_FOUND');
|
||||||
|
}
|
||||||
|
Database::delete($cfg['table'], $where, $params);
|
||||||
|
$this->logAudit('raci_link_removed', $cfg['table'], null, ['link_type' => $linkType, 'a_id' => $aId]);
|
||||||
|
$this->jsonSuccess(null, 'Collegamento rimosso');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// PRIVATI
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Elenco oggetti di un tipo per l'org corrente: [{id, label}] (esclude soft-deleted). */
|
||||||
|
private function fetchObjectsForType(string $type, int $orgId): array
|
||||||
|
{
|
||||||
|
$cfg = self::OBJECT_MAP[$type];
|
||||||
|
$table = $cfg['table'];
|
||||||
|
$label = $cfg['label'];
|
||||||
|
$softClause = $cfg['soft_delete'] ? ' AND deleted_at IS NULL' : '';
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT id, {$label} AS label FROM {$table}
|
||||||
|
WHERE organization_id = ?{$softClause} ORDER BY {$label}",
|
||||||
|
[$orgId]
|
||||||
|
);
|
||||||
|
return array_map(fn($r) => ['id' => (int) $r['id'], 'label' => $r['label']], $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruolo dell'organigramma nell'org corrente oppure 404 (anti-IDOR). */
|
||||||
|
private function fetchRoleOrFail(int $id): array
|
||||||
|
{
|
||||||
|
$r = Database::fetchOne('SELECT * FROM org_roles WHERE id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
||||||
|
if (!$r) {
|
||||||
|
$this->jsonError('Ruolo non trovato', 404, 'ROLE_NOT_FOUND');
|
||||||
|
}
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verifica che l'oggetto (assets/policies/risks/suppliers) esista nell'org. */
|
||||||
|
private function assertObjectExists(string $type, int $id): void
|
||||||
|
{
|
||||||
|
$cfg = self::OBJECT_MAP[$type];
|
||||||
|
$this->assertRowExists($cfg['table'], $id, $this->getCurrentOrgId(), $cfg['soft_delete']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verifica generica di esistenza riga org-scoped (tabella dalla const-map). */
|
||||||
|
private function assertRowExists(string $table, int $id, int $orgId, bool $soft): void
|
||||||
|
{
|
||||||
|
$where = 'id = ? AND organization_id = ?';
|
||||||
|
$params = [$id, $orgId];
|
||||||
|
if ($soft) {
|
||||||
|
$where .= ' AND deleted_at IS NULL';
|
||||||
|
}
|
||||||
|
if (Database::count($table, $where, $params) === 0) {
|
||||||
|
$this->jsonError('Oggetto non valido per questa organizzazione', 422, 'INVALID_OBJECT');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateObjectType($t): string
|
||||||
|
{
|
||||||
|
$t = (string) $t;
|
||||||
|
if (!isset(self::OBJECT_MAP[$t])) {
|
||||||
|
$this->jsonError('Tipo oggetto non valido', 422, 'INVALID_OBJECT_TYPE');
|
||||||
|
}
|
||||||
|
return $t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateRaci($r): string
|
||||||
|
{
|
||||||
|
$r = strtoupper((string) $r);
|
||||||
|
if (!in_array($r, ['R', 'A', 'C', 'I'], true)) {
|
||||||
|
$this->jsonError('Valore RACI non valido (atteso R/A/C/I)', 422, 'INVALID_RACI');
|
||||||
|
}
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateLinkType($lt): string
|
||||||
|
{
|
||||||
|
$lt = (string) $lt;
|
||||||
|
if (!isset(self::LINK_MAP[$lt])) {
|
||||||
|
$this->jsonError('Tipo di collegamento non valido', 422, 'INVALID_LINK_TYPE');
|
||||||
|
}
|
||||||
|
return $lt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Carica e memoizza il catalogo misure ACN dal JSON. */
|
||||||
|
private function loadMeasures(): array
|
||||||
|
{
|
||||||
|
if (self::$measuresCache === null) {
|
||||||
|
$path = APP_PATH . '/data/acn_measures.json';
|
||||||
|
$raw = is_readable($path) ? file_get_contents($path) : false;
|
||||||
|
$data = $raw !== false ? json_decode($raw, true) : null;
|
||||||
|
self::$measuresCache = is_array($data) && isset($data['measures']) && is_array($data['measures'])
|
||||||
|
? $data['measures'] : [];
|
||||||
|
}
|
||||||
|
return self::$measuresCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verifica che il codice misura esista nel catalogo ACN. */
|
||||||
|
private function assertMeasureExists(string $code): void
|
||||||
|
{
|
||||||
|
foreach ($this->loadMeasures() as $m) {
|
||||||
|
if ((string) ($m['code'] ?? '') === $code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->jsonError('Misura ACN non valida', 422, 'INVALID_MEASURE');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Label leggibile per un codice misura ('CODE — titolo'); fallback al solo codice. */
|
||||||
|
private function measureLabel(string $code): string
|
||||||
|
{
|
||||||
|
foreach ($this->loadMeasures() as $m) {
|
||||||
|
if ((string) ($m['code'] ?? '') === $code) {
|
||||||
|
return $code . ' — ' . ($m['title'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Migration 043 — A4 Fase 4.3: Matrice RACI + link molti-a-molti tra
|
||||||
|
-- inventario (assets), procedure (policies), rischi (risks) e misure ACN.
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 5 tabelle ADDITIVE. Idempotente (CREATE TABLE IF NOT EXISTS). Applicare con la
|
||||||
|
-- STESSA connessione PDO dell'app (container nis2-db via TCP+TLS), via runner.
|
||||||
|
--
|
||||||
|
-- Ancoraggio normativo (docs/DESIGN_A4_RELATIONAL.md): la matrice RACI collega i
|
||||||
|
-- RUOLI dell'organigramma (mig.041, org_roles) agli OGGETTI di compliance
|
||||||
|
-- (inventario/procedure/rischi/fornitori) con responsabilita R/A/C/I.
|
||||||
|
-- - GV.RR-02 (NIST CSF 2.0): ruoli, responsabilita e poteri stabiliti/comunicati
|
||||||
|
-- - ID.AM (asset management): inventario dei sistemi rilevanti
|
||||||
|
-- - GV.PO (policy/governance): procedure
|
||||||
|
-- - art. 24 D.Lgs. 138/2024 + ID.RA (risk assessment): rischi
|
||||||
|
--
|
||||||
|
-- NOTE strutturali (verificate sul codice reale):
|
||||||
|
-- - 'inventario' nel prodotto = tabella `assets` (mig.001 riga 363).
|
||||||
|
-- - 'procedure' = tabella `policies` (mig.001 riga 256).
|
||||||
|
-- - rischi = `risks` (riga 141); fornitori = `suppliers` (riga 286).
|
||||||
|
-- - org_roles = mig.041 (id + organization_id).
|
||||||
|
-- - misure ACN = catalogo JSON application/data/acn_measures.json (campo 'code',
|
||||||
|
-- es. GV.OC-04) — NON e una tabella DB: 'risk_measure.measure_code' NON ha FK.
|
||||||
|
-- - raci_assignments.object_id e POLIMORFICO (punta a assets/policies/risks/
|
||||||
|
-- suppliers a seconda di object_type) -> NON puo avere FK; l'esistenza e
|
||||||
|
-- verificata in PHP (RaciController::assertObjectExists), org-scoped.
|
||||||
|
--
|
||||||
|
-- Multi-tenancy: ogni tabella ha organization_id NOT NULL (FK organizations,
|
||||||
|
-- ON DELETE CASCADE) e ogni query del controller filtra organization_id.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Assegnazione RACI: per ogni (ruolo, oggetto) una sola responsabilita R/A/C/I.
|
||||||
|
-- object_id e polimorfico (vedi nota sopra) -> nessuna FK su object_id.
|
||||||
|
CREATE TABLE IF NOT EXISTS raci_assignments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
role_id INT NOT NULL,
|
||||||
|
object_type ENUM('inventory','procedure','risk','supplier') NOT NULL,
|
||||||
|
object_id INT NOT NULL,
|
||||||
|
raci ENUM('R','A','C','I') NOT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES org_roles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_raci (role_id, object_type, object_id),
|
||||||
|
INDEX idx_raci_org (organization_id),
|
||||||
|
INDEX idx_raci_object (object_type, object_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Link m2m: procedura (policy) <-> inventario (asset).
|
||||||
|
CREATE TABLE IF NOT EXISTS procedure_inventory (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
policy_id INT NOT NULL,
|
||||||
|
asset_id INT NOT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_proc_inv (policy_id, asset_id),
|
||||||
|
INDEX idx_pi_org (organization_id),
|
||||||
|
INDEX idx_pi_asset (asset_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Link m2m: procedura (policy) <-> rischio (risk).
|
||||||
|
CREATE TABLE IF NOT EXISTS procedure_risk (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
policy_id INT NOT NULL,
|
||||||
|
risk_id INT NOT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (risk_id) REFERENCES risks(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_proc_risk (policy_id, risk_id),
|
||||||
|
INDEX idx_pr_org (organization_id),
|
||||||
|
INDEX idx_pr_risk (risk_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Link m2m: inventario (asset) <-> rischio (risk).
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory_risk (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
asset_id INT NOT NULL,
|
||||||
|
risk_id INT NOT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (risk_id) REFERENCES risks(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_inv_risk (asset_id, risk_id),
|
||||||
|
INDEX idx_ir_org (organization_id),
|
||||||
|
INDEX idx_ir_risk (risk_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Link m2m: rischio (risk) <-> misura ACN (codice del catalogo JSON, non FK).
|
||||||
|
CREATE TABLE IF NOT EXISTS risk_measure (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
risk_id INT NOT NULL,
|
||||||
|
measure_code VARCHAR(20) NOT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (risk_id) REFERENCES risks(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_risk_measure (risk_id, measure_code),
|
||||||
|
INDEX idx_rm_org (organization_id),
|
||||||
|
INDEX idx_rm_measure (measure_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script src="js/common-bi.js?v=20260618"></script>
|
<script src="js/common-bi.js?v=20260618"></script>
|
||||||
<script>
|
<script>
|
||||||
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
||||||
|
|||||||
+4
-4
@@ -152,8 +152,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
'use strict';
|
'use strict';
|
||||||
const FUNC_LABELS = {
|
const FUNC_LABELS = {
|
||||||
|
|||||||
@@ -317,8 +317,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -147,8 +147,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../js/api.js?v=20260618"></script>
|
<script src="../js/api.js?v=20260619"></script>
|
||||||
<script src="../js/common.js?v=20260618"></script>
|
<script src="../js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="../js/common-bi.js?v=20260618"></script>
|
<script src="../js/common-bi.js?v=20260618"></script>
|
||||||
<script src="../js/i18n.js?v=20260618"></script>
|
<script src="../js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -169,8 +169,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../js/api.js?v=20260618"></script>
|
<script src="../js/api.js?v=20260619"></script>
|
||||||
<script src="../js/common.js?v=20260618"></script>
|
<script src="../js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="../js/common-bi.js?v=20260618"></script>
|
<script src="../js/common-bi.js?v=20260618"></script>
|
||||||
<script src="../js/i18n.js?v=20260618"></script>
|
<script src="../js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -1088,10 +1088,10 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script src="js/i18n.js?v=20260618"></script>
|
<script src="js/i18n.js?v=20260619"></script>
|
||||||
<script src="js/help.js?v=20260618"></script>
|
<script src="js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -154,8 +154,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -166,8 +166,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+4
-4
@@ -366,8 +366,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -378,8 +378,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -349,14 +349,14 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
||||||
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
||||||
<script src="js/common-bi.js?v=20260618"></script>
|
<script src="js/common-bi.js?v=20260618"></script>
|
||||||
<script src="js/i18n.js?v=20260618"></script>
|
<script src="js/i18n.js?v=20260619"></script>
|
||||||
<script src="js/help.js?v=20260618"></script>
|
<script src="js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
|||||||
@@ -183,12 +183,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script src="/js/competenze.js?v=20260617"></script>
|
<script src="/js/competenze.js?v=20260617"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -382,8 +382,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
|
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
|
||||||
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
|
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -155,8 +155,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
|
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
const form = document.getElementById('forgot-form');
|
const form = document.getElementById('forgot-form');
|
||||||
const err = document.getElementById('err');
|
const err = document.getElementById('err');
|
||||||
|
|||||||
+4
-4
@@ -1142,8 +1142,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
@@ -1153,8 +1153,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
||||||
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
||||||
|
|||||||
@@ -351,8 +351,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -363,8 +363,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ──────────────────────────────────────────────
|
// ── Auth & Init ──────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ $controllerMap = [
|
|||||||
'organizations' => 'OrganizationController',
|
'organizations' => 'OrganizationController',
|
||||||
'org-roles' => 'OrgRoleController', // A4 Fase 4.1 — Organigramma (ruoli/gerarchia, Art.23 governance)
|
'org-roles' => 'OrgRoleController', // A4 Fase 4.1 — Organigramma (ruoli/gerarchia, Art.23 governance)
|
||||||
'competences' => 'CompetenceController', // A4 Fase 4.2 — Competenze (skill, requisiti ruolo, gap, alert->azione)
|
'competences' => 'CompetenceController', // A4 Fase 4.2 — Competenze (skill, requisiti ruolo, gap, alert->azione)
|
||||||
|
'raci' => 'RaciController', // A4 Fase 4.3 — Matrice RACI + link m2m (procedure/inventario/rischi/misure)
|
||||||
'assessments' => 'AssessmentController',
|
'assessments' => 'AssessmentController',
|
||||||
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
||||||
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
||||||
@@ -223,6 +224,17 @@ $actionMap = [
|
|||||||
'POST:openAction' => 'openAction',
|
'POST:openAction' => 'openAction',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ── RaciController — Matrice RACI + link m2m (A4 Fase 4.3) ──
|
||||||
|
'raci' => [
|
||||||
|
'GET:matrix' => 'matrix',
|
||||||
|
'POST:assign' => 'assign',
|
||||||
|
'DELETE:assign' => 'unassign',
|
||||||
|
'GET:objects' => 'objects',
|
||||||
|
'GET:links' => 'links',
|
||||||
|
'POST:link' => 'link',
|
||||||
|
'DELETE:link' => 'unlink',
|
||||||
|
],
|
||||||
|
|
||||||
// ── AssessmentController ────────────────────────
|
// ── AssessmentController ────────────────────────
|
||||||
'assessments' => [
|
'assessments' => [
|
||||||
'GET:list' => 'list',
|
'GET:list' => 'list',
|
||||||
|
|||||||
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script src="js/i18n.js?v=20260618"></script>
|
<script src="js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
function showTab(id) {
|
function showTab(id) {
|
||||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||||
|
|||||||
+4
-4
@@ -184,8 +184,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -196,8 +196,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script src="/js/isms.js"></script>
|
<script src="/js/isms.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -254,6 +254,22 @@ class NIS2API {
|
|||||||
compGapGrid() { return this._acn(this.get('/competences/gap-grid')); }
|
compGapGrid() { return this._acn(this.get('/competences/gap-grid')); }
|
||||||
compOpenAction(roleSkillId) { return this._acn(this.post('/competences/open-action', { role_skill_id: roleSkillId })); }
|
compOpenAction(roleSkillId) { return this._acn(this.post('/competences/open-action', { role_skill_id: roleSkillId })); }
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Matrice RACI (A4 Fase 4.3) — hub ruoli↔oggetti (R/A/C/I) + link m2m
|
||||||
|
// (procedura↔inventario, procedura↔rischio, inventario↔rischio, rischio↔misura).
|
||||||
|
// Stesso contratto _acn: ritornano `data`, lanciano su success=false.
|
||||||
|
// NB: i DELETE non hanno body (del() in request() invia body solo per POST/PUT),
|
||||||
|
// quindi i parametri compositi vanno passati come query string → il backend
|
||||||
|
// li legge da getParam() ($_REQUEST).
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
raciMatrix() { return this._acn(this.get('/raci/matrix')); }
|
||||||
|
raciAssign(d) { return this._acn(this.post('/raci/assign', d)); }
|
||||||
|
raciUnassign(d) { return this._acn(this.del('/raci/assign?role_id=' + encodeURIComponent(d.role_id) + '&object_type=' + encodeURIComponent(d.object_type) + '&object_id=' + encodeURIComponent(d.object_id))); }
|
||||||
|
raciObjects(type) { return this._acn(this.get('/raci/objects?type=' + encodeURIComponent(type))); }
|
||||||
|
raciLinks(linkType, id) { return this._acn(this.get('/raci/links?link_type=' + encodeURIComponent(linkType) + '&id=' + encodeURIComponent(id))); }
|
||||||
|
raciLink(d) { return this._acn(this.post('/raci/link', d)); }
|
||||||
|
raciUnlink(d) { return this._acn(this.del('/raci/link?link_type=' + encodeURIComponent(d.link_type) + '&a_id=' + encodeURIComponent(d.a_id) + '&b_id=' + encodeURIComponent(d.b_id))); }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// Dashboard
|
// Dashboard
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ function loadSidebar() {
|
|||||||
items: [
|
items: [
|
||||||
{ name: 'Organigramma', href: 'organigramma.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>`, i18nKey: 'nav.org_chart' },
|
{ name: 'Organigramma', href: 'organigramma.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>`, i18nKey: 'nav.org_chart' },
|
||||||
{ name: 'Competenze', href: 'competenze.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>`, i18nKey: 'nav.competences' },
|
{ name: 'Competenze', href: 'competenze.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>`, i18nKey: 'nav.competences' },
|
||||||
|
{ name: 'Matrice RACI', href: 'raci.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 6a1 1 0 011-1h5a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zm9 0a1 1 0 011-1h3a1 1 0 011 1v6a1 1 0 01-1 1h-3a1 1 0 01-1-1v-6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.raci' },
|
||||||
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
|
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
|
||||||
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
|
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
|
||||||
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
|
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
|
||||||
|
|||||||
@@ -276,6 +276,47 @@ const HelpSystem = (function () {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ─── Matrice RACI (A4 Fase 4.3) ──────────────────────────────
|
||||||
|
'raci': {
|
||||||
|
title: 'Guida - Matrice RACI',
|
||||||
|
intro: 'La Matrice RACI è l\'hub del modello relazionale: collega i ruoli dell\'organigramma agli oggetti del sistema (inventario, procedure, rischi, fornitori) indicando chi è Responsabile (R), chi è Approvatore/responsabile finale (A), chi va Consultato (C) e chi va Informato (I). In più gestisce i collegamenti molti-a-molti fra procedure, inventario, rischi e misure, così da tracciare la rete delle dipendenze. E\' uno strumento di supporto, non un parere legale.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: 'Cos\'è la RACI',
|
||||||
|
items: [
|
||||||
|
'Per ogni coppia <strong>ruolo × oggetto</strong> assegni una lettera: <strong>R</strong>esponsible (esegue), <strong>A</strong>ccountable (risponde del risultato, di norma uno solo), <strong>C</strong>onsulted (consultato prima), <strong>I</strong>nformed (informato dopo).',
|
||||||
|
'Gli oggetti collegabili sono: <strong>Inventario</strong> (gli asset), <strong>Procedure</strong> (le policy), <strong>Rischi</strong> (il registro rischi) e <strong>Fornitori</strong> (la supply chain).',
|
||||||
|
'Lasciando vuota una cella, il ruolo non ha responsabilità formalizzate su quell\'oggetto.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'I collegamenti (link)',
|
||||||
|
items: [
|
||||||
|
'<strong>Procedura ↔ Inventario</strong>: quali asset sono coperti/governati da una procedura.',
|
||||||
|
'<strong>Procedura ↔ Rischio</strong>: quali rischi una procedura contribuisce a trattare.',
|
||||||
|
'<strong>Inventario ↔ Rischio</strong>: quali rischi insistono su un determinato asset.',
|
||||||
|
'<strong>Rischio ↔ Misura</strong>: a quali misure ACN (per codice, es. GV.OC-04) un rischio è ricondotto.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Riferimenti normativi',
|
||||||
|
items: [
|
||||||
|
'<strong>GV.RR-02</strong>: ruoli, responsabilità e poteri per la cybersicurezza sono stabiliti e comunicati (base della RACI).',
|
||||||
|
'<strong>Inventario</strong>: famiglia <strong>ID.AM</strong> (asset management) — censimento e responsabilità sugli asset.',
|
||||||
|
'<strong>Procedure</strong>: <strong>GV.PO</strong> (policy/governance) — le procedure attuano le politiche.',
|
||||||
|
'<strong>Rischi</strong>: <strong>art. 24 D.Lgs. 138/2024</strong> e famiglia <strong>ID.RA</strong> (risk assessment).'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
references: [
|
||||||
|
'NIST CSF 2.0 / GV.RR-02 - Ruoli, responsabilità e poteri stabiliti e comunicati',
|
||||||
|
'Inventario: famiglia ID.AM (asset management)',
|
||||||
|
'Procedure: GV.PO (policy/governance)',
|
||||||
|
'Rischi: art. 24 D.Lgs. 138/2024 + famiglia ID.RA (risk assessment)',
|
||||||
|
'NOTA: gli obblighi normativi in Italia derivano da NIS2 (Dir. UE 2022/2555) e dal D.Lgs. 138/2024; la matrice RACI e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ─── Risk Management ─────────────────────────────────────────
|
// ─── Risk Management ─────────────────────────────────────────
|
||||||
'risks': {
|
'risks': {
|
||||||
title: 'Guida - Gestione Rischi',
|
title: 'Guida - Gestione Rischi',
|
||||||
@@ -1118,6 +1159,8 @@ const HelpSystem = (function () {
|
|||||||
'organigramma': 'org',
|
'organigramma': 'org',
|
||||||
'competenze.html': 'competences',
|
'competenze.html': 'competences',
|
||||||
'competenze': 'competences',
|
'competenze': 'competences',
|
||||||
|
'raci.html': 'raci',
|
||||||
|
'raci': 'raci',
|
||||||
'risks.html': 'risks',
|
'risks.html': 'risks',
|
||||||
'risks': 'risks',
|
'risks': 'risks',
|
||||||
'incidents.html': 'incidents',
|
'incidents.html': 'incidents',
|
||||||
|
|||||||
@@ -80,10 +80,12 @@ const I18n = (function () {
|
|||||||
'nav.management': { it: 'Gestione', en: 'Management' },
|
'nav.management': { it: 'Gestione', en: 'Management' },
|
||||||
'nav.org_chart': { it: 'Organigramma', en: 'Org Chart' },
|
'nav.org_chart': { it: 'Organigramma', en: 'Org Chart' },
|
||||||
'nav.competences': { it: 'Competenze', en: 'Skills' },
|
'nav.competences': { it: 'Competenze', en: 'Skills' },
|
||||||
|
'nav.raci': { it: 'Matrice RACI', en: 'RACI Matrix' },
|
||||||
'org.title': { it: 'Organigramma', en: 'Org Chart' },
|
'org.title': { it: 'Organigramma', en: 'Org Chart' },
|
||||||
'org.subtitle': { it: 'Ruoli, responsabilità e organi di governance', en: 'Roles, responsibilities and governance bodies' },
|
'org.subtitle': { it: 'Ruoli, responsabilità e organi di governance', en: 'Roles, responsibilities and governance bodies' },
|
||||||
'org.new_role': { it: 'Nuovo ruolo', en: 'New role' },
|
'org.new_role': { it: 'Nuovo ruolo', en: 'New role' },
|
||||||
'comp.title': { it: 'Competenze', en: 'Skills' },
|
'comp.title': { it: 'Competenze', en: 'Skills' },
|
||||||
|
'raci.title': { it: 'Matrice RACI', en: 'RACI Matrix' },
|
||||||
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
||||||
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
||||||
'nav.policies': { it: 'Policy', en: 'Policies' },
|
'nav.policies': { it: 'Policy', en: 'Policies' },
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* NIS2 Agile - Matrice RACI (A4 Fase 4.3)
|
||||||
|
* 2 tab: Matrice RACI (ruoli × oggetti con celle R/A/C/I) · Collegamenti (link m2m).
|
||||||
|
* Client api.raci*: ritorna `data`, lancia su success=false (api.js _acn).
|
||||||
|
* I DELETE non hanno body → i parametri compositi viaggiano in query string (vedi api.js).
|
||||||
|
* Ancoraggio: GV.RR-02 (ruoli/responsabilità), ID.AM (inventario), GV.PO (procedure),
|
||||||
|
* art.24 D.Lgs. 138/2024 + ID.RA (rischi). Strumento di supporto, non un parere legale.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let RACI = { roles: [], objects: [], assignments: {}, tab: 'matrix' };
|
||||||
|
|
||||||
|
function el(id) { return document.getElementById(id); }
|
||||||
|
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||||
|
|
||||||
|
const OBJ_TYPE_LABELS = { inventory: 'Inventario', procedure: 'Procedura', risk: 'Rischio', supplier: 'Fornitore' };
|
||||||
|
const RACI_LETTERS = ['R', 'A', 'C', 'I'];
|
||||||
|
|
||||||
|
// link_type → { label, aType, bType } (bType 'measure' = catalogo misure ACN).
|
||||||
|
const LINK_TYPES = {
|
||||||
|
procedure_inventory: { label: 'Procedura ↔ Inventario', aType: 'procedure', bType: 'inventory', aLabel: 'Procedura', bLabel: 'Inventario (asset)' },
|
||||||
|
procedure_risk: { label: 'Procedura ↔ Rischio', aType: 'procedure', bType: 'risk', aLabel: 'Procedura', bLabel: 'Rischio' },
|
||||||
|
inventory_risk: { label: 'Inventario ↔ Rischio', aType: 'inventory', bType: 'risk', aLabel: 'Inventario (asset)', bLabel: 'Rischio' },
|
||||||
|
risk_measure: { label: 'Rischio ↔ Misura', aType: 'risk', bType: 'measure', aLabel: 'Rischio', bLabel: 'Misura ACN' }
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function () {
|
||||||
|
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||||
|
if (window.I18n && I18n.init) I18n.init('it');
|
||||||
|
if (typeof loadSidebar === 'function') loadSidebar();
|
||||||
|
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||||
|
|
||||||
|
await raciLoadMatrix();
|
||||||
|
raciRenderMatrix();
|
||||||
|
});
|
||||||
|
|
||||||
|
function raciKey(roleId, type, objId) { return roleId + '|' + type + '|' + objId; }
|
||||||
|
|
||||||
|
function raciTab(name) {
|
||||||
|
RACI.tab = name;
|
||||||
|
document.querySelectorAll('.cmp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
||||||
|
document.querySelectorAll('.cmp-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
const panel = el('panel-' + name);
|
||||||
|
if (panel) panel.classList.add('active');
|
||||||
|
if (name === 'links' && !RACI._linksInit) { RACI._linksInit = true; raciLinkTypeChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════ TAB 1 — Matrice RACI ════════════════ */
|
||||||
|
async function raciLoadMatrix() {
|
||||||
|
try {
|
||||||
|
const m = await api.raciMatrix();
|
||||||
|
RACI.roles = (m && m.roles) || [];
|
||||||
|
RACI.objects = (m && m.objects) || [];
|
||||||
|
RACI.assignments = {};
|
||||||
|
((m && m.assignments) || []).forEach(function (a) {
|
||||||
|
RACI.assignments[raciKey(a.role_id, a.object_type, a.object_id)] = a.raci;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('Errore nel caricamento della matrice: ' + (e.message || e), 'error');
|
||||||
|
RACI.roles = []; RACI.objects = []; RACI.assignments = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function raciRenderMatrix() {
|
||||||
|
const wrap = el('raci-matrix');
|
||||||
|
el('raci-matrix-summary').textContent = RACI.roles.length + ' ruoli · ' + RACI.objects.length + ' oggetti collegabili';
|
||||||
|
if (!RACI.roles.length) {
|
||||||
|
wrap.innerHTML = '<div class="cmp-empty">Nessun ruolo: creane nell\'Organigramma per popolare la matrice.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!RACI.objects.length) {
|
||||||
|
wrap.innerHTML = '<div class="cmp-empty">Nessun oggetto collegabile (inventario, procedure, rischi, fornitori). Aggiungili nei rispettivi moduli.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header: oggetti raggruppati per tipo
|
||||||
|
let head = '<thead><tr><th class="raci-corner">Ruolo \\ Oggetto</th>';
|
||||||
|
RACI.objects.forEach(function (o) {
|
||||||
|
head += '<th class="raci-objhead"><span class="raci-objtype">' + esc(OBJ_TYPE_LABELS[o.type] || o.type) + '</span>' + esc(o.label) + '</th>';
|
||||||
|
});
|
||||||
|
head += '</tr></thead>';
|
||||||
|
|
||||||
|
let body = '<tbody>';
|
||||||
|
RACI.roles.forEach(function (r) {
|
||||||
|
body += '<tr><th class="raci-rolehead">' + esc(r.role_name) + (r.is_governance_body ? ' <span class="gap-role-sub">(governance)</span>' : '') + '</th>';
|
||||||
|
RACI.objects.forEach(function (o) {
|
||||||
|
const cur = RACI.assignments[raciKey(r.role_id, o.type, o.id)] || '';
|
||||||
|
const cls = cur ? ' has-' + cur.toLowerCase() : '';
|
||||||
|
let opts = '<option value="">—</option>';
|
||||||
|
RACI_LETTERS.forEach(function (L) { opts += '<option value="' + L + '"' + (cur === L ? ' selected' : '') + '>' + L + '</option>'; });
|
||||||
|
const aria = 'RACI per ruolo ' + (r.role_name || '') + ' su oggetto ' + (o.label || '');
|
||||||
|
body += '<td><select class="raci-cell-sel' + cls + '" aria-label="' + esc(aria) + '" '
|
||||||
|
+ 'onchange="raciSetCell(' + r.role_id + ',\'' + o.type + '\',' + o.id + ',this.value,this)">' + opts + '</select></td>';
|
||||||
|
});
|
||||||
|
body += '</tr>';
|
||||||
|
});
|
||||||
|
body += '</tbody>';
|
||||||
|
|
||||||
|
wrap.innerHTML = '<div class="raci-matrix-wrap"><table class="raci-matrix">' + head + body + '</table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function raciSetCell(roleId, type, objId, value, selEl) {
|
||||||
|
const prev = RACI.assignments[raciKey(roleId, type, objId)] || '';
|
||||||
|
try {
|
||||||
|
if (value) {
|
||||||
|
await api.raciAssign({ role_id: roleId, object_type: type, object_id: objId, raci: value });
|
||||||
|
RACI.assignments[raciKey(roleId, type, objId)] = value;
|
||||||
|
showNotification('Responsabilità ' + value + ' assegnata.', 'success');
|
||||||
|
} else {
|
||||||
|
await api.raciUnassign({ role_id: roleId, object_type: type, object_id: objId });
|
||||||
|
delete RACI.assignments[raciKey(roleId, type, objId)];
|
||||||
|
showNotification('Responsabilità rimossa.', 'success');
|
||||||
|
}
|
||||||
|
if (selEl) {
|
||||||
|
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||||||
|
if (value) selEl.classList.add('has-' + value.toLowerCase());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ripristina il valore precedente in caso di errore
|
||||||
|
if (selEl) {
|
||||||
|
selEl.value = prev;
|
||||||
|
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||||||
|
if (prev) selEl.classList.add('has-' + prev.toLowerCase());
|
||||||
|
}
|
||||||
|
showNotification(e.message || 'Errore.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════ TAB 2 — Collegamenti (link m2m) ════════════════ */
|
||||||
|
function currentLinkType() { return el('raci-linktype').value; }
|
||||||
|
|
||||||
|
async function raciLinkTypeChanged() {
|
||||||
|
const lt = currentLinkType();
|
||||||
|
const cfg = LINK_TYPES[lt];
|
||||||
|
if (!cfg) return;
|
||||||
|
el('raci-alabel').textContent = cfg.aLabel;
|
||||||
|
el('raci-blabel').textContent = cfg.bLabel;
|
||||||
|
const aSel = el('raci-aside');
|
||||||
|
const bSel = el('raci-bside');
|
||||||
|
aSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||||||
|
bSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||||||
|
try {
|
||||||
|
const [aList, bList] = await Promise.all([api.raciObjects(cfg.aType), api.raciObjects(cfg.bType)]);
|
||||||
|
aSel.innerHTML = raciObjOptions(aList, cfg.aType);
|
||||||
|
bSel.innerHTML = raciObjOptions(bList, cfg.bType);
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('Errore nel caricamento degli oggetti: ' + (e.message || e), 'error');
|
||||||
|
aSel.innerHTML = '<option value="">— Errore —</option>';
|
||||||
|
bSel.innerHTML = '<option value="">— Errore —</option>';
|
||||||
|
}
|
||||||
|
await raciLoadLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function raciObjOptions(list, type) {
|
||||||
|
list = Array.isArray(list) ? list : [];
|
||||||
|
if (!list.length) return '<option value="">— Nessun elemento —</option>';
|
||||||
|
// misure: value = code (string); altri: value = id (int)
|
||||||
|
return list.map(function (o) {
|
||||||
|
const v = (type === 'measure') ? o.code : o.id;
|
||||||
|
const label = (type === 'measure') ? o.label : o.label;
|
||||||
|
return '<option value="' + esc(v) + '">' + esc(label) + '</option>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function raciLoadLinks() {
|
||||||
|
const lt = currentLinkType();
|
||||||
|
const cfg = LINK_TYPES[lt];
|
||||||
|
const aId = el('raci-aside').value;
|
||||||
|
const wrap = el('raci-links');
|
||||||
|
if (!aId) { wrap.innerHTML = '<div class="cmp-empty">Seleziona un elemento "Lato A" per vederne i collegamenti.</div>'; return; }
|
||||||
|
wrap.innerHTML = '<div class="cmp-empty">Caricamento…</div>';
|
||||||
|
try {
|
||||||
|
const links = await api.raciLinks(lt, aId);
|
||||||
|
const rows = (Array.isArray(links) ? links : []);
|
||||||
|
if (!rows.length) { wrap.innerHTML = '<div class="cmp-empty">Nessun collegamento per questo elemento.</div>'; return; }
|
||||||
|
const body = rows.map(function (lk) {
|
||||||
|
// risk_measure: { link_id, measure_code, measure_label }; altri: { link_id, b_id, b_label }
|
||||||
|
const bId = (lt === 'risk_measure') ? lk.measure_code : lk.b_id;
|
||||||
|
const bLabel = (lt === 'risk_measure') ? lk.measure_label : lk.b_label;
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td>' + esc(cfg.bLabel) + '</td>'
|
||||||
|
+ '<td><strong>' + esc(bLabel) + '</strong></td>'
|
||||||
|
+ '<td style="text-align:right;"><button class="btn btn-outline btn-sm" onclick="raciRemoveLink(\'' + esc(String(bId)) + '\')">Rimuovi</button></td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}).join('');
|
||||||
|
wrap.innerHTML = '<table class="raci-linklist"><thead><tr><th>Tipo</th><th>Elemento collegato</th><th></th></tr></thead><tbody>' + body + '</tbody></table>';
|
||||||
|
} catch (e) {
|
||||||
|
wrap.innerHTML = '<div class="cmp-empty">Errore: ' + esc(e.message || e) + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function raciAddLink() {
|
||||||
|
const lt = currentLinkType();
|
||||||
|
const aId = el('raci-aside').value;
|
||||||
|
const bId = el('raci-bside').value;
|
||||||
|
if (!aId || !bId) { showNotification('Seleziona entrambi gli elementi.', 'error'); return; }
|
||||||
|
try {
|
||||||
|
await api.raciLink({ link_type: lt, a_id: aId, b_id: bId });
|
||||||
|
showNotification('Collegamento creato.', 'success');
|
||||||
|
await raciLoadLinks();
|
||||||
|
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function raciRemoveLink(bId) {
|
||||||
|
const lt = currentLinkType();
|
||||||
|
const aId = el('raci-aside').value;
|
||||||
|
if (!confirm('Rimuovere questo collegamento?')) return;
|
||||||
|
try {
|
||||||
|
await api.raciUnlink({ link_type: lt, a_id: aId, b_id: bId });
|
||||||
|
showNotification('Collegamento rimosso.', 'success');
|
||||||
|
await raciLoadLinks();
|
||||||
|
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||||||
|
}
|
||||||
+4
-4
@@ -151,8 +151,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
@@ -162,8 +162,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script src="/js/kb.js"></script>
|
<script src="/js/kb.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Gate auth + chrome come le altre pagine -bi.
|
// Gate auth + chrome come le altre pagine -bi.
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
// CONFIG
|
// CONFIG
|
||||||
|
|||||||
+2
-2
@@ -94,8 +94,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -124,8 +124,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -494,8 +494,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ──────────────────────────────────────────────────
|
// ── Auth check ──────────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -138,12 +138,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script src="/js/organigramma.js?v=20260617"></script>
|
<script src="/js/organigramma.js?v=20260617"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -333,8 +333,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -345,8 +345,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
<title>Matrice RACI - NIS2 Agile</title>
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css?v=20260617">
|
||||||
|
<style>
|
||||||
|
.raci-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
|
||||||
|
.raci-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||||
|
.cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; }
|
||||||
|
.cmp-tab { flex:1 1 200px; min-width:160px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.85rem; font-weight:600; text-align:center; }
|
||||||
|
.cmp-tab.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); }
|
||||||
|
.cmp-panel { display:none; }
|
||||||
|
.cmp-panel.active { display:block; }
|
||||||
|
.cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||||
|
.cmp-toolbar select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||||||
|
.cmp-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||||
|
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); }
|
||||||
|
/* Matrice RACI */
|
||||||
|
.raci-matrix-wrap { overflow-x:auto; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; }
|
||||||
|
.raci-matrix { border-collapse:collapse; font-size:.84rem; min-width:100%; }
|
||||||
|
.raci-matrix th, .raci-matrix td { padding:8px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); border-right:1px solid var(--gray-100,#f3f4f6); text-align:center; vertical-align:middle; }
|
||||||
|
.raci-matrix thead th { background:var(--gray-50,#f9fafb); font-size:.72rem; text-transform:uppercase; letter-spacing:.03em; color:var(--gray-500,#6b7280); position:sticky; top:0; }
|
||||||
|
.raci-matrix thead th.raci-grp { text-align:left; }
|
||||||
|
.raci-matrix th.raci-rolehead { text-align:left; font-weight:700; color:var(--gray-700,#374151); background:#fff; position:sticky; left:0; z-index:2; min-width:180px; }
|
||||||
|
.raci-matrix thead th.raci-corner { position:sticky; left:0; z-index:3; background:var(--gray-50,#f9fafb); text-align:left; }
|
||||||
|
.raci-objhead { font-weight:600; min-width:130px; }
|
||||||
|
.raci-objhead .raci-objtype { display:block; font-size:.66rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-400,#9ca3af); }
|
||||||
|
.raci-cell-sel { width:100%; padding:5px 6px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.82rem; background:#fff; cursor:pointer; }
|
||||||
|
.raci-cell-sel.has-r { background:#dcfce7; border-color:#86efac; color:#166534; font-weight:700; }
|
||||||
|
.raci-cell-sel.has-a { background:#fee2e2; border-color:#fca5a5; color:#991b1b; font-weight:700; }
|
||||||
|
.raci-cell-sel.has-c { background:#fef9c3; border-color:#fde047; color:#854d0e; font-weight:700; }
|
||||||
|
.raci-cell-sel.has-i { background:#e0f2fe; border-color:#7dd3fc; color:#075985; font-weight:700; }
|
||||||
|
.raci-legend { display:flex; gap:14px; flex-wrap:wrap; font-size:.78rem; margin:12px 2px 0; color:var(--gray-600,#4b5563); }
|
||||||
|
.raci-legend span b { display:inline-block; width:18px; text-align:center; border-radius:4px; margin-right:4px; }
|
||||||
|
.raci-legend .l-r b { background:#dcfce7; color:#166534; }
|
||||||
|
.raci-legend .l-a b { background:#fee2e2; color:#991b1b; }
|
||||||
|
.raci-legend .l-c b { background:#fef9c3; color:#854d0e; }
|
||||||
|
.raci-legend .l-i b { background:#e0f2fe; color:#075985; }
|
||||||
|
/* Link m2m */
|
||||||
|
.raci-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#f9fafb; border:1px solid var(--gray-100,#f3f4f6); border-radius:10px; padding:12px; margin-bottom:14px; }
|
||||||
|
.raci-inline .fld { display:flex; flex-direction:column; gap:3px; }
|
||||||
|
.raci-inline .fld label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); }
|
||||||
|
.raci-inline select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; min-width:200px; }
|
||||||
|
.raci-linklist { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||||
|
.raci-linklist th, .raci-linklist td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||||
|
.raci-linklist th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||||
|
.raci-linklist .btn { padding:4px 9px; font-size:.76rem; }
|
||||||
|
.raci-arrow { color:var(--gray-400,#9ca3af); padding:0 6px; }
|
||||||
|
</style>
|
||||||
|
<!-- PWA:start -->
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#0066CC">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
||||||
|
<meta name="application-name" content="NIS2 Agile">
|
||||||
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
||||||
|
<!-- PWA:end -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<aside class="sidebar" id="sidebar"></aside>
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="content-header">
|
||||||
|
<h2 data-i18n="raci.title">Matrice RACI</h2>
|
||||||
|
<div class="content-header-actions">
|
||||||
|
<span class="savehint" id="raci-hint" role="status" aria-live="polite"></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-body">
|
||||||
|
<div class="raci-intro">
|
||||||
|
<strong>Matrice RACI & collegamenti.</strong>
|
||||||
|
Collega i ruoli dell'organigramma agli oggetti del sistema (inventario, procedure, rischi, fornitori) assegnando le responsabilità <strong>R</strong>esponsible / <strong>A</strong>ccountable / <strong>C</strong>onsulted / <strong>I</strong>nformed. Gestisci inoltre i collegamenti molti-a-molti fra procedure, inventario, rischi e misure.
|
||||||
|
</div>
|
||||||
|
<div class="raci-note">
|
||||||
|
Ancoraggio: <strong>GV.RR-02</strong> (ruoli, responsabilità e poteri stabiliti e comunicati), <strong>ID.AM</strong> (inventario), <strong>GV.PO</strong> (procedure), <strong>art. 24 D.Lgs. 138/2024</strong> e <strong>ID.RA</strong> (rischi). Strumento di supporto, non un parere legale.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cmp-tabs" id="raci-tabs">
|
||||||
|
<div class="cmp-tab active" data-tab="matrix" onclick="raciTab('matrix')">Matrice RACI</div>
|
||||||
|
<div class="cmp-tab" data-tab="links" onclick="raciTab('links')">Collegamenti (link m2m)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 1 — Matrice RACI -->
|
||||||
|
<div class="cmp-panel active" id="panel-matrix">
|
||||||
|
<div class="cmp-toolbar">
|
||||||
|
<span class="gap-role-sub" id="raci-matrix-summary"></span>
|
||||||
|
</div>
|
||||||
|
<div id="raci-matrix"></div>
|
||||||
|
<div class="raci-legend">
|
||||||
|
<span class="l-r"><b>R</b> Responsible (esegue)</span>
|
||||||
|
<span class="l-a"><b>A</b> Accountable (risponde del risultato)</span>
|
||||||
|
<span class="l-c"><b>C</b> Consulted (consultato)</span>
|
||||||
|
<span class="l-i"><b>I</b> Informed (informato)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 2 — Collegamenti (link m2m) -->
|
||||||
|
<div class="cmp-panel" id="panel-links">
|
||||||
|
<div class="raci-inline">
|
||||||
|
<div class="fld">
|
||||||
|
<label for="raci-linktype">Tipo di collegamento</label>
|
||||||
|
<select id="raci-linktype" onchange="raciLinkTypeChanged()">
|
||||||
|
<option value="procedure_inventory">Procedura ↔ Inventario</option>
|
||||||
|
<option value="procedure_risk">Procedura ↔ Rischio</option>
|
||||||
|
<option value="inventory_risk">Inventario ↔ Rischio</option>
|
||||||
|
<option value="risk_measure">Rischio ↔ Misura</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fld">
|
||||||
|
<label id="raci-alabel" for="raci-aside">Lato A</label>
|
||||||
|
<select id="raci-aside" onchange="raciLoadLinks()"></select>
|
||||||
|
</div>
|
||||||
|
<div class="fld">
|
||||||
|
<label id="raci-blabel" for="raci-bside">Lato B</label>
|
||||||
|
<select id="raci-bside"></select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="raciAddLink()">Aggiungi collegamento</button>
|
||||||
|
</div>
|
||||||
|
<div id="raci-links"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
|
<script src="/js/raci.js?v=20260619"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -268,8 +268,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -443,8 +443,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -455,8 +455,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+4
-4
@@ -494,8 +494,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -506,8 +506,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ──────────────────────────────────────────────
|
// ── Auth & Init ──────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -672,8 +672,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -684,8 +684,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -165,8 +165,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -477,8 +477,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -489,8 +489,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -292,8 +292,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -304,8 +304,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth & Init ─────────────────────────────────────────
|
// ── Auth & Init ─────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":"1.17.0","build":"2026-06-15-v1.17.0","date":"2026-06-15","changelog":"A4 Fase 4.2 - Competenze: nuovo modulo per catalogo competenze, requisiti per ruolo, competenze possedute dalle persone, mappatura competenza-corso, calcolo gap competenze e apertura azione correttiva dal gap (riuso workflow NCR/CAPA). Backend CompetenceController + tabelle skills/role_skills/user_skills/skill_course_map (migration 042, additiva). Pagina competenze.html a 4 schede, help contestuale, voce di menu. Ancoraggio PR.AT-01/02, GV.RR-04, art.24 D.Lgs.138/2024. PWA cache bump v1.17.0."}
|
{"version": "1.17.1", "build": "2026-06-15-v1.17.1", "date": "2026-06-15", "changelog": "A4 Fase 4.3 — Matrice RACI (ruoli x oggetti) + link molti-a-molti procedure/inventario/rischi/misure ACN. Nuove tabelle raci_assignments + procedure_inventory/procedure_risk/inventory_risk/risk_measure (mig.043). RaciController org-scoped (anti-IDOR), pagina raci.html (Bootstrap Italia/AGID), help+i18n. Nessuna modifica a dati esistenti."}
|
||||||
@@ -207,8 +207,8 @@
|
|||||||
|
|
||||||
<!-- Report Detail Modal handled by common.js showModal -->
|
<!-- Report Detail Modal handled by common.js showModal -->
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260618"></script>
|
<script src="/js/api.js?v=20260619"></script>
|
||||||
<script src="/js/common.js?v=20260618"></script>
|
<script src="/js/common.js?v=20260619"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
@@ -219,8 +219,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260618"></script>
|
<script src="/js/common-bi.js?v=20260618"></script>
|
||||||
<script src="/js/i18n.js?v=20260618"></script>
|
<script src="/js/i18n.js?v=20260619"></script>
|
||||||
<script src="/js/help.js?v=20260618"></script>
|
<script src="/js/help.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
@@ -319,9 +319,9 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260618"></script>
|
<script src="js/api.js?v=20260619"></script>
|
||||||
<script src="js/common.js?v=20260618"></script>
|
<script src="js/common.js?v=20260619"></script>
|
||||||
<script src="js/i18n.js?v=20260618"></script>
|
<script src="js/i18n.js?v=20260619"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
loadSidebar();
|
loadSidebar();
|
||||||
|
|||||||
Reference in New Issue
Block a user