refactor: replace inline event handlers with bind function for improved readability and maintainability
All checks were successful
Build & Push / Build & Push image (push) Successful in 39s

This commit is contained in:
2026-03-02 22:35:37 +01:00
parent 9f797a211b
commit 771a742c9a
9 changed files with 251 additions and 112 deletions

View File

@@ -30,10 +30,7 @@ def get_active_year(school_id=None):
@login_required
def doelen_index():
data = load_index()
if not data['vakken']:
return jsonify({
'error': 'Geen doelen gevonden. Upload eerst de JSON bestanden via het beheerderspaneel.'
}), 404
# Altijd een geldig object teruggeven — lege vakkenlijst is geen fout
return jsonify(data)
@@ -126,6 +123,7 @@ def save_assessment():
detail={'status': status})
return jsonify({'assessment': assessment.to_dict()})
@api_bp.route('/assessments/bulk-import', methods=['POST'])
@login_required
@limiter.limit('5 per minute')
@@ -151,6 +149,7 @@ def bulk_import_assessments():
fouten = 0
for vak_id, vak_data in vakken.items():
# Sanitiseer vak_id
if not isinstance(vak_id, str) or len(vak_id) > 100:
fouten += 1
continue
@@ -201,6 +200,7 @@ def bulk_import_assessments():
detail={'totaal': totaal, 'fouten': fouten})
return jsonify({'totaal': totaal, 'fouten': fouten})
# ── Directeur schooloverzicht ──────────────────────────────────────────────────
@api_bp.route('/school/overview')

View File

@@ -325,15 +325,20 @@
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('btnCancelSg').addEventListener('click', () => { closeModal() });
document.getElementById('btnConfirmSg').addEventListener('click', () => { addSgIct() });
document.getElementById('btnCancelSchool').addEventListener('click', () => { closeModal() });
document.getElementById('btnConfirmSchool').addEventListener('click', () => { addSchool() });
document.getElementById('btnCancelEdit').addEventListener('click', () => { closeModal() });
document.getElementById('btnConfirmEdit').addEventListener('click', () => { saveSchool() });
document.getElementById('btnAddSgIct').addEventListener('click', () => { openModal('addSgIct') });
document.getElementById('btnAddSchool').addEventListener('click', () => { openModal('addSchool') });
bind('btnCancelSg', 'click', () => { closeModal() });
bind('btnConfirmSg', 'click', () => { addSgIct() });
bind('btnCancelSchool', 'click', () => { closeModal() });
bind('btnConfirmSchool', 'click', () => { addSchool() });
bind('btnCancelEdit', 'click', () => { closeModal() });
bind('btnConfirmEdit', 'click', () => { saveSchool() });
bind('btnAddSgIct', 'click', () => { openModal('addSgIct') });
bind('btnAddSchool', 'click', () => { openModal('addSchool') });
await loadStats();
await loadSgIct();
await loadSchools();
@@ -363,7 +368,7 @@ async function loadSgIct() {
<td>${u.full_name}</td>
<td>${u.email}</td>
<td style="color:var(--gray-500);font-size:0.8rem;">${u.last_login ? new Date(u.last_login).toLocaleDateString('nl-BE') : 'Nog niet ingelogd'}</td>
<td><button class="btn btn-danger btn-sm" onclick="removeSgIct(${u.id})">Verwijderen</button></td>
<td><button class="btn btn-danger btn-sm" data-action="removeSgIct" data-id="${u.id}">Verwijderen</button></td>
</tr>`).join('');
}
@@ -382,8 +387,8 @@ async function loadSchools() {
<td>${(s.email_domains||[]).map(d=>`<span class="domain-chip">${d}</span>`).join('')||'<em style="color:var(--gray-400)">geen</em>'}</td>
<td style="color:var(--gray-500);">${s.user_count}</td>
<td style="display:flex;gap:0.4rem;">
<button class="btn btn-secondary btn-sm" onclick="editSchool(${s.id},'${s.name.replace(/'/g,"\\'")}','${(s.email_domains||[]).join(', ')}')">Bewerken</button>
<button class="btn btn-danger btn-sm" onclick="deleteSchool(${s.id},'${s.name.replace(/'/g,"\\'")}')">Verwijderen</button>
<button class="btn btn-secondary btn-sm" data-action="editSchool" data-id="${s.id}" data-name="${s.name.replace(/'/g,'&#39;')}" data-domains="${(s.email_domains||[]).join(', ')}">Bewerken</button>
<button class="btn btn-danger btn-sm" data-action="deleteSchool" data-id="${s.id}" data-name="${s.name.replace(/'/g,'&#39;')}">Verwijderen</button>
</td>
</tr>`).join('');
}
@@ -484,6 +489,16 @@ function notify(msg, type='success') {
el.textContent = msg; el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3000);
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'removeSgIct') { removeSgIct(btn.dataset.id); }
if (action === 'editSchool') { editSchool(btn.dataset.id, btn.dataset.name, btn.dataset.domains); }
if (action === 'deleteSchool') { deleteSchool(btn.dataset.id, btn.dataset.name); }
});
</script>
</body>
</html>

View File

@@ -349,6 +349,11 @@
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
let teachers = [];
let allGoals = {};
let vakData = {};
@@ -356,19 +361,19 @@ let overviewData = null;
let activeYearId = null; // null = huidig actief jaar
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('jaarSelector').addEventListener('change', switchJaar);
document.getElementById('btnVernieuw').addEventListener('click', loadOverview);
document.getElementById('btnAddTeacher').addEventListener('click', openAddTeacher);
document.getElementById('tab-doelen').addEventListener('click', () => switchTab('doelen'));
document.getElementById('tab-klassen').addEventListener('click', () => switchTab('klassen'));
document.getElementById('tab-vergelijk') && document.getElementById('tab-vergelijk').addEventListener('click', () => switchTab('vergelijk'));
document.getElementById('btnCancelTeacher').addEventListener('click', closeModal);
document.getElementById('btnConfirmTeacher').addEventListener('click', addTeacher);
document.getElementById('filterVak').addEventListener('change', applyFilters);
document.getElementById('filterTeacher').addEventListener('change', applyFilters);
document.getElementById('filterKlas') && document.getElementById('filterKlas').addEventListener('change', applyFilters);
document.getElementById('filterStatus').addEventListener('change', applyFilters);
document.getElementById('filterSearch').addEventListener('input', applyFilters);
bind('jaarSelector', 'change', switchJaar);
bind('btnVernieuw', 'click', loadOverview);
bind('btnAddTeacher', 'click', openAddTeacher);
bind('tab-doelen', 'click', () => switchTab('doelen'));
bind('tab-klassen', 'click', () => switchTab('klassen'));
document.getElementById('tab-vergelijk') && bind('tab-vergelijk', 'click', () => switchTab('vergelijk'));
bind('btnCancelTeacher', 'click', closeModal);
bind('btnConfirmTeacher', 'click', addTeacher);
bind('filterVak', 'change', applyFilters);
bind('filterTeacher', 'change', applyFilters);
document.getElementById('filterKlas') && bind('filterKlas', 'change', applyFilters);
bind('filterStatus', 'change', applyFilters);
bind('filterSearch', 'input', applyFilters);
document.querySelectorAll('.leeftijd-checkbox input').forEach(cb => cb.addEventListener('change', applyFilters));
await loadUser();
await loadJaren();
@@ -432,7 +437,7 @@ function renderTeacherList() {
el.innerHTML = teachers.map(t => `
<div class="teacher-chip">
<span>${t.full_name}</span>
<button onclick="removeTeacher(${t.id})"
<button data-action="removeTeacher" data-id="${t.id}"
style="width:18px;height:18px;border-radius:50%;border:none;background:var(--gray-300);cursor:pointer;font-size:0.7rem;"
title="Verwijderen">×</button>
</div>`).join('');
@@ -944,6 +949,14 @@ function renderVergelijking() {
</div>`;
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'removeTeacher') { removeTeacher(btn.dataset.id); }
});
</script>
</body>
</html>

View File

@@ -181,18 +181,23 @@
</div>
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
document.addEventListener('DOMContentLoaded', () => {
// ── Tab knoppen ──────────────────────────────────────────────────────────
document.getElementById('tabXlsx').addEventListener('click', () => switchUploadTab('xlsx'));
document.getElementById('tabJson').addEventListener('click', () => switchUploadTab('json'));
bind('tabXlsx', 'click', () => switchUploadTab('xlsx'));
bind('tabJson', 'click', () => switchUploadTab('json'));
// ── Drop zones ───────────────────────────────────────────────────────────
setupDropZone('dropZoneXlsx', 'fileInputXlsx', uploadXlsx);
setupDropZone('dropZoneJson', 'fileInputJson', uploadJson);
// ── File inputs ──────────────────────────────────────────────────────────
document.getElementById('fileInputXlsx').addEventListener('change', function() { uploadXlsx(this.files); });
document.getElementById('fileInputJson').addEventListener('change', function() { uploadJson(this.files); });
bind('fileInputXlsx', 'change', function() { uploadXlsx(this.files); });
bind('fileInputJson', 'change', function() { uploadJson(this.files); });
// ── Init ─────────────────────────────────────────────────────────────────
loadDoelen();

View File

@@ -192,7 +192,7 @@
.leeftijd-checkbox:has(input:checked) { background: var(--primary) !important; }
/* Vak indicator */
.vak-indicator { /* gradient blijft, ziet er goed uit */ }
/* .vak-indicator — stijl via inline op het element */
/* Progress bars achtergrond */
.progress-bar { background: #334155 !important; }
@@ -232,7 +232,9 @@
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
}
</style>
.btn-import { background: var(--warning); color: white; }
.btn-import:hover { background: #d97706; }
</style>
</head>
<body>
<div class="container">
@@ -250,8 +252,8 @@
✎ Wijzigen
</button>
</div>
<button id="btnImportJson" class="btn btn-secondary" title="Importeer beoordelingen uit vorige versie (JSON)">
📥 Importeer JSON
<button id="btnImportJson" class="btn btn-import" title="Importeer beoordelingen uit de vorige standalone versie van de app (JSON bestand)">
📥 Vorige beoordelingen importeren
</button>
<input type="file" id="importJsonFile" accept=".json" style="display:none">
<a href="/auth/logout" class="btn btn-secondary">Uitloggen</a>
@@ -365,27 +367,42 @@ let filteredData = [];
let saveTimeout = null;
// ── Init ─────────────────────────────────────────────────────────────────────
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
else console.warn('Element niet gevonden:', id);
}
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('btnOpenKlas').addEventListener('click', openKlasModal);
document.getElementById('btnSluitKlas').addEventListener('click', closeKlasModal);
document.getElementById('btnSlaKlas').addEventListener('click', saveKlassen);
document.getElementById('vakSelector').addEventListener('change', switchVak);
document.getElementById('searchInput').addEventListener('input', applyFilters);
document.getElementById('statusFilter').addEventListener('change', applyFilters);
document.getElementById('ebgFilter').addEventListener('change', applyFilters);
document.getElementById('sectieFilter').addEventListener('change', applyFilters);
bind('btnImportJson', 'click', function() { document.getElementById('importJsonFile').click(); });
bind('importJsonFile', 'change', function() { importLegacyJson(this.files[0]); this.value=''; });
bind('btnOpenKlas', 'click', openKlasModal);
bind('btnSluitKlas', 'click', closeKlasModal);
bind('btnSlaKlas', 'click', saveKlassen);
bind('vakSelector', 'change', switchVak);
bind('searchInput', 'input', applyFilters);
bind('statusFilter', 'change', applyFilters);
bind('ebgFilter', 'change', applyFilters);
bind('sectieFilter', 'change', applyFilters);
document.querySelectorAll('.leeftijd-checkboxes input').forEach(cb => cb.addEventListener('change', applyFilters));
await loadUser();
await loadVakken();
try { await loadUser(); } catch(e) { console.error('loadUser fout:', e); }
try { await loadVakken(); } catch(e) { console.error('loadVakken fout:', e); }
});
async function loadUser() {
const res = await fetch('/api/me');
const data = await res.json();
currentUser = data.user;
document.getElementById('userInfo').textContent =
`${currentUser.full_name}${currentUser.school?.name || ''}`;
await loadKlassen();
try {
const res = await fetch('/api/me');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
currentUser = data.user;
if (!currentUser) throw new Error('Geen gebruikersdata');
document.getElementById('userInfo').textContent =
`${currentUser.full_name}${currentUser.school?.name || ''}`;
await loadKlassen();
} catch(e) {
console.error('loadUser fout:', e);
document.getElementById('userInfo').textContent = 'Fout bij laden gebruiker';
}
}
// ── Klassen ───────────────────────────────────────────────────────────────────
@@ -445,21 +462,45 @@ async function saveKlassen() {
}
async function loadVakken() {
const res = await fetch('/api/doelen/index');
if (!res.ok) { showNotification('Kon vakken niet laden', 'error'); return; }
const data = await res.json();
const sel = document.getElementById('vakSelector');
try {
const res = await fetch('/api/doelen/index');
if (!res.ok) {
showNotification('Fout bij laden vakken (HTTP ' + res.status + ')', 'error');
return;
}
const data = await res.json();
const sel = document.getElementById('vakSelector');
// Sorteer op naam — API levert ze al gesorteerd, maar voor de zekerheid
const sorted = [...data.vakken].sort((a,b) => (a.naam||a.id).localeCompare(b.naam||b.id, 'nl'));
if (!data.vakken?.length) {
// Geen doelen geüpload — toon duidelijke boodschap in selector
const opt = document.createElement('option');
opt.value = '';
opt.textContent = '⚠️ Geen vakken beschikbaar — beheerder moet doelen uploaden';
opt.disabled = true;
sel.appendChild(opt);
document.getElementById('tableBody').innerHTML =
`<tr><td colspan="6" style="padding:2rem;text-align:center;color:var(--gray-500);">
<strong>Geen leerdoelen beschikbaar.</strong><br><br>
Vraag je beheerder om de doelensets te uploaden via het beheerderspaneel
(<em>Beheer → Leerdoelen bestanden</em>).<br><br>
<span style="font-size:.85rem;">Heb je al beoordelingen uit de vorige versie?
Gebruik de <strong>📥 Vorige beoordelingen importeren</strong> knop hierboven
om ze te migreren zodra de vakken beschikbaar zijn.</span>
</td></tr>`;
return;
}
sorted.forEach(v => {
const opt = document.createElement('option');
opt.value = v.id;
// Gebruik naam uit API, fallback op lokale functie
opt.textContent = `${v.naam || vakNaam(v.id)} (${v.aantalDoelzinnen} doelen)`;
sel.appendChild(opt);
});
const sorted = [...data.vakken].sort((a,b) => (a.naam||a.id).localeCompare(b.naam||b.id, 'nl'));
sorted.forEach(v => {
const opt = document.createElement('option');
opt.value = v.id;
opt.textContent = `${v.naam || vakNaam(v.id)} (${v.aantalDoelzinnen} doelen)`;
sel.appendChild(opt);
});
} catch(e) {
console.error('loadVakken fout:', e);
showNotification('Netwerk­fout bij laden vakken', 'error');
}
}
// ── Vak wisselen ─────────────────────────────────────────────────────────────
@@ -568,7 +609,7 @@ function renderTable() {
const ebg = (d.kennisverwerking||'').toLowerCase();
return `
<tr class="${s ? 'status-'+s : ''}">
<td><button class="status-selector status-${s||'none'}" onclick="cycleStatus('${d.id}')"></button></td>
<td><button class="status-selector status-${s||'none'}" data-action="cycleStatus" data-id="${d.id}"></button></td>
<td><strong>${d.goNr}</strong></td>
<td>${ebg ? `<span class="ebg-badge ebg-${ebg}">${ebg.charAt(0).toUpperCase()+ebg.slice(1)}</span>` : '-'}</td>
<td><div class="leeftijden">${d.leeftijden.map(l=>`<span class="leeftijd-badge">${l}</span>`).join('')}</div></td>
@@ -677,12 +718,26 @@ function showNotification(msg, type='success') {
setTimeout(() => el.classList.remove('show'), 3000);
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'cycleStatus') { cycleStatus(btn.dataset.id); }
});
// ── Legacy JSON import (uit vorige standalone versie) ────────────────────────
async function importLegacyJson(file) {
if (!file) return;
let data;
try {
data = JSON.parse(await file.text());
const text = await file.text();
data = JSON.parse(text);
// Valideer dat het een herkenbaar formaat is
if (!data.vakken) {
showNotification('Ongeldig bestand — geen vakken gevonden. Verwacht een export uit de Leerdoelen Tracker.', 'error');
return;
}
} catch(e) {
showNotification('Ongeldig JSON bestand', 'error'); return;
}

View File

@@ -312,6 +312,11 @@
</div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
let saVisible = false;
function toggleSuperadmin() {
@@ -347,8 +352,8 @@
});
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('btnToggleSuperadmin').addEventListener('click', () => { toggleSuperadmin() });
document.getElementById('btnSuperadminLogin').addEventListener('click', () => { superadminLogin() });
bind('btnToggleSuperadmin', 'click', () => { toggleSuperadmin() });
bind('btnSuperadminLogin', 'click', () => { superadminLogin() });
});
</script>
</body>

View File

@@ -333,6 +333,11 @@ toevoegen</button>
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
const IS_SUPERADMIN = {{ 'true' if is_superadmin else 'false' }};
let schools = [];
@@ -343,21 +348,21 @@ const SCHOOL_ROLLEN = [
];
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('btnAddSgIct') && document.getElementById('btnAddSgIct').addEventListener('click', () => openModal('addSgIct'));
document.getElementById('btnAddJaar') && document.getElementById('btnAddJaar').addEventListener('click', () => openModal('addJaar'));
document.getElementById('btnAddSchool') && document.getElementById('btnAddSchool').addEventListener('click', () => openModal('addSchool'));
document.getElementById('btnAddUser') && document.getElementById('btnAddUser').addEventListener('click', () => openModal('addUser'));
document.getElementById('auditCategory').addEventListener('change', loadAuditLog);
document.getElementById('auditSchoolFilter') && document.getElementById('auditSchoolFilter').addEventListener('change', loadAuditLog);
document.getElementById('auditSearch').addEventListener('input', loadAuditLog);
document.getElementById('btnCancelSchool') && document.getElementById('btnCancelSchool').addEventListener('click', closeModal);
document.getElementById('btnSaveSchool') && document.getElementById('btnSaveSchool').addEventListener('click', addSchool);
document.getElementById('btnCancelSgIct') && document.getElementById('btnCancelSgIct').addEventListener('click', closeModal);
document.getElementById('btnSaveSgIct') && document.getElementById('btnSaveSgIct').addEventListener('click', addSgIct);
document.getElementById('btnCancelJaar') && document.getElementById('btnCancelJaar').addEventListener('click', closeModal);
document.getElementById('btnSaveJaar') && document.getElementById('btnSaveJaar').addEventListener('click', addJaar);
document.getElementById('btnCancelUser') && document.getElementById('btnCancelUser').addEventListener('click', closeModal);
document.getElementById('btnSaveUser') && document.getElementById('btnSaveUser').addEventListener('click', addUser);
document.getElementById('btnAddSgIct') && bind('btnAddSgIct', 'click', () => openModal('addSgIct'));
document.getElementById('btnAddJaar') && bind('btnAddJaar', 'click', () => openModal('addJaar'));
document.getElementById('btnAddSchool') && bind('btnAddSchool', 'click', () => openModal('addSchool'));
document.getElementById('btnAddUser') && bind('btnAddUser', 'click', () => openModal('addUser'));
bind('auditCategory', 'change', loadAuditLog);
document.getElementById('auditSchoolFilter') && bind('auditSchoolFilter', 'change', loadAuditLog);
bind('auditSearch', 'input', loadAuditLog);
document.getElementById('btnCancelSchool') && bind('btnCancelSchool', 'click', closeModal);
document.getElementById('btnSaveSchool') && bind('btnSaveSchool', 'click', addSchool);
document.getElementById('btnCancelSgIct') && bind('btnCancelSgIct', 'click', closeModal);
document.getElementById('btnSaveSgIct') && bind('btnSaveSgIct', 'click', addSgIct);
document.getElementById('btnCancelJaar') && bind('btnCancelJaar', 'click', closeModal);
document.getElementById('btnSaveJaar') && bind('btnSaveJaar', 'click', addJaar);
document.getElementById('btnCancelUser') && bind('btnCancelUser', 'click', closeModal);
document.getElementById('btnSaveUser') && bind('btnSaveUser', 'click', addUser);
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid()];
if (IS_SUPERADMIN) tasks.push(loadSgIct());
await Promise.all(tasks);
@@ -391,7 +396,7 @@ async function loadSgIct() {
<td>${u.full_name}</td>
<td style="color:var(--gray-500);font-size:.82rem;">${u.email}</td>
<td style="color:var(--gray-500);font-size:.8rem;">${u.last_login ? new Date(u.last_login).toLocaleDateString('nl-BE') : 'Nog niet ingelogd'}</td>
<td><button class="btn btn-danger btn-sm" onclick="removeSgIct(${u.id},'${u.full_name.replace(/'/g,"\\'")}')">Verwijderen</button></td>
<td><button class="btn btn-danger btn-sm" data-action="removeSgIct" data-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">Verwijderen</button></td>
</tr>`).join('');
}
@@ -432,8 +437,8 @@ async function loadSchoolsTable() {
<td>${(s.email_domains||[]).map(d=>`<span class="domain-chip">${d}</span>`).join('') || '<em style="color:var(--gray-400)">geen</em>'}</td>
<td style="color:var(--gray-500);">${s.user_count}</td>
<td style="display:flex;gap:.35rem;">
<button class="btn btn-secondary btn-sm" onclick="editSchool(${s.id},'${s.name.replace(/'/g,"\\'")}','${(s.email_domains||[]).join(', ')}')">Bewerken</button>
<button class="btn btn-danger btn-sm" onclick="deleteSchool(${s.id},'${s.name.replace(/'/g,"\\'")}')">Verwijderen</button>
<button class="btn btn-secondary btn-sm" data-action="editSchool" data-id="${s.id}" data-name="${s.name.replace(/'/g,'&#39;')}" data-domains="${(s.email_domains||[]).join(', ')}">Bewerken</button>
<button class="btn btn-danger btn-sm" data-action="deleteSchool" data-id="${s.id}" data-name="${s.name.replace(/'/g,'&#39;')}">Verwijderen</button>
</td>
</tr>`).join('');
@@ -544,10 +549,10 @@ function renderUserGroup(schoolId, label, users, maxShow=99) {
</div>
</div>
<div class="user-actions">
<select class="role-select" onchange="changeRole(${schoolId},${u.id},this.value,'${u.full_name.replace(/'/g,"\\'")}',this)">
<select class="role-select" data-action="changeRole" data-school-id="${schoolId}" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">
${SCHOOL_ROLLEN.map(r=>`<option value="${r.value}" ${r.value===u.role?'selected':''}>${r.label}</option>`).join('')}
</select>
<button class="btn btn-danger btn-sm" onclick="removeUser(${schoolId},${u.id},'${u.full_name.replace(/'/g,"\\'")}')">×</button>
<button class="btn btn-danger btn-sm" data-action="removeUser" data-school-id="${schoolId}" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">×</button>
</div>
</div>`).join('')}
${hidden > 0 ? `<div style="color:var(--gray-500);font-size:.8rem;padding:.3rem 0 0;">+ ${hidden} meer...</div>` : ''}`;
@@ -610,7 +615,7 @@ async function loadJaren() {
: '<span style="color:var(--gray-400);font-size:.82rem;">Inactief</span>'}
</td>
<td>${!y.is_active
? `<button class="btn btn-secondary btn-sm" onclick="activeerJaar(${y.id},'${y.label}')">Activeren</button>`
? `<button class="btn btn-secondary btn-sm" data-action="activeerJaar" data-id="${y.id}" data-label="${y.label}">Activeren</button>`
: ''}
</td>
</tr>`).join('');
@@ -696,7 +701,7 @@ async function loadAuditLog(page = 1) {
const pager = document.getElementById('auditPager');
if (data.pages <= 1) { pager.innerHTML = ''; return; }
pager.innerHTML = Array.from({length: data.pages}, (_, i) => `
<button onclick="loadAuditLog(${i+1})"
<button data-action="auditPage" data-page="${i+1}"
style="padding:.3rem .6rem;border:1px solid var(--gray-300);border-radius:4px;cursor:pointer;min-width:36px;
${i+1 === data.page ? 'background:var(--primary);color:white;border-color:var(--primary);' : ''}">
${i+1}
@@ -715,6 +720,23 @@ function notify(msg, type='success') {
el.textContent = msg; el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3500);
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'removeSgIct') { removeSgIct(btn.dataset.id, btn.dataset.name); }
if (action === 'editSchool') { editSchool(btn.dataset.id, btn.dataset.name, btn.dataset.domains); }
if (action === 'deleteSchool') { deleteSchool(btn.dataset.id, btn.dataset.name); }
if (action === 'removeUser') { removeUser(btn.dataset.schoolId, btn.dataset.userId, btn.dataset.name); }
if (action === 'activeerJaar') { activeerJaar(btn.dataset.id, btn.dataset.label); }
if (action === 'auditPage') { loadAuditLog(parseInt(btn.dataset.page)); }
});
document.addEventListener('change', function(e) {
const sel = e.target.closest('[data-action="changeRole"]');
if (sel) { changeRole(sel.dataset.schoolId, sel.dataset.userId, sel.value, sel.dataset.name, sel); }
});
</script>
</body>
</html>

View File

@@ -319,6 +319,11 @@
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
let mySchoolId = null;
const ROLLEN = [
@@ -328,12 +333,13 @@ const ROLLEN = [
];
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('btnAddUser') && document.getElementById('btnAddUser').addEventListener('click', openModal);
document.getElementById('btnAddKlas') && document.getElementById('btnAddKlas').addEventListener('click', openAddKlas);
document.getElementById('btnCancelUser') && document.getElementById('btnCancelUser').addEventListener('click', closeModal);
document.getElementById('btnConfirmUser') && document.getElementById('btnConfirmUser').addEventListener('click', addUser);
document.getElementById('auditCategory') && document.getElementById('auditCategory').addEventListener('change', loadAuditLog);
document.getElementById('auditSearch') && document.getElementById('auditSearch').addEventListener('input', loadAuditLog);
const addModalEl = document.getElementById('addModal'); if (addModalEl) addModalEl.addEventListener('click', e => { if (e.target === addModalEl) closeModal(); });
document.getElementById('btnAddUser') && bind('btnAddUser', 'click', openModal);
document.getElementById('btnAddKlas') && bind('btnAddKlas', 'click', openAddKlas);
document.getElementById('btnCancelUser') && bind('btnCancelUser', 'click', closeModal);
document.getElementById('btnConfirmUser') && bind('btnConfirmUser', 'click', addUser);
document.getElementById('auditCategory') && bind('auditCategory', 'change', loadAuditLog);
document.getElementById('auditSearch') && bind('auditSearch', 'input', loadAuditLog);
const me = await fetch('/api/me').then(r => r.json());
mySchoolId = me.user?.school_id;
document.getElementById('schoolName').textContent = me.user?.school_name || '';
@@ -366,7 +372,7 @@ async function loadUsers() {
<td style="color:var(--gray-500);font-size:.82rem;">${u.email}</td>
<td>
<select class="role-select"
onchange="changeRole(${u.id}, this.value, '${u.full_name.replace(/'/g,"\\'")}', this)">
data-action="changeRole" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">
${ROLLEN.map(r =>
`<option value="${r.value}" ${r.value === u.role ? 'selected' : ''}>${r.label}</option>`
).join('')}
@@ -374,7 +380,7 @@ async function loadUsers() {
</td>
<td>
<button class="btn btn-danger btn-sm"
onclick="removeUser(${u.id}, '${u.full_name.replace(/'/g,"\\'")}')">
data-action="removeUser" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">
Verwijderen
</button>
</td>
@@ -432,9 +438,7 @@ function closeModal() {
document.getElementById('addModal').classList.remove('active');
document.getElementById('addError').style.display = 'none';
}
document.getElementById('addModal').addEventListener('click', e => {
if (e.target === document.getElementById('addModal')) closeModal();
});
function notify(msg, type = 'success') {
const el = document.getElementById('notification');
@@ -471,10 +475,10 @@ async function loadKlassen() {
: '<em>Geen leerkracht gekoppeld</em>'}
</td>
<td style="white-space:nowrap;">
<button class="btn btn-secondary btn-sm" onclick="openAssignTeachers(${c.id}, '${c.name.replace(/'/g,"\'")}', ${JSON.stringify(c.teachers?.map(t=>t.id)||[])})">
<button class="btn btn-secondary btn-sm" data-action="assignTeachers" data-id="${c.id}" data-name="${c.name.replace(/'/g,'&#39;')}" data-teachers="${JSON.stringify(c.teachers?.map(t=>t.id)||[]).replace(/"/g,'&quot;')}">
Leerkrachten
</button>
<button class="btn btn-danger btn-sm" onclick="deleteKlas(${c.id}, '${c.name.replace(/'/g,"\'")}')">
<button class="btn btn-danger btn-sm" data-action="deleteKlas" data-id="${c.id}" data-name="${c.name.replace(/'/g,'&#39;')}">
×
</button>
</td>
@@ -525,8 +529,8 @@ async function openAssignTeachers(classId, className, currentTeacherIds) {
: '<em style="color:gray;">Geen leerkrachten beschikbaar</em>'}
</div>
<div style="display:flex;gap:.5rem;justify-content:flex-end;">
<button onclick="document.getElementById('assignModal').remove()" class="btn btn-secondary">Annuleren</button>
<button onclick="saveAssignTeachers()" class="btn btn-primary">Opslaan</button>
<button data-action="closeAssignModal" class="btn btn-secondary">Annuleren</button>
<button data-action="saveAssignTeachers" class="btn btn-primary">Opslaan</button>
</div>
</div>
</div>`;
@@ -581,12 +585,29 @@ async function loadAuditLog(page = 1) {
const pager = document.getElementById('auditPager');
if (data.pages <= 1) { pager.innerHTML = ''; return; }
pager.innerHTML = Array.from({length: data.pages}, (_, i) => `
<button onclick="loadAuditLog(${i+1})"
<button data-action="auditPage" data-page="${i+1}"
style="padding:.3rem .6rem;border:1px solid var(--gray-300);border-radius:4px;cursor:pointer;
${i+1 === data.page ? 'background:var(--primary);color:white;border-color:var(--primary);' : ''}">
${i+1}
</button>`).join('');
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'removeUser') { removeUser(btn.dataset.userId, btn.dataset.name); }
if (action === 'assignTeachers') { openAssignTeachers(btn.dataset.id, btn.dataset.name, JSON.parse(btn.dataset.teachers.replace(/&quot;/g,'"'))); }
if (action === 'deleteKlas') { deleteKlas(btn.dataset.id, btn.dataset.name); }
if (action === 'closeAssignModal') { document.getElementById('assignModal')?.remove(); }
if (action === 'saveAssignTeachers'){ saveAssignTeachers(); }
if (action === 'auditPage') { loadAuditLog(parseInt(btn.dataset.page)); }
});
document.addEventListener('change', function(e) {
const sel = e.target.closest('[data-action="changeRole"]');
if (sel) { changeRole(sel.dataset.userId, sel.value, sel.dataset.name, sel); }
});
</script>
</body>
</html>

View File

@@ -244,9 +244,12 @@
</div>
<script nonce="{{ csp_nonce() }}">
document.getElementById('password').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
const pwEl = document.getElementById('password'); if (pwEl) pwEl.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
async function doLogin() {
const errorEl = document.getElementById('errorMsg');
@@ -271,7 +274,7 @@
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('btnLogin').addEventListener('click', () => { doLogin() });
bind('btnLogin', 'click', () => { doLogin() });
});
</script>
</body>