feat: add CRUD functionality for classes with API endpoints and UI integration
All checks were successful
Build & Push / Build & Push image (push) Successful in 41s

This commit is contained in:
2026-03-06 09:12:32 +01:00
parent 653cc6cd74
commit bbd4e332f4
2 changed files with 91 additions and 5 deletions

View File

@@ -460,6 +460,52 @@ def set_my_classes():
# ── Klassen CRUD (directeur) ───────────────────────────────────────────────────
@api_bp.route('/classes', methods=['GET'])
@login_required
@director_required
def list_classes():
"""Alle klassen van de school."""
classes = Class.query.filter_by(school_id=current_user.school_id) .order_by(Class.name).all()
return jsonify({'classes': [
{'id': c.id, 'name': c.name,
'teachers': [{'id': t.id, 'full_name': t.full_name} for t in c.teachers]}
for c in classes
]})
@api_bp.route('/classes', methods=['POST'])
@login_required
@director_required
def create_class():
"""Nieuwe klas aanmaken."""
data = request.get_json() or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'error': 'Naam is verplicht'}), 400
if Class.query.filter_by(school_id=current_user.school_id, name=name).first():
return jsonify({'error': 'Een klas met deze naam bestaat al'}), 409
klas = Class(name=name, school_id=current_user.school_id)
db.session.add(klas)
audit_log('class.create', 'class', detail={'name': name})
db.session.commit()
return jsonify({'class': {'id': klas.id, 'name': klas.name, 'teachers': []}}), 201
@api_bp.route('/classes/<int:class_id>', methods=['DELETE'])
@login_required
@director_required
def delete_class(class_id):
"""Klas verwijderen (enkel eigen school)."""
klas = Class.query.filter_by(id=class_id, school_id=current_user.school_id).first_or_404()
name = klas.name
db.session.delete(klas)
audit_log('class.delete', 'class', target_id=str(class_id), detail={'name': name})
db.session.commit()
return jsonify({'deleted': class_id})
# ── Klas-leerkracht koppeling (directeur) ──────────────────────────────────────
@api_bp.route('/classes/<int:class_id>/teachers', methods=['PUT'])

View File

@@ -188,6 +188,9 @@
<label>Status</label>
<select id="filterStatus">
<option value="all">Alle statussen</option>
<option value="groen">✓ Minstens één groen</option>
<option value="oranje">~ Minstens één oranje</option>
<option value="roze">! Minstens één roze</option>
<option value="consensus">✓ Consensus (iedereen groen)</option>
<option value="verschil">⚠ Verschillen</option>
<option value="niemand">○ Niemand beoordeeld</option>
@@ -247,9 +250,12 @@
<!-- TAB: Leerkrachten koppelen -->
<div class="tab-content" id="tab-koppeling">
<div class="card">
<h2>👥 Leerkrachten aan klassen koppelen</h2>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.5rem;">
<h2 style="margin:0;">👥 Klassen &amp; Leerkrachten</h2>
<button class="btn btn-primary" id="btnNieuweKlas">+ Klas toevoegen</button>
</div>
<p style="font-size:.85rem;color:var(--gray-500);margin-bottom:1rem;">
Klik op een klas om de gekoppelde leerkrachten te wijzigen.
Klik op een klas om leerkrachten te wijzigen. Gebruik het prullenbakje om een klas te verwijderen.
</p>
<div id="koppelingContent"><div class="empty">Laden...</div></div>
</div>
@@ -314,6 +320,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Export knoppen
document.getElementById('btnExportCSV').addEventListener('click', exportCSV);
document.getElementById('btnNieuweKlas').addEventListener('click', openNieuweKlasDialog);
document.getElementById('btnKoppelingAnnuleer').addEventListener('click', () => {
document.getElementById('koppelingModal').style.display = 'none';
});
@@ -574,6 +581,9 @@ function applyFilters() {
if (leeftFilter.length > 0 && !leeftFilter.some(l=>goal.leeftijden.includes(l))) return;
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
const statussen = klassen.map(k => (overviewData.assessments_by_class[k.id]?.[vakId]?.[goal.id])||'');
if (statusFilter==='groen' && !statussen.some(s=>s==='groen')) return;
if (statusFilter==='oranje' && !statussen.some(s=>s==='oranje')) return;
if (statusFilter==='roze' && !statussen.some(s=>s==='roze')) return;
if (statusFilter==='consensus' && !statussen.every(s=>s==='groen')) return;
if (statusFilter==='verschil') {
const filled = statussen.filter(Boolean);
@@ -739,17 +749,33 @@ async function renderKoppelingTab() {
const teacherUsers = allUsers.filter(u => u.role === 'teacher' || u.role === 'director');
el.innerHTML = '<div class="vak-grid">' + klassen.map(k => {
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="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="vak-card-header">
return `<div class="vak-card" style="position:relative;">
<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;')}">
<h3>🏫 ${k.name}</h3>
<span style="font-size:.75rem;background:var(--primary);color:white;padding:.2rem .5rem;border-radius:4px;">Wijzigen</span>
</div>
<div style="font-size:.82rem;color:var(--gray-600);">${teachers}</div>
<div style="font-size:.82rem;color:var(--gray-600);margin-top:.4rem;">${teachers}</div>
<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>`;
}).join('') + '</div>';
}
document.addEventListener('click', function(e) {
// Verwijder klas
const delBtn = e.target.closest('[data-action="deleteKlas"]');
if (delBtn) {
const id = parseInt(delBtn.dataset.id);
const name = delBtn.dataset.name;
if (!confirm(`Klas "${name}" verwijderen? Alle beoordelingen voor deze klas gaan verloren.`)) return;
fetch(`/api/classes/${id}`, {method: 'DELETE'})
.then(r => { if (r.ok) { showNotification(`Klas "${name}" verwijderd`, 'success'); loadOverview(); }
else showNotification('Verwijderen mislukt', 'error'); });
return;
}
// Wijzig leerkrachten
const card = e.target.closest('[data-action="openKoppeling"]');
if (!card) return;
activeKlasKoppeling = parseInt(card.dataset.id);
@@ -772,6 +798,20 @@ document.addEventListener('click', function(e) {
document.getElementById('koppelingModal').style.display = 'flex';
});
async function openNieuweKlasDialog() {
const name = prompt('Naam van de nieuwe klas:');
if (!name || !name.trim()) return;
const res = await fetch('/api/classes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name.trim()})
});
const data = await res.json();
if (!res.ok) { showNotification(data.error || 'Aanmaken mislukt', 'error'); return; }
showNotification(`Klas "${data.class.name}" aangemaakt`, 'success');
await loadOverview();
}
async function saveKoppeling() {
if (!activeKlasKoppeling) return;
const checked = [...document.querySelectorAll('#koppelingCheckboxes input:checked')].map(i => parseInt(i.value));