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 + Categorie + Template Importa + Nuovo Fornitore @@ -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 @@ `${label}` ).join(''); + const categoryOptions = '— Nessuna —' + + allCategories.map(c => + `${escapeHtml(c.name)}${Number(c.is_system) === 1 ? '' : ' (personalizzata)'}` + ).join(''); + const content = ` @@ -931,6 +950,13 @@ + + Categoria fornitore + + ${categoryOptions} + + Classifica il fornitore (es. Cloud Provider). Gestisci le categorie dalla tab "Categorie". + Descrizione Servizio ${isEdit && supplier.service_description ? escapeHtml(supplier.service_description) : ''} @@ -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' : ` + Modifica + Elimina`; + return ` + + ${escapeHtml(c.name)} ${badge} + ${c.description ? `${escapeHtml(c.description)}` : ''} + + ${actions} + `; + }).join(''); + const content = ` + + + Nuova categoria + + ${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: `Chiudi` + }); + } + + function newCategory() { categoryForm(null); } + function editCategory(id) { categoryForm(allCategories.find(c => c.id === id) || null); } + + function categoryForm(cat) { + const isEdit = !!cat; + const content = ` + + Nome categoria * + + + + Descrizione + ${isEdit && cat.description ? escapeHtml(cat.description) : ''} + `; + showModal(isEdit ? 'Modifica categoria' : 'Nuova categoria', content, { + footer: ` + Annulla + ${isEdit ? 'Salva' : 'Crea'}` + }); + } + + 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: `Chiudi` + }); + 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)):''} + + Vedi domande + `; + }).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 = ` + ← Torna ai template + ${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."}
Nessuna categoria.
Le categorie preset sono fornite dal sistema e non sono modificabili. Puoi creare categorie personalizzate per la tua organizzazione.
Errore di connessione.
Nessun template questionario per questa organizzazione.
Template non trovato.
Nessuna domanda nel template.
${escapeHtml(tpl.description)}