feat: enhance class access logic for directors in my_classes endpoint
All checks were successful
Build & Push / Build & Push image (push) Successful in 40s
All checks were successful
Build & Push / Build & Push image (push) Successful in 40s
This commit is contained in:
@@ -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],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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> </label>
|
<label> </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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user