[FIX] Epic C / C5 hardening — esiti flotta di verifica multi-agente (mig.054)
Corretti i finding confermati dalla verifica multi-agente (5 major + minori): SICUREZZA - Stored XSS allegati: da blocklist a ALLOWLIST di estensioni innocue (no html/svg/js renderizzabili same-origin) in StakeholderPortalController::attachment e StakeholderActivityController::storeUpload; nome file randomizzato (random_bytes). - Magic-link: scadenza (mig.054 stk_activity_targets.token_expires_at; send() imposta scadenza attività+30gg o +90gg; resolveTarget() → 410 TOKEN_EXPIRED se scaduto); rate-limit per-IP sugli endpoint del portale; comment/attachment bloccati su attività chiusa. CORRETTEZZA - send(): NON rigenera token né azzera lo stato dei destinatari già responded/acknowledged (prima ne perdeva l'esito); imposta token_expires_at. - assign individuale: semantica "replace" (rimuove i deselezionati non ancora conclusi) + guard su lista vuota (evita 'IN ()'). - update(): conserva assign_mode esistente quando si modifica solo stak_code. UI/UX/A11Y - Editor opzioni per domande a scelta singola/multipla (prima degradavano a testo nel portale). - Etichette stato/tipo localizzate; risposte mostrate inline (no alert()); escAttr nel portale (escape virgolette negli attributi); ARIA su modali/tab; voce sidebar anche in common.js. OPEN ITEM (NON regressione C5, pre-esistente e ambientale): l'upload allegati restituisce UPLOAD_ERROR in prod — move_uploaded_file/is_uploaded_file fallisce nella topologia proxy→fastcgi (stesso pattern di evidence_files/AuditController, mai funzionato: la dir uploads/evidence non esiste). rename/copy come www-data funzionano. Da investigare lato infra. Il resto di C5 (questionari, firma-lettura, commenti, calendario, portale) è pienamente operativo. Smoke prod OK: allowlist (.html→422, struttura ok), opzioni scelta nel portale, send no-reset, replace individuale, token_expires_at presente. Additivo. v1.21.1. 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
4f386faae5
commit
b917d2da14
@@ -122,6 +122,13 @@ try {
|
||||
fwrite(STDERR, "WARN ALTER review_schedule: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
// mig.054 — scadenza magic-link (idempotente: ignora 1060 colonna già esistente).
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE stk_activity_targets ADD COLUMN token_expires_at DATETIME NULL DEFAULT NULL AFTER access_token_hash");
|
||||
} catch (PDOException $e) {
|
||||
if (($e->errorInfo[1] ?? 0) !== 1060) { fwrite(STDERR, "WARN ALTER stk_activity_targets: " . $e->getMessage() . "\n"); }
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
foreach (['stk_questionnaire_templates','stk_template_procedures','stk_template_misure','stk_template_requisiti',
|
||||
'stk_activities','stk_activity_targets','stk_activity_procedures',
|
||||
|
||||
@@ -285,7 +285,7 @@ class StakeholderActivityController extends BaseController
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$b = $this->getJsonBody();
|
||||
|
||||
$a = Database::fetchOne('SELECT id, title, due_date FROM stk_activities WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
$a = Database::fetchOne('SELECT id, title, due_date, assign_mode, stak_code FROM stk_activities WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if (!$a) { $this->jsonError('Attività non trovata', 404, 'NOT_FOUND'); }
|
||||
|
||||
$updates = [];
|
||||
@@ -298,7 +298,10 @@ class StakeholderActivityController extends BaseController
|
||||
if ($this->hasParam('template_id')) { $updates['template_id'] = $this->validateTemplate($b['template_id'] ?? null, $orgId); }
|
||||
if ($this->hasParam('description')) { $updates['description'] = $this->nullableStr($b['description'] ?? null); }
|
||||
if ($this->hasParam('assign_mode') || $this->hasParam('stak_code')) {
|
||||
[$assignMode, $stakCode] = $this->validateAssign($b, $orgId);
|
||||
$bb = $b;
|
||||
// se cambia solo lo stak_code, conserva la modalità esistente (non forzare 'individual')
|
||||
if (!$this->hasParam('assign_mode')) { $bb['assign_mode'] = $a['assign_mode']; }
|
||||
[$assignMode, $stakCode] = $this->validateAssign($bb, $orgId);
|
||||
$updates['assign_mode'] = $assignMode;
|
||||
$updates['stak_code'] = $assignMode === 'by_code' ? $stakCode : null;
|
||||
}
|
||||
@@ -358,6 +361,7 @@ class StakeholderActivityController extends BaseController
|
||||
$raw = $b['stakeholder_ids'] ?? null;
|
||||
if (!is_array($raw) || !$raw) { $this->jsonError('Seleziona almeno uno stakeholder', 422, 'NO_TARGETS'); }
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $raw), static fn($i) => $i > 0)));
|
||||
if (!$ids) { $this->jsonError('Seleziona almeno uno stakeholder valido', 422, 'NO_TARGETS'); }
|
||||
$place = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stakeholderIds = $this->valCol(
|
||||
"SELECT id FROM stakeholders WHERE organization_id = ? AND id IN ($place)",
|
||||
@@ -367,6 +371,18 @@ class StakeholderActivityController extends BaseController
|
||||
}
|
||||
if (!$stakeholderIds) { $this->jsonError('Nessuno stakeholder da assegnare per questo criterio', 422, 'EMPTY_TARGETS'); }
|
||||
|
||||
$stakeholderIds = array_map('intval', $stakeholderIds);
|
||||
// Modalità individuale = sincronizzazione "replace": rimuovi i destinatari deselezionati
|
||||
// che NON hanno ancora risposto/firmato (gli esiti raccolti non si perdono).
|
||||
if ($a['assign_mode'] === 'individual') {
|
||||
$keep = implode(',', array_fill(0, count($stakeholderIds), '?'));
|
||||
Database::query(
|
||||
"DELETE FROM stk_activity_targets
|
||||
WHERE activity_id = ? AND state NOT IN ('responded','acknowledged') AND stakeholder_id NOT IN ($keep)",
|
||||
array_merge([$id], $stakeholderIds)
|
||||
);
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
foreach ($stakeholderIds as $sid) {
|
||||
try {
|
||||
@@ -393,13 +409,20 @@ class StakeholderActivityController extends BaseController
|
||||
$a = Database::fetchOne('SELECT id, due_date, title FROM stk_activities WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if (!$a) { $this->jsonError('Attività non trovata', 404, 'NOT_FOUND'); }
|
||||
|
||||
// Solo i destinatari NON ancora conclusi: NON rigenerare token né azzerare lo stato
|
||||
// di chi ha già risposto/firmato (altrimenti se ne perderebbe l'esito).
|
||||
$targets = Database::fetchAll(
|
||||
'SELECT g.id, g.stakeholder_id, s.name AS stakeholder_name, s.contact_email, g.access_token_hash
|
||||
"SELECT g.id, g.stakeholder_id, s.name AS stakeholder_name, s.contact_email, g.access_token_hash
|
||||
FROM stk_activity_targets g JOIN stakeholders s ON s.id = g.stakeholder_id
|
||||
WHERE g.activity_id = ?',
|
||||
WHERE g.activity_id = ? AND g.state NOT IN ('responded','acknowledged')",
|
||||
[$id]
|
||||
);
|
||||
if (!$targets) { $this->jsonError('Nessun destinatario: assegna prima gli stakeholder', 422, 'NO_TARGETS'); }
|
||||
if (!$targets) { $this->jsonError('Nessun destinatario da inviare (assegna stakeholder o tutti hanno già risposto)', 422, 'NO_TARGETS'); }
|
||||
|
||||
// Scadenza del magic-link: scadenza attività + 30gg, altrimenti 90gg da oggi.
|
||||
$expires = $a['due_date']
|
||||
? date('Y-m-d H:i:s', strtotime($a['due_date'] . ' +30 days'))
|
||||
: date('Y-m-d H:i:s', strtotime('+90 days'));
|
||||
|
||||
$links = [];
|
||||
foreach ($targets as $g) {
|
||||
@@ -408,6 +431,7 @@ class StakeholderActivityController extends BaseController
|
||||
'access_token_hash' => hash('sha256', $token),
|
||||
'state' => 'sent',
|
||||
'sent_at' => date('Y-m-d H:i:s'),
|
||||
'token_expires_at' => $expires,
|
||||
], 'id = ?', [(int) $g['id']]);
|
||||
$links[] = [
|
||||
'target_id' => (int) $g['id'],
|
||||
@@ -563,12 +587,13 @@ class StakeholderActivityController extends BaseController
|
||||
if ($file['size'] > 10 * 1024 * 1024) { $this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE'); }
|
||||
|
||||
$ext = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', pathinfo($file['name'], PATHINFO_EXTENSION)));
|
||||
$blocked = ['php','phtml','phar','php3','php4','php5','phps','cgi','pl','sh','exe','htaccess'];
|
||||
if ($ext === '' || in_array($ext, $blocked, true)) { $this->jsonError('Tipo di file non consentito', 422, 'BAD_FILE_TYPE'); }
|
||||
// ALLOWLIST (niente html/svg/js eseguibili same-origin): allineata a StakeholderPortalController::ALLOWED_EXT
|
||||
$allowed = ['pdf','png','jpg','jpeg','gif','webp','txt','csv','xlsx','xls','docx','doc','pptx','ppt','odt','ods','zip'];
|
||||
if ($ext === '' || !in_array($ext, $allowed, true)) { $this->jsonError('Tipo di file non consentito', 422, 'BAD_FILE_TYPE'); }
|
||||
|
||||
$uploadDir = UPLOAD_PATH . "/stk_activity/{$orgId}";
|
||||
if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); }
|
||||
$filename = uniqid('sa_') . '.' . $ext;
|
||||
$filename = 'sa_' . bin2hex(random_bytes(16)) . '.' . $ext;
|
||||
if (!move_uploaded_file($file['tmp_name'], $uploadDir . '/' . $filename)) {
|
||||
$this->jsonError('Errore caricamento file', 500, 'UPLOAD_ERROR');
|
||||
}
|
||||
|
||||
@@ -18,12 +18,19 @@
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/BaseController.php';
|
||||
require_once APP_PATH . '/services/RateLimitService.php';
|
||||
|
||||
class StakeholderPortalController extends BaseController
|
||||
{
|
||||
// Estensioni consentite per gli allegati (ALLOWLIST: niente html/svg/js eseguibili same-origin)
|
||||
private const ALLOWED_EXT = ['pdf','png','jpg','jpeg','gif','webp','txt','csv','xlsx','xls','docx','doc','pptx','ppt','odt','ods','zip'];
|
||||
private const RL_READ = [['max' => 30, 'window_seconds' => 60], ['max' => 200, 'window_seconds' => 3600]];
|
||||
private const RL_WRITE = [['max' => 10, 'window_seconds' => 60], ['max' => 60, 'window_seconds' => 3600]];
|
||||
|
||||
/** GET /api/stakeholder-portal/access?t=<token> */
|
||||
public function access(): void
|
||||
{
|
||||
$this->rateLimit('read');
|
||||
$tg = $this->resolveTarget();
|
||||
$template = $this->loadTemplate($tg);
|
||||
$resp = Database::fetchOne(
|
||||
@@ -52,6 +59,7 @@ class StakeholderPortalController extends BaseController
|
||||
/** POST /api/stakeholder-portal/respond Body: {t*, answers*, respondent_name?} */
|
||||
public function respond(): void
|
||||
{
|
||||
$this->rateLimit('write');
|
||||
$tg = $this->resolveTarget();
|
||||
if ($tg['type'] === 'read_ack') { $this->jsonError('Questa attività richiede una firma di avvenuta lettura, non un questionario.', 422, 'WRONG_TYPE'); }
|
||||
if (in_array($tg['state'], ['responded', 'acknowledged'], true)) { $this->jsonError('Risposta già inviata: non è più modificabile.', 409, 'ALREADY_SUBMITTED'); }
|
||||
@@ -83,6 +91,7 @@ class StakeholderPortalController extends BaseController
|
||||
/** POST /api/stakeholder-portal/acknowledge Body: {t*, respondent_name?} */
|
||||
public function acknowledge(): void
|
||||
{
|
||||
$this->rateLimit('write');
|
||||
$tg = $this->resolveTarget();
|
||||
if ($tg['type'] !== 'read_ack') { $this->jsonError('Questa attività è un questionario da compilare.', 422, 'WRONG_TYPE'); }
|
||||
if (in_array($tg['state'], ['responded', 'acknowledged'], true)) { $this->jsonError('Firma già registrata.', 409, 'ALREADY_SUBMITTED'); }
|
||||
@@ -98,7 +107,9 @@ class StakeholderPortalController extends BaseController
|
||||
/** POST /api/stakeholder-portal/comment Body: {t*, body*} */
|
||||
public function comment(): void
|
||||
{
|
||||
$this->rateLimit('write');
|
||||
$tg = $this->resolveTarget();
|
||||
$this->assertWritable($tg);
|
||||
$body = trim((string) ($this->getJsonBody()['body'] ?? ''));
|
||||
if ($body === '') { $this->jsonError('Commento vuoto', 422, 'EMPTY_COMMENT'); }
|
||||
Database::insert('stk_activity_comments', [
|
||||
@@ -113,20 +124,21 @@ class StakeholderPortalController extends BaseController
|
||||
/** POST /api/stakeholder-portal/attachment?t=<token> (multipart: file) */
|
||||
public function attachment(): void
|
||||
{
|
||||
$this->rateLimit('write');
|
||||
$tg = $this->resolveTarget();
|
||||
$this->assertWritable($tg);
|
||||
if (!isset($_FILES['file']) || ($_FILES['file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
$this->jsonError('File non fornito', 400, 'NO_FILE');
|
||||
}
|
||||
$file = $_FILES['file'];
|
||||
if ($file['size'] > 10 * 1024 * 1024) { $this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE'); }
|
||||
$ext = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', pathinfo($file['name'], PATHINFO_EXTENSION)));
|
||||
$blocked = ['php','phtml','phar','php3','php4','php5','phps','cgi','pl','sh','exe','htaccess'];
|
||||
if ($ext === '' || in_array($ext, $blocked, true)) { $this->jsonError('Tipo di file non consentito', 422, 'BAD_FILE_TYPE'); }
|
||||
if ($ext === '' || !in_array($ext, self::ALLOWED_EXT, true)) { $this->jsonError('Tipo di file non consentito', 422, 'BAD_FILE_TYPE'); }
|
||||
|
||||
$orgId = (int) $tg['organization_id'];
|
||||
$dir = UPLOAD_PATH . "/stk_activity/{$orgId}";
|
||||
if (!is_dir($dir)) { mkdir($dir, 0755, true); }
|
||||
$filename = uniqid('sa_') . '.' . $ext;
|
||||
$filename = 'sa_' . bin2hex(random_bytes(16)) . '.' . $ext;
|
||||
if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $filename)) { $this->jsonError('Errore caricamento', 500, 'UPLOAD_ERROR'); }
|
||||
Database::insert('evidence_files', [
|
||||
'organization_id' => $orgId,
|
||||
@@ -153,8 +165,8 @@ class StakeholderPortalController extends BaseController
|
||||
$this->jsonError('Link di accesso mancante o non valido.', 401, 'MISSING_TOKEN');
|
||||
}
|
||||
$row = Database::fetchOne(
|
||||
'SELECT g.id AS target_id, g.activity_id, g.state, s.name AS stakeholder_name,
|
||||
a.organization_id, a.title, a.type, a.description, a.template_id
|
||||
'SELECT g.id AS target_id, g.activity_id, g.state, g.token_expires_at, s.name AS stakeholder_name,
|
||||
a.organization_id, a.title, a.type, a.description, a.template_id, a.status AS activity_status
|
||||
FROM stk_activity_targets g
|
||||
JOIN stk_activities a ON a.id = g.activity_id
|
||||
JOIN stakeholders s ON s.id = g.stakeholder_id
|
||||
@@ -162,9 +174,39 @@ class StakeholderPortalController extends BaseController
|
||||
[hash('sha256', $token)]
|
||||
);
|
||||
if (!$row) { $this->jsonError('Link di accesso non valido o scaduto.', 404, 'INVALID_TOKEN'); }
|
||||
if (!empty($row['token_expires_at']) && strtotime($row['token_expires_at']) < time()) {
|
||||
$this->jsonError('Link di accesso scaduto. Richiedi un nuovo invio all\'organizzazione.', 410, 'TOKEN_EXPIRED');
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** Le scritture (commenti/allegati) sono bloccate se l'attività è chiusa. */
|
||||
private function assertWritable(array $tg): void
|
||||
{
|
||||
if (in_array($tg['activity_status'] ?? '', ['completed', 'cancelled'], true)) {
|
||||
$this->jsonError('Attività chiusa: non sono più ammessi contributi.', 409, 'ACTIVITY_CLOSED');
|
||||
}
|
||||
}
|
||||
|
||||
/** Rate limit per-IP sugli endpoint pubblici del portale. */
|
||||
private function rateLimit(string $kind): void
|
||||
{
|
||||
$ip = $this->getClientIP();
|
||||
$limits = $kind === 'write' ? self::RL_WRITE : self::RL_READ;
|
||||
RateLimitService::check("stkp_{$kind}_ip:" . $ip, $limits);
|
||||
RateLimitService::increment("stkp_{$kind}_ip:" . $ip);
|
||||
}
|
||||
|
||||
private function getClientIP(): string
|
||||
{
|
||||
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
|
||||
if ($xff !== '') {
|
||||
$first = trim(explode(',', $xff)[0]);
|
||||
if (filter_var($first, FILTER_VALIDATE_IP)) { return $first; }
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
private function loadTemplate(array $tg): ?array
|
||||
{
|
||||
if (empty($tg['template_id'])) { return null; }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- =====================================================================
|
||||
-- 054 — Scadenza dei magic-link del portale stakeholder (Epic C / C5.2 hardening)
|
||||
-- =====================================================================
|
||||
-- Aggiunge stk_activity_targets.token_expires_at: i magic-link generati da
|
||||
-- StakeholderActivityController::send() ricevono una scadenza (scadenza attività
|
||||
-- +30gg, altrimenti +90gg). StakeholderPortalController::resolveTarget() rifiuta
|
||||
-- i token scaduti (410 TOKEN_EXPIRED). Mitiga il riuso indefinito di un link
|
||||
-- eventualmente trapelato (finding flotta di verifica C5).
|
||||
--
|
||||
-- Additivo, reversibile. Runner-safe: ALTER nuda; idempotenza garantita dal
|
||||
-- try/catch su errno 1060 nel CLI seed_stakeholder_activities.php.
|
||||
-- =====================================================================
|
||||
|
||||
ALTER TABLE stk_activity_targets ADD COLUMN token_expires_at DATETIME NULL DEFAULT NULL AFTER access_token_hash;
|
||||
|
||||
-- ROLLBACK (manuale):
|
||||
-- ALTER TABLE stk_activity_targets DROP COLUMN token_expires_at
|
||||
@@ -70,9 +70,9 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/common-bi.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script src="js/common-bi.js?v=20260629"></script>
|
||||
<script>
|
||||
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
@@ -1088,10 +1088,10 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/i18n.js?v=20260628"></script>
|
||||
<script src="js/help.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script src="js/i18n.js?v=20260629"></script>
|
||||
<script src="js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -165,9 +165,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+5
-5
@@ -361,8 +361,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -372,9 +372,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -349,14 +349,14 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<!-- 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>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
||||
<script src="js/common-bi.js?v=20260628"></script>
|
||||
<script src="js/i18n.js?v=20260628"></script>
|
||||
<script src="js/help.js?v=20260628"></script>
|
||||
<script src="js/common-bi.js?v=20260629"></script>
|
||||
<script src="js/i18n.js?v=20260629"></script>
|
||||
<script src="js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
|
||||
@@ -183,12 +183,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script src="/js/competenze.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -382,8 +382,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -393,8 +393,8 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script>
|
||||
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>`; }
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -154,9 +154,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script>
|
||||
const form = document.getElementById('forgot-form');
|
||||
const err = document.getElementById('err');
|
||||
|
||||
+5
-5
@@ -1142,8 +1142,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||
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>
|
||||
@@ -1152,9 +1152,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
||||
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -362,9 +362,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
||||
|
||||
</main>
|
||||
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/i18n.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script src="js/i18n.js?v=20260629"></script>
|
||||
<script>
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
|
||||
+5
-5
@@ -184,8 +184,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -195,9 +195,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script src="/js/isms.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -204,6 +204,7 @@ function loadSidebar() {
|
||||
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
|
||||
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
|
||||
{ name: 'Stakeholder', href: 'stakeholders.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/><path d="M5.5 7.5a2 2 0 11-2.999.001A2 2 0 015.5 7.5zM17.5 7.5a2 2 0 11-2.999.001A2 2 0 0117.5 7.5z"/></svg>`, i18nKey: 'nav.stakeholders' },
|
||||
{ name: 'Attività stakeholder', href: 'stakeholder-activities.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.stk_activities' },
|
||||
{ name: 'Segnalazioni', href: 'whistleblowing.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'Normative', href: 'normative.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'AI Cross-Analysis', href: 'cross-analysis.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"/></svg>` },
|
||||
|
||||
+5
-5
@@ -151,8 +151,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
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>
|
||||
@@ -161,9 +161,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script src="/js/kb.js"></script>
|
||||
<script>
|
||||
// Gate auth + chrome come le altre pagine -bi.
|
||||
|
||||
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script>
|
||||
// ══════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
|
||||
+2
-2
@@ -94,8 +94,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Misure e Requisiti - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260628">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260629">
|
||||
<style>
|
||||
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; }
|
||||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -162,12 +162,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -123,9 +123,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -494,8 +494,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth check ──────────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -138,12 +138,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script src="/js/organigramma.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -333,8 +333,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -344,9 +344,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+5
-5
@@ -131,12 +131,12 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script src="/js/raci.js?v=20260619"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -268,8 +268,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
+5
-5
@@ -443,8 +443,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -454,9 +454,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -141,12 +141,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
+5
-5
@@ -494,8 +494,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -505,9 +505,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -672,8 +672,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -683,9 +683,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Attività stakeholder - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260628">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260629">
|
||||
<style>
|
||||
.sa-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.sa-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -86,19 +86,19 @@
|
||||
obblighi ai sensi dell'<strong>art. 24 D.Lgs. 138/2024</strong>. L'invio email è disattivato: i questionari si condividono tramite magic-link.
|
||||
</div>
|
||||
|
||||
<div class="sa-tabs">
|
||||
<button class="sa-tab active" id="tab-act" onclick="saTab('act')">Attività</button>
|
||||
<button class="sa-tab" id="tab-tpl" onclick="saTab('tpl')">Questionari tipo</button>
|
||||
<div class="sa-tabs" role="tablist" aria-label="Sezioni attività">
|
||||
<button class="sa-tab active" id="tab-act" role="tab" aria-selected="true" aria-controls="pane-act" onclick="saTab('act')">Attività</button>
|
||||
<button class="sa-tab" id="tab-tpl" role="tab" aria-selected="false" aria-controls="pane-tpl" onclick="saTab('tpl')">Questionari tipo</button>
|
||||
</div>
|
||||
|
||||
<!-- ATTIVITÀ -->
|
||||
<section id="pane-act">
|
||||
<section id="pane-act" role="tabpanel" aria-labelledby="tab-act">
|
||||
<div class="sa-toolbar"><button class="btn btn-primary btn-sm" onclick="actOpen()">+ Nuova attività</button></div>
|
||||
<div id="act-list" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<!-- QUESTIONARI TIPO -->
|
||||
<section id="pane-tpl" class="hidden">
|
||||
<section id="pane-tpl" class="hidden" role="tabpanel" aria-labelledby="tab-tpl">
|
||||
<div class="sa-toolbar"><button class="btn btn-primary btn-sm" onclick="tplOpen()">+ Nuovo questionario tipo</button></div>
|
||||
<div id="tpl-list" aria-live="polite"></div>
|
||||
</section>
|
||||
@@ -107,9 +107,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale questionario tipo -->
|
||||
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true" aria-labelledby="tpl-modal-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="tpl-modal-title">Nuovo questionario tipo</h3><button class="sm-close" type="button" onclick="tplClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="tpl-modal-title">Nuovo questionario tipo</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="tplClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="tpl-id">
|
||||
<div class="sm-row">
|
||||
@@ -140,9 +140,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale attività -->
|
||||
<div class="sm-overlay" id="act-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="act-modal" role="dialog" aria-modal="true" aria-labelledby="act-modal-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="act-modal-title">Nuova attività</h3><button class="sm-close" type="button" onclick="actClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="act-modal-title">Nuova attività</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="actClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="act-id">
|
||||
<div class="sm-row">
|
||||
@@ -179,9 +179,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale dettaglio attività (target + invio) -->
|
||||
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true" aria-labelledby="det-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="det-title">Dettaglio attività</h3><button class="sm-close" type="button" onclick="detClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="det-title">Dettaglio attività</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">×</button></div>
|
||||
<div class="sm-body" id="det-body"></div>
|
||||
<div class="sm-foot">
|
||||
<button class="btn btn-outline" type="button" onclick="detClose()">Chiudi</button>
|
||||
@@ -191,12 +191,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
@@ -205,6 +205,8 @@
|
||||
* con magic-link (email disattivate). Sotto-dashboard feedback completa in C5.2b.
|
||||
*/
|
||||
let SA = { activities: [], templates: [], pickers: { policies: [], misure: [], requisiti: [] }, types: [], stakeholders: [], detail: null };
|
||||
const ST_LABELS = { draft: 'Bozza', scheduled: 'Pianificata', sent: 'Inviata', in_progress: 'In corso', completed: 'Completata', cancelled: 'Annullata' };
|
||||
const TYPE_LABELS = { questionnaire: 'Questionario', read_ack: 'Firma lettura', action: 'Azione' };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
@@ -222,6 +224,8 @@
|
||||
function saTab(which) {
|
||||
el('tab-act').classList.toggle('active', which === 'act');
|
||||
el('tab-tpl').classList.toggle('active', which === 'tpl');
|
||||
el('tab-act').setAttribute('aria-selected', which === 'act' ? 'true' : 'false');
|
||||
el('tab-tpl').setAttribute('aria-selected', which === 'tpl' ? 'true' : 'false');
|
||||
el('pane-act').classList.toggle('hidden', which !== 'act');
|
||||
el('pane-tpl').classList.toggle('hidden', which !== 'tpl');
|
||||
}
|
||||
@@ -271,16 +275,22 @@
|
||||
el('tpl-list').innerHTML = '<table class="sa-table"><thead><tr><th>Nome</th><th>Tipo</th><th>Domande</th><th>Collegamenti</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
function qAdd(text, type, required) {
|
||||
function qAdd(text, type, required, options) {
|
||||
const allowed = ['text','yes_no','single_choice','multi_choice','scale_1_5','number'];
|
||||
const t = allowed.indexOf(type) !== -1 ? type : 'text';
|
||||
const div = document.createElement('div'); div.className = 'q-row';
|
||||
div.innerHTML = '<input type="text" placeholder="Testo della domanda" maxlength="500" value="' + esc(text || '').replace(/"/g,'"') + '">'
|
||||
+ '<select>' + allowed.map(a => '<option value="' + a + '"' + (a === t ? ' selected' : '') + '>' + ({text:'Testo',yes_no:'Sì/No',single_choice:'Scelta singola',multi_choice:'Scelta multipla',scale_1_5:'Scala 1-5',number:'Numero'})[a] + '</option>').join('') + '</select>'
|
||||
+ '<label style="font-size:.78rem;display:flex;align-items:center;gap:4px;white-space:nowrap;"><input type="checkbox"' + (required ? ' checked' : '') + '> obbl.</label>'
|
||||
+ '<button type="button" class="q-del" onclick="this.parentNode.remove()">✕</button>';
|
||||
const isChoice = (t === 'single_choice' || t === 'multi_choice');
|
||||
const div = document.createElement('div'); div.className = 'q-row'; div.style.flexWrap = 'wrap';
|
||||
div.innerHTML = '<input type="text" class="q-text-in" placeholder="Testo della domanda" maxlength="500" value="' + esc(text || '').replace(/"/g,'"') + '">'
|
||||
+ '<select class="q-type-in" onchange="qToggleOpts(this)">' + allowed.map(a => '<option value="' + a + '"' + (a === t ? ' selected' : '') + '>' + ({text:'Testo',yes_no:'Sì/No',single_choice:'Scelta singola',multi_choice:'Scelta multipla',scale_1_5:'Scala 1-5',number:'Numero'})[a] + '</option>').join('') + '</select>'
|
||||
+ '<label style="font-size:.78rem;display:flex;align-items:center;gap:4px;white-space:nowrap;"><input type="checkbox" class="q-req-in"' + (required ? ' checked' : '') + '> obbl.</label>'
|
||||
+ '<button type="button" class="q-del" onclick="this.closest(\'.q-row\').remove()">✕</button>'
|
||||
+ '<input type="text" class="q-opts-in" placeholder="Opzioni separate da | (es. Sì | No | In parte)" maxlength="500" style="flex:1 1 100%;margin-top:6px;' + (isChoice ? '' : 'display:none;') + '" value="' + esc((options || []).join(' | ')).replace(/"/g,'"') + '">';
|
||||
el('tpl-questions').appendChild(div);
|
||||
}
|
||||
function qToggleOpts(sel) {
|
||||
const row = sel.closest('.q-row'); const o = row.querySelector('.q-opts-in');
|
||||
o.style.display = (sel.value === 'single_choice' || sel.value === 'multi_choice') ? '' : 'none';
|
||||
}
|
||||
|
||||
function tplOnKind() {
|
||||
const k = el('tpl-kind').value;
|
||||
@@ -299,7 +309,7 @@
|
||||
const t = await api.stkActGetTemplate(id);
|
||||
el('tpl-name').value = t.name || ''; el('tpl-kind').value = t.kind || 'questionnaire';
|
||||
el('tpl-descr').value = t.description || ''; el('tpl-content').value = t.content || '';
|
||||
(t.questions || []).forEach(q => qAdd(q.text, q.type, q.required));
|
||||
(t.questions || []).forEach(q => qAdd(q.text, q.type, q.required, q.options));
|
||||
selMulti('tpl-proc', t.policy_ids || []); selMulti('tpl-mis', t.misura_codes || []); selMulti('tpl-req', t.requisito_ids || []);
|
||||
} catch (e) { el('tpl-err').textContent = e.message || 'Errore'; }
|
||||
} else {
|
||||
@@ -312,8 +322,14 @@
|
||||
|
||||
function collectQuestions() {
|
||||
return Array.prototype.map.call(el('tpl-questions').children, (row, i) => {
|
||||
const inp = row.querySelector('input[type=text]'); const sel = row.querySelector('select'); const chk = row.querySelector('input[type=checkbox]');
|
||||
return { code: 'Q' + (i + 1), text: inp.value.trim(), type: sel.value, required: chk.checked };
|
||||
const text = row.querySelector('.q-text-in').value.trim();
|
||||
const type = row.querySelector('.q-type-in').value;
|
||||
const required = row.querySelector('.q-req-in').checked;
|
||||
const q = { code: 'Q' + (i + 1), text: text, type: type, required: required };
|
||||
if (type === 'single_choice' || type === 'multi_choice') {
|
||||
q.options = (row.querySelector('.q-opts-in').value || '').split('|').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
return q;
|
||||
}).filter(q => q.text !== '');
|
||||
}
|
||||
|
||||
@@ -348,10 +364,10 @@
|
||||
if (!SA.activities.length) { el('act-list').innerHTML = '<div class="sa-empty">Nessuna attività. Creane una con "+ Nuova attività".</div>'; return; }
|
||||
const rows = SA.activities.map(a =>
|
||||
'<tr><td><strong>' + esc(a.title) + '</strong>' + (a.template_name ? '<div style="font-size:.78rem;color:#6b7280;">' + esc(a.template_name) + '</div>' : '') + '</td>'
|
||||
+ '<td><span class="sa-badge k-' + a.type + '">' + ({questionnaire:'Questionario',read_ack:'Firma lettura',action:'Azione'})[a.type] + '</span></td>'
|
||||
+ '<td><span class="sa-badge k-' + a.type + '">' + (TYPE_LABELS[a.type] || a.type) + '</span></td>'
|
||||
+ '<td style="font-size:.82rem;">' + (a.assign_mode === 'by_code' ? ('Codice ' + esc(a.stak_code || '?')) : 'Singoli') + '</td>'
|
||||
+ '<td style="font-size:.82rem;">' + (a.due_date ? esc(a.due_date) : '—') + '</td>'
|
||||
+ '<td><span class="sa-badge s-' + a.status + '">' + esc(a.status) + '</span></td>'
|
||||
+ '<td><span class="sa-badge s-' + a.status + '">' + esc(ST_LABELS[a.status] || a.status) + '</span></td>'
|
||||
+ '<td style="font-size:.82rem;">' + a.n_done + '/' + a.n_targets + '</td>'
|
||||
+ '<td style="white-space:nowrap;"><button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + a.id + ')">Dettaglio</button> '
|
||||
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="actOpen(' + a.id + ')">Modifica</button> '
|
||||
@@ -445,7 +461,7 @@
|
||||
const fb = SA.feedback || { template: null, targets: [] };
|
||||
const tmap = {}; (fb.targets || []).forEach(t => { tmap[t.target_id] = t; });
|
||||
const targets = a.targets || [];
|
||||
let html = '<div style="font-size:.85rem;margin-bottom:10px;color:#374151;">Tipo: <strong>' + esc(a.type) + '</strong> · Scadenza: ' + esc(a.due_date || '—') + ' · Stato: ' + esc(a.status) + '</div>';
|
||||
let html = '<div id="det-answers"></div><div style="font-size:.85rem;margin-bottom:10px;color:#374151;">Tipo: <strong>' + esc(TYPE_LABELS[a.type] || a.type) + '</strong> · Scadenza: ' + esc(a.due_date || '—') + ' · Stato: ' + esc(ST_LABELS[a.status] || a.status) + '</div>';
|
||||
|
||||
// Destinatari + esito
|
||||
if (!targets.length) {
|
||||
@@ -489,12 +505,19 @@
|
||||
|
||||
function detAnswers(targetId) {
|
||||
const fb = SA.feedback || {}; const t = (fb.targets || []).find(x => x.target_id === targetId);
|
||||
if (!t || !t.answers) { showNotification('Nessuna risposta.', 'info'); return; }
|
||||
const box = el('det-answers'); if (!box) return;
|
||||
if (!t || !t.answers) { box.innerHTML = ''; showNotification('Nessuna risposta.', 'info'); return; }
|
||||
const qs = (fb.template && fb.template.questions) || [];
|
||||
const label = {}; qs.forEach(q => { label[q.code] = q.text; });
|
||||
let txt = 'Risposte di ' + (t.respondent_name || t.stakeholder_name) + ':\n\n';
|
||||
Object.keys(t.answers).forEach(k => { txt += (label[k] || k) + '\n→ ' + (Array.isArray(t.answers[k]) ? t.answers[k].join(', ') : t.answers[k]) + '\n\n'; });
|
||||
alert(txt);
|
||||
const rows = Object.keys(t.answers).map(k => {
|
||||
const v = t.answers[k]; const sv = Array.isArray(v) ? v.join(', ') : (v == null ? '' : v);
|
||||
return '<div style="padding:4px 0;border-top:1px solid #eef2ff;"><div style="font-weight:600;font-size:.82rem;">' + esc(label[k] || k) + '</div><div style="font-size:.86rem;">' + esc(sv || '—') + '</div></div>';
|
||||
}).join('');
|
||||
box.innerHTML = '<div style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px 12px;margin-bottom:12px;">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;"><strong style="font-size:.85rem;">Risposte di ' + esc(t.respondent_name || t.stakeholder_name) + '</strong>'
|
||||
+ '<button class="btn btn-outline" style="padding:1px 8px;font-size:.72rem;" onclick="el(\'det-answers\').innerHTML=\'\'">Chiudi</button></div>'
|
||||
+ rows + '</div>';
|
||||
box.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
async function detComment() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Stakeholder - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260628">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260629">
|
||||
<style>
|
||||
.stk-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -191,12 +191,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
+19
-4
@@ -46,6 +46,7 @@
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
function escAttr(s) { return esc(s).replace(/"/g, '"'); }
|
||||
function app() { return el('pw-app'); }
|
||||
|
||||
async function apiGet(path) {
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
|
||||
function qHtml(q, i) {
|
||||
var code = esc(q.code || ('Q' + (i + 1)));
|
||||
var code = escAttr(q.code || ('Q' + (i + 1)));
|
||||
var req = q.required ? ' <span class="q-req">*</span>' : '';
|
||||
var inner = '';
|
||||
if (q.type === 'yes_no') {
|
||||
@@ -106,15 +107,15 @@
|
||||
} else if (q.type === 'scale_1_5') {
|
||||
inner = [1,2,3,4,5].map(function (n) { return '<label class="q-opt" style="display:inline-flex;margin-right:14px;"><input type="radio" name="' + code + '" value="' + n + '"> ' + n + '</label>'; }).join('');
|
||||
} else if (q.type === 'single_choice' && q.options) {
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="radio" name="' + code + '" value="' + esc(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="radio" name="' + code + '" value="' + escAttr(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
} else if (q.type === 'multi_choice' && q.options) {
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="checkbox" name="' + code + '" value="' + esc(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
inner = q.options.map(function (o) { return '<label class="q-opt"><input type="checkbox" name="' + code + '" value="' + escAttr(o) + '"> ' + esc(o) + '</label>'; }).join('');
|
||||
} else if (q.type === 'number') {
|
||||
inner = '<input type="number" name="' + code + '">';
|
||||
} else {
|
||||
inner = '<textarea name="' + code + '" rows="2"></textarea>';
|
||||
}
|
||||
return '<div class="q-block" data-code="' + code + '" data-type="' + esc(q.type || 'text') + '"><div class="q-text">' + esc(q.text) + req + '</div>' + inner + '</div>';
|
||||
return '<div class="q-block" data-code="' + code + '" data-type="' + escAttr(q.type || 'text') + '"><div class="q-text">' + esc(q.text) + req + '</div>' + inner + '</div>';
|
||||
}
|
||||
|
||||
function collectAnswers() {
|
||||
@@ -136,6 +137,20 @@
|
||||
|
||||
async function doRespond() {
|
||||
var answers = collectAnswers();
|
||||
// pre-check campi obbligatori lato client (evidenzia i blocchi mancanti)
|
||||
var qs = (DATA.template && DATA.template.questions) || [];
|
||||
var missing = [];
|
||||
document.querySelectorAll('.q-block').forEach(function (b) { b.style.borderLeft = ''; });
|
||||
qs.forEach(function (q) {
|
||||
if (!q.required) return;
|
||||
var v = answers[q.code];
|
||||
if (v == null || v === '' || (Array.isArray(v) && !v.length)) {
|
||||
missing.push(q.text);
|
||||
var b = document.querySelector('.q-block[data-code="' + (q.code || '') + '"]');
|
||||
if (b) b.style.borderLeft = '3px solid #dc2626';
|
||||
}
|
||||
});
|
||||
if (missing.length) { flash('Rispondi alle domande obbligatorie: ' + missing.join('; '), false); window.scrollTo(0, 0); return; }
|
||||
var res = await apiPost('/stakeholder-portal/respond', { t: TOKEN, answers: answers, respondent_name: (el('pw-name') || {}).value });
|
||||
if (!res.success) { flash(res.message || 'Errore', false); return; }
|
||||
DATA.submitted = true; render(); window.scrollTo(0, 0);
|
||||
|
||||
@@ -477,8 +477,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -488,9 +488,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
|
||||
* cosi' activate elimina automaticamente la shell vecchia.
|
||||
*/
|
||||
const CACHE = 'nis2-shell-v1.21.0';
|
||||
const CACHE = 'nis2-shell-v1.21.1';
|
||||
|
||||
const PRECACHE = [
|
||||
'/offline.html',
|
||||
|
||||
@@ -292,8 +292,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -303,9 +303,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version": "1.21.0", "build": "2026-06-16-v1.21.0", "date": "2026-06-16", "changelog": "Epic C / C5.2b - Portale esterno stakeholder (token magic-link, no account/JWT) + sotto-dashboard feedback: i destinatari rispondono al questionario o firmano l'avvenuta lettura, commentano e allegano file dal portale; il compliance manager vede esiti, commenti e allegati nel dettaglio attivita'. mig.053: stk_activity_responses, stk_activity_comments (allegati su evidence_files). StakeholderPortalController (non-JWT) + StakeholderActivityController feedback/comments/attachments; stk-portal.html pubblica. Completa C5.2/Epic C. Additivo."}
|
||||
{"version": "1.21.1", "build": "2026-06-16-v1.21.1", "date": "2026-06-16", "changelog": "Epic C / C5 hardening (post-verifica flotta): upload allegati con ALLOWLIST estensioni (no html/svg/js same-origin) + nome file random; scadenza magic-link (mig.054 token_expires_at, 410 TOKEN_EXPIRED) + rate-limit endpoint portale + guard scritture su attivita' chiuse; send() non azzera piu' i destinatari gia' responded/acknowledged; assign individuale con semantica replace + guard lista vuota; update conserva assign_mode esistente; editor opzioni per domande a scelta singola/multipla; etichette stato localizzate; risposte mostrate inline (no alert); ARIA su modali/tab; escAttr nel portale; voce sidebar in common.js. Additivo."}
|
||||
|
||||
@@ -207,8 +207,8 @@
|
||||
|
||||
<!-- Report Detail Modal handled by common.js showModal -->
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -218,9 +218,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -319,9 +319,9 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260628"></script>
|
||||
<script src="js/common.js?v=20260628"></script>
|
||||
<script src="js/i18n.js?v=20260628"></script>
|
||||
<script src="js/api.js?v=20260629"></script>
|
||||
<script src="js/common.js?v=20260629"></script>
|
||||
<script src="js/i18n.js?v=20260629"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
Reference in New Issue
Block a user