Files
leerdoelen_tracker/backend/templates/doelen_beheer.html
Sam 5ea998c3d6
All checks were successful
Build & Push / Build & Push image (push) Successful in 1m29s
remove inline event handlers, add addEventListener
2026-03-01 01:13:24 +01:00

318 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Leerdoelen bestanden {{ org_name }}</title>
<style>
:root {
--primary:#4f46e5;--primary-dark:#4338ca;--success:#10b981;
--danger:#ef4444;--warning:#f59e0b;
--gray-50:#f9fafb;--gray-100:#f3f4f6;--gray-200:#e5e7eb;
--gray-300:#d1d5db;--gray-500:#6b7280;--gray-600:#4b5563;
--gray-700:#374151;--gray-800:#1f2937;
}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--gray-100);color:var(--gray-800);}
.container{max-width:900px;margin:0 auto;padding:1rem;}
.header{background:linear-gradient(135deg,#065f46,#047857);color:white;border-radius:12px;padding:1.25rem 1.5rem;margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;}
.header h1{font-size:1.3rem;display:flex;align-items:center;gap:.6rem;}
.header-sub{opacity:.85;font-size:.85rem;margin-top:.2rem;}
.nav-link{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem .9rem;background:rgba(255,255,255,.15);color:white;border:1px solid rgba(255,255,255,.25);border-radius:6px;text-decoration:none;font-size:.85rem;font-weight:500;transition:background .2s;}
.nav-link:hover{background:rgba(255,255,255,.25);}
.section{background:white;border-radius:12px;padding:1.5rem;margin-bottom:1.5rem;box-shadow:0 1px 3px rgba(0,0,0,.1);}
.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:.5rem;}
.section-header h2{font-size:1.05rem;color:var(--gray-700);}
.section-hint{color:var(--gray-500);font-size:.83rem;margin-bottom:1.25rem;line-height:1.6;}
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem 1rem;border:none;border-radius:6px;font-size:.85rem;font-weight:500;cursor:pointer;transition:all .2s;text-decoration:none;}
.btn-primary{background:var(--primary);color:white;} .btn-primary:hover{background:var(--primary-dark);}
.btn-secondary{background:var(--gray-200);color:var(--gray-700);} .btn-secondary:hover{background:var(--gray-300);}
.btn-danger{background:var(--danger);color:white;} .btn-danger:hover{background:#dc2626;}
.btn-sm{padding:.25rem .55rem;font-size:.78rem;}
table{width:100%;border-collapse:collapse;font-size:.875rem;}
th{padding:.65rem .75rem;text-align:left;font-weight:600;color:var(--gray-600);border-bottom:2px solid var(--gray-200);background:var(--gray-50);white-space:nowrap;}
td{padding:.6rem .75rem;border-bottom:1px solid var(--gray-100);vertical-align:middle;}
tr:hover td{background:var(--gray-50);}
.empty-row td{text-align:center;color:var(--gray-500);padding:2.5rem;font-style:italic;}
.drop-zone{border:2px dashed var(--gray-300);border-radius:10px;padding:2.5rem;text-align:center;cursor:pointer;transition:all .2s;background:var(--gray-50);}
.drop-zone:hover,.drop-zone.over{border-color:var(--primary);background:rgba(79,70,229,.04);}
.drop-icon{font-size:2.5rem;margin-bottom:.75rem;}
.drop-zone strong{font-size:1rem;}
.drop-zone p{color:var(--gray-500);font-size:.83rem;margin-top:.4rem;}
.versie-badge{background:#d1fae5;color:#065f46;padding:.2rem .5rem;border-radius:4px;font-size:.75rem;font-weight:600;}
.upload-ok{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:.85rem 1rem;margin-bottom:.5rem;}
.upload-ok strong{color:#15803d;}
.upload-err{background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:.85rem 1rem;}
.upload-err strong{color:#dc2626;}
.upload-ok ul,.upload-err ul{margin:.4rem 0 0 1.25rem;font-size:.82rem;}
.upload-ok li{color:#166534;}
.upload-err li{color:#991b1b;}
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.75rem;margin-bottom:1.5rem;}
.stat{background:white;border-radius:10px;padding:.9rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.08);}
.stat-value{font-size:1.7rem;font-weight:700;color:#047857;}
.stat-label{font-size:.72rem;color:var(--gray-500);text-transform:uppercase;letter-spacing:.04em;margin-top:.2rem;}
.notification{position:fixed;bottom:1rem;right:1rem;padding:.85rem 1.25rem;border-radius:8px;color:white;font-weight:500;transform:translateY(100px);opacity:0;transition:all .3s;z-index:2000;}
.notification.show{transform:translateY(0);opacity:1;}
.notification.success{background:var(--success);}
.notification.error{background:var(--danger);}
.notification.warning{background:var(--warning);}
.info-box{background:var(--gray-50);border:1px solid var(--gray-200);border-left:4px solid var(--primary);border-radius:6px;padding:.85rem 1rem;margin-bottom:1rem;font-size:.83rem;color:var(--gray-600);}
.info-box a{color:var(--primary);}
@media (prefers-color-scheme: dark) {
:root {
--gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;
--gray-300:#1a1a3e;--gray-500:#9ca3af;--gray-600:#d1d5db;
--gray-700:#e5e7eb;--gray-800:#f3f4f6;
}
body{background:#0f172a;color:#e2e8f0;}
.section,.stat{background:#1e293b !important;border-color:#334155;}
th{background:#1e293b !important;color:#94a3b8 !important;border-color:#334155 !important;}
td{border-color:#1e293b !important;color:#e2e8f0;}
tr:hover td{background:#263548 !important;}
.drop-zone{background:#162032 !important;border-color:#334155 !important;}
.drop-zone:hover,.drop-zone.over{background:#1a2744 !important;border-color:#6366f1 !important;}
.upload-ok{background:#064e3b !important;border-color:#065f46 !important;}
.upload-err{background:#450a0a !important;border-color:#7f1d1d !important;}
.versie-badge{background:#064e3b !important;color:#6ee7b7 !important;}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>📂 Leerdoelen bestanden</h1>
<div class="header-sub">{{ org_name }}</div>
</div>
<a href="/dashboard" class="nav-link">← Terug naar beheer</a>
</div>
<!-- Stats -->
<div class="stat-row" id="statRow">
<div class="stat"><div class="stat-value" id="statVakken">-</div><div class="stat-label">Vakken</div></div>
<div class="stat"><div class="stat-value" id="statDoelen">-</div><div class="stat-label">Doelzinnen</div></div>
<div class="stat"><div class="stat-value" id="statVersie">-</div><div class="stat-label">Versie</div></div>
</div>
<!-- Upload -->
<div class="section">
<div class="section-header">
<h2>⬆️ Nieuwe bestanden uploaden</h2>
</div>
<!-- Format tabs -->
<div style="display:flex;gap:.5rem;margin-bottom:1rem;">
<button class="btn btn-primary" id="tabXlsx" style="font-size:.85rem;">
📊 Excel (.xlsx) — aanbevolen
</button>
<button class="btn btn-secondary" id="tabJson" style="font-size:.85rem;">
📄 JSON
</button>
</div>
<!-- XLSX upload -->
<div id="panelXlsx">
<p class="section-hint">
Upload de originele Excel bestanden van GO!
(<code>Doelenset_BaO_*.xlsx</code>). De conversie naar JSON
gebeurt automatisch op de server — geen tussentijdse stap nodig.
Bij een update gewoon opnieuw uploaden.
</p>
<div class="info-box">
💡 <strong>Nieuwe versie van GO!?</strong>
De GO! publiceert updates van de doelensets op
<a href="https://pro.g-o.be/themas/leerplannen/basisonderwijs/nieuw-leerplan-basisonderwijs/"
target="_blank" rel="noopener noreferrer">
pro.g-o.be → Leerplannen → BaO
</a>.
Er is geen automatische synchronisatie — download de nieuwe xlsx bestanden
manueel en upload ze hier.
</div>
<div class="drop-zone" id="dropZoneXlsx">
<div class="drop-icon">📊</div>
<strong>Klik of sleep Excel bestanden hier</strong>
<p>Meerdere bestanden tegelijk · Enkel .xlsx · Max. 10 MB per bestand</p>
</div>
<input type="file" id="fileInputXlsx" accept=".xlsx" multiple style="display:none">
</div>
<!-- JSON upload -->
<div id="panelJson" style="display:none;">
<p class="section-hint">
Upload reeds geconverteerde JSON bestanden. Gebruik dit enkel als je
de Excel bestanden niet beschikbaar hebt of als je handmatig aangepaste
JSON wil laden.
</p>
<div class="drop-zone" id="dropZoneJson">
<div class="drop-icon">📄</div>
<strong>Klik of sleep JSON bestanden hier</strong>
<p>Meerdere bestanden tegelijk · Enkel .json · Max. 10 MB per bestand</p>
</div>
<input type="file" id="fileInputJson" accept=".json" multiple style="display:none">
</div>
<div id="uploadResults" style="margin-top:.85rem;display:none;"></div>
</div>
<!-- Geïnstalleerde vakken -->
<div class="section">
<div class="section-header">
<h2>📚 Geïnstalleerde vakken</h2>
<span id="doelenVersie"></span>
</div>
<table>
<thead>
<tr>
<th>Vak</th>
<th>Bestand ID</th>
<th>Doelzinnen</th>
<th>Versie</th>
<th>Gewijzigd door GO!</th>
<th></th>
</tr>
</thead>
<tbody id="doelenBody">
<tr class="empty-row"><td colspan="6">Laden...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
document.addEventListener('DOMContentLoaded', () => {
// ── Tab knoppen ──────────────────────────────────────────────────────────
document.getElementById('tabXlsx').addEventListener('click', () => switchUploadTab('xlsx'));
document.getElementById('tabJson').addEventListener('click', () => switchUploadTab('json'));
// ── Drop zones ───────────────────────────────────────────────────────────
setupDropZone('dropZoneXlsx', 'fileInputXlsx', uploadXlsx);
setupDropZone('dropZoneJson', 'fileInputJson', uploadJson);
// ── File inputs ──────────────────────────────────────────────────────────
document.getElementById('fileInputXlsx').addEventListener('change', function() { uploadXlsx(this.files); });
document.getElementById('fileInputJson').addEventListener('change', function() { uploadJson(this.files); });
// ── Init ─────────────────────────────────────────────────────────────────
loadDoelen();
switchUploadTab('xlsx');
});
function setupDropZone(zoneId, inputId, uploadFn) {
const zone = document.getElementById(zoneId);
const input = document.getElementById(inputId);
zone.addEventListener('click', () => input.click());
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('over'); });
zone.addEventListener('dragleave', () => zone.classList.remove('over'));
zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('over'); uploadFn(e.dataTransfer.files); });
}
// ── Tab wisselen ──────────────────────────────────────────────────────────────
function switchUploadTab(tab) {
document.getElementById('panelXlsx').style.display = tab === 'xlsx' ? '' : 'none';
document.getElementById('panelJson').style.display = tab === 'json' ? '' : 'none';
document.getElementById('tabXlsx').className = 'btn ' + (tab === 'xlsx' ? 'btn-primary' : 'btn-secondary');
document.getElementById('tabJson').className = 'btn ' + (tab === 'json' ? 'btn-primary' : 'btn-secondary');
document.getElementById('tabXlsx').style.fontSize = '.85rem';
document.getElementById('tabJson').style.fontSize = '.85rem';
}
// ── Doelen overzicht laden ────────────────────────────────────────────────────
async function loadDoelen() {
const res = await fetch('/admin/doelen');
if (!res.ok) return;
const data = await res.json();
if (data.versie) {
document.getElementById('doelenVersie').innerHTML =
`<span class="versie-badge">index versie ${data.versie}</span>`;
document.getElementById('statVersie').textContent = data.versie;
}
const vakken = data.vakken || [];
document.getElementById('statVakken').textContent = vakken.length;
document.getElementById('statDoelen').textContent =
vakken.reduce((s, v) => s + (v.aantalDoelzinnen || 0), 0).toLocaleString('nl-BE');
const tbody = document.getElementById('doelenBody');
if (!vakken.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">Nog geen doelen geüpload</td></tr>';
return;
}
tbody.innerHTML = vakken.map(v => {
const datum = v.bronDatum
? `<span title="'Gewijzigd op' datum uit Excel metadata van GO!">${v.bronDatum}</span>`
: `<span style="color:var(--gray-400)">onbekend</span>`;
return `<tr>
<td><strong>${v.naam}</strong></td>
<td style="font-family:monospace;font-size:.78rem;color:var(--gray-500);">${v.id}</td>
<td>${(v.aantalDoelzinnen||0).toLocaleString('nl-BE')}</td>
<td><span class="versie-badge">${v.versie || '?'}</span></td>
<td style="font-size:.83rem;">${datum}</td>
<td><button class="btn btn-danger btn-sm" data-vak-id="${v.id}" data-vak-naam="${v.naam}">Verwijderen</button></td>
</tr>`;
}).join('');
// Event listeners op verwijder knoppen — na renderen
tbody.querySelectorAll('.btn-danger').forEach(btn => {
btn.addEventListener('click', () => deleteDoelen(btn.dataset.vakId, btn.dataset.vakNaam));
});
}
// ── Upload helpers ────────────────────────────────────────────────────────────
function toonResultaten(data) {
const el = document.getElementById('uploadResults');
const ok = data.results.filter(r => r.ok);
const err = data.results.filter(r => !r.ok);
let html = '';
if (ok.length)
html += `<div class="upload-ok"><strong>✓ ${ok.length} bestand(en) verwerkt</strong><ul>`
+ ok.map(r => `<li><strong>${r.vak_id}</strong> — ${r.aantalDoelzinnen} doelzinnen (v${r.versie})</li>`).join('')
+ `</ul></div>`;
if (err.length)
html += `<div class="upload-err"><strong>✗ ${err.length} mislukt</strong><ul>`
+ err.map(r => `<li><strong>${r.filename}</strong>: ${r.error}</li>`).join('')
+ `</ul></div>`;
el.innerHTML = html;
el.style.display = 'block';
loadDoelen();
setTimeout(() => { el.style.display = 'none'; }, 15000);
}
async function doUpload(endpoint, files) {
if (!files?.length) return;
const el = document.getElementById('uploadResults');
el.style.display = 'block';
el.innerHTML = `<p style="color:var(--gray-500);font-size:.85rem;">⏳ Verwerken van ${files.length} bestand(en)…`
+ (endpoint.includes('xlsx') ? ' (Excel conversie kan even duren)' : '') + `</p>`;
const fd = new FormData();
for (const f of files) fd.append('files', f);
try {
const res = await fetch(endpoint, { method: 'POST', body: fd });
const data = await res.json();
toonResultaten(data);
} catch (e) {
el.innerHTML = `<div class="upload-err"><strong>✗ Netwerkfout:</strong> ${e.message}</div>`;
}
}
function uploadXlsx(files) { doUpload('/admin/doelen/upload-xlsx', files); }
function uploadJson(files) { doUpload('/admin/doelen/upload', files); }
// ── Verwijderen ───────────────────────────────────────────────────────────────
async function deleteDoelen(vakId, vakNaam) {
if (!confirm(`Doelen voor "${vakNaam}" verwijderen? Bestaande beoordelingen blijven bewaard.`)) return;
const res = await fetch(`/admin/doelen/${vakId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`${vakNaam} verwijderd`, 'success');
await loadDoelen();
}
function notify(msg, type='success') {
const el = document.getElementById('notification');
el.textContent = msg;
el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3500);
}
</script>
</body>
</html>