diff --git a/public/js/api.js b/public/js/api.js index bafe57d..e6a49a1 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -273,6 +273,20 @@ class NIS2API { sendSupplierQuestionnaire(id, email) { return this.post(`/supply-chain/${id}/send-questionnaire`, email ? { email } : {}); } // self-assessment fornitore (link esterno) getSupplierQuestionnaireStatus(id) { return this.get(`/supply-chain/${id}/questionnaire-status`); } // P1 continuous control monitoring (JWT) + // Modulo questionari configurabile (Fase 1): categorie, template, domande, import + getSupplierCategories() { return this.get('/supply-chain/categories'); } + createSupplierCategory(data) { return this.post('/supply-chain/categories', data); } + updateSupplierCategory(id, data) { return this.put(`/supply-chain/categories/${id}`, data); } + deleteSupplierCategory(id) { return this.del(`/supply-chain/categories/${id}`); } + getQuestionnaireTemplates() { return this.get('/supply-chain/templates'); } + getQuestionnaireTemplate(id) { return this.get(`/supply-chain/templates/${id}`); } + createQuestionnaireTemplate(data) { return this.post('/supply-chain/templates', data); } + updateQuestionnaireTemplate(id, data) { return this.put(`/supply-chain/templates/${id}`, data); } + addTemplateQuestion(templateId, data) { return this.post(`/supply-chain/${templateId}/questions`, data); } + updateTemplateQuestion(id, data) { return this.put(`/supply-chain/questions/${id}`, data); } + deleteTemplateQuestion(id) { return this.del(`/supply-chain/questions/${id}`); } + importSuppliers(suppliers) { return this.post('/supply-chain/import', { suppliers }); } + // ═══════════════════════════════════════════════════════════════════ // Audit // ═══════════════════════════════════════════════════════════════════ diff --git a/public/supply-chain.html b/public/supply-chain.html index 00ca5f1..1c1e954 100644 --- a/public/supply-chain.html +++ b/public/supply-chain.html @@ -370,6 +370,7 @@

Sicurezza Supply Chain

+
@@ -1160,6 +1161,109 @@ } } + // ── Import fornitori (CSV / incolla) → upsert per external_ref ── + function openImportModal() { + const ex = document.getElementById('importModalDyn'); + if (ex) ex.remove(); + const ov = document.createElement('div'); + ov.id = 'importModalDyn'; + ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:1000;padding:16px;'; + ov.innerHTML = ` +
+
+

Importa fornitori

+ +
+
+

+ Incolla un CSV o caricane uno. Prima riga = intestazioni. Colonne: name (obbligatoria), + vat_number, contact_email, contact_name, + service_type, criticality (low/medium/high/critical), + category_slug, external_ref. I record con stesso external_ref vengono aggiornati. +

+ + +
+
+
+ + +
+
`; + document.body.appendChild(ov); + document.getElementById('importFile').addEventListener('change', (e) => { + const f = e.target.files[0]; + if (!f) return; + const r = new FileReader(); + r.onload = () => { document.getElementById('importCsv').value = r.result; }; + r.readAsText(f); + }); + } + + function parseCsv(text) { + const rows = []; + const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').filter(l => l.trim() !== ''); + if (lines.length < 2) return []; + const split = (line) => { + const out = []; let cur = ''; let inQ = false; + for (let i = 0; i < line.length; i++) { + const c = line[i]; + if (inQ) { + if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; } + else if (c === '"') inQ = false; + else cur += c; + } else { + if (c === '"') inQ = true; + else if (c === ',' || c === ';') { out.push(cur); cur = ''; } + else cur += c; + } + } + out.push(cur); + return out.map(s => s.trim()); + }; + const headers = split(lines[0]).map(h => h.toLowerCase()); + for (let i = 1; i < lines.length; i++) { + const cells = split(lines[i]); + const obj = {}; + headers.forEach((h, idx) => { if (h) obj[h] = cells[idx] ?? ''; }); + if ((obj.name || '').trim() !== '') rows.push(obj); + } + return rows; + } + + async function runSupplierImport() { + const txt = document.getElementById('importCsv').value || ''; + const resEl = document.getElementById('importResult'); + const btn = document.getElementById('importRunBtn'); + const rows = parseCsv(txt); + if (rows.length === 0) { + resEl.innerHTML = 'Nessuna riga valida (serve intestazione + almeno un fornitore con "name").'; + return; + } + if (rows.length > 1000) { + resEl.innerHTML = 'Troppi record (max 1000).'; + return; + } + btn.disabled = true; btn.innerHTML = ' Importo...'; + try { + const res = await api.importSuppliers(rows); + if (res.success) { + const d = res.data || {}; + resEl.innerHTML = `Completato: creati ${d.created||0}, aggiornati ${d.updated||0}, saltati ${d.skipped||0}.`; + showNotification(`Import: ${d.created||0} creati, ${d.updated||0} aggiornati`, 'success'); + loadSuppliers(); + setTimeout(() => { const m = document.getElementById('importModalDyn'); if (m) m.remove(); }, 1800); + } else { + resEl.innerHTML = `${res.message || 'Errore import'}`; + } + } catch (e) { + resEl.innerHTML = `Errore: ${(e && e.message) || e}`; + } finally { + btn.disabled = false; btn.innerHTML = ' Importa'; + } + } + // ── Init ──────────────────────────────────────────────── loadSuppliers();