Files
leerdoelen_tracker/backend/templates/directeur.html
Sam Geyskens 04fe593d0c
All checks were successful
Build & Push / Build & Push image (push) Successful in 41s
feat: implement modal functionality for linking teachers and adding new classes with improved UI
2026-03-06 10:07:32 +01:00

894 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Directeur Dashboard - Leerdoelen Tracker</title>
<style>
:root {
--primary: #4f46e5; --primary-dark: #4338ca;
--success: #10b981; --warning: #f59e0b; --danger: #ef4444;
--gray-50: #f9fafb; --gray-100: #f3f4f6; --gray-200: #e5e7eb;
--gray-300: #d1d5db; --gray-400: #9ca3af; --gray-500: #6b7280;
--gray-600: #4b5563; --gray-700: #374151; --gray-800: #1f2937;
--status-groen: #10b981; --status-oranje: #f59e0b; --status-roze: #ec4899;
}
* { 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: 1600px; margin: 0 auto; padding: 1rem; }
.header { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; border-radius: 12px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
.header h1 { font-size: 1.4rem; display: flex; align-items: center; gap: 0.5rem; }
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 0.85rem; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; }
.btn-light { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); }
.btn-light:hover { background: rgba(255,255,255,0.3); }
.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-success { background: var(--success); color: white; }
.btn-success:hover { background: #059669; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
.stat-card { background: white; border-radius: 10px; padding: 1rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.stat-card.highlight { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; }
.stat-value { font-size: 1.8rem; font-weight: 700; line-height: 1.2; }
.stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .05em; opacity: .8; margin-top: .2rem; }
.tabs { display: flex; gap: .25rem; background: white; border-radius: 10px; padding: .35rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); flex-wrap: wrap; }
.tab-btn { flex: 1; min-width: 100px; padding: .5rem .75rem; border: none; border-radius: 7px; font-size: .85rem; font-weight: 500; cursor: pointer; background: transparent; color: var(--gray-600); transition: all .2s; }
.tab-btn.active { background: var(--primary); color: white; }
.tab-btn:hover:not(.active) { background: var(--gray-100); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.card h2 { font-size: 1.05rem; color: var(--gray-700); margin-bottom: 1rem; display: flex; align-items: center; gap: .5rem; }
.klas-chips { display: flex; flex-wrap: wrap; gap: .6rem; }
.klas-chip { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: .5rem .85rem; font-size: .85rem; }
.klas-chip .klas-name { font-weight: 600; color: var(--gray-800); }
.klas-chip .klas-teachers { font-size: .75rem; color: var(--gray-500); margin-top: .15rem; }
.vak-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: .75rem; }
.vak-card { background: var(--gray-50); border-radius: 8px; padding: 1rem; }
.vak-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .6rem; }
.vak-card-header h3 { font-size: .9rem; font-weight: 600; }
.progress-bar { height: 8px; background: var(--gray-200); border-radius: 4px; overflow: hidden; }
.progress-inner { height: 100%; display: flex; }
.p-groen { background: var(--status-groen); }
.p-oranje { background: var(--status-oranje); }
.p-roze { background: var(--status-roze); }
.vak-legend { display: flex; gap: .75rem; font-size: .75rem; color: var(--gray-600); margin-top: .4rem; flex-wrap: wrap; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 2px; }
.filter-bar { background: white; border-radius: 10px; padding: .85rem 1.25rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); display: flex; gap: .75rem; flex-wrap: wrap; align-items: flex-end; }
.fg { display: flex; flex-direction: column; gap: .2rem; }
.fg label { font-size: .72rem; font-weight: 600; color: var(--gray-500); text-transform: uppercase; letter-spacing: .04em; }
.fg select, .fg input { padding: .45rem .65rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: .88rem; min-width: 140px; }
.fg select:focus, .fg input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
.leeftijd-cbs { display: flex; flex-wrap: wrap; gap: .3rem; }
.lft-cb { display: flex; align-items: center; gap: .2rem; padding: .28rem .5rem; border: 1px solid var(--gray-300); border-radius: 5px; font-size: .78rem; cursor: pointer; user-select: none; }
.lft-cb:has(input:checked) { background: var(--primary); border-color: var(--primary); color: white; }
.lft-cb input { display: none; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .83rem; }
thead { background: var(--gray-50); position: sticky; top: 0; z-index: 5; }
th { padding: .65rem .6rem; text-align: center; font-weight: 600; color: var(--gray-600); border-bottom: 2px solid var(--gray-200); white-space: nowrap; }
th.goal-col { text-align: left; min-width: 240px; }
th.klas-col { min-width: 75px; font-size: .75rem; }
td { padding: .45rem .6rem; border-bottom: 1px solid var(--gray-100); text-align: center; }
td.goal-cell { text-align: left; }
tr:hover { background: var(--gray-50); }
.goal-nr { font-weight: 600; font-size: .82rem; }
.goal-desc { font-size: .77rem; color: var(--gray-500); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lft-badges { display: flex; flex-wrap: wrap; gap: .2rem; }
.lft-badge { font-size: .68rem; padding: .1rem .3rem; background: var(--gray-200); border-radius: 3px; color: var(--gray-600); }
.si { width: 26px; height: 26px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; font-size: .78rem; font-weight: 700; }
.si.groen { background: var(--status-groen); color: white; }
.si.oranje { background: var(--status-oranje); color: white; }
.si.roze { background: var(--status-roze); color: white; }
.si.none { background: var(--gray-200); color: var(--gray-400); }
.sum-bar { display: flex; height: 18px; border-radius: 3px; overflow: hidden; min-width: 50px; }
.sum-cell { background: var(--gray-50); }
.vergelijk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
@media (max-width: 600px) { .vergelijk-grid { grid-template-columns: 1fr; } }
.vergelijk-select { padding: .5rem .75rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: .9rem; width: 100%; }
.diff-row-same { background: #f0fdf4 !important; }
.diff-row-differ { background: #fff7ed !important; }
.empty { text-align: center; padding: 3rem; color: var(--gray-400); font-size: .9rem; }
.notification { position: fixed; bottom: 1rem; right: 1rem; padding: .9rem 1.4rem; border-radius: 8px; color: white; font-weight: 500; transform: translateY(100px); opacity: 0; transition: all .3s; z-index: 1001; }
.notification.show { transform: translateY(0); opacity: 1; }
.notification.success { background: var(--success); }
.notification.error { background: var(--danger); }
.notification.warning { background: var(--warning); }
.modal-overlay { display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center; }
.modal-overlay.active { display:flex; }
.modal-inner { background:white;border-radius:12px;padding:1.5rem;max-width:440px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.3); }
.modal-inner h2 { font-size:1.05rem;margin-bottom:.35rem; }
.modal-inner p { font-size:.82rem;color:var(--gray-500);margin-bottom:1rem; }
.modal-buttons { display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.1rem; }
.form-input { width:100%;padding:.55rem .75rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.9rem;margin-top:.5rem; }
.form-input:focus { outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(79,70,229,.1); }
.form-error { font-size:.82rem;color:var(--danger);margin-top:.5rem;min-height:1.2em; }
.klas-chip-card { background:var(--gray-50);border:1px solid var(--gray-200);border-radius:8px;padding:.6rem .85rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem; }
.klas-chip-card .klas-info { flex:1;cursor:pointer; }
.klas-chip-card .klas-name { font-weight:600;color:var(--gray-800); }
.klas-chip-card .klas-teachers { font-size:.75rem;color:var(--gray-500);margin-top:.15rem; }
.klas-chip-card .btn-delete { background:none;border:1px solid var(--gray-200);border-radius:6px;padding:.3rem .45rem;cursor:pointer;color:var(--gray-400);font-size:.85rem;transition:all .15s;flex-shrink:0; }
.klas-chip-card .btn-delete:hover { background:var(--danger);border-color:var(--danger);color:white; }
.legend-footer { padding: .85rem 1.25rem; border-top: 1px solid var(--gray-200); display: flex; gap: 1.25rem; flex-wrap: wrap; font-size: .78rem; color: var(--gray-600); }
.legend-item { display: flex; align-items: center; gap: .35rem; }
@media (prefers-color-scheme: dark) {
:root { --gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;--gray-300:#1a1a3e;--gray-700:#e5e7eb;--gray-800:#f3f4f6; }
body { background:#0f172a;color:#e2e8f0; }
.card,.tabs,.stats-grid .stat-card,.filter-bar { background:#1e293b!important; }
th,thead { background:#1e293b!important;color:#94a3b8!important;border-color:#334155!important; }
td { border-color:#1e293b!important; }
tr:hover { background:#263548!important; }
select,input { background:#0f172a!important;color:#e2e8f0!important;border-color:#334155!important; }
.vak-card,.klas-chip { background:#162032!important;border-color:#334155!important; }
.lft-badge { background:#334155!important;color:#94a3b8!important; }
.lft-cb { border-color:#334155!important;color:#e2e8f0; }
.si.none { background:#334155!important; }
.sum-cell { background:#1e293b!important; }
.tab-btn { color:#94a3b8; }
.btn-secondary { background:#334155!important;color:#e2e8f0!important; }
.diff-row-same { background:#064e3b!important; }
.diff-row-differ { background:#451a03!important; }
.modal-overlay .modal-inner { background:#1e293b!important;color:#e2e8f0!important; }
#koppelingCheckboxes label { color:#e2e8f0!important; }
#koppelingCheckboxes label:hover { background:#263548!important; }
#koppelingCheckboxes span { color:#94a3b8!important; }
#koppelingCheckboxes input[type=checkbox] { accent-color:var(--primary); }
.modal-overlay .form-input { background:#0f172a!important;color:#e2e8f0!important;border-color:#334155!important; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>🏫 Directeur Dashboard <span style="font-size:.7rem;background:rgba(255,255,255,.2);padding:.15rem .5rem;border-radius:99px;font-weight:500;">v5.0</span></h1>
<div style="opacity:.85;font-size:.85rem;margin-top:.25rem;" id="schoolInfo">Laden...</div>
</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;">
<select id="jaarSelector" class="btn btn-light" style="min-width:120px;cursor:pointer;">
<option>Laden...</option>
</select>
<a href="/leerkracht-view" class="btn btn-light">👤 Leerkrachtenweergave</a>
<a href="/auth/logout" class="btn btn-light">Uitloggen</a>
</div>
</div>
<div class="stats-grid">
<div class="stat-card highlight"><div class="stat-value" id="statKlassen">-</div><div class="stat-label">Klassen</div></div>
<div class="stat-card"><div class="stat-value" id="statVakken">-</div><div class="stat-label">Vakken</div></div>
<div class="stat-card"><div class="stat-value" id="statBeoordeeld">-</div><div class="stat-label">Beoordelingen</div></div>
<div class="stat-card" style="border-left:3px solid var(--status-groen);"><div class="stat-value" id="statGroen">-</div><div class="stat-label">Groen</div></div>
<div class="stat-card" style="border-left:3px solid var(--status-oranje);"><div class="stat-value" id="statOranje">-</div><div class="stat-label">Oranje</div></div>
<div class="stat-card" style="border-left:3px solid var(--status-roze);"><div class="stat-value" id="statRoze">-</div><div class="stat-label">Roze</div></div>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="klassen">🏫 Klassen</button>
<button class="tab-btn" data-tab="vakken">📚 Per vak</button>
<button class="tab-btn" data-tab="doelen">📋 Doelen detail</button>
<button class="tab-btn" data-tab="vergelijk">⚖️ Vergelijken</button>
<button class="tab-btn" data-tab="koppeling">👥 Leerkrachten</button>
</div>
<div class="tab-content active" id="tab-klassen">
<div class="card">
<h2>🏫 Klassen overzicht</h2>
<div class="klas-chips" id="klasChips"><div class="empty">Laden...</div></div>
</div>
<div class="card">
<h2>📊 Voortgang per klas</h2>
<div id="klasProgressContent"><div class="empty">Laden...</div></div>
</div>
</div>
<div class="tab-content" id="tab-vakken">
<div class="card">
<h2>📚 Statistieken per vak</h2>
<div class="vak-grid" id="vakStatsContent"><div class="empty">Laden...</div></div>
</div>
</div>
<div class="tab-content" id="tab-doelen">
<div class="filter-bar">
<div class="fg">
<label>Vak</label>
<select id="filterVak"><option value="all">Alle vakken</option></select>
</div>
<div class="fg">
<label>Klas</label>
<select id="filterKlas"><option value="all">Alle klassen</option></select>
</div>
<div class="fg">
<label>Status</label>
<select id="filterStatus">
<option value="all">Alle statussen</option>
<option value="groen">✓ Minstens één groen</option>
<option value="oranje">~ Minstens één oranje</option>
<option value="roze">! Minstens één roze</option>
<option value="consensus">✓ Consensus (iedereen groen)</option>
<option value="verschil">⚠ Verschillen</option>
<option value="niemand">○ Niemand beoordeeld</option>
</select>
</div>
<div class="fg">
<label>Leeftijd</label>
<div class="leeftijd-cbs" id="leeftijdCbs"></div>
</div>
<div class="fg">
<label>Sectie</label>
<select id="filterSectie"><option value="all">Alle secties</option></select>
</div>
<div class="fg">
<label>Zoeken</label>
<input type="text" id="filterSearch" placeholder="Code of beschrijving...">
</div>
<div class="fg" style="justify-content:flex-end;">
<label>&nbsp;</label>
<div style="display:flex;gap:.5rem;">
<button class="btn btn-success" id="btnExportCSV">⬇ CSV</button>
<button class="btn btn-secondary" id="btnExportPDF">⬇ PDF</button>
</div>
</div>
</div>
<div class="card" style="padding:0;overflow:hidden;">
<div class="table-wrap">
<table>
<thead id="tblHead"><tr><th class="goal-col">Doel</th><th>Samenvatting</th></tr></thead>
<tbody id="tblBody"><tr><td colspan="3" class="empty">Selecteer een vak om te beginnen</td></tr></tbody>
</table>
</div>
<div class="legend-footer">
<div class="legend-item"><div class="si groen"></div> Doen we al</div>
<div class="legend-item"><div class="si oranje">~</div> Doen we ongeveer</div>
<div class="legend-item"><div class="si roze">!</div> Nieuw</div>
<div class="legend-item"><div class="si none"></div> Niet beoordeeld</div>
</div>
</div>
</div>
<div class="tab-content" id="tab-vergelijk">
<div class="card">
<h2>⚖️ Twee klassen vergelijken</h2>
<div class="vergelijk-grid">
<div class="fg"><label>Klas A</label><select class="vergelijk-select" id="vergKlasA"><option value="">-- Kies klas --</option></select></div>
<div class="fg"><label>Klas B</label><select class="vergelijk-select" id="vergKlasB"><option value="">-- Kies klas --</option></select></div>
</div>
<div class="fg" style="margin-bottom:1rem;max-width:200px;">
<label>Vak</label>
<select class="vergelijk-select" id="vergVak"><option value="">-- Kies vak --</option></select>
</div>
<div id="vergelijkResult"><div class="empty">Kies twee klassen en een vak om te vergelijken</div></div>
</div>
</div>
<!-- TAB: Leerkrachten koppelen -->
<div class="tab-content" id="tab-koppeling">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.5rem;">
<h2 style="margin:0;">👥 Klassen &amp; Leerkrachten</h2>
<button class="btn btn-primary" id="btnNieuweKlas">+ Klas toevoegen</button>
</div>
<p style="font-size:.85rem;color:var(--gray-500);margin-bottom:1rem;">
Klik op een klas om leerkrachten te wijzigen. Gebruik het prullenbakje om een klas te verwijderen.
</p>
<div id="koppelingContent"><div class="empty">Laden...</div></div>
</div>
</div>
<!-- Modal: leerkrachten koppelen -->
<div id="koppelingModal" class="modal-overlay">
<div class="modal-inner">
<h2 id="koppelingModalTitle">Leerkrachten koppelen</h2>
<p>Selecteer de leerkrachten voor deze klas.</p>
<div id="koppelingCheckboxes" style="display:flex;flex-direction:column;gap:.4rem;max-height:280px;overflow-y:auto;"></div>
<div class="modal-buttons">
<button id="btnKoppelingAnnuleer" class="btn btn-secondary">Annuleren</button>
<button id="btnKoppelingOpslaan" class="btn btn-primary">Opslaan</button>
</div>
</div>
</div>
<!-- Modal: nieuwe klas -->
<div id="nieuweKlasModal" class="modal-overlay">
<div class="modal-inner">
<h2>Nieuwe klas toevoegen</h2>
<p>Geef een naam op voor de nieuwe klas (bv. "1A", "3B").</p>
<input type="text" id="nieuweKlasNaam" class="form-input" placeholder="Naam van de klas...">
<div class="form-error" id="nieuweKlasError"></div>
<div class="modal-buttons">
<button id="btnNieuweKlasAnnuleer" class="btn btn-secondary">Annuleren</button>
<button id="btnNieuweKlasBevestig" class="btn btn-primary">Aanmaken</button>
</div>
</div>
</div>
</div>
<div class="notification" id="notification"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
<script nonce="{{ csp_nonce() }}">
// ── State ──────────────────────────────────────────────────────────────────────
let overviewData = { classes: [], assessments_by_class: {} };
let allGoals = {};
let vakCache = {};
let currentUser = null;
// ── Init ───────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
// Leeftijd checkboxes dynamisch aanmaken (vermijdt Jinja in inline handlers)
const ages = ['2,5-4','4-5','5-6','6-7','7-8','8-9','9-10','10-11','11-12'];
const cbContainer = document.getElementById('leeftijdCbs');
ages.forEach(age => {
const lbl = document.createElement('label');
lbl.className = 'lft-cb';
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.value = age;
cb.addEventListener('change', applyFilters);
lbl.appendChild(cb);
lbl.appendChild(Object.assign(document.createElement('span'), {textContent: age}));
cbContainer.appendChild(lbl);
});
// Tabs
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
// Jaar selector
document.getElementById('jaarSelector').addEventListener('change', switchJaar);
// Filters
document.getElementById('filterVak').addEventListener('change', applyFilters);
document.getElementById('filterKlas').addEventListener('change', applyFilters);
document.getElementById('filterStatus').addEventListener('change', applyFilters);
document.getElementById('filterSearch').addEventListener('input', applyFilters);
document.getElementById('filterSectie').addEventListener('change', applyFilters);
// Export knoppen
document.getElementById('btnExportCSV').addEventListener('click', exportCSV);
document.getElementById('btnNieuweKlas').addEventListener('click', openNieuweKlasDialog);
document.getElementById('btnNieuweKlasAnnuleer').addEventListener('click', () => {
document.getElementById('nieuweKlasModal').classList.remove('active');
});
document.getElementById('btnNieuweKlasBevestig').addEventListener('click', bevestigNieuweKlas);
document.getElementById('nieuweKlasNaam').addEventListener('keydown', e => {
if (e.key === 'Enter') bevestigNieuweKlas();
});
// Sluit modals bij klik op overlay
['koppelingModal','nieuweKlasModal'].forEach(id => {
document.getElementById(id).addEventListener('click', e => {
if (e.target.id === id) document.getElementById(id).classList.remove('active');
});
});
document.getElementById('btnKoppelingAnnuleer').addEventListener('click', () => {
document.getElementById('koppelingModal').classList.remove('active');
});
document.getElementById('btnKoppelingOpslaan').addEventListener('click', saveKoppeling);
document.getElementById('btnExportPDF').addEventListener('click', exportPDF);
// Vergelijk selects
document.getElementById('vergKlasA').addEventListener('change', renderVergelijk);
document.getElementById('vergKlasB').addEventListener('change', renderVergelijk);
document.getElementById('vergVak').addEventListener('change', renderVergelijk);
await loadUser();
await loadJaren();
await loadOverview();
});
// ── Gebruiker ──────────────────────────────────────────────────────────────────
async function loadUser() {
const res = await fetch('/api/me');
if (!res.ok) return;
const data = await res.json();
currentUser = data.user;
document.getElementById('schoolInfo').textContent =
(currentUser.school?.name || '') + ' — schooloverzicht leerdoelen';
}
// ── Schooljaren ────────────────────────────────────────────────────────────────
async function loadJaren() {
const res = await fetch('/api/school/years');
if (!res.ok) return;
const data = await res.json();
const sel = document.getElementById('jaarSelector');
sel.innerHTML = '';
(data.years || []).forEach(y => {
const opt = document.createElement('option');
opt.value = y.id; opt.textContent = y.label;
if (y.is_active) opt.selected = true;
sel.appendChild(opt);
});
}
async function switchJaar() {
await loadOverview();
}
// ── Overview ───────────────────────────────────────────────────────────────────
async function loadOverview() {
const yearId = document.getElementById('jaarSelector').value;
const url = '/api/school/overview' + (yearId ? '?year_id=' + yearId : '');
const res = await fetch(url);
if (!res.ok) { showNotification('Fout bij laden overzicht', 'error'); return; }
overviewData = await res.json();
await loadVakData();
updateStats();
renderKlassen();
renderKlasProgress();
renderKoppelingTab();
renderVakStats();
populateFilters();
applyFilters();
populateVergelijkSelects();
}
// ── Vak data ───────────────────────────────────────────────────────────────────
async function loadVakData() {
const vakIds = new Set();
Object.values(overviewData.assessments_by_class).forEach(vakken => {
Object.keys(vakken).forEach(v => vakIds.add(v));
});
for (const vakId of vakIds) {
if (!vakCache[vakId]) {
try {
const res = await fetch('/api/doelen/' + vakId);
if (res.ok) { vakCache[vakId] = await res.json(); processVakGoals(vakId); }
} catch(e) { console.warn('Kon', vakId, 'niet laden'); }
}
}
}
function processVakGoals(vakId) {
const data = vakCache[vakId];
if (!data?.rijen) return;
const sectieLookup = {};
data.rijen.forEach(r => {
if (['onderwerp','rubriek','subrubriek','subthema'].includes(r.type))
sectieLookup[r.id] = r.inhoud;
});
allGoals[vakId] = data.rijen
.filter(r => r.type === 'doelzin' && r.goNr)
.map(r => ({
id: r.goNr, goNr: r.goNr, inhoud: r.inhoud,
sectie: r.parentId ? sectieLookup[r.parentId] : null,
leeftijden: r.leeftijden || []
}));
}
function maakVakNaam(id) {
const m = { aardrijkskunde:'Aardrijkskunde', burgerschap:'Burgerschap', frans:'Frans',
geschiedenis:'Geschiedenis', ict:'ICT', 'leren-leren':'Leren leren',
'lichamelijke-opvoeding':'Lichamelijke opvoeding', 'muzische-vorming':'Muzische vorming',
nederlands:'Nederlands', 'sociale-vaardigheden':'Sociale vaardigheden',
'wetenschap-techniek':'Wetenschap en techniek', wiskunde:'Wiskunde' };
return m[id] || id.replace(/-/g,' ').replace(/\b\w/g,c=>c.toUpperCase());
}
// ── Stats ──────────────────────────────────────────────────────────────────────
function updateStats() {
const byClass = overviewData.assessments_by_class || {};
const vakIds = new Set();
let groen=0, oranje=0, roze=0;
Object.values(byClass).forEach(vakken => {
Object.entries(vakken).forEach(([v, goals]) => {
vakIds.add(v);
Object.values(goals).forEach(s => {
if (s==='groen') groen++;
else if (s==='oranje') oranje++;
else if (s==='roze') roze++;
});
});
});
document.getElementById('statKlassen').textContent = overviewData.classes?.length || 0;
document.getElementById('statVakken').textContent = vakIds.size;
document.getElementById('statBeoordeeld').textContent = groen+oranje+roze;
document.getElementById('statGroen').textContent = groen;
document.getElementById('statOranje').textContent = oranje;
document.getElementById('statRoze').textContent = roze;
}
// ── Tab: Klassen ───────────────────────────────────────────────────────────────
function renderKlassen() {
const chips = document.getElementById('klasChips');
const klassen = overviewData.classes || [];
if (!klassen.length) { chips.innerHTML = '<div class="empty">Geen klassen gevonden</div>'; return; }
chips.innerHTML = klassen.map(k => {
const teachers = (k.teachers||[]).map(t=>t.full_name).join(', ') || '(geen leerkrachten)';
return `<div class="klas-chip">
<div class="klas-name">🏫 ${k.name}</div>
<div class="klas-teachers">${teachers}</div>
</div>`;
}).join('');
}
function renderKlasProgress() {
const el = document.getElementById('klasProgressContent');
const klassen = overviewData.classes || [];
const byClass = overviewData.assessments_by_class || {};
if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen</div>'; return; }
// Filter op leerkrachten (niet directeurs/ICT)
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
el.innerHTML = '<div style="display:flex;flex-direction:column;gap:.5rem;">' + klassen.map(k => {
const vakken = byClass[k.id] || {};
let g=0,o=0,r=0;
Object.values(vakken).forEach(goals => Object.values(goals).forEach(s => {
if (s==='groen') g++; else if (s==='oranje') o++; else if (s==='roze') r++;
}));
const t = g+o+r;
return `<div class="vak-card">
<div class="vak-card-header"><h3>${k.name}</h3><span style="font-size:.8rem;color:var(--gray-500)">${t} beoordelingen</span></div>
<div class="progress-bar"><div class="progress-inner">
<div class="p-groen" style="width:${t>0?g/t*100:0}%"></div>
<div class="p-oranje" style="width:${t>0?o/t*100:0}%"></div>
<div class="p-roze" style="width:${t>0?r/t*100:0}%"></div>
</div></div>
<div class="vak-legend">
<span><span class="dot" style="background:var(--status-groen)"></span>${g} groen</span>
<span><span class="dot" style="background:var(--status-oranje)"></span>${o} oranje</span>
<span><span class="dot" style="background:var(--status-roze)"></span>${r} roze</span>
</div>
</div>`;
}).join('') + '</div>';
}
// ── Tab: Per vak ───────────────────────────────────────────────────────────────
function renderVakStats() {
const el = document.getElementById('vakStatsContent');
const byClass = overviewData.assessments_by_class || {};
const totals = {};
Object.values(byClass).forEach(vakken => {
Object.entries(vakken).forEach(([v, goals]) => {
if (!totals[v]) totals[v] = {groen:0,oranje:0,roze:0};
Object.values(goals).forEach(s => {
if (s==='groen') totals[v].groen++;
else if (s==='oranje') totals[v].oranje++;
else if (s==='roze') totals[v].roze++;
});
});
});
const sorted = Object.entries(totals).sort((a,b)=>maakVakNaam(a[0]).localeCompare(maakVakNaam(b[0]),'nl'));
if (!sorted.length) { el.innerHTML = '<div class="empty">Nog geen beoordelingen</div>'; return; }
el.innerHTML = sorted.map(([v,st]) => {
const t = st.groen+st.oranje+st.roze;
return `<div class="vak-card">
<div class="vak-card-header"><h3>${maakVakNaam(v)}</h3><span style="font-size:.8rem;color:var(--gray-500)">${t} beoordelingen</span></div>
<div class="progress-bar"><div class="progress-inner">
<div class="p-groen" style="width:${t>0?st.groen/t*100:0}%"></div>
<div class="p-oranje" style="width:${t>0?st.oranje/t*100:0}%"></div>
<div class="p-roze" style="width:${t>0?st.roze/t*100:0}%"></div>
</div></div>
<div class="vak-legend">
<span><span class="dot" style="background:var(--status-groen)"></span>${st.groen} groen</span>
<span><span class="dot" style="background:var(--status-oranje)"></span>${st.oranje} oranje</span>
<span><span class="dot" style="background:var(--status-roze)"></span>${st.roze} roze</span>
</div>
</div>`;
}).join('');
}
// ── Tab: Doelen detail ─────────────────────────────────────────────────────────
function populateFilters() {
const byClass = overviewData.assessments_by_class || {};
const vakIds = new Set();
Object.values(byClass).forEach(v => Object.keys(v).forEach(id => vakIds.add(id)));
const vakSel = document.getElementById('filterVak');
vakSel.innerHTML = '<option value="all">Alle vakken</option>';
[...vakIds].sort((a,b)=>maakVakNaam(a).localeCompare(maakVakNaam(b),'nl')).forEach(v => {
const o = document.createElement('option'); o.value=v; o.textContent=maakVakNaam(v); vakSel.appendChild(o);
});
const klasSel = document.getElementById('filterKlas');
klasSel.innerHTML = '<option value="all">Alle klassen</option>';
(overviewData.classes||[]).forEach(k => {
const o = document.createElement('option'); o.value=k.id; o.textContent=k.name; klasSel.appendChild(o);
});
}
function populateSectieFilter(vakId) {
const sel = document.getElementById('filterSectie');
sel.innerHTML = '<option value="all">Alle secties</option>';
if (!vakId || vakId === 'all' || !allGoals[vakId]) return;
const secties = [...new Set(allGoals[vakId].map(g => g.sectie).filter(Boolean))].sort();
secties.forEach(s => {
const o = document.createElement('option'); o.value = s; o.textContent = s; sel.appendChild(o);
});
}
function applyFilters() {
const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value;
const statusFilter = document.getElementById('filterStatus').value;
const leeftFilter = [...document.querySelectorAll('#leeftijdCbs input:checked')].map(c=>c.value);
const search = document.getElementById('filterSearch').value.toLowerCase();
const sectieFilter = document.getElementById('filterSectie').value;
// Sectie filter opties bijwerken als vak wijzigt
populateSectieFilter(vakFilter);
const vakkenToShow = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
const klassen = klasFilter === 'all'
? (overviewData.classes||[])
: (overviewData.classes||[]).filter(k => k.id == klasFilter);
const rows = [];
vakkenToShow.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => {
if (search && !(goal.goNr+' '+goal.inhoud).toLowerCase().includes(search)) return;
if (leeftFilter.length > 0 && !leeftFilter.some(l=>goal.leeftijden.includes(l))) return;
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
const statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
if (statusFilter==='groen' && !statussen.some(s=>s==='groen')) return;
if (statusFilter==='oranje' && !statussen.some(s=>s==='oranje')) return;
if (statusFilter==='roze' && !statussen.some(s=>s==='roze')) return;
if (statusFilter==='consensus' && !statussen.every(s=>s==='groen')) return;
if (statusFilter==='verschil') {
const filled = statussen.filter(Boolean);
if (filled.length<=1 || new Set(filled).size<=1) return;
}
if (statusFilter==='niemand' && statussen.some(Boolean)) return;
rows.push({vakId, goal, statussen, klassen});
});
});
renderTable(rows, klassen);
}
function renderTable(rows, klassen) {
const thead = document.getElementById('tblHead');
const tbody = document.getElementById('tblBody');
let hdr = '<tr><th class="goal-col">Doel</th><th>Leeftijden</th>';
klassen.forEach(k => hdr += `<th class="klas-col">${k.name}</th>`);
hdr += '<th>Samenvatting</th></tr>';
thead.innerHTML = hdr;
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="${klassen.length+3}" class="empty">Geen doelen gevonden met huidige filters</td></tr>`;
return;
}
tbody.innerHTML = rows.map(({goal,statussen}) => {
let tr = `<tr>
<td class="goal-cell"><div class="goal-nr">${goal.goNr}</div><div class="goal-desc" title="${goal.inhoud}">${goal.inhoud}</div></td>
<td><div class="lft-badges">${goal.leeftijden.map(l=>`<span class="lft-badge">${l}</span>`).join('')}</div></td>`;
statussen.forEach(s => {
const sym = s==='groen'?'✓':s==='oranje'?'~':s==='roze'?'!':'○';
tr += `<td><div class="si ${s||'none'}">${sym}</div></td>`;
});
const g=statussen.filter(s=>s==='groen').length;
const o=statussen.filter(s=>s==='oranje').length;
const r=statussen.filter(s=>s==='roze').length;
const t=statussen.length||1;
tr += `<td class="sum-cell"><div class="sum-bar">
<div class="p-groen" style="width:${g/t*100}%"></div>
<div class="p-oranje" style="width:${o/t*100}%"></div>
<div class="p-roze" style="width:${r/t*100}%"></div>
</div></td></tr>`;
return tr;
}).join('');
}
// ── Tab: Vergelijken ───────────────────────────────────────────────────────────
function populateVergelijkSelects() {
['vergKlasA','vergKlasB'].forEach(id => {
const sel = document.getElementById(id);
sel.innerHTML = '<option value="">-- Kies klas --</option>';
(overviewData.classes||[]).forEach(k => {
const o = document.createElement('option'); o.value=k.id; o.textContent=k.name; sel.appendChild(o);
});
});
const vakSel = document.getElementById('vergVak');
vakSel.innerHTML = '<option value="">-- Kies vak --</option>';
const vakIds = new Set();
Object.values(overviewData.assessments_by_class||{}).forEach(v => Object.keys(v).forEach(id => vakIds.add(id)));
[...vakIds].sort((a,b)=>maakVakNaam(a).localeCompare(maakVakNaam(b),'nl')).forEach(v => {
const o = document.createElement('option'); o.value=v; o.textContent=maakVakNaam(v); vakSel.appendChild(o);
});
}
function renderVergelijk() {
const idA = document.getElementById('vergKlasA').value;
const idB = document.getElementById('vergKlasB').value;
const vakId = document.getElementById('vergVak').value;
const el = document.getElementById('vergelijkResult');
if (!idA || !idB || !vakId) { el.innerHTML = '<div class="empty">Kies twee klassen en een vak</div>'; return; }
if (idA === idB) { el.innerHTML = '<div class="empty">Kies twee verschillende klassen</div>'; return; }
const klasA = overviewData.classes.find(k=>k.id==idA);
const klasB = overviewData.classes.find(k=>k.id==idB);
const goalsA = overviewData.assessments_by_class[idA]?.[vakId] || {};
const goalsB = overviewData.assessments_by_class[idB]?.[vakId] || {};
const goals = allGoals[vakId] || [];
if (!goals.length) { el.innerHTML = '<div class="empty">Vak nog niet geladen of geen doelen</div>'; return; }
let html = `<div class="table-wrap"><table>
<thead><tr><th class="goal-col">Doel</th><th class="klas-col">${klasA?.name||'A'}</th><th class="klas-col">${klasB?.name||'B'}</th></tr></thead><tbody>`;
goals.forEach(goal => {
const sA = goalsA[goal.id]||'', sB = goalsB[goal.id]||'';
const symA = sA==='groen'?'✓':sA==='oranje'?'~':sA==='roze'?'!':'○';
const symB = sB==='groen'?'✓':sB==='oranje'?'~':sB==='roze'?'!':'○';
html += `<tr class="${sA===sB?'diff-row-same':'diff-row-differ'}">
<td class="goal-cell"><div class="goal-nr">${goal.goNr}</div><div class="goal-desc">${goal.inhoud}</div></td>
<td><div class="si ${sA||'none'}">${symA}</div></td>
<td><div class="si ${sB||'none'}">${symB}</div></td>
</tr>`;
});
const differ = goals.filter(g=>(goalsA[g.id]||'')!==(goalsB[g.id]||'')).length;
html += `</tbody></table></div><div style="margin-top:.75rem;font-size:.85rem;color:var(--gray-500);">${goals.length} doelen — ${differ} verschillen</div>`;
el.innerHTML = html;
}
// ── Export CSV ─────────────────────────────────────────────────────────────────
function exportCSV() {
const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value;
const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter);
let csv = 'Vak,Doelnummer,Doel,Leeftijden';
klassen.forEach(k => csv += ',' + k.name);
csv += ',Groen,Oranje,Roze\n';
const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter];
vakken.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => {
const st = klassen.map(k=>(overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
csv += `"${maakVakNaam(vakId)}","${goal.goNr}","${goal.inhoud.replace(/"/g,'""')}","${goal.leeftijden.join(', ')}"`;
st.forEach(s => csv += `,${s||'-'}`);
csv += `,${st.filter(s=>s==='groen').length},${st.filter(s=>s==='oranje').length},${st.filter(s=>s==='roze').length}\n`;
});
});
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8;'}));
a.download = `Leerdoelen_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
showNotification('CSV geëxporteerd', 'success');
}
// ── Export PDF ─────────────────────────────────────────────────────────────────
function exportPDF() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l','mm','a4');
const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value;
const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter);
const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter];
doc.setFontSize(14); doc.text('Leerdoelen Overzicht', 14, 14);
doc.setFontSize(9); doc.text(`Export: ${new Date().toLocaleDateString('nl-BE')}`, 14, 20);
const headers = [['Vak','Code','Doel','Leeftijd',...klassen.map(k=>k.name),'G','O','R']];
const rows = [];
vakken.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => {
const st = klassen.map(k=>(overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
rows.push([maakVakNaam(vakId), goal.goNr,
goal.inhoud.substring(0,55)+(goal.inhoud.length>55?'...':''),
goal.leeftijden.join(', '),
...st.map(s=>s==='groen'?'v':s==='oranje'?'~':s==='roze'?'!':'-'),
st.filter(s=>s==='groen').length, st.filter(s=>s==='oranje').length, st.filter(s=>s==='roze').length,
]);
});
});
doc.autoTable({head:headers,body:rows,startY:24,
styles:{fontSize:7,cellPadding:1.5},headStyles:{fillColor:[79,70,229]},
columnStyles:{0:{cellWidth:22},1:{cellWidth:14},2:{cellWidth:'auto'}},margin:{left:14,right:14}});
doc.save(`Leerdoelen_${new Date().toISOString().split('T')[0]}.pdf`);
showNotification('PDF geëxporteerd', 'success');
}
// ── Koppeling tab ─────────────────────────────────────────────────────────────
let allUsers = [];
let activeKlasKoppeling = null;
async function renderKoppelingTab() {
const el = document.getElementById('koppelingContent');
const klassen = overviewData.classes || [];
if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen gevonden</div>'; return; }
// Laad alle leerkrachten van de school
try {
const res = await fetch('/api/users');
if (res.ok) { const d = await res.json(); allUsers = d.users || []; }
} catch(e) { console.warn('Kon gebruikers niet laden'); }
// Filter op leerkrachten (niet directeurs/ICT)
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
el.innerHTML = '<div style="display:flex;flex-direction:column;gap:.5rem;">' + klassen.map(k => {
const teachers = (k.teachers||[]).map(t => t.full_name).join(', ') || '<em style="color:var(--gray-400)">Geen leerkrachten</em>';
return `<div class="klas-chip-card">
<div class="klas-info" data-action="openKoppeling" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}" data-teachers="${JSON.stringify((k.teachers||[]).map(t=>t.id)).replace(/"/g,'&quot;')}">
<div class="klas-name">🏫 ${k.name} <span style="font-size:.72rem;background:var(--primary);color:white;padding:.1rem .4rem;border-radius:4px;margin-left:.35rem;">Wijzigen</span></div>
<div class="klas-teachers">${teachers}</div>
</div>
<button class="btn-delete" data-action="deleteKlas" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}" title="Klas verwijderen">🗑</button>
</div>`;
}).join('') + '</div>';
}
document.addEventListener('click', function(e) {
// Verwijder klas
const delBtn = e.target.closest('[data-action="deleteKlas"]');
if (delBtn) {
const id = parseInt(delBtn.dataset.id);
const name = delBtn.dataset.name;
if (!confirm(`Klas "${name}" verwijderen? Alle beoordelingen voor deze klas gaan verloren.`)) return;
fetch(`/api/classes/${id}`, {method: 'DELETE'})
.then(r => { if (r.ok) { showNotification(`Klas "${name}" verwijderd`, 'success'); loadOverview(); }
else showNotification('Verwijderen mislukt', 'error'); });
return;
}
// Wijzig leerkrachten
const card = e.target.closest('[data-action="openKoppeling"]');
if (!card) return;
activeKlasKoppeling = parseInt(card.dataset.id);
const name = card.dataset.name;
const teachers = JSON.parse(card.dataset.teachers.replace(/&quot;/g, '"'));
document.getElementById('koppelingModalTitle').textContent = `Leerkrachten voor ${name}`;
const container = document.getElementById('koppelingCheckboxes');
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
if (!teacherUsers.length) {
container.innerHTML = '<em style="color:var(--gray-400)">Geen leerkrachten beschikbaar. Voeg eerst leerkrachten toe via Gebruikersbeheer.</em>';
} else {
container.innerHTML = teacherUsers.map(u => `
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;padding:.35rem;border-radius:4px;">
<input type="checkbox" value="${u.id}" ${teachers.includes(u.id)?'checked':''}>
<span>${u.full_name} <span style="color:var(--gray-400);font-size:.8rem;">(${u.email})</span></span>
</label>`).join('');
}
document.getElementById('koppelingModal').classList.add('active');
});
function openNieuweKlasDialog() {
document.getElementById('nieuweKlasNaam').value = '';
document.getElementById('nieuweKlasError').textContent = '';
document.getElementById('nieuweKlasModal').classList.add('active');
setTimeout(() => document.getElementById('nieuweKlasNaam').focus(), 50);
}
async function bevestigNieuweKlas() {
const name = document.getElementById('nieuweKlasNaam').value.trim();
const errEl = document.getElementById('nieuweKlasError');
if (!name) { errEl.textContent = 'Vul een naam in.'; return; }
const res = await fetch('/api/classes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
});
const data = await res.json();
if (!res.ok) { errEl.textContent = data.error || 'Aanmaken mislukt'; return; }
document.getElementById('nieuweKlasModal').classList.remove('active');
showNotification(`Klas "${data.class.name}" aangemaakt`, 'success');
await loadOverview();
}
async function saveKoppeling() {
if (!activeKlasKoppeling) return;
const checked = [...document.querySelectorAll('#koppelingCheckboxes input:checked')].map(i => parseInt(i.value));
// Gebruik de admin route om leerkrachten aan klas te koppelen
const res = await fetch(`/api/classes/${activeKlasKoppeling}/teachers`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ teacher_ids: checked })
});
if (!res.ok) { showNotification('Opslaan mislukt', 'error'); return; }
document.getElementById('koppelingModal').classList.remove('active');
showNotification('Koppeling opgeslagen', 'success');
await loadOverview(); // herlaad zodat klas-chips bijgewerkt worden
}
// ── Tabs ───────────────────────────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab===name));
document.querySelectorAll('.tab-content').forEach(el => el.classList.toggle('active', el.id==='tab-'+name));
}
// ── Notificaties ───────────────────────────────────────────────────────────────
function showNotification(msg, type) {
const el = document.getElementById('notification');
el.textContent = msg; el.className = 'notification '+type+' show';
setTimeout(() => el.classList.remove('show'), 3000);
}
</script>
</body>
</html>