Files
leerdoelen_tracker/backend/templates/scholengroep_ict.html
Sam 28c05edb0b
All checks were successful
Build & Push / Build & Push image (push) Successful in 38s
Update access control for Scholengroep ICT management and adjust visibility of management buttons
2026-03-03 10:23:33 +01:00

742 lines
40 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;}
/* Scholen grid */
.schools-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:1rem;}
.school-card{border:1px solid var(--gray-200);border-radius:10px;overflow:hidden;}
.school-card-header{padding:.9rem 1.1rem;background:var(--gray-50);border-bottom:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;}
.school-card-header h3{font-size:.95rem;}
.school-card-body{padding:.75rem 1.1rem;}
.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;}
/* 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-card{border-color:#334155 !important;}
.school-card-header{background:#162032 !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 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>
<!-- Scholen & gebruikers detail -->
<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="schools-grid" id="schoolsGrid">Laden...</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">
<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>
<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 = {{ 'true' if is_superadmin else 'false' }};
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('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(), 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>'}</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');
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;
openModal('editSchool');
}
async function saveSchool() {
const err = document.getElementById('edit-school-error');
err.style.display = 'none';
const id = document.getElementById('editSchoolId').value;
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; }
closeModal(); notify('School opgeslagen', 'success');
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');
await Promise.all([loadSchoolsTable(), loadSchoolsGrid(), loadStats()]);
}
// ── Gebruikers grid ───────────────────────────────────────────────────────────
async function loadSchoolsGrid() {
const res = await fetch('/admin/schools');
const data = await res.json();
schools = data.schools || [];
const grid = document.getElementById('schoolsGrid');
if (!schools.length) {
grid.innerHTML = '<p style="color:var(--gray-500);font-style:italic;padding:.5rem 0;">Nog geen scholen aangemaakt.</p>';
return;
}
grid.innerHTML = '';
await Promise.all(schools.map(s => renderSchoolCard(s, grid)));
}
async function renderSchoolCard(school, container) {
const res = await fetch(`/admin/schools/${school.id}/users`);
const data = await res.json();
const users = data.users || [];
const byRole = {
school_ict: users.filter(u => u.role === 'school_ict'),
director: users.filter(u => u.role === 'director'),
teacher: users.filter(u => u.role === 'teacher'),
};
const card = document.createElement('div');
card.className = 'school-card';
card.id = `school-card-${school.id}`;
card.innerHTML = `
<div class="school-card-header">
<div>
<h3>${school.name}</h3>
<div style="margin-top:.35rem;">${(school.email_domains||[]).map(d=>`<span class="domain-chip">${d}</span>`).join('') || '<span style="color:var(--gray-400);font-size:.75rem;">geen domeinen</span>'}</div>
</div>
<span style="color:var(--gray-500);font-size:.8rem;white-space:nowrap;">${users.length} gebruikers</span>
</div>
<div class="school-card-body">
${renderUserGroup(school.id,'School ICT',byRole.school_ict)}
${renderUserGroup(school.id,'Directeurs',byRole.director)}
${renderUserGroup(school.id,'Leerkrachten',byRole.teacher,5)}
</div>`;
container.appendChild(card);
}
function renderUserGroup(schoolId, label, users, maxShow=99) {
if (!users.length) return '';
const shown = users.slice(0, maxShow);
const hidden = users.length - shown.length;
return `
<div class="group-label"><span>${label}</span><span style="font-weight:400;text-transform:none;letter-spacing:0;">${users.length}</span></div>
${shown.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('')}
${hidden > 0 ? `<div style="color:var(--gray-500);font-size:.8rem;padding:.3rem 0 0;">+ ${hidden} meer...</div>` : ''}`;
}
async function changeRole(schoolId, userId, newRole, naam, selectEl) {
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 refreshCard(schoolId); return; }
notify(`${naam} is nu ${SCHOOL_ROLLEN.find(r=>r.value===newRole)?.label}`, 'success');
await refreshCard(schoolId);
}
async function addUser() {
const err = document.getElementById('addUser-error');
err.style.display = 'none';
const schoolId = 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');
await Promise.all([refreshCard(parseInt(schoolId)), loadStats()]);
}
async function removeUser(schoolId, userId, naam) {
if (!confirm(`${naam} verwijderen?`)) return;
const res = await fetch(`/admin/schools/${schoolId}/users/${userId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`${naam} verwijderd`, 'success');
await Promise.all([refreshCard(schoolId), loadStats()]);
}
async function refreshCard(schoolId) {
const card = document.getElementById(`school-card-${schoolId}`);
const school = schools.find(s => s.id === schoolId);
if (card && school) { card.remove(); await renderSchoolCard(school, document.getElementById('schoolsGrid')); }
}
// ── 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) {
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>