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
All checks were successful
Build & Push / Build & Push image (push) Successful in 39s
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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,''')}" 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,''')}">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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,6 +232,8 @@
|
||||
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
||||
}
|
||||
.btn-import { background: var(--warning); color: white; }
|
||||
.btn-import:hover { background: #d97706; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -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() {
|
||||
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 sel = document.getElementById('vakSelector');
|
||||
try {
|
||||
const res = await fetch('/api/doelen/index');
|
||||
if (!res.ok) { showNotification('Kon vakken niet laden', 'error'); return; }
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
// Gebruik naam uit API, fallback op lokale functie
|
||||
opt.textContent = `${v.naam || vakNaam(v.id)} (${v.aantalDoelzinnen} doelen)`;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
} catch(e) {
|
||||
console.error('loadVakken fout:', e);
|
||||
showNotification('Netwerkfout 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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,''')}">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,''')}" 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,''')}">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,''')}">
|
||||
${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,''')}">×</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>
|
||||
|
||||
@@ -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,''')}">
|
||||
${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,''')}">
|
||||
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,''')}" data-teachers="${JSON.stringify(c.teachers?.map(t=>t.id)||[]).replace(/"/g,'"')}">
|
||||
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,''')}">
|
||||
×
|
||||
</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(/"/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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user