All checks were successful
Build & Push / Build & Push image (push) Successful in 40s
364 lines
18 KiB
HTML
364 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="nl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Klassenbeheer — {{ 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-400: #9ca3af; --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', Roboto, sans-serif;
|
||
background: var(--gray-100); color: var(--gray-800); line-height: 1.5; }
|
||
.container { max-width: 900px; margin: 0 auto; padding: 1.25rem; }
|
||
|
||
/* Header */
|
||
.header { background: white; border-radius: 12px; padding: 1.25rem 1.5rem;
|
||
margin-bottom: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,.1);
|
||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .75rem; }
|
||
.header-left { display: flex; align-items: center; gap: .75rem; }
|
||
.back-link { display: inline-flex; align-items: center; gap: .35rem; padding: .4rem .75rem;
|
||
border: 1px solid var(--gray-300); border-radius: 6px; font-size: .85rem;
|
||
color: var(--gray-600); text-decoration: none; transition: all .15s; }
|
||
.back-link:hover { background: var(--gray-100); border-color: var(--gray-400); }
|
||
.header h1 { font-size: 1.25rem; color: var(--gray-900);
|
||
display: flex; align-items: center; gap: .5rem; }
|
||
.school-badge { font-size: .75rem; background: var(--primary); color: white;
|
||
padding: .2rem .55rem; border-radius: 9999px; font-weight: 500; }
|
||
|
||
/* Section */
|
||
.section { background: white; border-radius: 12px; padding: 1.5rem;
|
||
margin-bottom: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
|
||
.section-header { display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 1.1rem; flex-wrap: wrap; gap: .5rem; }
|
||
.section-header h2 { font-size: 1rem; color: var(--gray-700);
|
||
display: flex; align-items: center; gap: .4rem; }
|
||
|
||
/* Buttons */
|
||
.btn { display: inline-flex; align-items: center; gap: .35rem; padding: .45rem .9rem;
|
||
border: none; border-radius: 6px; font-size: .85rem; font-weight: 500;
|
||
cursor: pointer; transition: all .15s; }
|
||
.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-sm { padding: .3rem .65rem; font-size: .8rem; }
|
||
|
||
/* Tabel */
|
||
table { width: 100%; border-collapse: collapse; font-size: .875rem; }
|
||
thead th { padding: .6rem .85rem; text-align: left; font-size: .75rem; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: .04em; color: var(--gray-500);
|
||
border-bottom: 2px solid var(--gray-200); background: var(--gray-50); }
|
||
tbody td { padding: .7rem .85rem; border-bottom: 1px solid var(--gray-100); vertical-align: middle; }
|
||
tbody tr:hover td { background: var(--gray-50); }
|
||
tbody tr:last-child td { border-bottom: none; }
|
||
.teacher-names { font-size: .82rem; color: var(--gray-500); }
|
||
.no-teacher { font-style: italic; color: var(--gray-400); font-size: .82rem; }
|
||
td:last-child { white-space: nowrap; text-align: right; }
|
||
|
||
/* Empty state */
|
||
.empty { text-align: center; padding: 2.5rem; color: var(--gray-500); font-style: italic; }
|
||
|
||
/* Assign modal (dynamisch ingeladen) */
|
||
.assign-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5);
|
||
z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||
.assign-modal { background: white; border-radius: 12px; padding: 1.5rem;
|
||
max-width: 420px; width: 90%; max-height: 80vh; overflow-y: auto; }
|
||
.assign-modal h3 { font-size: 1rem; margin-bottom: 1rem; }
|
||
.checkbox-list { max-height: 240px; overflow-y: auto; display: flex;
|
||
flex-direction: column; gap: .4rem; margin-bottom: 1.1rem; }
|
||
.checkbox-list label { display: flex; align-items: center; gap: .5rem;
|
||
cursor: pointer; font-size: .875rem; padding: .25rem 0; }
|
||
.checkbox-list label:hover { color: var(--primary); }
|
||
.modal-buttons { display: flex; gap: .5rem; justify-content: flex-end; }
|
||
.form-error { color: var(--danger); font-size: .82rem; margin-top: .4rem; display: none; }
|
||
|
||
/* Notification */
|
||
.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); }
|
||
|
||
/* Dark mode */
|
||
@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; }
|
||
.header, .section, .assign-modal { background: #1e293b !important; }
|
||
.back-link { border-color: #334155; color: #94a3b8; }
|
||
.back-link:hover { background: #263548; }
|
||
thead th { background: #1e293b !important; border-color: #334155 !important; }
|
||
tbody td { border-color: #1e293b !important; }
|
||
tbody tr:hover td { background: #263548 !important; }
|
||
.btn-secondary { background: #334155 !important; color: #e2e8f0 !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<a href="/dashboard" class="back-link">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="15 18 9 12 15 6"></polyline>
|
||
</svg>
|
||
Terug naar dashboard
|
||
</a>
|
||
<h1>
|
||
🏫 Klassenbeheer
|
||
<span class="school-badge" id="schoolBadge">Laden...</span>
|
||
</h1>
|
||
</div>
|
||
<a href="/auth/logout" class="btn btn-secondary btn-sm">Uitloggen</a>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>
|
||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/>
|
||
</svg>
|
||
Klassen
|
||
</h2>
|
||
<button class="btn btn-primary btn-sm" id="btnAddKlas">+ Klas toevoegen</button>
|
||
</div>
|
||
|
||
<div id="klassenList">
|
||
<div class="empty">Laden...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2>
|
||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||
<circle cx="9" cy="7" r="4"/>
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
|
||
</svg>
|
||
Leerkrachten van deze school
|
||
</h2>
|
||
</div>
|
||
<div id="teachersList">
|
||
<div class="empty">Laden...</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="notification" id="notification"></div>
|
||
|
||
<script nonce="{{ csp_nonce() }}">
|
||
let mySchoolId = null;
|
||
let allTeachers = [];
|
||
|
||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
document.getElementById('btnAddKlas').addEventListener('click', openAddKlas);
|
||
|
||
const me = await fetch('/api/me').then(r => r.json());
|
||
mySchoolId = me.user?.school_id;
|
||
document.getElementById('schoolBadge').textContent = me.user?.school_name || 'Mijn school';
|
||
|
||
await Promise.all([loadKlassen(), loadTeachers()]);
|
||
});
|
||
|
||
// ── Klassen laden & renderen ──────────────────────────────────────────────────
|
||
async function loadKlassen() {
|
||
if (!mySchoolId) return;
|
||
const res = await fetch(`/admin/schools/${mySchoolId}/classes`);
|
||
const data = await res.json();
|
||
const klassen = data.classes || [];
|
||
|
||
const container = document.getElementById('klassenList');
|
||
if (!klassen.length) {
|
||
container.innerHTML = '<div class="empty">Nog geen klassen aangemaakt. Klik op "+ Klas toevoegen" om te starten.</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Klas</th>
|
||
<th>Leerkrachten</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${klassen.map(c => `
|
||
<tr>
|
||
<td><strong>${c.name}</strong></td>
|
||
<td>
|
||
${c.teachers?.length
|
||
? `<span class="teacher-names">${c.teachers.map(t => t.full_name).join(', ')}</span>`
|
||
: '<span class="no-teacher">Geen leerkracht gekoppeld</span>'}
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-secondary btn-sm"
|
||
data-action="assignTeachers"
|
||
data-id="${c.id}"
|
||
data-name="${c.name.replace(/'/g,''')}"
|
||
data-teachers="${JSON.stringify(c.teachers?.map(t=>t.id)||[]).replace(/"/g,'"')}">
|
||
Leerkrachten koppelen
|
||
</button>
|
||
<button class="btn btn-danger btn-sm"
|
||
data-action="deleteKlas"
|
||
data-id="${c.id}"
|
||
data-name="${c.name.replace(/'/g,''')}">
|
||
×
|
||
</button>
|
||
</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
}
|
||
|
||
// ── Leerkrachtenlijst ─────────────────────────────────────────────────────────
|
||
async function loadTeachers() {
|
||
if (!mySchoolId) return;
|
||
const res = await fetch(`/admin/schools/${mySchoolId}/users`);
|
||
const data = await res.json();
|
||
allTeachers = (data.users || []).filter(u => u.role === 'teacher');
|
||
|
||
const container = document.getElementById('teachersList');
|
||
if (!allTeachers.length) {
|
||
container.innerHTML = '<div class="empty">Nog geen leerkrachten geregistreerd via SSO.</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Naam</th>
|
||
<th>E-mail</th>
|
||
<th>Laatste login</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${allTeachers.map(u => `
|
||
<tr>
|
||
<td><strong>${u.full_name}</strong></td>
|
||
<td style="color:var(--gray-500);font-size:.85rem;">${u.email}</td>
|
||
<td style="color:var(--gray-500);font-size:.82rem;">
|
||
${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'})
|
||
: '<em>Nog niet ingelogd</em>'}
|
||
</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
}
|
||
|
||
// ── Klas toevoegen ────────────────────────────────────────────────────────────
|
||
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.trim()}" aangemaakt`, 'success');
|
||
await loadKlassen();
|
||
}
|
||
|
||
// ── Klas verwijderen ──────────────────────────────────────────────────────────
|
||
async function deleteKlas(classId, name) {
|
||
if (!confirm(`Klas "${name}" verwijderen? Alle 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();
|
||
}
|
||
|
||
// ── Leerkrachten koppelen ─────────────────────────────────────────────────────
|
||
let assignClassId = null;
|
||
|
||
async function openAssignTeachers(classId, className, currentTeacherIds) {
|
||
assignClassId = classId;
|
||
|
||
// Haal verse leerkrachtenlijst op als nog niet geladen
|
||
if (!allTeachers.length) await loadTeachers();
|
||
|
||
const html = `
|
||
<div class="assign-overlay" id="assignModal">
|
||
<div class="assign-modal">
|
||
<h3>Leerkrachten koppelen aan <strong>${className}</strong></h3>
|
||
<div class="checkbox-list">
|
||
${allTeachers.length
|
||
? allTeachers.map(t => `
|
||
<label>
|
||
<input type="checkbox" value="${t.id}"
|
||
${currentTeacherIds.includes(t.id) ? 'checked' : ''}>
|
||
<span>${t.full_name}</span>
|
||
<span style="font-size:.75rem;color:var(--gray-400);">(${t.email})</span>
|
||
</label>`).join('')
|
||
: '<em style="color:var(--gray-400);">Geen leerkrachten beschikbaar.<br>Leerkrachten verschijnen hier zodra ze voor het eerst inloggen via SSO.</em>'}
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="btn btn-secondary" id="btnCancelAssign">Annuleren</button>
|
||
<button class="btn btn-primary" id="btnSaveAssign">Opslaan</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
document.body.insertAdjacentHTML('beforeend', html);
|
||
document.getElementById('btnCancelAssign').addEventListener('click', () => document.getElementById('assignModal')?.remove());
|
||
document.getElementById('btnSaveAssign').addEventListener('click', saveAssignTeachers);
|
||
// Klik buiten modal sluit ook
|
||
document.getElementById('assignModal').addEventListener('click', e => {
|
||
if (e.target.id === 'assignModal') document.getElementById('assignModal').remove();
|
||
});
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
// ── Event delegation ──────────────────────────────────────────────────────────
|
||
document.addEventListener('click', e => {
|
||
const btn = e.target.closest('[data-action]');
|
||
if (!btn) return;
|
||
const action = btn.dataset.action;
|
||
if (action === 'deleteKlas') {
|
||
deleteKlas(btn.dataset.id, btn.dataset.name);
|
||
}
|
||
if (action === 'assignTeachers') {
|
||
const ids = JSON.parse(btn.dataset.teachers.replace(/"/g, '"'));
|
||
openAssignTeachers(btn.dataset.id, btn.dataset.name, ids);
|
||
}
|
||
});
|
||
|
||
// ── Notificaties ──────────────────────────────────────────────────────────────
|
||
function notify(msg, type = 'success') {
|
||
const el = document.getElementById('notification');
|
||
el.textContent = msg;
|
||
el.className = `notification ${type} show`;
|
||
setTimeout(() => el.classList.remove('show'), 3500);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|