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']) @api_bp.route('/my/classes', methods=['GET'])
@login_required @login_required
def my_classes(): 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: if not current_user.school_id:
return jsonify({'all_classes': [], 'my_classes': []}) return jsonify({'all_classes': [], 'my_classes': []})
all_cls = Class.query.filter_by(school_id=current_user.school_id)\ all_cls = Class.query.filter_by(school_id=current_user.school_id)\
.order_by(Class.name).all() .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({ return jsonify({
'all_classes': [{'id': c.id, 'name': c.name} for c in all_cls], '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; } * { 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); } 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; } .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 { 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; } .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 { 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-secondary:hover { background: var(--gray-300); }
.btn-success { background: var(--success); color: white; } .btn-success { background: var(--success); color: white; }
.btn-success:hover { background: #059669; } .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; } .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 { 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-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-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; } .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; } .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 { 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.active { background: var(--primary); color: white; }
.tab-btn:hover:not(.active) { background: var(--gray-100); } .tab-btn:hover:not(.active) { background: var(--gray-100); }
.tab-content { display: none; } .tab-content { display: none; }
.tab-content.active { display: block; } .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 { 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; } .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-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 { 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-name { font-weight: 600; color: var(--gray-800); }
.klas-chip .klas-teachers { font-size: .75rem; color: var(--gray-500); margin-top: .15rem; } .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-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 { 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 { display: flex; justify-content: space-between; align-items: center; margin-bottom: .6rem; }
@@ -66,8 +55,6 @@
.p-roze { background: var(--status-roze); } .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; } .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; } .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; } .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 { 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 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 { 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:has(input:checked) { background: var(--primary); border-color: var(--primary); color: white; }
.lft-cb input { display: none; } .lft-cb input { display: none; }
/* Detail tabel */
.table-wrap { overflow-x: auto; } .table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .83rem; } table { width: 100%; border-collapse: collapse; font-size: .83rem; }
thead { background: var(--gray-50); position: sticky; top: 0; z-index: 5; } 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); } .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-bar { display: flex; height: 18px; border-radius: 3px; overflow: hidden; min-width: 50px; }
.sum-cell { background: var(--gray-50); } .sum-cell { background: var(--gray-50); }
/* Klasvergelijking */
.vergelijk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; } .vergelijk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
@media (max-width: 600px) { .vergelijk-grid { grid-template-columns: 1fr; } } @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%; } .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-same { background: #f0fdf4 !important; }
.diff-row-differ { background: #fff7ed !important; } .diff-row-differ { background: #fff7ed !important; }
/* Empty / loading */
.empty { text-align: center; padding: 3rem; color: var(--gray-400); font-size: .9rem; } .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 { 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.show { transform: translateY(0); opacity: 1; }
.notification.success { background: var(--success); } .notification.success { background: var(--success); }
.notification.error { background: var(--danger); } .notification.error { background: var(--danger); }
.notification.warning { background: var(--warning); } .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-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; } .legend-item { display: flex; align-items: center; gap: .35rem; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { --gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;--gray-300:#1a1a3e;--gray-700:#e5e7eb;--gray-800:#f3f4f6; } :root { --gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;--gray-300:#1a1a3e;--gray-700:#e5e7eb;--gray-800:#f3f4f6; }
body { background:#0f172a;color:#e2e8f0; } body { background:#0f172a;color:#e2e8f0; }
@@ -146,14 +120,13 @@
<body> <body>
<div class="container"> <div class="container">
<!-- Header -->
<div class="header"> <div class="header">
<div> <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> <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 style="opacity:.85;font-size:.85rem;margin-top:.25rem;" id="schoolInfo">Laden...</div>
</div> </div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;"> <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> <option>Laden...</option>
</select> </select>
<a href="/leerkracht-view" class="btn btn-light">👤 Leerkrachtenweergave</a> <a href="/leerkracht-view" class="btn btn-light">👤 Leerkrachtenweergave</a>
@@ -161,7 +134,6 @@
</div> </div>
</div> </div>
<!-- Stats -->
<div class="stats-grid"> <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 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="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 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>
<!-- Tabs -->
<div class="tabs"> <div class="tabs">
<button class="tab-btn active" onclick="switchTab('klassen')">🏫 Klassen</button> <button class="tab-btn active" data-tab="klassen">🏫 Klassen</button>
<button class="tab-btn" onclick="switchTab('vakken')">📚 Per vak</button> <button class="tab-btn" data-tab="vakken">📚 Per vak</button>
<button class="tab-btn" onclick="switchTab('doelen')">📋 Doelen detail</button> <button class="tab-btn" data-tab="doelen">📋 Doelen detail</button>
<button class="tab-btn" onclick="switchTab('vergelijk')">⚖️ Vergelijken</button> <button class="tab-btn" data-tab="vergelijk">⚖️ Vergelijken</button>
</div> </div>
<!-- TAB: Klassen -->
<div class="tab-content active" id="tab-klassen"> <div class="tab-content active" id="tab-klassen">
<div class="card"> <div class="card">
<h2>🏫 Klassen overzicht</h2> <h2>🏫 Klassen overzicht</h2>
<div class="klas-chips" id="klasChips"> <div class="klas-chips" id="klasChips"><div class="empty">Laden...</div></div>
<div class="empty">Laden...</div>
</div>
</div> </div>
<div class="card"> <div class="card">
<h2>📊 Voortgang per klas</h2> <h2>📊 Voortgang per klas</h2>
@@ -193,7 +161,6 @@
</div> </div>
</div> </div>
<!-- TAB: Per vak -->
<div class="tab-content" id="tab-vakken"> <div class="tab-content" id="tab-vakken">
<div class="card"> <div class="card">
<h2>📚 Statistieken per vak</h2> <h2>📚 Statistieken per vak</h2>
@@ -201,24 +168,19 @@
</div> </div>
</div> </div>
<!-- TAB: Doelen detail -->
<div class="tab-content" id="tab-doelen"> <div class="tab-content" id="tab-doelen">
<div class="filter-bar"> <div class="filter-bar">
<div class="fg"> <div class="fg">
<label>Vak</label> <label>Vak</label>
<select id="filterVak" onchange="applyFilters()"> <select id="filterVak"><option value="all">Alle vakken</option></select>
<option value="all">Alle vakken</option>
</select>
</div> </div>
<div class="fg"> <div class="fg">
<label>Klas</label> <label>Klas</label>
<select id="filterKlas" onchange="applyFilters()"> <select id="filterKlas"><option value="all">Alle klassen</option></select>
<option value="all">Alle klassen</option>
</select>
</div> </div>
<div class="fg"> <div class="fg">
<label>Status</label> <label>Status</label>
<select id="filterStatus" onchange="applyFilters()"> <select id="filterStatus">
<option value="all">Alle statussen</option> <option value="all">Alle statussen</option>
<option value="consensus">✓ Consensus (iedereen groen)</option> <option value="consensus">✓ Consensus (iedereen groen)</option>
<option value="verschil">⚠ Verschillen</option> <option value="verschil">⚠ Verschillen</option>
@@ -227,21 +189,17 @@
</div> </div>
<div class="fg"> <div class="fg">
<label>Leeftijd</label> <label>Leeftijd</label>
<div class="leeftijd-cbs"> <div class="leeftijd-cbs" id="leeftijdCbs"></div>
{% 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> </div>
<div class="fg"> <div class="fg">
<label>Zoeken</label> <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>
<div class="fg" style="justify-content:flex-end;"> <div class="fg" style="justify-content:flex-end;">
<label>&nbsp;</label> <label>&nbsp;</label>
<div style="display:flex;gap:.5rem;"> <div style="display:flex;gap:.5rem;">
<button class="btn btn-success" onclick="exportCSV()">⬇ CSV</button> <button class="btn btn-success" id="btnExportCSV">⬇ CSV</button>
<button class="btn btn-secondary" onclick="exportPDF()">⬇ PDF</button> <button class="btn btn-secondary" id="btnExportPDF">⬇ PDF</button>
</div> </div>
</div> </div>
</div> </div>
@@ -261,19 +219,16 @@
</div> </div>
</div> </div>
<!-- TAB: Vergelijken -->
<div class="tab-content" id="tab-vergelijk"> <div class="tab-content" id="tab-vergelijk">
<div class="card"> <div class="card">
<h2>⚖️ Twee klassen vergelijken</h2> <h2>⚖️ Twee klassen vergelijken</h2>
<div class="vergelijk-grid"> <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 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" onchange="renderVergelijk()"><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>
<div class="fg" style="margin-bottom:1rem;max-width:200px;"> <div class="fg" style="margin-bottom:1rem;max-width:200px;">
<label>Vak</label> <label>Vak</label>
<select class="vergelijk-select" id="vergVak" onchange="renderVergelijk()"> <select class="vergelijk-select" id="vergVak"><option value="">-- Kies vak --</option></select>
<option value="">-- Kies vak --</option>
</select>
</div> </div>
<div id="vergelijkResult"><div class="empty">Kies twee klassen en een vak om te vergelijken</div></div> <div id="vergelijkResult"><div class="empty">Kies twee klassen en een vak om te vergelijken</div></div>
</div> </div>
@@ -288,19 +243,55 @@
<script nonce="{{ csp_nonce() }}"> <script nonce="{{ csp_nonce() }}">
// ── State ────────────────────────────────────────────────────────────────────── // ── State ──────────────────────────────────────────────────────────────────────
let overviewData = { classes: [], assessments_by_class: {} }; let overviewData = { classes: [], assessments_by_class: {} };
let allGoals = {}; // { vakId: [{id,goNr,inhoud,sectie,leeftijden}] } let allGoals = {};
let vakCache = {}; // { vakId: rawJson } let vakCache = {};
let currentUser = null; let currentUser = null;
// ── Init ─────────────────────────────────────────────────────────────────────── // ── Init ───────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => { 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 loadUser();
await loadJaren(); await loadJaren();
await loadOverview(); await loadOverview();
}); });
function bind(id, ev, fn) { const el = document.getElementById(id); if (el) el.addEventListener(ev, fn); } // ── Gebruiker ──────────────────────────────────────────────────────────────────
async function loadUser() { async function loadUser() {
const res = await fetch('/api/me'); const res = await fetch('/api/me');
if (!res.ok) return; if (!res.ok) return;
@@ -319,8 +310,7 @@ async function loadJaren() {
sel.innerHTML = ''; sel.innerHTML = '';
(data.years || []).forEach(y => { (data.years || []).forEach(y => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = y.id; opt.value = y.id; opt.textContent = y.label;
opt.textContent = y.label;
if (y.is_active) opt.selected = true; if (y.is_active) opt.selected = true;
sel.appendChild(opt); sel.appendChild(opt);
}); });
@@ -330,14 +320,13 @@ async function switchJaar() {
await loadOverview(); await loadOverview();
} }
// ── Overview laden ───────────────────────────────────────────────────────────── // ── Overview ───────────────────────────────────────────────────────────────────
async function loadOverview() { async function loadOverview() {
const yearId = document.getElementById('jaarSelector').value; const yearId = document.getElementById('jaarSelector').value;
const url = '/api/school/overview' + (yearId ? '?year_id=' + yearId : ''); const url = '/api/school/overview' + (yearId ? '?year_id=' + yearId : '');
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) { showNotification('Fout bij laden overzicht', 'error'); return; } if (!res.ok) { showNotification('Fout bij laden overzicht', 'error'); return; }
overviewData = await res.json(); overviewData = await res.json();
await loadVakData(); await loadVakData();
updateStats(); updateStats();
renderKlassen(); renderKlassen();
@@ -348,7 +337,7 @@ async function loadOverview() {
populateVergelijkSelects(); populateVergelijkSelects();
} }
// ── Vak JSON laden ───────────────────────────────────────────────────────────── // ── Vak data ───────────────────────────────────────────────────────────────────
async function loadVakData() { async function loadVakData() {
const vakIds = new Set(); const vakIds = new Set();
Object.values(overviewData.assessments_by_class).forEach(vakken => { Object.values(overviewData.assessments_by_class).forEach(vakken => {
@@ -375,10 +364,8 @@ function processVakGoals(vakId) {
allGoals[vakId] = data.rijen allGoals[vakId] = data.rijen
.filter(r => r.type === 'doelzin' && r.goNr) .filter(r => r.type === 'doelzin' && r.goNr)
.map(r => ({ .map(r => ({
id: r.goNr, id: r.goNr, goNr: r.goNr, inhoud: r.inhoud,
goNr: r.goNr, sectie: r.parentId ? sectieLookup[r.parentId] : null,
inhoud: r.inhoud,
sectie: r.parentId ? sectieLookup[r.parentId] : null,
leeftijden: r.leeftijden || [] leeftijden: r.leeftijden || []
})); }));
} }
@@ -407,21 +394,21 @@ function updateStats() {
}); });
}); });
}); });
document.getElementById('statKlassen').textContent = overviewData.classes?.length || 0; document.getElementById('statKlassen').textContent = overviewData.classes?.length || 0;
document.getElementById('statVakken').textContent = vakIds.size; document.getElementById('statVakken').textContent = vakIds.size;
document.getElementById('statBeoordeeld').textContent = groen+oranje+roze; document.getElementById('statBeoordeeld').textContent = groen+oranje+roze;
document.getElementById('statGroen').textContent = groen; document.getElementById('statGroen').textContent = groen;
document.getElementById('statOranje').textContent = oranje; document.getElementById('statOranje').textContent = oranje;
document.getElementById('statRoze').textContent = roze; document.getElementById('statRoze').textContent = roze;
} }
// ── Tab: Klassen ─────────────────────────────────────────────────────────────── // ── Tab: Klassen ───────────────────────────────────────────────────────────────
function renderKlassen() { function renderKlassen() {
const chips = document.getElementById('klasChips'); const chips = document.getElementById('klasChips');
const klassen = overviewData.classes || []; const klassen = overviewData.classes || [];
if (!klassen.length) { chips.innerHTML = '<div class="empty">Geen klassen gevonden</div>'; return; } if (!klassen.length) { chips.innerHTML = '<div class="empty">Geen klassen gevonden</div>'; return; }
chips.innerHTML = klassen.map(k => { 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"> return `<div class="klas-chip">
<div class="klas-name">🏫 ${k.name}</div> <div class="klas-name">🏫 ${k.name}</div>
<div class="klas-teachers">${teachers}</div> <div class="klas-teachers">${teachers}</div>
@@ -433,30 +420,25 @@ function renderKlasProgress() {
const el = document.getElementById('klasProgressContent'); const el = document.getElementById('klasProgressContent');
const klassen = overviewData.classes || []; const klassen = overviewData.classes || [];
const byClass = overviewData.assessments_by_class || {}; const byClass = overviewData.assessments_by_class || {};
if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen</div>'; return; } if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen</div>'; return; }
el.innerHTML = '<div class="vak-grid">' + klassen.map(k => { el.innerHTML = '<div class="vak-grid">' + klassen.map(k => {
const vakken = byClass[k.id] || {}; 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 => { 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 t = g+o+r;
const gPct = total>0?groen/total*100:0;
const oPct = total>0?oranje/total*100:0;
const rPct = total>0?roze/total*100:0;
return `<div class="vak-card"> 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="progress-bar"><div class="progress-inner">
<div class="p-groen" style="width:${gPct}%"></div> <div class="p-groen" style="width:${t>0?g/t*100:0}%"></div>
<div class="p-oranje" style="width:${oPct}%"></div> <div class="p-oranje" style="width:${t>0?o/t*100:0}%"></div>
<div class="p-roze" style="width:${rPct}%"></div> <div class="p-roze" style="width:${t>0?r/t*100:0}%"></div>
</div></div> </div></div>
<div class="vak-legend"> <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-groen)"></span>${g} groen</span>
<span><span class="dot" style="background:var(--status-oranje)"></span>${oranje} oranje</span> <span><span class="dot" style="background:var(--status-oranje)"></span>${o} oranje</span>
<span><span class="dot" style="background:var(--status-roze)"></span>${roze} roze</span> <span><span class="dot" style="background:var(--status-roze)"></span>${r} roze</span>
</div> </div>
</div>`; </div>`;
}).join('') + '</div>'; }).join('') + '</div>';
@@ -466,18 +448,18 @@ function renderKlasProgress() {
function renderVakStats() { function renderVakStats() {
const el = document.getElementById('vakStatsContent'); const el = document.getElementById('vakStatsContent');
const byClass = overviewData.assessments_by_class || {}; const byClass = overviewData.assessments_by_class || {};
const vakTotals = {}; const totals = {};
Object.values(byClass).forEach(vakken => { Object.values(byClass).forEach(vakken => {
Object.entries(vakken).forEach(([v, goals]) => { 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 => { Object.values(goals).forEach(s => {
if (s==='groen') vakTotals[v].groen++; if (s==='groen') totals[v].groen++;
else if (s==='oranje') vakTotals[v].oranje++; else if (s==='oranje') totals[v].oranje++;
else if (s==='roze') vakTotals[v].roze++; 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; } if (!sorted.length) { el.innerHTML = '<div class="empty">Nog geen beoordelingen</div>'; return; }
el.innerHTML = sorted.map(([v,st]) => { el.innerHTML = sorted.map(([v,st]) => {
const t = st.groen+st.oranje+st.roze; const t = st.groen+st.oranje+st.roze;
@@ -499,17 +481,16 @@ function renderVakStats() {
// ── Tab: Doelen detail ───────────────────────────────────────────────────────── // ── Tab: Doelen detail ─────────────────────────────────────────────────────────
function populateFilters() { function populateFilters() {
// Vakken
const vakSel = document.getElementById('filterVak');
const byClass = overviewData.assessments_by_class || {}; const byClass = overviewData.assessments_by_class || {};
const vakIds = new Set(); const vakIds = new Set();
Object.values(byClass).forEach(v => Object.keys(v).forEach(id => vakIds.add(id))); 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>'; 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); const o = document.createElement('option'); o.value=v; o.textContent=maakVakNaam(v); vakSel.appendChild(o);
}); });
// Klassen
const klasSel = document.getElementById('filterKlas'); const klasSel = document.getElementById('filterKlas');
klasSel.innerHTML = '<option value="all">Alle klassen</option>'; klasSel.innerHTML = '<option value="all">Alle klassen</option>';
(overviewData.classes||[]).forEach(k => { (overviewData.classes||[]).forEach(k => {
@@ -521,11 +502,13 @@ function applyFilters() {
const vakFilter = document.getElementById('filterVak').value; const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value; const klasFilter = document.getElementById('filterKlas').value;
const statusFilter = document.getElementById('filterStatus').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(); const search = document.getElementById('filterSearch').value.toLowerCase();
let vakkenToShow = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter]; const vakkenToShow = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
let klassen = klasFilter === 'all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter); const klassen = klasFilter === 'all'
? (overviewData.classes||[])
: (overviewData.classes||[]).filter(k => k.id == klasFilter);
const rows = []; const rows = [];
vakkenToShow.forEach(vakId => { vakkenToShow.forEach(vakId => {
@@ -542,26 +525,21 @@ function applyFilters() {
rows.push({vakId, goal, statussen, klassen}); rows.push({vakId, goal, statussen, klassen});
}); });
}); });
renderTable(rows, klassen); renderTable(rows, klassen);
} }
function renderTable(rows, klassen) { function renderTable(rows, klassen) {
const thead = document.getElementById('tblHead'); const thead = document.getElementById('tblHead');
const tbody = document.getElementById('tblBody'); const tbody = document.getElementById('tblBody');
// Header
let hdr = '<tr><th class="goal-col">Doel</th><th>Leeftijden</th>'; let hdr = '<tr><th class="goal-col">Doel</th><th>Leeftijden</th>';
klassen.forEach(k => hdr += `<th class="klas-col">${k.name}</th>`); klassen.forEach(k => hdr += `<th class="klas-col">${k.name}</th>`);
hdr += '<th>Samenvatting</th></tr>'; hdr += '<th>Samenvatting</th></tr>';
thead.innerHTML = hdr; thead.innerHTML = hdr;
if (!rows.length) { if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="${klassen.length+3}" class="empty">Geen doelen gevonden met huidige filters</td></tr>`; tbody.innerHTML = `<tr><td colspan="${klassen.length+3}" class="empty">Geen doelen gevonden met huidige filters</td></tr>`;
return; return;
} }
tbody.innerHTML = rows.map(({goal,statussen}) => {
tbody.innerHTML = rows.map(({goal,statussen,klassen:kl}) => {
let tr = `<tr> 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 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>`; <td><div class="lft-badges">${goal.leeftijden.map(l=>`<span class="lft-badge">${l}</span>`).join('')}</div></td>`;
@@ -601,47 +579,32 @@ function populateVergelijkSelects() {
} }
function renderVergelijk() { function renderVergelijk() {
const idA = document.getElementById('vergKlasA').value; const idA = document.getElementById('vergKlasA').value;
const idB = document.getElementById('vergKlasB').value; const idB = document.getElementById('vergKlasB').value;
const vakId = document.getElementById('vergVak').value; const vakId = document.getElementById('vergVak').value;
const el = document.getElementById('vergelijkResult'); 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 || !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; } if (idA === idB) { el.innerHTML = '<div class="empty">Kies twee verschillende klassen</div>'; return; }
const klasA = overviewData.classes.find(k=>k.id==idA);
const klasA = overviewData.classes.find(k=>k.id==idA); const klasB = overviewData.classes.find(k=>k.id==idB);
const klasB = overviewData.classes.find(k=>k.id==idB);
const goalsA = overviewData.assessments_by_class[idA]?.[vakId] || {}; const goalsA = overviewData.assessments_by_class[idA]?.[vakId] || {};
const goalsB = overviewData.assessments_by_class[idB]?.[vakId] || {}; const goalsB = overviewData.assessments_by_class[idB]?.[vakId] || {};
const goals = allGoals[vakId] || []; const goals = allGoals[vakId] || [];
if (!goals.length) { el.innerHTML = '<div class="empty">Vak nog niet geladen of geen doelen</div>'; return; } if (!goals.length) { el.innerHTML = '<div class="empty">Vak nog niet geladen of geen doelen</div>'; return; }
let html = `<div class="table-wrap"><table> let html = `<div class="table-wrap"><table>
<thead><tr> <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>`;
<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 => { goals.forEach(goal => {
const sA = goalsA[goal.id] || ''; const sA = goalsA[goal.id]||'', sB = goalsB[goal.id]||'';
const sB = goalsB[goal.id] || '';
const same = sA === sB;
const symA = sA==='groen'?'✓':sA==='oranje'?'~':sA==='roze'?'!':'○'; const symA = sA==='groen'?'✓':sA==='oranje'?'~':sA==='roze'?'!':'○';
const symB = sB==='groen'?'✓':sB==='oranje'?'~':sB==='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 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 ${sA||'none'}">${symA}</div></td>
<td><div class="si ${sB||'none'}">${symB}</div></td> <td><div class="si ${sB||'none'}">${symB}</div></td>
</tr>`; </tr>`;
}); });
const differ = goals.filter(g=>(goalsA[g.id]||'')!==(goalsB[g.id]||'')).length;
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>`;
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; el.innerHTML = html;
} }
@@ -650,24 +613,18 @@ function exportCSV() {
const vakFilter = document.getElementById('filterVak').value; const vakFilter = document.getElementById('filterVak').value;
const klasFilter = document.getElementById('filterKlas').value; const klasFilter = document.getElementById('filterKlas').value;
const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter); const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter);
let csv = 'Vak,Doelnummer,Doel,Leeftijden'; let csv = 'Vak,Doelnummer,Doel,Leeftijden';
klassen.forEach(k => csv += ',' + k.name); klassen.forEach(k => csv += ',' + k.name);
csv += ',Groen,Oranje,Roze\n'; csv += ',Groen,Oranje,Roze\n';
const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter]; const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter];
vakken.forEach(vakId => { vakken.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => { (allGoals[vakId]||[]).forEach(goal => {
const statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||''); const st = 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;
csv += `"${maakVakNaam(vakId)}","${goal.goNr}","${goal.inhoud.replace(/"/g,'""')}","${goal.leeftijden.join(', ')}"`; csv += `"${maakVakNaam(vakId)}","${goal.goNr}","${goal.inhoud.replace(/"/g,'""')}","${goal.leeftijden.join(', ')}"`;
statussen.forEach(s => csv += `,${s||'-'}`); st.forEach(s => csv += `,${s||'-'}`);
csv += `,${g},${o},${r}\n`; 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'); const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8;'})); a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8;'}));
a.download = `Leerdoelen_${new Date().toISOString().split('T')[0]}.csv`; a.download = `Leerdoelen_${new Date().toISOString().split('T')[0]}.csv`;
@@ -683,43 +640,31 @@ function exportPDF() {
const klasFilter = document.getElementById('filterKlas').value; const klasFilter = document.getElementById('filterKlas').value;
const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter); const klassen = klasFilter==='all' ? (overviewData.classes||[]) : (overviewData.classes||[]).filter(k=>k.id==klasFilter);
const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter]; const vakken = vakFilter==='all' ? Object.keys(allGoals) : [vakFilter];
doc.setFontSize(14); doc.text('Leerdoelen Overzicht', 14, 14); doc.setFontSize(14); doc.text('Leerdoelen Overzicht', 14, 14);
doc.setFontSize(9); doc.text(`Export: ${new Date().toLocaleDateString('nl-BE')}`, 14, 20); 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 headers = [['Vak','Code','Doel','Leeftijd',...klassen.map(k=>k.name),'✓','~','!']];
const rows = []; const rows = [];
vakken.forEach(vakId => { vakken.forEach(vakId => {
(allGoals[vakId]||[]).forEach(goal => { (allGoals[vakId]||[]).forEach(goal => {
const statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||''); const st = klassen.map(k=>(overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
rows.push([ rows.push([maakVakNaam(vakId), goal.goNr,
maakVakNaam(vakId), goal.goNr,
goal.inhoud.substring(0,55)+(goal.inhoud.length>55?'...':''), goal.inhoud.substring(0,55)+(goal.inhoud.length>55?'...':''),
goal.leeftijden.join(', '), goal.leeftijden.join(', '),
...statussen.map(s=>s==='groen'?'':s==='oranje'?'~':s==='roze'?'!':'-'), ...st.map(s=>s==='groen'?'v':s==='oranje'?'~':s==='roze'?'!':'-'),
statussen.filter(s=>s==='groen').length, st.filter(s=>s==='groen').length, st.filter(s=>s==='oranje').length, st.filter(s=>s==='roze').length,
statussen.filter(s=>s==='oranje').length,
statussen.filter(s=>s==='roze').length,
]); ]);
}); });
}); });
doc.autoTable({head:headers,body:rows,startY:24,
doc.autoTable({ head: headers, body: rows, startY: 24, styles:{fontSize:7,cellPadding:1.5},headStyles:{fillColor:[79,70,229]},
styles: { fontSize: 7, cellPadding: 1.5 }, columnStyles:{0:{cellWidth:22},1:{cellWidth:14},2:{cellWidth:'auto'}},margin:{left:14,right:14}});
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`); doc.save(`Leerdoelen_${new Date().toISOString().split('T')[0]}.pdf`);
showNotification('PDF geëxporteerd', 'success'); showNotification('PDF geëxporteerd', 'success');
} }
// ── Tabs ─────────────────────────────────────────────────────────────────────── // ── Tabs ───────────────────────────────────────────────────────────────────────
function switchTab(name) { function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach((b,i) => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab===name));
const tabs = ['klassen','vakken','doelen','vergelijk'];
b.classList.toggle('active', tabs[i]===name);
});
document.querySelectorAll('.tab-content').forEach(el => el.classList.toggle('active', el.id==='tab-'+name)); document.querySelectorAll('.tab-content').forEach(el => el.classList.toggle('active', el.id==='tab-'+name));
} }