Files
leerdoelen_tracker/backend/templates/scholengroep_ict.html
Sam ee8fcb231b
All checks were successful
Build & Push / Build & Push image (push) Successful in 40s
feat: enhance user management table with search functionality and improved layout
2026-03-03 23:10:31 +01:00

993 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beheer {{ org_name }}</title>
<style>
:root {
--primary:#4f46e5;--primary-dark:#4338ca;--success:#10b981;
--danger:#ef4444;--warning:#f59e0b;
--gray-50:#f9fafb;--gray-100:#f3f4f6;--gray-200:#e5e7eb;
--gray-300:#d1d5db;--gray-500:#6b7280;--gray-600:#4b5563;
--gray-700:#374151;--gray-800:#1f2937;
}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--gray-100);color:var(--gray-800);}
.container{max-width:1200px;margin:0 auto;padding:1rem;}
.header{color:white;border-radius:12px;padding:1.25rem 1.5rem;margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;}
.header.is-superadmin{background:linear-gradient(135deg,#1f2937,#374151);}
.header.is-sgict{background:linear-gradient(135deg,#4c1d95,#5b21b6);}
.header-left h1{font-size:1.35rem;display:flex;align-items:center;gap:.6rem;}
.header-left .org{opacity:.8;font-size:.85rem;margin-top:.25rem;}
.header-badge{font-size:.7rem;padding:.2rem .5rem;border-radius:4px;background:rgba(255,255,255,.2);font-weight:500;}
.header-actions{display:flex;gap:.5rem;flex-wrap:wrap;}
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem 1rem;border:none;border-radius:6px;font-size:.85rem;font-weight:500;cursor:pointer;transition:all .2s;text-decoration:none;}
.btn-primary{background:var(--primary);color:white;} .btn-primary:hover{background:var(--primary-dark);}
.btn-secondary{background:var(--gray-200);color:var(--gray-700);} .btn-secondary:hover{background:var(--gray-300);}
.btn-danger{background:var(--danger);color:white;} .btn-danger:hover{background:#dc2626;}
.btn-light{background:rgba(255,255,255,.15);color:white;border:1px solid rgba(255,255,255,.25);} .btn-light:hover{background:rgba(255,255,255,.25);}
.btn-sm{padding:.25rem .55rem;font-size:.78rem;}
/* Stats */
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-bottom:1.5rem;}
.stat-card{background:white;border-radius:10px;padding:.9rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.08);}
.stat-value{font-size:1.7rem;font-weight:700;color:var(--primary);}
.stat-label{font-size:.72rem;color:var(--gray-500);text-transform:uppercase;letter-spacing:.05em;margin-top:.2rem;}
.section{background:white;border-radius:12px;padding:1.5rem;margin-bottom:1.5rem;box-shadow:0 1px 3px rgba(0,0,0,.1);}
.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;flex-wrap:wrap;gap:.5rem;}
.section-header h2{font-size:1.05rem;color:var(--gray-700);}
.section-hint{color:var(--gray-500);font-size:.83rem;margin-bottom:1rem;line-height:1.5;}
table{width:100%;border-collapse:collapse;font-size:.875rem;}
th{padding:.6rem .75rem;text-align:left;font-weight:600;color:var(--gray-600);border-bottom:2px solid var(--gray-200);background:var(--gray-50);white-space:nowrap;}
td{padding:.55rem .75rem;border-bottom:1px solid var(--gray-100);vertical-align:middle;}
tr:hover td{background:var(--gray-50);}
.empty-row td{text-align:center;color:var(--gray-500);padding:2rem;font-style:italic;}
.domain-chip{display:inline-block;padding:.15rem .45rem;background:#eff6ff;color:#1d4ed8;border-radius:4px;font-size:.72rem;margin:.1rem;border:1px solid #bfdbfe;}
/* Gebruikers-per-school tabel */
.schools-user-table{width:100%;border-collapse:collapse;}
.schools-user-table th{padding:.6rem .85rem;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--gray-500);border-bottom:2px solid var(--gray-200);background:var(--gray-50);}
.school-row td{padding:.7rem .85rem;border-bottom:1px solid var(--gray-100);vertical-align:middle;font-size:.875rem;}
.school-row{cursor:pointer;transition:background .12s;}
.school-row:hover td{background:var(--gray-50);}
.school-row.expanded td{background:#f0f9ff;border-bottom:none;}
.expand-icon{display:inline-block;transition:transform .2s;font-style:normal;width:18px;text-align:center;}
.school-row.expanded .expand-icon{transform:rotate(90deg);}
.users-panel-row td{padding:0;border-bottom:1px solid var(--gray-200);}
.users-panel{padding:.75rem 1.1rem 1rem;background:#f8fafc;display:none;}
.users-panel.open{display:block;}
.users-search{width:100%;max-width:280px;padding:.4rem .65rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.82rem;margin-bottom:.75rem;}
.users-search:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 2px rgba(79,70,229,.1);}
.user-row{display:flex;align-items:center;justify-content:space-between;padding:.35rem 0;border-bottom:1px solid var(--gray-100);font-size:.84rem;gap:.5rem;}
.user-row:last-child{border-bottom:none;}
.user-info{flex:1;min-width:0;}
.user-name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.user-email{color:var(--gray-400);font-size:.75rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.user-actions{display:flex;align-items:center;gap:.3rem;flex-shrink:0;}
.role-select{padding:.2rem .4rem;border:1px solid var(--gray-300);border-radius:4px;font-size:.75rem;cursor:pointer;background:white;color:var(--gray-700);}
.role-select:focus{outline:none;border-color:var(--primary);}
.group-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--gray-400);margin:.6rem 0 .3rem;display:flex;justify-content:space-between;}
.school-search-bar{display:flex;align-items:center;gap:.5rem;margin-bottom:.85rem;}
.school-search-input{flex:1;max-width:320px;padding:.5rem .75rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.875rem;}
.school-search-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(79,70,229,.1);}
.school-count-badge{font-size:.75rem;color:var(--gray-500);}
/* Modal */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center;}
.modal-overlay.active{display:flex;}
.modal{background:white;border-radius:12px;padding:1.75rem;max-width:490px;width:90%;max-height:90vh;overflow-y:auto;}
.modal h2{font-size:1.1rem;margin-bottom:1.2rem;}
.form-group{margin-bottom:1rem;}
.form-group label{display:block;font-size:.82rem;font-weight:600;color:var(--gray-600);margin-bottom:.35rem;}
.form-group input,.form-group select,.form-group textarea{width:100%;padding:.6rem .75rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.9rem;}
.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(79,70,229,.1);}
.form-hint{font-size:.75rem;color:var(--gray-500);margin-top:.3rem;}
.modal-buttons{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.25rem;}
.form-error{color:var(--danger);font-size:.82rem;margin-top:.5rem;display:none;}
.notification{position:fixed;bottom:1rem;right:1rem;padding:.85rem 1.25rem;border-radius:8px;color:white;font-weight:500;transform:translateY(100px);opacity:0;transition:all .3s;z-index:2000;}
.notification.show{transform:translateY(0);opacity:1;}
.notification.success{background:var(--success);}
.notification.error{background:var(--danger);}
.notification.warning{background:var(--warning);}
@media (prefers-color-scheme: dark) {
:root{--gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;--gray-300:#1a1a3e;--gray-500:#9ca3af;--gray-600:#d1d5db;--gray-700:#e5e7eb;--gray-800:#f3f4f6;}
body{background:#0f172a;color:#e2e8f0;}
.section,.stat-card,.modal{background:#1e293b !important;}
th{background:#1e293b !important;color:#94a3b8 !important;border-color:#334155 !important;}
td{border-color:#1e293b !important;color:#e2e8f0;}
tr:hover td{background:#263548 !important;}
.school-row.expanded td{background:#1a2744 !important;}
.users-panel{background:#162032 !important;}
.users-search{background:#0f172a !important;color:#e2e8f0 !important;border-color:#334155 !important;}
.school-search-input{background:#0f172a !important;color:#e2e8f0 !important;border-color:#334155 !important;}
.domain-chip{background:#1e3a5f !important;border-color:#2563eb !important;color:#93c5fd !important;}
.role-select,.form-group input,.form-group select{background:#0f172a !important;color:#e2e8f0 !important;border-color:#334155 !important;}
.btn-secondary{background:#334155 !important;color:#e2e8f0 !important;}
.btn-secondary:hover{background:#475569 !important;}
::-webkit-scrollbar{width:8px;} ::-webkit-scrollbar-track{background:#0f172a;} ::-webkit-scrollbar-thumb{background:#334155;border-radius:4px;}
}
</style>
</head>
<body>
<div class="container">
<div class="header {{ 'is-superadmin' if is_superadmin else 'is-sgict' }}">
<div class="header-left">
<h1>
{{ '⚙️' if is_superadmin else '🔧' }}
{{ 'Platform Beheer' if is_superadmin else 'Scholengroep ICT Beheer' }}
{% if is_superadmin %}<span class="header-badge">Superadmin</span>{% endif %}
</h1>
<div class="org">{{ org_name }}</div>
</div>
<div class="header-actions">
<a href="/doelen-beheer" class="btn btn-light">📂 Leerdoelen bestanden</a>
<a href="/auth/logout" class="btn btn-light">Uitloggen</a>
</div>
</div>
<!-- Stats — zichtbaar voor iedereen in deze pagina -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card"><div class="stat-value" id="st-schools">-</div><div class="stat-label">Scholen</div></div>
<div class="stat-card"><div class="stat-value" id="st-sg-ict">-</div><div class="stat-label">Scholengr. ICT</div></div>
<div class="stat-card"><div class="stat-value" id="st-school-ict">-</div><div class="stat-label">School ICT</div></div>
<div class="stat-card"><div class="stat-value" id="st-directors">-</div><div class="stat-label">Directeurs</div></div>
<div class="stat-card"><div class="stat-value" id="st-teachers">-</div><div class="stat-label">Leerkrachten</div></div>
</div>
<!-- Scholengroep ICT accounts — iedereen ziet lijst, enkel superadmin kan beheren -->
<div class="section">
<div class="section-header">
<h2>👥 Scholengroep ICT medewerkers</h2>
{% if is_superadmin %}
<button class="btn btn-primary btn-sm" id="btnAddSgIct">+ Toevoegen</button>
{% endif %}
</div>
<p class="section-hint">
Scholengroep ICT medewerkers kunnen alle scholen en gebruikers beheren,
maar kunnen geen scholen aanmaken of andere scholengroep ICT accounts toevoegen.
</p>
<table>
<thead><tr><th>Naam</th><th>E-mail</th><th>Laatste login</th><th></th></tr></thead>
<tbody id="sgIctTable"><tr class="empty-row"><td colspan="4">Laden...</td></tr></tbody>
</table>
</div>
<!-- Schooljaren — globaal -->
<div class="section">
<div class="section-header">
<h2>📅 Schooljaren</h2>
<button id="btnAddJaar" class="btn btn-primary btn-sm">+ Nieuw schooljaar</button>
</div>
<p class="section-hint">
Het actieve schooljaar geldt voor alle scholen tegelijk.
Leerkrachten werken automatisch in het actieve jaar.
Directeurs kunnen alle jaren raadplegen via hun dashboard.
</p>
<table>
<thead><tr><th>Schooljaar</th><th>Status</th><th></th></tr></thead>
<tbody id="jarenTable"><tr class="empty-row"><td colspan="3">Laden...</td></tr></tbody>
</table>
</div>
<!-- Scholen beheren — iedereen -->
<div class="section">
<div class="section-header">
<h2>🏫 Scholen</h2>
<button id="btnAddSchool" class="btn btn-primary btn-sm">+ School
toevoegen</button>
</div>
<table>
<thead>
<tr>
<th>Naam</th>
<th>E-maildomeinen</th>
<th>Gebruikers</th>
<th></th>
</tr>
</thead>
<tbody id="schoolsTable"><tr class="empty-row"><td colspan="4">Laden...</td></tr></tbody>
</table>
</div>
<!-- Gebruikers per school — lazy tabel -->
<div class="section">
<div class="section-header">
<h2>👥 Gebruikers per school</h2>
<button id="btnAddUser" class="btn btn-primary btn-sm">+ Gebruiker toevoegen</button>
</div>
<div class="school-search-bar">
<input type="text" id="schoolSearchInput" class="school-search-input"
placeholder="Zoek op schoolnaam of gebruikersnaam...">
<span class="school-count-badge" id="schoolCountBadge"></span>
</div>
<div style="overflow-x:auto;">
<table class="schools-user-table">
<thead>
<tr>
<th style="width:32px;"></th>
<th>School</th>
<th>Domeinen</th>
<th>SSO</th>
<th style="text-align:center;">Gebruikers</th>
<th style="text-align:right;">Acties</th>
</tr>
</thead>
<tbody id="schoolsUserTbody"></tbody>
</table>
</div>
</div>
<!-- Auditlog -->
<div class="section">
<div class="section-header">
<h2>📋 Auditlog</h2>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
<select id="auditCategory"
style="padding:.35rem .5rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.85rem;">
<option value="">Alle categorieën</option>
<option value="auth">Aanmeldingen</option>
<option value="user">Gebruikers</option>
<option value="school">Scholen</option>
<option value="class">Klassen</option>
<option value="assessment">Beoordelingen</option>
<option value="system">Systeem</option>
<option value="doelen">Leerdoelen</option>
</select>
<select id="auditSchoolFilter"
style="padding:.35rem .5rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.85rem;">
<option value="">Alle scholen</option>
</select>
<input id="auditSearch" type="text" placeholder="Zoeken..."
style="padding:.35rem .5rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.85rem;width:150px;">
</div>
</div>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>Tijdstip</th>
<th>Gebruiker</th>
<th>School</th>
<th>Actie</th>
<th>Detail</th>
<th>IP</th>
</tr>
</thead>
<tbody id="auditTable">
<tr><td colspan="6" style="text-align:center;color:var(--gray-400);">Laden...</td></tr>
</tbody>
</table>
</div>
<div id="auditPager" style="display:flex;gap:.5rem;justify-content:center;padding:1rem;flex-wrap:wrap;"></div>
</div>
</div>
<!-- Modals -->
<div class="modal-overlay" id="modal-addSchool">
<div class="modal">
<h2>🏫 School toevoegen</h2>
<div class="form-group"><label>Naam</label><input type="text" id="schoolName" placeholder="Basisschool De Krekel"></div>
<div class="form-group"><label>Slug (optioneel)</label><input type="text" id="schoolSlug" placeholder="auto-gegenereerd"><div class="form-hint">Kleine letters en streepjes.</div></div>
<div class="form-group"><label>E-maildomeinen</label><input type="text" id="schoolDomains" placeholder="dekrekel.be, sintjan.gent.be"><div class="form-hint">Komma-gescheiden. Gebruikers met dit domein worden automatisch gekoppeld.</div></div>
<div class="form-error" id="school-error"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelSchool">Annuleren</button>
<button class="btn btn-primary" id="btnSaveSchool">Toevoegen</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modal-editSchool">
<div class="modal" style="max-width:520px;">
<h2>School bewerken</h2>
<input type="hidden" id="editSchoolId">
<div class="form-group"><label>Naam</label><input type="text" id="editSchoolName"></div>
<div class="form-group"><label>E-maildomeinen</label><input type="text" id="editSchoolDomains"><div class="form-hint">Komma-gescheiden.</div></div>
<hr style="border:none;border-top:1px solid var(--gray-200);margin:1rem 0;">
<div style="font-size:.82rem;font-weight:700;color:var(--gray-600);margin-bottom:.65rem;">
<svg width="13" height="13" viewBox="0 0 48 48" style="vertical-align:middle;margin-right:.3rem;">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.18 1.48-4.97 2.31-8.16 2.31-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
</svg>
Google Workspace SSO
</div>
<div id="editSsoStatus" style="margin-bottom:.75rem;font-size:.8rem;"></div>
<div class="form-group">
<label>Client ID</label>
<input type="text" id="editGoogleClientId" placeholder="...apps.googleusercontent.com" style="font-family:monospace;font-size:.82rem;">
<div class="form-hint">Leeg laten = huidige waarde behouden. Vul in om te wijzigen.</div>
</div>
<div class="form-group">
<label>Client Secret</label>
<input type="password" id="editGoogleClientSecret" placeholder="GOCSPX-..." style="font-family:monospace;font-size:.82rem;">
</div>
<div style="margin-bottom:.75rem;">
<label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;cursor:pointer;color:var(--danger);">
<input type="checkbox" id="editGoogleClear"> Google SSO verwijderen voor deze school
</label>
</div>
<div class="form-error" id="edit-school-error"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelEditSch">Annuleren</button>
<button class="btn btn-primary" id="btnSaveEditSch">Opslaan</button>
</div>
</div>
</div>
{% if is_superadmin %}
<div class="modal-overlay" id="modal-addSgIct">
<div class="modal">
<h2>Scholengroep ICT toevoegen</h2>
<p style="color:var(--gray-500);font-size:.85rem;margin-bottom:1rem;">Logt in via Microsoft. Kan alle scholen en gebruikers beheren.</p>
<div class="form-group"><label>Voornaam</label><input type="text" id="sgFirstName"></div>
<div class="form-group"><label>Achternaam</label><input type="text" id="sgLastName"></div>
<div class="form-group"><label>E-mailadres</label><input type="email" id="sgEmail"></div>
<div class="form-error" id="sg-error"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelSgIct">Annuleren</button>
<button class="btn btn-primary" id="btnSaveSgIct">Toevoegen</button>
</div>
</div>
</div>
{% endif %}
<div class="modal-overlay" id="modal-addJaar">
<div class="modal">
<h2>📅 Nieuw schooljaar aanmaken</h2>
<p style="color:var(--gray-500);font-size:.85rem;margin-bottom:1rem;">
Geldt voor alle scholen tegelijk. Het nieuwe jaar wordt automatisch
actief — het vorige jaar blijft bewaard voor historiek.
</p>
<div class="form-group">
<label>Schooljaar label</label>
<input type="text" id="jaarLabel" placeholder="bv. 2026-2027">
<div class="form-hint">Formaat: JJJJ-JJJJ</div>
</div>
<div class="form-error" id="jaar-error"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelJaar">Annuleren</button>
<button class="btn btn-primary" id="btnSaveJaar">Aanmaken</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modal-addUser">
<div class="modal">
<h2>Gebruiker toevoegen</h2>
<p style="color:var(--gray-500);font-size:.85rem;margin-bottom:1rem;">Bestaand e-mailadres? Account wordt heractiveerd en bijgewerkt.</p>
<div class="form-group"><label>School</label><select id="addUserSchool"></select></div>
<div class="form-group"><label>Voornaam</label><input type="text" id="addUserFirst"></div>
<div class="form-group"><label>Achternaam</label><input type="text" id="addUserLast"></div>
<div class="form-group"><label>E-mailadres (Microsoft account)</label><input type="email" id="addUserEmail"></div>
<div class="form-group">
<label>Rol</label>
<select id="addUserRole">
<option value="teacher">Leerkracht</option>
<option value="director">Directeur</option>
<option value="school_ict">School ICT</option>
</select>
</div>
<div class="form-error" id="addUser-error"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelUser">Annuleren</button>
<button class="btn btn-primary" id="btnSaveUser">Toevoegen</button>
</div>
</div>
</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);
}
const IS_SUPERADMIN = {{ is_superadmin | tojson }};
let schools = [];
const SCHOOL_ROLLEN = [
{ value: 'teacher', label: 'Leerkracht' },
{ value: 'director', label: 'Directeur' },
{ value: 'school_ict', label: 'School ICT' },
];
document.addEventListener('DOMContentLoaded', async () => {
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('btnCancelEditSch') && bind('btnCancelEditSch', 'click', closeModal);
document.getElementById('btnSaveEditSch') && bind('btnSaveEditSch', 'click', saveSchool);
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);
// Zoekbalk scholen/gebruikers
const schoolSearchEl = document.getElementById('schoolSearchInput');
if (schoolSearchEl) {
schoolSearchEl.addEventListener('input', () => {
schoolFilter = schoolSearchEl.value.trim();
renderSchoolsUserTable();
});
}
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid(), loadSgIct()];
await Promise.all(tasks);
await loadJaren();
await loadAuditLog();
});
// ── Stats ─────────────────────────────────────────────────────────────────────
async function loadStats() {
const res = await fetch('/admin/stats');
if (!res.ok) return;
const d = await res.json();
document.getElementById('st-schools').textContent = d.schools;
document.getElementById('st-sg-ict').textContent = d.scholengroep_ict;
document.getElementById('st-school-ict').textContent = d.school_ict;
document.getElementById('st-directors').textContent = d.directors;
document.getElementById('st-teachers').textContent = d.teachers;
}
// ── Scholengroep ICT (superadmin) ─────────────────────────────────────────────
async function loadSgIct() {
const res = await fetch('/admin/scholengroep-ict');
const data = await res.json();
const tbody = document.getElementById('sgIctTable');
if (!data.users?.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="4">Geen scholengroep ICT medewerkers</td></tr>';
return;
}
tbody.innerHTML = data.users.map(u => `
<tr>
<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>${IS_SUPERADMIN ? `<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('');
}
async function addSgIct() {
const err = document.getElementById('sg-error');
err.style.display = 'none';
const res = await fetch('/admin/scholengroep-ict', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ email: document.getElementById('sgEmail').value, first_name: document.getElementById('sgFirstName').value, last_name: document.getElementById('sgLastName').value })
});
const data = await res.json();
if (!res.ok) { err.textContent = data.error; err.style.display = 'block'; return; }
closeModal(); notify('Scholengroep ICT toegevoegd', 'success');
await Promise.all([loadSgIct(), loadStats()]);
}
async function removeSgIct(userId, naam) {
if (!confirm(`${naam} verwijderen als scholengroep ICT?`)) return;
const res = await fetch(`/admin/scholengroep-ict/${userId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`${naam} verwijderd`, 'success');
await Promise.all([loadSgIct(), loadStats()]);
}
// ── Scholen tabel ─────────────────────────────────────────────────────────────
async function loadSchoolsTable() {
const res = await fetch('/admin/schools');
const data = await res.json();
schools = data.schools || [];
const tbody = document.getElementById('schoolsTable');
if (!schools.length) {
tbody.innerHTML = `<tr class="empty-row"><td colspan="4">Nog geen scholen aangemaakt</td></tr>`;
return;
}
tbody.innerHTML = schools.map(s => `
<tr>
<td><strong>${s.name}</strong></td>
<td>${(s.email_domains||[]).map(d=>`<span class="domain-chip">${d}</span>`).join('') || '<em style="color:var(--gray-400)">geen</em>'}
${s.google_sso_configured
? '<span style="margin-left:.4rem;font-size:.7rem;background:#d1fae5;color:#065f46;padding:.15rem .4rem;border-radius:4px;font-weight:600;">G SSO ✓</span>'
: ''}</td>
<td style="color:var(--gray-500);">${s.user_count}</td>
<td style="display:flex;gap:.35rem;">
<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('');
// Vul ook de school select in het "gebruiker toevoegen" modal
document.getElementById('addUserSchool').innerHTML =
schools.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
}
async function addSchool() {
const err = document.getElementById('school-error');
err.style.display = 'none';
const res = await fetch('/admin/schools', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ name: document.getElementById('schoolName').value, slug: document.getElementById('schoolSlug').value, email_domains: document.getElementById('schoolDomains').value.split(',').map(d=>d.trim()).filter(Boolean) })
});
const data = await res.json();
if (!res.ok) { err.textContent = data.error; err.style.display = 'block'; return; }
closeModal(); notify('School aangemaakt', 'success');
delete loadedUsers[data.school?.id]; // forceer herlaad als al gecached
await Promise.all([loadSchoolsTable(), loadSchoolsGrid(), loadStats()]);
}
function editSchool(id, name, domainsStr) {
document.getElementById('editSchoolId').value = id;
document.getElementById('editSchoolName').value = name;
document.getElementById('editSchoolDomains').value = domainsStr;
// Reset Google velden
document.getElementById('editGoogleClientId').value = '';
document.getElementById('editGoogleClientSecret').value = '';
document.getElementById('editGoogleClear').checked = false;
// Toon huidige SSO status
const school = schools.find(s => s.id == id);
const ssoStatusEl = document.getElementById('editSsoStatus');
if (ssoStatusEl && school) {
ssoStatusEl.innerHTML = school.google_sso_configured
? `<span style="color:#065f46;background:#d1fae5;padding:.25rem .5rem;border-radius:4px;">✅ Google SSO actief — Client ID: ${school.google_client_id}</span>`
: `<span style="color:#92400e;background:#fef3c7;padding:.25rem .5rem;border-radius:4px;">⚠️ Google SSO nog niet ingesteld</span>`;
}
openModal('editSchool');
}
async function saveSchool() {
const err = document.getElementById('edit-school-error');
err.style.display = 'none';
const id = document.getElementById('editSchoolId').value;
// 1. Sla naam en domeinen op (bestaand endpoint — scholengroep_ict)
const res = await fetch(`/admin/schools/${id}`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
name: document.getElementById('editSchoolName').value,
email_domains: document.getElementById('editSchoolDomains').value.split(',').map(d=>d.trim()).filter(Boolean)
})
});
const data = await res.json();
if (!res.ok) { err.textContent = data.error; err.style.display = 'block'; return; }
// 2. Verwerk Google SSO velden (apart endpoint — ondersteunt ook school_ict)
const clearSso = document.getElementById('editGoogleClear').checked;
const clientId = document.getElementById('editGoogleClientId').value.trim();
const clientSec = document.getElementById('editGoogleClientSecret').value.trim();
if (clearSso) {
const ssoRes = await fetch(`/admin/schools/${id}/google-sso`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ clear: true })
});
if (!ssoRes.ok) {
const ssoData = await ssoRes.json();
err.textContent = 'SSO: ' + (ssoData.error || 'Verwijderen mislukt');
err.style.display = 'block'; return;
}
} else if (clientId || clientSec) {
const ssoRes = await fetch(`/admin/schools/${id}/google-sso`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ google_client_id: clientId, google_client_secret: clientSec })
});
if (!ssoRes.ok) {
const ssoData = await ssoRes.json();
err.textContent = 'SSO: ' + (ssoData.error || 'Opslaan mislukt');
err.style.display = 'block'; return;
}
}
closeModal();
notify('School opgeslagen', 'success');
// Gebruikerscache wissen zodat heropenen verse data toont
delete loadedUsers[parseInt(id)];
await Promise.all([loadSchoolsTable(), loadSchoolsGrid()]);
}
async function deleteSchool(id, name) {
if (!confirm(`School "${name}" permanent verwijderen? Dit verwijdert alle gekoppelde data!`)) return;
const res = await fetch(`/admin/schools/${id}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify('School verwijderd', 'success');
expandedSchools.delete(parseInt(id));
delete loadedUsers[parseInt(id)];
await Promise.all([loadSchoolsTable(), loadSchoolsGrid(), loadStats()]);
}
// ── Gebruikers-per-school: lazy tabel ────────────────────────────────────────
// State
const expandedSchools = new Set(); // welke school-rijen zijn open
const loadedUsers = {}; // cache: school_id -> users array
const loadingSchools = new Set(); // bezig met laden
let schoolFilter = ''; // huidig zoekterm
// Laad de scholenlijst (zonder gebruikers) en render de tabel
async function loadSchoolsGrid() {
const res = await fetch('/admin/schools');
const data = await res.json();
schools = data.schools || [];
renderSchoolsUserTable();
updateSchoolCountBadge();
// Vul ook de school select in het "gebruiker toevoegen" modal
const sel = document.getElementById('addUserSchool');
if (sel) sel.innerHTML = schools.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
}
// Teken (of herteken) de volledige tabel op basis van huidige filter + expanded state
function renderSchoolsUserTable() {
const tbody = document.getElementById('schoolsUserTbody');
const term = schoolFilter.toLowerCase();
// Filter: schoolnaam OF (als school al geladen) gebruikersnaam
const filtered = schools.filter(s => {
if (s.name.toLowerCase().includes(term)) return true;
const users = loadedUsers[s.id];
if (users && term) return users.some(u => u.full_name.toLowerCase().includes(term));
return !term;
});
updateSchoolCountBadge(filtered.length);
if (!filtered.length) {
tbody.innerHTML = `<tr class="empty-row"><td colspan="6">Geen scholen gevonden voor "${schoolFilter}"</td></tr>`;
return;
}
const rows = filtered.map(s => buildSchoolRows(s, term)).join('');
tbody.innerHTML = rows;
}
function buildSchoolRows(school, term) {
const isOpen = expandedSchools.has(school.id);
const isLoading = loadingSchools.has(school.id);
const ssoIcon = school.google_sso_configured
? '<span style="font-size:.7rem;background:#d1fae5;color:#065f46;padding:.15rem .4rem;border-radius:4px;font-weight:600;">G ✓</span>'
: '<span style="font-size:.7rem;color:var(--gray-400);">—</span>';
const domainHtml = (school.email_domains||[]).map(d=>`<span class="domain-chip">${d}</span>`).join('')
|| '<em style="color:var(--gray-400);font-size:.75rem;">geen</em>';
// Hoofdrij
const mainRow = `
<tr class="school-row${isOpen ? ' expanded' : ''}" data-action="toggleSchool" data-id="${school.id}">
<td><i class="expand-icon">▶</i></td>
<td><strong>${school.name}</strong></td>
<td>${domainHtml}</td>
<td>${ssoIcon}</td>
<td style="text-align:center;color:var(--gray-500);">${school.user_count}</td>
<td style="text-align:right;white-space:nowrap;">
<button class="btn btn-secondary btn-sm" data-action="editSchool"
data-id="${school.id}"
data-name="${school.name.replace(/'/g,'&#39;')}"
data-domains="${(school.email_domains||[]).join(', ')}">Bewerken</button>
<button class="btn btn-danger btn-sm" data-action="deleteSchool"
data-id="${school.id}"
data-name="${school.name.replace(/'/g,'&#39;')}">Verwijderen</button>
</td>
</tr>`;
// Uitklapbare gebruikerspaneel rij
const panelRow = `
<tr class="users-panel-row" id="panel-row-${school.id}">
<td colspan="6">
<div class="users-panel${isOpen ? ' open' : ''}" id="users-panel-${school.id}">
${isLoading
? '<div style="padding:.5rem;color:var(--gray-500);font-size:.85rem;">⏳ Laden...</div>'
: renderUsersPanel(school.id, term)}
</div>
</td>
</tr>`;
return mainRow + panelRow;
}
function renderUsersPanel(schoolId, term) {
const users = loadedUsers[schoolId];
if (!users) return ''; // nog niet geladen
// Filter op gebruikersnaam als er een zoekterm is
const filtered = term
? users.filter(u => u.full_name.toLowerCase().includes(term) || u.email.toLowerCase().includes(term))
: users;
const byRole = {
school_ict: filtered.filter(u => u.role === 'school_ict'),
director: filtered.filter(u => u.role === 'director'),
teacher: filtered.filter(u => u.role === 'teacher'),
};
const total = filtered.length;
if (!total) return `<div style="color:var(--gray-500);font-size:.85rem;padding:.25rem 0;">Geen gebruikers gevonden${term ? ` voor "${term}"` : ''}.</div>`;
return [
renderUserGroup(schoolId, 'School ICT', byRole.school_ict),
renderUserGroup(schoolId, 'Directeurs', byRole.director),
renderUserGroup(schoolId, 'Leerkrachten', byRole.teacher),
].join('');
}
function renderUserGroup(schoolId, label, users) {
if (!users.length) return '';
return `
<div class="group-label">
<span>${label}</span>
<span style="font-weight:400;text-transform:none;letter-spacing:0;">${users.length}</span>
</div>
${users.map(u => `
<div class="user-row">
<div class="user-info">
<div class="user-name">${u.full_name}</div>
<div class="user-email">${u.email}</div>
<div class="user-email" style="color:var(--gray-400);font-size:.7rem;">
${u.last_login
? '↩ ' + new Date(u.last_login).toLocaleString('nl-BE',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})
: 'Nog niet ingelogd'}
</div>
</div>
<div class="user-actions">
<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" data-action="removeUser"
data-school-id="${schoolId}" data-user-id="${u.id}"
data-name="${u.full_name.replace(/'/g,'&#39;')}">×</button>
</div>
</div>`).join('')}`;
}
function updateSchoolCountBadge(visibleCount) {
const el = document.getElementById('schoolCountBadge');
if (!el) return;
const total = schools.length;
if (visibleCount === undefined || visibleCount === total) {
el.textContent = `${total} school${total !== 1 ? 'en' : ''}`;
} else {
el.textContent = `${visibleCount} van ${total} scholen`;
}
}
// Toggle een school open/dicht; laad gebruikers als dat nog niet is gebeurd
async function toggleSchool(schoolId) {
if (expandedSchools.has(schoolId)) {
expandedSchools.delete(schoolId);
renderSchoolsUserTable();
return;
}
expandedSchools.add(schoolId);
if (!loadedUsers[schoolId]) {
// Toon laad-indicator via hertekenen (loading state)
loadingSchools.add(schoolId);
renderSchoolsUserTable();
const res = await fetch(`/admin/schools/${schoolId}/users`);
const data = await res.json();
loadedUsers[schoolId] = data.users || [];
loadingSchools.delete(schoolId);
}
renderSchoolsUserTable();
}
// Herlaad de gebruikers van één school (na wijziging) en herteken
async function refreshSchool(schoolId) {
const res = await fetch(`/admin/schools/${schoolId}/users`);
const data = await res.json();
loadedUsers[schoolId] = data.users || [];
// Bijwerken user_count in schools array
const school = schools.find(s => s.id === schoolId);
if (school) school.user_count = (data.users || []).length;
renderSchoolsUserTable();
}
async function changeRole(schoolId, userId, newRole, naam) {
const res = await fetch(`/admin/schools/${schoolId}/users/${userId}/role`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ role: newRole })
});
if (!res.ok) { notify((await res.json()).error || 'Wijzigen mislukt', 'error'); await refreshSchool(schoolId); return; }
notify(`${naam} is nu ${SCHOOL_ROLLEN.find(r=>r.value===newRole)?.label}`, 'success');
await refreshSchool(schoolId);
}
async function addUser() {
const err = document.getElementById('addUser-error');
err.style.display = 'none';
const schoolId = parseInt(document.getElementById('addUserSchool').value);
const res = await fetch(`/admin/schools/${schoolId}/users`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
email: document.getElementById('addUserEmail').value,
first_name: document.getElementById('addUserFirst').value,
last_name: document.getElementById('addUserLast').value,
role: document.getElementById('addUserRole').value,
})
});
const data = await res.json();
if (!res.ok) { err.textContent = data.error; err.style.display = 'block'; return; }
closeModal();
notify(data.linked ? 'Account heractiveerd en bijgewerkt' : 'Gebruiker toegevoegd', 'success');
// Zorg dat de school open staat zodat de nieuwe gebruiker zichtbaar is
expandedSchools.add(schoolId);
delete loadedUsers[schoolId]; // forceer herlaad
await Promise.all([refreshSchool(schoolId), loadStats()]);
}
async function removeUser(schoolId, userId, naam) {
if (!confirm(`${naam} verwijderen?`)) return;
const res = await fetch(`/admin/schools/${parseInt(schoolId)}/users/${userId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`${naam} verwijderd`, 'success');
delete loadedUsers[parseInt(schoolId)]; // forceer herlaad
await Promise.all([refreshSchool(parseInt(schoolId)), loadStats()]);
}
// ── Schooljaren ───────────────────────────────────────────────────────────────
async function loadJaren() {
const tbody = document.getElementById('jarenTable');
const res = await fetch('/admin/years');
if (!res.ok) { tbody.innerHTML = '<tr class="empty-row"><td colspan="3">Fout bij laden</td></tr>'; return; }
const data = await res.json();
const years = data.years || [];
if (!years.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="3">Nog geen schooljaren aangemaakt</td></tr>';
} else {
tbody.innerHTML = years.map(y => `
<tr>
<td><strong>${y.label}</strong></td>
<td>${y.is_active
? '<span style="background:#d1fae5;color:#065f46;padding:.2rem .5rem;border-radius:4px;font-size:.75rem;font-weight:600;">✓ Actief</span>'
: '<span style="color:var(--gray-400);font-size:.82rem;">Inactief</span>'}
</td>
<td>${!y.is_active
? `<button class="btn btn-secondary btn-sm" data-action="activeerJaar" data-id="${y.id}" data-label="${y.label}">Activeren</button>`
: ''}
</td>
</tr>`).join('');
}
// Stel default jaar label in op volgend schooljaar
const now = new Date();
const startJr = now.getMonth() >= 8 ? now.getFullYear() + 1 : now.getFullYear();
const suggest = `${startJr}-${startJr + 1}`;
const inp = document.getElementById('jaarLabel');
if (inp && !inp.value) inp.value = suggest;
}
async function addJaar() {
const err = document.getElementById('jaar-error');
err.style.display = 'none';
const label = document.getElementById('jaarLabel').value.trim();
const res = await fetch('/admin/years', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ label, set_active: true })
});
const data = await res.json();
if (!res.ok) { err.textContent = data.error; err.style.display = 'block'; return; }
closeModal();
notify(`Schooljaar ${label} aangemaakt en geactiveerd`, 'success');
await loadJaren();
}
async function activeerJaar(yearId, label) {
if (!confirm(`Schooljaar ${label} activeren? Leerkrachten werken daarna in dit jaar.`)) return;
const res = await fetch(`/admin/years/${yearId}/activate`, { method: 'PUT' });
if (!res.ok) { notify('Activeren mislukt', 'error'); return; }
notify(`${label} is nu het actieve schooljaar`, 'success');
await loadJaren();
}
// ── Auditlog ──────────────────────────────────────────────────────────────────
async function loadAuditLog(page = 1) {
const category = document.getElementById('auditCategory')?.value || '';
const search = document.getElementById('auditSearch')?.value || '';
const schoolF = document.getElementById('auditSchoolFilter')?.value || '';
const params = new URLSearchParams({ page, per_page: 25 });
if (category) params.set('category', category);
if (search) params.set('search', search);
const res = await fetch(`/api/audit-log?${params}`);
if (!res.ok) return;
const data = await res.json();
// Vul school filter als die leeg is
const schoolSel = document.getElementById('auditSchoolFilter');
if (schoolSel && schoolSel.options.length <= 1 && schools.length) {
schools.forEach(s => {
const o = document.createElement('option');
o.value = s.id; o.textContent = s.name;
schoolSel.appendChild(o);
});
}
// Filter client-side op school (API filtert niet op school_id voor superadmin)
let entries = data.entries;
if (schoolF) entries = entries.filter(e => e.school_id == schoolF);
const tbody = document.getElementById('auditTable');
if (!entries.length) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--gray-400);">Geen entries gevonden</td></tr>';
} else {
tbody.innerHTML = entries.map(e => {
const ts = new Date(e.timestamp).toLocaleString('nl-BE', {day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'});
const detail = e.detail ? (() => { try { return Object.entries(JSON.parse(e.detail)).map(([k,v]) => `<span style="font-size:.73rem;background:var(--gray-100);padding:.1rem .3rem;border-radius:3px;display:inline-block;">${k}: ${v}</span>`).join(' '); } catch { return e.detail; } })() : '';
return `<tr>
<td style="white-space:nowrap;font-size:.8rem;">${ts}</td>
<td style="font-size:.82rem;">${e.user_name || '—'}</td>
<td style="font-size:.8rem;color:var(--gray-500);">${e.school_name || '—'}</td>
<td><code style="font-size:.78rem;background:var(--gray-100);padding:.15rem .35rem;border-radius:3px;">${e.action}</code></td>
<td style="font-size:.78rem;">${detail}</td>
<td style="font-size:.75rem;color:var(--gray-400);">${e.ip_address || ''}</td>
</tr>`;
}).join('');
}
const pager = document.getElementById('auditPager');
if (data.pages <= 1) { pager.innerHTML = ''; return; }
pager.innerHTML = Array.from({length: data.pages}, (_, i) => `
<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}
</button>`).join('');
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function openModal(id) {
document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));
document.getElementById(`modal-${id}`)?.classList.add('active');
}
function closeModal() { document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active')); }
document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', e => { if (e.target===o) closeModal(); }));
function notify(msg, type='success') {
const el = document.getElementById('notification');
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) {
// toggleSchool: klik op de rij zelf, maar NIET op knoppen erin
const schoolRow = e.target.closest('.school-row[data-action="toggleSchool"]');
if (schoolRow && !e.target.closest('button') && !e.target.closest('select')) {
toggleSchool(parseInt(schoolRow.dataset.id));
return;
}
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); }
});
</script>
</body>
</html>