Files
leerdoelen_tracker/backend/templates/school_ict.html
Sam 771a742c9a
All checks were successful
Build & Push / Build & Push image (push) Successful in 39s
refactor: replace inline event handlers with bind function for improved readability and maintainability
2026-03-02 22:35:37 +01:00

614 lines
29 KiB
HTML
Raw Permalink 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>School ICT - Leerdoelen Tracker</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:900px;margin:0 auto;padding:1rem;}
.header{background:linear-gradient(135deg,#1e40af,#1d4ed8);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 h1{font-size:1.3rem;}
.header .school-name{opacity:.85;font-size:.9rem;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);}
.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;}
table{width:100%;border-collapse:collapse;font-size:.875rem;}
th{padding:.65rem .75rem;text-align:left;font-weight:600;color:var(--gray-600);border-bottom:2px solid var(--gray-200);background:var(--gray-50);}
td{padding:.6rem .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:2.5rem;font-style:italic;}
.role-select{padding:.25rem .45rem;border:1px solid var(--gray-300);border-radius:4px;font-size:.8rem;cursor:pointer;background:white;}
.role-select:focus{outline:none;border-color:var(--primary);}
.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:440px;width:90%;}
.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{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);}
.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);}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-bottom:1.5rem;}
.stat{background:var(--gray-50);border-radius:8px;padding:.85rem;text-align:center;}
.stat-value{font-size:1.75rem;font-weight:700;color:var(--primary);}
.stat-label{font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--gray-500);margin-top:.2rem;}
@media (prefers-color-scheme: dark) {
:root {
--gray-50: #1a1a2e;
--gray-100: #16213e;
--gray-200: #0f3460;
--gray-300: #1a1a3e;
--gray-400: #6b7280;
--gray-500: #9ca3af;
--gray-600: #d1d5db;
--gray-700: #e5e7eb;
--gray-800: #f3f4f6;
--gray-900: #f9fafb;
}
body { background: #0f172a; color: #e2e8f0; }
/* Kaarten en secties */
.card, .section, .stat-card, .school-card,
.table-container, .filters-container, .legend-container,
.stats-bar .stat-card, .stats-overview, .vak-stats,
.import-section, .detail-section, .filters-bar,
.header:not([class*="gradient"]) {
background: #1e293b !important;
border-color: #334155 !important;
}
/* Header kaart in leerkracht.html */
.header { background: #1e293b !important; }
/* Tabellen */
thead, th { background: #1e293b !important; color: #94a3b8 !important; border-color: #334155 !important; }
td { border-color: #1e293b !important; color: #e2e8f0; }
tr:hover td, tr:hover { background: #263548 !important; }
tr.status-groen { background: #064e3b !important; }
tr.status-groen:hover { background: #065f46 !important; }
tr.status-oranje { background: #451a03 !important; }
tr.status-oranje:hover { background: #78350f !important; }
tr.status-roze { background: #500724 !important; }
tr.status-roze:hover { background: #701a35 !important; }
/* Inputs en selects */
input, select, textarea {
background: #0f172a !important;
color: #e2e8f0 !important;
border-color: #334155 !important;
}
input::placeholder { color: #64748b !important; }
input:focus, select:focus, textarea:focus {
border-color: #6366f1 !important;
box-shadow: 0 0 0 3px rgba(99,102,241,0.2) !important;
}
/* Role select inline */
.role-select {
background: #1e293b !important;
color: #e2e8f0 !important;
border-color: #334155 !important;
}
/* Modals */
.modal { background: #1e293b !important; color: #e2e8f0; }
.modal h2 { color: #f1f5f9; }
/* Knoppen */
.btn-secondary { background: #334155 !important; color: #e2e8f0 !important; }
.btn-secondary:hover { background: #475569 !important; }
/* Status selector knoppen (leerkracht tabel) */
.status-selector.status-none { background: #1e293b !important; border-color: #475569 !important; color: #64748b !important; }
/* Stat cards */
.stat-card { background: #1e293b !important; }
/* School card header */
.school-card-header { background: #162032 !important; border-color: #334155 !important; }
.school-card { border-color: #334155 !important; }
/* Drop zone */
.drop-zone { background: #162032 !important; border-color: #334155 !important; }
.drop-zone:hover, .drop-zone.over { background: #1a2744 !important; border-color: #6366f1 !important; }
/* Domain chips */
.domain-chip { background: #1e3a5f !important; border-color: #2563eb !important; color: #93c5fd !important; }
/* Badges */
.leeftijd-badge { background: #334155 !important; color: #94a3b8 !important; }
.ebg-begrijpen { color: #1f2937 !important; }
/* MIA container */
.mia-container { background: #162032 !important; }
.mia-item { background: #1e293b !important; border-color: #334155 !important; color: #e2e8f0 !important; }
.mia-item.niet-aanklikbaar { background: #162032 !important; color: #64748b !important; }
/* Not configured box */
.not-configured { background: #162032 !important; border-color: #334155 !important; color: #94a3b8 !important; }
.not-configured code { background: #0f172a !important; color: #a5b4fc !important; }
/* Profile section */
.profile-section { background: #162032 !important; }
/* Leeftijd checkboxes */
.leeftijd-checkbox { border-color: #334155 !important; color: #e2e8f0; }
.leeftijd-checkbox:has(input:checked) { background: var(--primary) !important; }
/* Vak indicator */
.vak-indicator { /* gradient blijft, ziet er goed uit */ }
/* Progress bars achtergrond */
.progress-bar { background: #334155 !important; }
/* Vak card */
.vak-card { background: #162032 !important; }
/* Upload results */
.upload-ok { background: #064e3b !important; border-color: #065f46 !important; }
.upload-err { background: #450a0a !important; border-color: #7f1d1d !important; }
/* Alert boxes */
.alert-error { background: #450a0a !important; border-color: #7f1d1d !important; color: #fca5a5 !important; }
.alert-success { background: #064e3b !important; border-color: #065f46 !important; color: #6ee7b7 !important; }
.alert-warning { background: #451a03 !important; border-color: #78350f !important; color: #fcd34d !important; }
.alert-info { background: #1e3a5f !important; border-color: #1d4ed8 !important; color: #93c5fd !important; }
/* Error text */
.form-error, #sa-error, #addUser-error { color: #f87171 !important; }
.form-hint { color: #64748b !important; }
/* Superadmin toggle */
.superadmin-toggle { border-color: #334155 !important; }
.superadmin-toggle button { color: #475569 !important; }
.superadmin-toggle button:hover { color: #94a3b8 !important; }
/* Superadmin form inputs */
.superadmin-form label { color: #94a3b8 !important; }
/* Footer */
.footer { color: #64748b !important; }
.status-legend { background: #162032 !important; border-color: #334155 !important; color: #94a3b8 !important; }
/* Scrollbar (webkit) */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #0f172a; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>🏫 School ICT Beheer</h1>
<div class="school-name" id="schoolName">Laden...</div>
</div>
<a href="/auth/logout" class="btn btn-light">Uitloggen</a>
</div>
<!-- Statistieken -->
<div class="stat-grid" id="statGrid">
<div class="stat"><div class="stat-value" id="statTotal">-</div><div class="stat-label">Gebruikers</div></div>
<div class="stat"><div class="stat-value" id="statICT">-</div><div class="stat-label">School ICT</div></div>
<div class="stat"><div class="stat-value" id="statDir">-</div><div class="stat-label">Directeurs</div></div>
<div class="stat"><div class="stat-value" id="statTeach">-</div><div class="stat-label">Leerkrachten</div></div>
</div>
<!-- Gebruikersbeheer -->
<div class="section">
<div class="section-header">
<h2>👥 Gebruikers</h2>
<button id="btnAddUser" class="btn btn-primary btn-sm">+ Gebruiker toevoegen</button>
</div>
<table>
<thead>
<tr>
<th>Naam</th>
<th>E-mail</th>
<th>Rol</th>
<th></th>
</tr>
</thead>
<tbody id="usersBody">
<tr class="empty-row"><td colspan="4">Laden...</td></tr>
</tbody>
</table>
</div>
<!-- Klassen beheer -->
<div class="section">
<div class="section-header">
<h2>🏫 Klassen</h2>
<button id="btnAddKlas" class="btn btn-primary btn-sm">+ Klas toevoegen</button>
</div>
<div id="klassenList">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="class">Klassen</option>
<option value="assessment">Beoordelingen</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>Actie</th>
<th>Detail</th>
<th>IP</th>
</tr>
</thead>
<tbody id="auditTable">
<tr><td colspan="5" 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>
<!-- Modal: gebruiker toevoegen -->
<div class="modal-overlay" id="addModal">
<div class="modal">
<h2>Gebruiker toevoegen</h2>
<p style="color:var(--gray-500);font-size:.85rem;margin-bottom:1rem;">
Het account wordt aangemaakt en geactiveerd bij de eerste Microsoft login.
Als het e-mailadres al bestaat wordt het account heractiveerd.
</p>
<div class="form-group"><label>Voornaam</label><input type="text" id="addFirst"></div>
<div class="form-group"><label>Achternaam</label><input type="text" id="addLast"></div>
<div class="form-group">
<label>E-mailadres (Microsoft account)</label>
<input type="email" id="addEmail">
</div>
<div class="form-group">
<label>Rol</label>
<select id="addRole">
<option value="teacher">Leerkracht — vult leerdoelen in</option>
<option value="director">Directeur — leest schooloverzicht</option>
<option value="school_ict">School ICT — beheert gebruikers</option>
</select>
</div>
<div class="form-error" id="addError"></div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="btnCancelUser">Annuleren</button>
<button id="btnConfirmUser" class="btn btn-primary">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);
}
let mySchoolId = null;
const ROLLEN = [
{ value: 'teacher', label: 'Leerkracht' },
{ value: 'director', label: 'Directeur' },
{ value: 'school_ict', label: 'School ICT' },
];
document.addEventListener('DOMContentLoaded', async () => {
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 || '';
await loadUsers();
await loadKlassen();
await loadAuditLog();
});
async function loadUsers() {
if (!mySchoolId) return;
const res = await fetch(`/admin/schools/${mySchoolId}/users`);
const data = await res.json();
const users = data.users || [];
// Stats
document.getElementById('statTotal').textContent = users.length;
document.getElementById('statICT').textContent = users.filter(u => u.role === 'school_ict').length;
document.getElementById('statDir').textContent = users.filter(u => u.role === 'director').length;
document.getElementById('statTeach').textContent = users.filter(u => u.role === 'teacher').length;
const tbody = document.getElementById('usersBody');
if (!users.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="4">Nog geen gebruikers</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr>
<td><strong>${u.full_name}</strong></td>
<td style="color:var(--gray-500);font-size:.82rem;">${u.email}</td>
<td>
<select class="role-select"
data-action="changeRole" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">
${ROLLEN.map(r =>
`<option value="${r.value}" ${r.value === u.role ? 'selected' : ''}>${r.label}</option>`
).join('')}
</select>
</td>
<td>
<button class="btn btn-danger btn-sm"
data-action="removeUser" data-user-id="${u.id}" data-name="${u.full_name.replace(/'/g,'&#39;')}">
Verwijderen
</button>
</td>
</tr>`).join('');
}
async function changeRole(userId, newRole, naam, selectEl) {
const res = await fetch(`/admin/schools/${mySchoolId}/users/${userId}/role`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: newRole }),
});
if (!res.ok) {
const data = await res.json();
notify(data.error || 'Wijzigen mislukt', 'error');
await loadUsers(); // reset
return;
}
notify(`${naam} is nu ${ROLLEN.find(r => r.value === newRole)?.label}`, 'success');
await loadUsers();
}
async function addUser() {
const errEl = document.getElementById('addError');
errEl.style.display = 'none';
const res = await fetch(`/admin/schools/${mySchoolId}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: document.getElementById('addEmail').value,
first_name: document.getElementById('addFirst').value,
last_name: document.getElementById('addLast').value,
role: document.getElementById('addRole').value,
}),
});
const data = await res.json();
if (!res.ok) { errEl.textContent = data.error; errEl.style.display = 'block'; return; }
closeModal();
notify(data.linked ? 'Account heractiveerd en bijgewerkt' : 'Gebruiker toegevoegd', 'success');
await loadUsers();
}
async function removeUser(userId, naam) {
if (!confirm(`${naam} verwijderen?`)) return;
const res = await fetch(`/admin/schools/${mySchoolId}/users/${userId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`${naam} verwijderd`, 'success');
await loadUsers();
}
function openModal() {
document.getElementById('addModal').classList.add('active');
}
function closeModal() {
document.getElementById('addModal').classList.remove('active');
document.getElementById('addError').style.display = 'none';
}
function notify(msg, type = 'success') {
const el = document.getElementById('notification');
el.textContent = msg;
el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3500);
}
// ── Klassen ───────────────────────────────────────────────────────────────────
let allKlassen = [];
async function loadKlassen() {
if (!mySchoolId) return;
const res = await fetch(`/admin/schools/${mySchoolId}/classes`);
const data = await res.json();
allKlassen = data.classes || [];
const container = document.getElementById('klassenList');
if (!allKlassen.length) {
container.innerHTML = '<p style="color:var(--gray-400);font-style:italic;">Nog geen klassen aangemaakt.</p>';
return;
}
container.innerHTML = `
<table>
<thead><tr><th>Klas</th><th>Leerkrachten</th><th></th></tr></thead>
<tbody>
${allKlassen.map(c => `
<tr>
<td><strong>${c.name}</strong></td>
<td style="font-size:.85rem;color:var(--gray-500);">
${c.teachers?.length
? c.teachers.map(t => t.full_name).join(', ')
: '<em>Geen leerkracht gekoppeld</em>'}
</td>
<td style="white-space:nowrap;">
<button class="btn btn-secondary btn-sm" data-action="assignTeachers" data-id="${c.id}" data-name="${c.name.replace(/'/g,'&#39;')}" data-teachers="${JSON.stringify(c.teachers?.map(t=>t.id)||[]).replace(/"/g,'&quot;')}">
Leerkrachten
</button>
<button class="btn btn-danger btn-sm" data-action="deleteKlas" data-id="${c.id}" data-name="${c.name.replace(/'/g,'&#39;')}">
×
</button>
</td>
</tr>`).join('')}
</tbody>
</table>`;
}
async function openAddKlas() {
const name = prompt('Naam van de nieuwe klas (bv. 3A):');
if (!name?.trim()) return;
const res = await fetch(`/admin/schools/${mySchoolId}/classes`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ name: name.trim() })
});
const data = await res.json();
if (!res.ok) { notify(data.error || 'Aanmaken mislukt', 'error'); return; }
notify(`Klas ${name} aangemaakt`, 'success');
await loadKlassen();
}
async function deleteKlas(classId, name) {
if (!confirm(`Klas "${name}" verwijderen? Leerkrachtkoppelingen worden ook verwijderd.`)) return;
const res = await fetch(`/admin/schools/${mySchoolId}/classes/${classId}`, { method: 'DELETE' });
if (!res.ok) { notify('Verwijderen mislukt', 'error'); return; }
notify(`Klas ${name} verwijderd`, 'success');
await loadKlassen();
}
// Assign modal voor leerkrachten aan klas
let assignClassId = null;
async function openAssignTeachers(classId, className, currentTeacherIds) {
assignClassId = classId;
const teachers = await fetch(`/admin/schools/${mySchoolId}/users`)
.then(r => r.json()).then(d => d.users?.filter(u => u.role === 'teacher') || []);
const html = `
<div style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;" id="assignModal">
<div style="background:white;border-radius:12px;padding:1.5rem;max-width:400px;width:90%;">
<h3 style="margin-bottom:1rem;">Leerkrachten voor klas ${className}</h3>
<div style="max-height:250px;overflow-y:auto;display:flex;flex-direction:column;gap:.4rem;margin-bottom:1rem;">
${teachers.length
? teachers.map(t => `
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" value="${t.id}" ${currentTeacherIds.includes(t.id) ? 'checked' : ''}>
${t.full_name} <span style="font-size:.75rem;color:gray;">(${t.email})</span>
</label>`).join('')
: '<em style="color:gray;">Geen leerkrachten beschikbaar</em>'}
</div>
<div style="display:flex;gap:.5rem;justify-content:flex-end;">
<button data-action="closeAssignModal" class="btn btn-secondary">Annuleren</button>
<button data-action="saveAssignTeachers" class="btn btn-primary">Opslaan</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
async function saveAssignTeachers() {
const ids = [...document.querySelectorAll('#assignModal input:checked')].map(i => parseInt(i.value));
const res = await fetch(`/admin/schools/${mySchoolId}/classes/${assignClassId}/teachers`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ user_ids: ids })
});
document.getElementById('assignModal')?.remove();
if (!res.ok) { notify('Opslaan mislukt', 'error'); return; }
notify('Leerkrachten bijgewerkt', 'success');
await loadKlassen();
}
// ── Auditlog ──────────────────────────────────────────────────────────────────
let auditPage = 1;
async function loadAuditLog(page = 1) {
auditPage = page;
const category = document.getElementById('auditCategory').value;
const search = document.getElementById('auditSearch').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();
const tbody = document.getElementById('auditTable');
if (!data.entries.length) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--gray-400);">Geen entries gevonden</td></tr>';
} else {
tbody.innerHTML = data.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:.75rem;background:var(--gray-100);padding:.1rem .3rem;border-radius:3px;">${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><code style="font-size:.78rem;background:var(--gray-100);padding:.15rem .35rem;border-radius:3px;">${e.action}</code></td>
<td style="font-size:.8rem;">${detail}</td>
<td style="font-size:.75rem;color:var(--gray-400);">${e.ip_address || ''}</td>
</tr>`;
}).join('');
}
// Paginering
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;
${i+1 === data.page ? 'background:var(--primary);color:white;border-color:var(--primary);' : ''}">
${i+1}
</button>`).join('');
}
// ── Event delegation voor dynamisch gegenereerde elementen ────────────────────
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'removeUser') { removeUser(btn.dataset.userId, btn.dataset.name); }
if (action === 'assignTeachers') { openAssignTeachers(btn.dataset.id, btn.dataset.name, JSON.parse(btn.dataset.teachers.replace(/&quot;/g,'"'))); }
if (action === 'deleteKlas') { deleteKlas(btn.dataset.id, btn.dataset.name); }
if (action === 'closeAssignModal') { document.getElementById('assignModal')?.remove(); }
if (action === 'saveAssignTeachers'){ saveAssignTeachers(); }
if (action === 'auditPage') { loadAuditLog(parseInt(btn.dataset.page)); }
});
document.addEventListener('change', function(e) {
const sel = e.target.closest('[data-action="changeRole"]');
if (sel) { changeRole(sel.dataset.userId, sel.value, sel.dataset.name, sel); }
});
</script>
</body>
</html>