All checks were successful
Build & Push / Build & Push image (push) Successful in 39s
310 lines
17 KiB
HTML
310 lines
17 KiB
HTML
<!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-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);}
|
||
|
||
@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;}
|
||
::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-track{background:#0f172a;} ::-webkit-scrollbar-thumb{background:#334155;border-radius:4px;}
|
||
}
|
||
</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" onclick="switchUploadTab('xlsx')"
|
||
style="font-size:.85rem;">
|
||
📊 Excel (.xlsx) — aanbevolen
|
||
</button>
|
||
<button class="btn btn-secondary" id="tabJson" onclick="switchUploadTab('json')"
|
||
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" style="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);">
|
||
💡 <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" style="color:var(--primary);">
|
||
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"
|
||
onclick="document.getElementById('fileInputXlsx').click()"
|
||
ondragover="event.preventDefault();this.classList.add('over')"
|
||
ondragleave="this.classList.remove('over')"
|
||
ondrop="this.classList.remove('over');handleDropXlsx(event)">
|
||
<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"
|
||
onchange="uploadXlsx(this.files)">
|
||
</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"
|
||
onclick="document.getElementById('fileInputJson').click()"
|
||
ondragover="event.preventDefault();this.classList.add('over')"
|
||
ondragleave="this.classList.remove('over')"
|
||
ondrop="this.classList.remove('over');handleDropJson(event)">
|
||
<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"
|
||
onchange="uploadJson(this.files)">
|
||
</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="5">Laden...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="notification" id="notification"></div>
|
||
<script nonce="{{ csp_nonce() }}">
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadDoelen();
|
||
switchUploadTab('xlsx');
|
||
});
|
||
|
||
// ── 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 vn = v.naam.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||
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"
|
||
onclick="deleteDoelen('${v.id}','${vn}')">Verwijderen</button></td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── 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>`;
|
||
}
|
||
}
|
||
|
||
// ── XLSX upload ───────────────────────────────────────────────────────────────
|
||
function handleDropXlsx(e) { e.preventDefault(); uploadXlsx(e.dataTransfer.files); }
|
||
function uploadXlsx(files) { doUpload('/admin/doelen/upload-xlsx', files); }
|
||
|
||
// ── JSON upload ───────────────────────────────────────────────────────────────
|
||
function handleDropJson(e) { e.preventDefault(); uploadJson(e.dataTransfer.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>
|