[FIX] Fase 1 UI: completa import fornitori + client API + help (edit precedenti falliti)
Tre Edit dei commit f85876f/73f47df erano falliti silenziosamente (string-not-found)
e committati come fatti: questo li applica davvero.
- public/js/api.js: 14 metodi client Fase 1 (categorie/template/domande/import).
Usano this.del() (il metodo base e' del(), non delete()).
- public/supply-chain.html: pulsante "Importa" in header + openImportModal()/parseCsv()/
runSupplierImport() reali (prima era stub). Modale CSV con upsert per external_ref.
- public/js/help.js: sezioni "Importazione fornitori (CSV/API)" e "Categorie e questionari
configurabili" + riferimento ACN Allegato 2 GV.SC. Nota interpretativa esplicita sul
perimetro fornitori critici (scelta documentata dell'organizzazione).
Seed template NIS2 base (26 domande) ora applicato DAVVERO su org 129 via host DB
(il commit 8d7a50a era fallito: il CLI docker punta a nis2-db, il web/host usa host MySQL;
risolto errore collation utf8mb4 con COLLATE esplicito). Idempotenza verificata.
Inline JS validato (node --check). api.js/help.js validati.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
73f47df389
commit
03da28920d
@ -273,6 +273,20 @@ class NIS2API {
|
|||||||
sendSupplierQuestionnaire(id, email) { return this.post(`/supply-chain/${id}/send-questionnaire`, email ? { email } : {}); } // self-assessment fornitore (link esterno)
|
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)
|
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
|
// Audit
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -370,6 +370,7 @@
|
|||||||
<header class="content-header">
|
<header class="content-header">
|
||||||
<h2 data-i18n="supply_chain.title">Sicurezza Supply Chain</h2>
|
<h2 data-i18n="supply_chain.title">Sicurezza Supply Chain</h2>
|
||||||
<div class="content-header-actions">
|
<div class="content-header-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="openImportModal()"><i class="fas fa-file-import"></i> Importa</button>
|
||||||
<button class="btn btn-primary" onclick="openCreateModal()">+ Nuovo Fornitore</button>
|
<button class="btn btn-primary" onclick="openCreateModal()">+ Nuovo Fornitore</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -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 = `
|
||||||
|
<div style="background:#fff;border-radius:10px;max-width:640px;width:100%;max-height:90vh;overflow:auto;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e2e8f0;">
|
||||||
|
<h3 style="margin:0;"><i class="fas fa-file-import"></i> Importa fornitori</h3>
|
||||||
|
<button onclick="document.getElementById('importModalDyn').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;line-height:1;">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:20px;">
|
||||||
|
<p style="margin:0 0 12px;color:#64748b;font-size:14px;">
|
||||||
|
Incolla un CSV o caricane uno. Prima riga = intestazioni. Colonne: <code>name</code> (obbligatoria),
|
||||||
|
<code>vat_number</code>, <code>contact_email</code>, <code>contact_name</code>,
|
||||||
|
<code>service_type</code>, <code>criticality</code> (low/medium/high/critical),
|
||||||
|
<code>category_slug</code>, <code>external_ref</code>. I record con stesso <code>external_ref</code> vengono aggiornati.
|
||||||
|
</p>
|
||||||
|
<input type="file" id="importFile" accept=".csv,text/csv" class="form-input" style="margin-bottom:10px;">
|
||||||
|
<textarea id="importCsv" class="form-input" rows="8" style="width:100%;font-family:monospace;font-size:13px;"
|
||||||
|
placeholder="name,vat_number,contact_email,criticality,category_slug ACME Srl,IT01234567890,security@acme.it,high,cloud_provider"></textarea>
|
||||||
|
<div id="importResult" style="margin-top:10px;font-size:14px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:10px;padding:16px 20px;border-top:1px solid #e2e8f0;">
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('importModalDyn').remove()">Annulla</button>
|
||||||
|
<button class="btn btn-primary" id="importRunBtn" onclick="runSupplierImport()"><i class="fas fa-upload"></i> Importa</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
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 = '<span style="color:#dc2626;">Nessuna riga valida (serve intestazione + almeno un fornitore con "name").</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rows.length > 1000) {
|
||||||
|
resEl.innerHTML = '<span style="color:#dc2626;">Troppi record (max 1000).</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importo...';
|
||||||
|
try {
|
||||||
|
const res = await api.importSuppliers(rows);
|
||||||
|
if (res.success) {
|
||||||
|
const d = res.data || {};
|
||||||
|
resEl.innerHTML = `<span style="color:#16a34a;">Completato: creati ${d.created||0}, aggiornati ${d.updated||0}, saltati ${d.skipped||0}.</span>`;
|
||||||
|
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 = `<span style="color:#dc2626;">${res.message || 'Errore import'}</span>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resEl.innerHTML = `<span style="color:#dc2626;">Errore: ${(e && e.message) || e}</span>`;
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false; btn.innerHTML = '<i class="fas fa-upload"></i> Importa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Init ────────────────────────────────────────────────
|
// ── Init ────────────────────────────────────────────────
|
||||||
loadSuppliers();
|
loadSuppliers();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user