feat: implement modal functionality for linking teachers and adding new classes with improved UI
All checks were successful
Build & Push / Build & Push image (push) Successful in 41s

This commit is contained in:
2026-03-06 10:07:32 +01:00
parent bbd4e332f4
commit 04fe593d0c

View File

@@ -95,6 +95,21 @@
.notification.success { background: var(--success); } .notification.success { background: var(--success); }
.notification.error { background: var(--danger); } .notification.error { background: var(--danger); }
.notification.warning { background: var(--warning); } .notification.warning { background: var(--warning); }
.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-inner { background:white;border-radius:12px;padding:1.5rem;max-width:440px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.3); }
.modal-inner h2 { font-size:1.05rem;margin-bottom:.35rem; }
.modal-inner p { font-size:.82rem;color:var(--gray-500);margin-bottom:1rem; }
.modal-buttons { display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.1rem; }
.form-input { width:100%;padding:.55rem .75rem;border:1px solid var(--gray-300);border-radius:6px;font-size:.9rem;margin-top:.5rem; }
.form-input:focus { outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(79,70,229,.1); }
.form-error { font-size:.82rem;color:var(--danger);margin-top:.5rem;min-height:1.2em; }
.klas-chip-card { background:var(--gray-50);border:1px solid var(--gray-200);border-radius:8px;padding:.6rem .85rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem; }
.klas-chip-card .klas-info { flex:1;cursor:pointer; }
.klas-chip-card .klas-name { font-weight:600;color:var(--gray-800); }
.klas-chip-card .klas-teachers { font-size:.75rem;color:var(--gray-500);margin-top:.15rem; }
.klas-chip-card .btn-delete { background:none;border:1px solid var(--gray-200);border-radius:6px;padding:.3rem .45rem;cursor:pointer;color:var(--gray-400);font-size:.85rem;transition:all .15s;flex-shrink:0; }
.klas-chip-card .btn-delete:hover { background:var(--danger);border-color:var(--danger);color:white; }
.legend-footer { padding: .85rem 1.25rem; border-top: 1px solid var(--gray-200); display: flex; gap: 1.25rem; flex-wrap: wrap; font-size: .78rem; color: var(--gray-600); } .legend-footer { padding: .85rem 1.25rem; border-top: 1px solid var(--gray-200); display: flex; gap: 1.25rem; flex-wrap: wrap; font-size: .78rem; color: var(--gray-600); }
.legend-item { display: flex; align-items: center; gap: .35rem; } .legend-item { display: flex; align-items: center; gap: .35rem; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -114,11 +129,12 @@
.btn-secondary { background:#334155!important;color:#e2e8f0!important; } .btn-secondary { background:#334155!important;color:#e2e8f0!important; }
.diff-row-same { background:#064e3b!important; } .diff-row-same { background:#064e3b!important; }
.diff-row-differ { background:#451a03!important; } .diff-row-differ { background:#451a03!important; }
#koppelingModal > div { background:#1e293b!important;color:#e2e8f0!important; } .modal-overlay .modal-inner { background:#1e293b!important;color:#e2e8f0!important; }
#koppelingCheckboxes label { color:#e2e8f0!important; } #koppelingCheckboxes label { color:#e2e8f0!important; }
#koppelingCheckboxes label:hover { background:#263548!important; } #koppelingCheckboxes label:hover { background:#263548!important; }
#koppelingCheckboxes span { color:#94a3b8!important; } #koppelingCheckboxes span { color:#94a3b8!important; }
#koppelingCheckboxes input[type=checkbox] { accent-color:var(--primary); } #koppelingCheckboxes input[type=checkbox] { accent-color:var(--primary); }
.modal-overlay .form-input { background:#0f172a!important;color:#e2e8f0!important;border-color:#334155!important; }
} }
</style> </style>
</head> </head>
@@ -262,18 +278,32 @@
</div> </div>
<!-- Modal: leerkrachten koppelen --> <!-- Modal: leerkrachten koppelen -->
<div id="koppelingModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center;"> <div id="koppelingModal" class="modal-overlay">
<div style="background:white;border-radius:12px;padding:1.5rem;max-width:420px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.3);"> <div class="modal-inner">
<h2 style="font-size:1.1rem;margin-bottom:.5rem;" id="koppelingModalTitle">Leerkrachten koppelen</h2> <h2 id="koppelingModalTitle">Leerkrachten koppelen</h2>
<p style="font-size:.82rem;color:var(--gray-500);margin-bottom:1rem;">Selecteer de leerkrachten voor deze klas.</p> <p>Selecteer de leerkrachten voor deze klas.</p>
<div id="koppelingCheckboxes" style="display:flex;flex-direction:column;gap:.4rem;max-height:280px;overflow-y:auto;margin-bottom:1rem;"></div> <div id="koppelingCheckboxes" style="display:flex;flex-direction:column;gap:.4rem;max-height:280px;overflow-y:auto;"></div>
<div style="display:flex;gap:.5rem;justify-content:flex-end;"> <div class="modal-buttons">
<button id="btnKoppelingAnnuleer" class="btn btn-secondary">Annuleren</button> <button id="btnKoppelingAnnuleer" class="btn btn-secondary">Annuleren</button>
<button id="btnKoppelingOpslaan" class="btn btn-primary">Opslaan</button> <button id="btnKoppelingOpslaan" class="btn btn-primary">Opslaan</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal: nieuwe klas -->
<div id="nieuweKlasModal" class="modal-overlay">
<div class="modal-inner">
<h2>Nieuwe klas toevoegen</h2>
<p>Geef een naam op voor de nieuwe klas (bv. "1A", "3B").</p>
<input type="text" id="nieuweKlasNaam" class="form-input" placeholder="Naam van de klas...">
<div class="form-error" id="nieuweKlasError"></div>
<div class="modal-buttons">
<button id="btnNieuweKlasAnnuleer" class="btn btn-secondary">Annuleren</button>
<button id="btnNieuweKlasBevestig" class="btn btn-primary">Aanmaken</button>
</div>
</div>
</div>
</div> </div>
<div class="notification" id="notification"></div> <div class="notification" id="notification"></div>
@@ -321,8 +351,21 @@ document.addEventListener('DOMContentLoaded', async () => {
// Export knoppen // Export knoppen
document.getElementById('btnExportCSV').addEventListener('click', exportCSV); document.getElementById('btnExportCSV').addEventListener('click', exportCSV);
document.getElementById('btnNieuweKlas').addEventListener('click', openNieuweKlasDialog); document.getElementById('btnNieuweKlas').addEventListener('click', openNieuweKlasDialog);
document.getElementById('btnNieuweKlasAnnuleer').addEventListener('click', () => {
document.getElementById('nieuweKlasModal').classList.remove('active');
});
document.getElementById('btnNieuweKlasBevestig').addEventListener('click', bevestigNieuweKlas);
document.getElementById('nieuweKlasNaam').addEventListener('keydown', e => {
if (e.key === 'Enter') bevestigNieuweKlas();
});
// Sluit modals bij klik op overlay
['koppelingModal','nieuweKlasModal'].forEach(id => {
document.getElementById(id).addEventListener('click', e => {
if (e.target.id === id) document.getElementById(id).classList.remove('active');
});
});
document.getElementById('btnKoppelingAnnuleer').addEventListener('click', () => { document.getElementById('btnKoppelingAnnuleer').addEventListener('click', () => {
document.getElementById('koppelingModal').style.display = 'none'; document.getElementById('koppelingModal').classList.remove('active');
}); });
document.getElementById('btnKoppelingOpslaan').addEventListener('click', saveKoppeling); document.getElementById('btnKoppelingOpslaan').addEventListener('click', saveKoppeling);
document.getElementById('btnExportPDF').addEventListener('click', exportPDF); document.getElementById('btnExportPDF').addEventListener('click', exportPDF);
@@ -470,7 +513,7 @@ function renderKlasProgress() {
if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen</div>'; return; } if (!klassen.length) { el.innerHTML = '<div class="empty">Geen klassen</div>'; return; }
// Filter op leerkrachten (niet directeurs/ICT) // Filter op leerkrachten (niet directeurs/ICT)
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director'); const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
el.innerHTML = '<div class="vak-grid">' + klassen.map(k => { el.innerHTML = '<div style="display:flex;flex-direction:column;gap:.5rem;">' + klassen.map(k => {
const vakken = byClass[k.id] || {}; const vakken = byClass[k.id] || {};
let g=0,o=0,r=0; let g=0,o=0,r=0;
Object.values(vakken).forEach(goals => Object.values(goals).forEach(s => { Object.values(vakken).forEach(goals => Object.values(goals).forEach(s => {
@@ -747,17 +790,14 @@ async function renderKoppelingTab() {
// Filter op leerkrachten (niet directeurs/ICT) // Filter op leerkrachten (niet directeurs/ICT)
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director'); const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
el.innerHTML = '<div class="vak-grid">' + klassen.map(k => { el.innerHTML = '<div style="display:flex;flex-direction:column;gap:.5rem;">' + klassen.map(k => {
const teachers = (k.teachers||[]).map(t => t.full_name).join(', ') || '<em style="color:var(--gray-400)">Geen leerkrachten</em>'; const teachers = (k.teachers||[]).map(t => t.full_name).join(', ') || '<em style="color:var(--gray-400)">Geen leerkrachten</em>';
return `<div class="vak-card" style="position:relative;"> return `<div class="klas-chip-card">
<div class="vak-card-header" style="cursor:pointer;" data-action="openKoppeling" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}" data-teachers="${JSON.stringify((k.teachers||[]).map(t=>t.id)).replace(/"/g,'&quot;')}"> <div class="klas-info" data-action="openKoppeling" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}" data-teachers="${JSON.stringify((k.teachers||[]).map(t=>t.id)).replace(/"/g,'&quot;')}">
<h3>🏫 ${k.name}</h3> <div class="klas-name">🏫 ${k.name} <span style="font-size:.72rem;background:var(--primary);color:white;padding:.1rem .4rem;border-radius:4px;margin-left:.35rem;">Wijzigen</span></div>
<span style="font-size:.75rem;background:var(--primary);color:white;padding:.2rem .5rem;border-radius:4px;">Wijzigen</span> <div class="klas-teachers">${teachers}</div>
</div> </div>
<div style="font-size:.82rem;color:var(--gray-600);margin-top:.4rem;">${teachers}</div> <button class="btn-delete" data-action="deleteKlas" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}" title="Klas verwijderen">🗑</button>
<button data-action="deleteKlas" data-id="${k.id}" data-name="${k.name.replace(/"/g,'&quot;')}"
style="position:absolute;top:.5rem;right:.5rem;background:none;border:none;cursor:pointer;font-size:1rem;color:var(--gray-400);line-height:1;"
title="Klas verwijderen">🗑</button>
</div>`; </div>`;
}).join('') + '</div>'; }).join('') + '</div>';
} }
@@ -795,19 +835,28 @@ document.addEventListener('click', function(e) {
<span>${u.full_name} <span style="color:var(--gray-400);font-size:.8rem;">(${u.email})</span></span> <span>${u.full_name} <span style="color:var(--gray-400);font-size:.8rem;">(${u.email})</span></span>
</label>`).join(''); </label>`).join('');
} }
document.getElementById('koppelingModal').style.display = 'flex'; document.getElementById('koppelingModal').classList.add('active');
}); });
async function openNieuweKlasDialog() { function openNieuweKlasDialog() {
const name = prompt('Naam van de nieuwe klas:'); document.getElementById('nieuweKlasNaam').value = '';
if (!name || !name.trim()) return; document.getElementById('nieuweKlasError').textContent = '';
document.getElementById('nieuweKlasModal').classList.add('active');
setTimeout(() => document.getElementById('nieuweKlasNaam').focus(), 50);
}
async function bevestigNieuweKlas() {
const name = document.getElementById('nieuweKlasNaam').value.trim();
const errEl = document.getElementById('nieuweKlasError');
if (!name) { errEl.textContent = 'Vul een naam in.'; return; }
const res = await fetch('/api/classes', { const res = await fetch('/api/classes', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name.trim()}) body: JSON.stringify({name})
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) { showNotification(data.error || 'Aanmaken mislukt', 'error'); return; } if (!res.ok) { errEl.textContent = data.error || 'Aanmaken mislukt'; return; }
document.getElementById('nieuweKlasModal').classList.remove('active');
showNotification(`Klas "${data.class.name}" aangemaakt`, 'success'); showNotification(`Klas "${data.class.name}" aangemaakt`, 'success');
await loadOverview(); await loadOverview();
} }
@@ -823,7 +872,7 @@ async function saveKoppeling() {
body: JSON.stringify({ teacher_ids: checked }) body: JSON.stringify({ teacher_ids: checked })
}); });
if (!res.ok) { showNotification('Opslaan mislukt', 'error'); return; } if (!res.ok) { showNotification('Opslaan mislukt', 'error'); return; }
document.getElementById('koppelingModal').style.display = 'none'; document.getElementById('koppelingModal').classList.remove('active');
showNotification('Koppeling opgeslagen', 'success'); showNotification('Koppeling opgeslagen', 'success');
await loadOverview(); // herlaad zodat klas-chips bijgewerkt worden await loadOverview(); // herlaad zodat klas-chips bijgewerkt worden
} }