feat: enhance class access logic for directors in my_classes endpoint
All checks were successful
Build & Push / Build & Push image (push) Successful in 40s

This commit is contained in:
2026-03-05 23:13:50 +01:00
parent d4f09bb368
commit 2782c3bea6
2 changed files with 120 additions and 172 deletions

View File

@@ -427,14 +427,17 @@ def me():
@api_bp.route('/my/classes', methods=['GET'])
@login_required
def my_classes():
"""Geeft alle klassen van de school en de eigen klassen van de leerkracht."""
"""Geeft alle klassen van de school en de eigen klassen van de leerkracht.
Directeurs en hoger zien automatisch alle klassen als my_classes."""
if not current_user.school_id:
return jsonify({'all_classes': [], 'my_classes': []})
all_cls = Class.query.filter_by(school_id=current_user.school_id)\
.order_by(Class.name).all()
# Directeurs en hoger hebben toegang tot alle klassen zonder expliciete koppeling
my_cls = all_cls if current_user.is_director else current_user.classes
return jsonify({
'all_classes': [{'id': c.id, 'name': c.name} for c in all_cls],
'my_classes': [{'id': c.id, 'name': c.name} for c in current_user.classes],
'my_classes': [{'id': c.id, 'name': c.name} for c in my_cls],
})

View File

@@ -16,7 +16,6 @@
* { 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; }
@@ -28,33 +27,23 @@
.btn-secondary:hover { background: var(--gray-300); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover { background: #059669; }
/* Stats */
.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 */
.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; }
/* Sectie card */
.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; }
/* Klassen chips */
.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 progress cards */
.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; }
@@ -66,8 +55,6 @@
.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 balk */
.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; }
@@ -77,8 +64,6 @@
.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; }
/* Detail tabel */
.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; }
@@ -99,30 +84,19 @@
.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); }
/* Klasvergelijking */
.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 / loading */
.empty { text-align: center; padding: 3rem; color: var(--gray-400); font-size: .9rem; }
.spinner { width: 36px; height: 36px; border: 3px solid var(--gray-200); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto .75rem; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Notificatie */
.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); }
/* Legenda footer */
.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; }
@@ -146,14 +120,13 @@
<body>
<div class="container">
<!-- Header -->
<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;" onchange="switchJaar()">
<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>
@@ -161,7 +134,6 @@
</div>
</div>
<!-- Stats -->
<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>
@@ -171,21 +143,17 @@
<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>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('klassen')">🏫 Klassen</button>
<button class="tab-btn" onclick="switchTab('vakken')">📚 Per vak</button>
<button class="tab-btn" onclick="switchTab('doelen')">📋 Doelen detail</button>
<button class="tab-btn" onclick="switchTab('vergelijk')">⚖️ Vergelijken</button>
<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>
</div>
<!-- TAB: Klassen -->
<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 class="klas-chips" id="klasChips"><div class="empty">Laden...</div></div>
</div>
<div class="card">
<h2>📊 Voortgang per klas</h2>
@@ -193,7 +161,6 @@
</div>
</div>
<!-- TAB: Per vak -->
<div class="tab-content" id="tab-vakken">
<div class="card">
<h2>📚 Statistieken per vak</h2>
@@ -201,24 +168,19 @@
</div>
</div>
<!-- TAB: Doelen detail -->
<div class="tab-content" id="tab-doelen">
<div class="filter-bar">
<div class="fg">
<label>Vak</label>
<select id="filterVak" onchange="applyFilters()">
<option value="all">Alle vakken</option>
</select>
<select id="filterVak"><option value="all">Alle vakken</option></select>
</div>
<div class="fg">
<label>Klas</label>
<select id="filterKlas" onchange="applyFilters()">
<option value="all">Alle klassen</option>
</select>
<select id="filterKlas"><option value="all">Alle klassen</option></select>
</div>
<div class="fg">
<label>Status</label>
<select id="filterStatus" onchange="applyFilters()">
<select id="filterStatus">
<option value="all">Alle statussen</option>
<option value="consensus">✓ Consensus (iedereen groen)</option>
<option value="verschil">⚠ Verschillen</option>
@@ -227,21 +189,17 @@
</div>
<div class="fg">
<label>Leeftijd</label>
<div class="leeftijd-cbs">
{% for age in ['2,5-4','4-5','5-6','6-7','7-8','8-9','9-10','10-11','11-12'] %}
<label class="lft-cb"><input type="checkbox" value="{{ age }}" onchange="applyFilters()"><span>{{ age }}</span></label>
{% endfor %}
</div>
<div class="leeftijd-cbs" id="leeftijdCbs"></div>
</div>
<div class="fg">
<label>Zoeken</label>
<input type="text" id="filterSearch" placeholder="Code of beschrijving..." oninput="applyFilters()">
<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" onclick="exportCSV()">⬇ CSV</button>
<button class="btn btn-secondary" onclick="exportPDF()">⬇ PDF</button>
<button class="btn btn-success" id="btnExportCSV">⬇ CSV</button>
<button class="btn btn-secondary" id="btnExportPDF">⬇ PDF</button>
</div>
</div>
</div>
@@ -261,19 +219,16 @@
</div>
</div>
<!-- TAB: Vergelijken -->
<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" onchange="renderVergelijk()"><option value="">-- Kies klas --</option></select></div>
<div class="fg"><label>Klas B</label><select class="vergelijk-select" id="vergKlasB" onchange="renderVergelijk()"><option value="">-- Kies klas --</option></select></div>
<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" onchange="renderVergelijk()">
<option value="">-- Kies vak --</option>
</select>
<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>
@@ -288,19 +243,55 @@
<script nonce="{{ csp_nonce() }}">
// ── State ──────────────────────────────────────────────────────────────────────
let overviewData = { classes: [], assessments_by_class: {} };
let allGoals = {}; // { vakId: [{id,goNr,inhoud,sectie,leeftijden}] }
let vakCache = {}; // { vakId: rawJson }
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);
// Export knoppen
document.getElementById('btnExportCSV').addEventListener('click', exportCSV);
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();
});
function bind(id, ev, fn) { const el = document.getElementById(id); if (el) el.addEventListener(ev, fn); }
// ── Gebruiker ──────────────────────────────────────────────────────────────────
async function loadUser() {
const res = await fetch('/api/me');
if (!res.ok) return;
@@ -319,8 +310,7 @@ async function loadJaren() {
sel.innerHTML = '';
(data.years || []).forEach(y => {
const opt = document.createElement('option');
opt.value = y.id;
opt.textContent = y.label;
opt.value = y.id; opt.textContent = y.label;
if (y.is_active) opt.selected = true;
sel.appendChild(opt);
});
@@ -330,14 +320,13 @@ async function switchJaar() {
await loadOverview();
}
// ── Overview laden ─────────────────────────────────────────────────────────────
// ── 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();
@@ -348,7 +337,7 @@ async function loadOverview() {
populateVergelijkSelects();
}
// ── Vak JSON laden ─────────────────────────────────────────────────────────────
// ── Vak data ───────────────────────────────────────────────────────────────────
async function loadVakData() {
const vakIds = new Set();
Object.values(overviewData.assessments_by_class).forEach(vakken => {
@@ -375,9 +364,7 @@ function processVakGoals(vakId) {
allGoals[vakId] = data.rijen
.filter(r => r.type === 'doelzin' && r.goNr)
.map(r => ({
id: r.goNr,
goNr: r.goNr,
inhoud: r.inhoud,
id: r.goNr, goNr: r.goNr, inhoud: r.inhoud,
sectie: r.parentId ? sectieLookup[r.parentId] : null,
leeftijden: r.leeftijden || []
}));
@@ -421,7 +408,7 @@ function renderKlassen() {
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)';
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>
@@ -433,30 +420,25 @@ 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; }
el.innerHTML = '<div class="vak-grid">' + klassen.map(k => {
const vakken = byClass[k.id] || {};
let groen=0,oranje=0,roze=0;
let g=0,o=0,r=0;
Object.values(vakken).forEach(goals => Object.values(goals).forEach(s => {
if (s==='groen') groen++; else if (s==='oranje') oranje++; else if (s==='roze') roze++;
if (s==='groen') g++; else if (s==='oranje') o++; else if (s==='roze') r++;
}));
const total = groen+oranje+roze;
const gPct = total>0?groen/total*100:0;
const oPct = total>0?oranje/total*100:0;
const rPct = total>0?roze/total*100:0;
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)">${total} beoordelingen</span></div>
<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:${gPct}%"></div>
<div class="p-oranje" style="width:${oPct}%"></div>
<div class="p-roze" style="width:${rPct}%"></div>
<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>${groen} groen</span>
<span><span class="dot" style="background:var(--status-oranje)"></span>${oranje} oranje</span>
<span><span class="dot" style="background:var(--status-roze)"></span>${roze} roze</span>
<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>';
@@ -466,18 +448,18 @@ function renderKlasProgress() {
function renderVakStats() {
const el = document.getElementById('vakStatsContent');
const byClass = overviewData.assessments_by_class || {};
const vakTotals = {};
const totals = {};
Object.values(byClass).forEach(vakken => {
Object.entries(vakken).forEach(([v, goals]) => {
if (!vakTotals[v]) vakTotals[v] = {groen:0,oranje:0,roze:0};
if (!totals[v]) totals[v] = {groen:0,oranje:0,roze:0};
Object.values(goals).forEach(s => {
if (s==='groen') vakTotals[v].groen++;
else if (s==='oranje') vakTotals[v].oranje++;
else if (s==='roze') vakTotals[v].roze++;
if (s==='groen') totals[v].groen++;
else if (s==='oranje') totals[v].oranje++;
else if (s==='roze') totals[v].roze++;
});
});
});
const sorted = Object.entries(vakTotals).sort((a,b) => maakVakNaam(a[0]).localeCompare(maakVakNaam(b[0]),'nl'));
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;
@@ -499,17 +481,16 @@ function renderVakStats() {
// ── Tab: Doelen detail ─────────────────────────────────────────────────────────
function populateFilters() {
// Vakken
const vakSel = document.getElementById('filterVak');
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 => {
[...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);
});
// Klassen
const klasSel = document.getElementById('filterKlas');
klasSel.innerHTML = '<option value="all">Alle klassen</option>';
(overviewData.classes||[]).forEach(k => {
@@ -521,11 +502,13 @@ function applyFilters() {
const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value;
const statusFilter = document.getElementById('filterStatus').value;
const leeftFilter = [...document.querySelectorAll('.leeftijd-cbs input:checked')].map(c=>c.value);
const leeftFilter = [...document.querySelectorAll('#leeftijdCbs input:checked')].map(c=>c.value);
const search = document.getElementById('filterSearch').value.toLowerCase();
let vakkenToShow = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
let klassen = klasFilter === 'all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter);
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 => {
@@ -542,26 +525,21 @@ function applyFilters() {
rows.push({vakId, goal, statussen, klassen});
});
});
renderTable(rows, klassen);
}
function renderTable(rows, klassen) {
const thead = document.getElementById('tblHead');
const tbody = document.getElementById('tblBody');
// Header
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,klassen:kl}) => {
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>`;
@@ -605,43 +583,28 @@ function renderVergelijk() {
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>`;
<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] || '';
const sB = goalsB[goal.id] || '';
const same = sA === sB;
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="${same?'diff-row-same':'diff-row-differ'}">
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>`;
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;
}
@@ -650,24 +613,18 @@ 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 statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
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 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(', ')}"`;
statussen.forEach(s => csv += `,${s||'-'}`);
csv += `,${g},${o},${r}\n`;
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`;
@@ -683,43 +640,31 @@ function exportPDF() {
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),'✓','~','!']];
const headers = [['Vak','Code','Doel','Leeftijd',...klassen.map(k=>k.name),'G','O','R']];
const rows = [];
vakken.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => {
const statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
rows.push([
maakVakNaam(vakId), goal.goNr,
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(', '),
...statussen.map(s=>s==='groen'?'':s==='oranje'?'~':s==='roze'?'!':'-'),
statussen.filter(s=>s==='groen').length,
statussen.filter(s=>s==='oranje').length,
statussen.filter(s=>s==='roze').length,
...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.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');
}
// ── Tabs ───────────────────────────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach((b,i) => {
const tabs = ['klassen','vakken','doelen','vergelijk'];
b.classList.toggle('active', tabs[i]===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));
}