From ce0387ed4f80dc2440342899567b0bf96c2ad1ec Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sun, 31 May 2026 14:30:20 +0200 Subject: [PATCH] [FEAT] Supply chain UI: dropdown categoria + gestione categorie + vista template GV.SC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colma il gap UI confermato dalla review (backend Fase 1 c'era, UI no): - Form fornitore: dropdown "Categoria fornitore" alimentato da getSupplierCategories (ensureCategories con cache); category_id ora salvato (backend gia' fixato 9fbf72a). - Pulsante "Categorie": modale con lista preset+custom, CRUD sulle custom (preset non modificabili), gestione errore CATEGORY_IN_USE. - Pulsante "Template": modale lista template + vista dettaglio domande read-only con badge nis2_ref/tipo/peso/obbligatoria — mostra le 26 domande GV.SC del DB. - Target modale via querySelector('#app-modal .modal-body') (showModal usa class). Inline JS validato (node --check exit 0). File statici -> live via nginx. version 1.10.0. Co-Authored-By: Claude Opus 4.8 --- public/supply-chain.html | 202 ++++++++++++++++++++++++++++++++++++++- public/version.json | 2 +- 2 files changed, 202 insertions(+), 2 deletions(-) diff --git a/public/supply-chain.html b/public/supply-chain.html index 1c1e954..10a2db8 100644 --- a/public/supply-chain.html +++ b/public/supply-chain.html @@ -370,6 +370,8 @@

Sicurezza Supply Chain

+ +
@@ -872,12 +874,24 @@ } // ── Create / Edit Modal ───────────────────────────────── - function openCreateModal() { + let allCategories = []; // cache categorie (preset org 0 + custom org) + + async function ensureCategories() { + if (allCategories.length > 0) return; + try { + const res = await api.getSupplierCategories(); + if (res.success) allCategories = res.data || []; + } catch (e) { /* non bloccante: la modale apre comunque */ } + } + + async function openCreateModal() { + await ensureCategories(); showSupplierFormModal(null); } async function openEditModal(id) { try { + await ensureCategories(); const result = await api.getSupplier(id); if (result.success && result.data) { showSupplierFormModal(result.data); @@ -897,6 +911,11 @@ `` ).join(''); + const categoryOptions = '' + + allCategories.map(c => + `` + ).join(''); + const content = `
@@ -931,6 +950,13 @@
+
+ + + Classifica il fornitore (es. Cloud Provider). Gestisci le categorie dalla tab "Categorie". +
@@ -982,6 +1008,7 @@ service_type: serviceType, service_description: document.getElementById('form-service-desc').value.trim() || null, criticality: document.getElementById('form-criticality').value, + category_id: (document.getElementById('form-category') && document.getElementById('form-category').value) || null, contract_start_date: document.getElementById('form-contract-start').value || null, contract_expiry_date: document.getElementById('form-contract-expiry').value || null, notes: document.getElementById('form-notes').value.trim() || null, @@ -1264,6 +1291,179 @@ } } + // ════════════════════════════════════════════════════════ + // CATEGORIE FORNITORE (preset di sistema + custom per-org) + // ════════════════════════════════════════════════════════ + async function openCategoriesModal() { + await ensureCategories(); + renderCategoriesModal(); + } + + function renderCategoriesModal() { + const rows = allCategories.map(c => { + const preset = Number(c.is_system) === 1; + const badge = preset + ? 'Preset' + : 'Personalizzata'; + const actions = preset ? 'non modificabile' : ` + + `; + return `
+
+
${escapeHtml(c.name)} ${badge}
+ ${c.description ? `
${escapeHtml(c.description)}
` : ''} +
+
${actions}
+
`; + }).join(''); + const content = ` +
+ +
+
${rows || '

Nessuna categoria.

'}
+

Le categorie preset sono fornite dal sistema e non sono modificabili. Puoi creare categorie personalizzate per la tua organizzazione.

`; + showModal('Categorie fornitore', content, { + size: 'lg', + footer: `` + }); + } + + function newCategory() { categoryForm(null); } + function editCategory(id) { categoryForm(allCategories.find(c => c.id === id) || null); } + + function categoryForm(cat) { + const isEdit = !!cat; + const content = ` +
+ + +
+
+ + +
`; + showModal(isEdit ? 'Modifica categoria' : 'Nuova categoria', content, { + footer: ` + + ` + }); + } + + async function saveCategory(id) { + const name = document.getElementById('cat-name').value.trim(); + if (!name) { showNotification('Il nome della categoria e\' obbligatorio.', 'warning'); return; } + const data = { name, description: document.getElementById('cat-desc').value.trim() || null }; + try { + const res = id ? await api.updateSupplierCategory(id, data) : await api.createSupplierCategory(data); + if (res.success) { + allCategories = []; // invalida cache + await ensureCategories(); + showNotification(id ? 'Categoria aggiornata.' : 'Categoria creata.', 'success'); + renderCategoriesModal(); + } else { + showNotification(res.message || 'Errore nel salvataggio.', 'error'); + } + } catch (e) { showNotification('Errore di connessione.', 'error'); } + } + + async function removeCategory(id) { + const cat = allCategories.find(c => c.id === id); + if (!confirm('Eliminare la categoria "' + (cat ? cat.name : '') + '"?')) return; + try { + const res = await api.deleteSupplierCategory(id); + if (res.success) { + allCategories = []; + await ensureCategories(); + showNotification('Categoria eliminata.', 'success'); + renderCategoriesModal(); + } else { + showNotification(res.message || 'Impossibile eliminare (categoria in uso?).', 'error'); + } + } catch (e) { showNotification('Errore di connessione.', 'error'); } + } + + // ════════════════════════════════════════════════════════ + // TEMPLATE QUESTIONARI (lista + dettaglio domande, read-only) + // ════════════════════════════════════════════════════════ + const QTYPE_LABELS = { + yes_no_partial: 'Si/Parziale/No', single_choice: 'Scelta singola', + multi_choice: 'Scelta multipla', scale_1_5: 'Scala 1-5', + text: 'Testo', number: 'Numero', file: 'Allegato' + }; + + async function openTemplatesModal() { + showModal('Template questionari', '
Caricamento...
', { + size: 'lg', footer: `` + }); + try { + const res = await api.getQuestionnaireTemplates(); + const list = (res.success && res.data) ? res.data : []; + renderTemplatesModal(list); + } catch (e) { + const b = document.querySelector('#app-modal .modal-body'); + if (b) b.innerHTML = '

Errore di connessione.

'; + } + } + + function renderTemplatesModal(templates) { + const b = document.querySelector('#app-modal .modal-body'); + if (!b) return; + if (!templates.length) { + b.innerHTML = '

Nessun template questionario per questa organizzazione.

'; + return; + } + b.innerHTML = templates.map(t => { + const qc = Number(t.question_count || 0); + const st = t.status === 'active' ? 'Attivo' : `${escapeHtml(t.status || 'bozza')}`; + return `
+
+
${escapeHtml(t.name)} ${Number(t.is_default)===1?'Default':''} ${st}
+
${t.category_name?escapeHtml(t.category_name)+' · ':''}${qc} domand${qc===1?'a':'e'}${t.current_version?' · v'+escapeHtml(String(t.current_version)):''}
+
+ +
`; + }).join(''); + } + + async function viewTemplate(id) { + const b = document.querySelector('#app-modal .modal-body'); + if (b) b.innerHTML = '
Caricamento domande...
'; + try { + const res = await api.getQuestionnaireTemplate(id); + if (res.success && res.data) renderTemplateDetail(res.data); + else if (b) b.innerHTML = '

Template non trovato.

'; + } catch (e) { + if (b) b.innerHTML = '

Errore di connessione.

'; + } + } + + function renderTemplateDetail(tpl) { + const b = document.querySelector('#app-modal .modal-body'); + if (!b) return; + const qs = tpl.questions || []; + const qHtml = qs.length ? qs.map((q, i) => ` +
+
+ ${i+1}. + ${escapeHtml(q.question_text)} +
+
+ ${q.nis2_ref?`${escapeHtml(q.nis2_ref)}`:''} + ${escapeHtml(QTYPE_LABELS[q.question_type]||q.question_type||'')} + ${q.weight!=null?`Peso ${escapeHtml(String(q.weight))}`:''} + ${Number(q.is_required)===1?'Obbligatoria':''} + ${Number(q.high_criticality_only)===1?'Solo alta criticita':''} +
+
`).join('') : '

Nessuna domanda nel template.

'; + b.innerHTML = ` +
+

${escapeHtml(tpl.name)}

+
${qs.length} domand${qs.length===1?'a':'e'}${tpl.current_version?' · versione '+escapeHtml(String(tpl.current_version)):''}${tpl.pass_threshold!=null?' · soglia '+escapeHtml(String(tpl.pass_threshold))+'%':''}
+ ${tpl.description?`

${escapeHtml(tpl.description)}

`:''} +
Sola lettura. L'editor delle domande sara' disponibile in una prossima versione.
+ ${qHtml}`; + } + // ── Init ──────────────────────────────────────────────── loadSuppliers(); diff --git a/public/version.json b/public/version.json index 5f0c0e7..6bab02e 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version":"1.9.2","build":"2026-05-31-v1.9.2","date":"2026-05-31","changelog":"Fase 1 fornitori + hardening post-review: fix incidents.html (SyntaxError apostrofi), dashboard gauge compliance, citazioni ACN GV.SC con disclaimer interpretazioni nel template, scope org category_id import. Guida: avvertenza generale (non parere legale) + capitolo fornitori (import CSV/API, categorie, template GV.SC, nota interpretativa perimetro). Help online allineato."} +{"version":"1.10.0","build":"2026-05-31-v1.10.0","date":"2026-05-31","changelog":"UI modulo questionari fornitori: dropdown categoria nel form fornitore, gestione categorie (preset+custom CRUD), visualizzazione template questionari con domande GV.SC e badge nis2_ref (read-only). Backend Fase 1 ora pienamente usabile dalla UI."}