Files
leerdoelen_tracker/backend/templates/leerkracht.html
Sam 50d029c67e
All checks were successful
Build & Push / Build & Push image (push) Successful in 39s
feat: add leerkracht-view route and update templates for director mode
2026-03-04 14:31:29 +01:00

908 lines
44 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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>Leerdoelen Tracker</title>
<style>
/* Zelfde CSS als origineel - ingekort voor leesbaarheid */
:root {
--primary: #4f46e5; --primary-dark: #4338ca;
--success: #10b981; --warning: #f59e0b; --danger: #ef4444;
--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;
--kleur-engageren: #c084fc; --kleur-begrijpen: #fbbf24; --kleur-gebruiken: #34d399;
--status-groen: #10b981; --status-groen-bg: #d1fae5;
--status-oranje: #f59e0b; --status-oranje-bg: #fef3c7;
--status-roze: #ec4899; --status-roze-bg: #fce7f3;
}
* { 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: 1400px; margin: 0 auto; padding: 1rem; }
.header { background: white; border-radius: 12px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
.header h1 { font-size: 1.4rem; color: var(--gray-900); display: flex; align-items: center; gap: 0.5rem; }
.user-info { display: flex; align-items: center; gap: 1rem; font-size: 0.9rem; color: var(--gray-600); }
.user-info strong { color: var(--gray-800); }
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 0.85rem; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; }
.btn-secondary { background: var(--gray-100); color: var(--gray-700); }
.btn-secondary:hover { background: var(--gray-200); }
.btn-danger { background: var(--danger); color: white; }
.vak-selector { background: white; border-radius: 12px; padding: 1rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; transition: background 0.2s; }
.vak-selector label { font-weight: 600; color: var(--gray-700); font-size: 0.9rem; }
.vak-selector select { padding: 0.5rem 0.75rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.95rem; min-width: 250px; cursor: pointer; background: white; color: var(--gray-800); }
.vak-selector select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
.stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
.stat-card { background: white; border-radius: 8px; padding: 0.75rem 1rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-card.highlight { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; }
.stat-value { font-size: 1.5rem; font-weight: 700; }
.stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.8; }
.filters-container { background: white; border-radius: 12px; padding: 1rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.filters-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; }
.filter-group { display: flex; flex-direction: column; gap: 0.25rem; }
.filter-group label { font-size: 0.75rem; font-weight: 500; color: var(--gray-500); }
.filter-group input, .filter-group select { padding: 0.5rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.9rem; }
.filter-group input:focus, .filter-group select:focus { outline: none; border-color: var(--primary); }
.leeftijd-checkboxes { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.leeftijd-checkbox { display: flex; align-items: center; gap: 0.25rem; padding: 0.3rem 0.5rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.8rem; cursor: pointer; transition: all 0.15s; user-select: none; }
.leeftijd-checkbox:has(input:checked) { background: var(--primary); border-color: var(--primary); color: white; }
.leeftijd-checkbox input { display: none; }
.table-container { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
thead { background: var(--gray-50); }
th { padding: 0.75rem 1rem; text-align: left; font-weight: 600; color: var(--gray-600); border-bottom: 2px solid var(--gray-200); white-space: nowrap; }
td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--gray-100); vertical-align: top; }
tr:hover { background: var(--gray-50); }
tr.status-groen { background: var(--status-groen-bg); }
tr.status-groen:hover { background: #a7f3d0; }
tr.status-oranje { background: var(--status-oranje-bg); }
tr.status-oranje:hover { background: #fde68a; }
tr.status-roze { background: var(--status-roze-bg); }
tr.status-roze:hover { background: #fbcfe8; }
.status-selector { width: 32px; height: 32px; border-radius: 6px; border: 2px solid var(--gray-300); background: white; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; font-weight: bold; transition: all 0.15s; }
.status-selector:hover { transform: scale(1.1); }
.status-selector.status-none { color: var(--gray-400); }
.status-selector.status-none::after { content: '○'; }
.status-selector.status-groen { background: var(--status-groen); border-color: var(--status-groen); color: white; }
.status-selector.status-groen::after { content: '✓'; }
.status-selector.status-oranje { background: var(--status-oranje); border-color: var(--status-oranje); color: white; }
.status-selector.status-oranje::after { content: '~'; }
.status-selector.status-roze { background: var(--status-roze); border-color: var(--status-roze); color: white; }
.status-selector.status-roze::after { content: '!'; }
.ebg-badge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.ebg-engageren { background: var(--kleur-engageren); color: white; }
.ebg-begrijpen { background: var(--kleur-begrijpen); color: var(--gray-800); }
.ebg-gebruiken { background: var(--kleur-gebruiken); color: white; }
.legend-container { background: white; border-radius: 12px; padding: 1rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
.legend-title { font-weight: 600; color: var(--gray-700); margin-bottom: .75rem; font-size: .9rem; }
.legend-grid { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; }
.legend-section { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }
.legend-item { display: flex; align-items: center; gap: .4rem; font-size: .85rem; }
.legend-color { width: 18px; height: 18px; border-radius: 4px; flex-shrink: 0; }
.legend-divider { width: 1px; height: 24px; background: var(--gray-300); }
.leeftijden { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.leeftijd-badge { font-size: 0.7rem; padding: 0.15rem 0.35rem; background: var(--gray-200); border-radius: 3px; color: var(--gray-600); }
.beschrijving-cell { max-width: 400px; }
/* Opmerkingen kolom */
.opm-col { width: 200px; min-width: 150px; }
.opm-cell { padding: 0.4rem 0.5rem; vertical-align: middle; }
.opm-input {
width: 100%; padding: 0.3rem 0.4rem;
border: 1px solid var(--gray-300); border-radius: 4px;
font-size: 0.8rem; background: transparent; color: var(--gray-700);
transition: border-color 0.15s;
}
.opm-input:focus {
outline: none; border-color: var(--primary); background: white;
box-shadow: 0 0 0 2px rgba(79,70,229,0.1);
}
.opm-input::placeholder { color: var(--gray-400); }
.mia-container { background: var(--gray-50); border-radius: 6px; padding: 0.5rem; font-size: 0.8rem; margin-top: 0.5rem; }
.mia-items { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; }
.mia-item { background: white; padding: 0.2rem 0.4rem; border-radius: 3px; border: 1px solid var(--gray-200); }
.empty-state { text-align: center; padding: 3rem; color: var(--gray-500); }
.loading { text-align: center; padding: 3rem; color: var(--gray-500); }
.spinner { width: 40px; height: 40px; border: 3px solid var(--gray-200); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem; }
@keyframes spin { to { transform: rotate(360deg); } }
.notification { position: fixed; bottom: 1rem; right: 1rem; padding: 1rem 1.5rem; border-radius: 8px; color: white; font-weight: 500; transform: translateY(100px); opacity: 0; transition: all 0.3s; z-index: 1001; }
.notification.show { transform: translateY(0); opacity: 1; }
.notification.success { background: var(--success); }
.notification.error { background: var(--danger); }
.saving-indicator { font-size: 0.8rem; color: var(--gray-400); margin-left: auto; }
@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-inner-box { background: #1e293b !important; color: #e2e8f0 !important; }
.modal h2, .modal-inner-box 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; }
/* Opmerkingen input */
.opm-input { background: transparent !important; color: #e2e8f0 !important; border-color: #334155 !important; }
.opm-input:focus { background: #0f172a !important; border-color: #6366f1 !important; }
.opm-input::placeholder { color: #475569 !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 — stijl via inline op het element */
/* Legend container */
.legend-container { background: #1e293b !important; border-color: #334155 !important; }
.legend-title { color: #94a3b8 !important; }
.legend-divider { background: #334155 !important; }
/* Vak selector balk */
.vak-selector { background: #1e293b !important; }
.vak-selector label { color: #94a3b8 !important; }
.vak-selector select { background: #0f172a !important; color: #e2e8f0 !important; border-color: #334155 !important; }
/* 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; }
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-import { background: var(--warning); color: white; }
.btn-import:hover { background: #d97706; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 Leerdoelen Tracker</h1>
<div class="user-info">
<span id="userInfo">Laden...</span>
<span class="saving-indicator" id="savingIndicator"></span>
</div>
<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;">
<div id="klasSelector" style="display:none;align-items:center;gap:.4rem;">
<label style="font-size:.75rem;color:var(--gray-400);white-space:nowrap;">Mijn klassen:</label>
<div id="klasChips" style="display:flex;gap:.3rem;flex-wrap:wrap;"></div>
<button id="btnOpenKlas" style="background:none;border:1px dashed var(--gray-500);border-radius:6px;padding:.4rem .8rem;font-size:.8rem;color:var(--gray-600);cursor:pointer;">
✎ Wijzigen
</button>
</div>
<button id="btnImportJson" class="btn btn-import" title="Importeer beoordelingen uit de vorige standalone versie van de app (JSON bestand)">
📥 Vorige beoordelingen importeren
</button>
<input type="file" id="importJsonFile" accept=".json" style="display:none">
{% if director_mode %}
<a href="/dashboard" class="btn btn-secondary" style="font-weight:600;">
← Terug naar directeurdashboard
</a>
{% else %}
<a href="/auth/logout" class="btn btn-secondary">Uitloggen</a>
{% endif %}
</div>
</div>
<div class="vak-selector">
<label>Vak:</label>
<select id="vakSelector">
<option value="">-- Kies een vak --</option>
</select>
<span id="vakProgress" style="color: var(--gray-500); font-size: 0.85rem;"></span>
</div>
<div class="stats-bar">
<div class="stat-card"><div class="stat-value" id="statTotal">-</div><div class="stat-label">Totaal</div></div>
<div class="stat-card" style="border-left: 3px solid var(--status-groen);"><div class="stat-value" id="statGroen">-</div><div class="stat-label">Groen</div></div>
<div class="stat-card" style="border-left: 3px solid var(--status-oranje);"><div class="stat-value" id="statOranje">-</div><div class="stat-label">Oranje</div></div>
<div class="stat-card" style="border-left: 3px solid var(--status-roze);"><div class="stat-value" id="statRoze">-</div><div class="stat-label">Roze</div></div>
<div class="stat-card highlight"><div class="stat-value" id="statBeoordeeld">-</div><div class="stat-label">Beoordeeld</div></div>
</div>
<!-- Legenda -->
<div class="legend-container">
<div class="legend-title">Legenda</div>
<div class="legend-grid">
<div class="legend-section">
<div class="legend-item">
<div class="status-selector status-groen" style="width:22px;height:22px;font-size:.85rem;pointer-events:none;flex-shrink:0;"></div>
<span>Doen we al</span>
</div>
<div class="legend-item">
<div class="status-selector status-oranje" style="width:22px;height:22px;font-size:.85rem;pointer-events:none;flex-shrink:0;"></div>
<span>Doen we ongeveer</span>
</div>
<div class="legend-item">
<div class="status-selector status-roze" style="width:22px;height:22px;font-size:.85rem;pointer-events:none;flex-shrink:0;"></div>
<span>Nieuw (doen we nog niet)</span>
</div>
<div class="legend-item">
<div class="status-selector status-none" style="width:22px;height:22px;font-size:.85rem;pointer-events:none;flex-shrink:0;"></div>
<span>Nog geen status</span>
</div>
</div>
<div class="legend-divider"></div>
<div class="legend-section">
<div class="legend-item">
<div class="legend-color" style="background:var(--kleur-engageren);"></div>
<span><strong>Engageren</strong></span>
</div>
<div class="legend-item">
<div class="legend-color" style="background:var(--kleur-begrijpen);"></div>
<span><strong>Begrijpen</strong></span>
</div>
<div class="legend-item">
<div class="legend-color" style="background:var(--kleur-gebruiken);"></div>
<span><strong>Gebruiken</strong></span>
</div>
</div>
</div>
</div>
<div class="filters-container">
<div class="filters-grid">
<div class="filter-group">
<label>Zoeken</label>
<input type="text" id="searchInput" placeholder="Zoek in beschrijving of code...">
</div>
<div class="filter-group">
<label>Status</label>
<select id="statusFilter">
<option value="all">Alle statussen</option>
<option value="groen">✓ Groen</option>
<option value="oranje">~ Oranje</option>
<option value="roze">! Roze</option>
<option value="none">○ Geen status</option>
</select>
</div>
<div class="filter-group">
<label>E/B/G</label>
<select id="ebgFilter">
<option value="all">Alle types</option>
<option value="engageren">Engageren</option>
<option value="begrijpen">Begrijpen</option>
<option value="gebruiken">Gebruiken</option>
</select>
</div>
<div class="filter-group">
<label>Sectie</label>
<select id="sectieFilter">
<option value="all">Alle secties</option>
</select>
</div>
<div class="filter-group" style="grid-column: span 2;">
<label>Leeftijd</label>
<div class="leeftijd-checkboxes">
{% for age in ['2,5-4','4-5','5-6','6-7','7-8','8-9','9-10','10-11','11-12'] %}
<label class="leeftijd-checkbox"><input type="checkbox" value="{{ age }}"><span>{{ age }}</span></label>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="table-container">
<div class="table-scroll">
<table>
<thead>
<tr>
<th style="width:50px;">Status</th>
<th>Code</th>
<th>E/B/G</th>
<th>Leeftijden</th>
<th>Sectie</th>
<th>Beschrijving</th>
<th class="opm-col">Opm.</th>
</tr>
</thead>
<tbody id="tableBody">
<tr><td colspan="7" class="empty-state">Selecteer een vak om te beginnen</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Klassen modal -->
<div id="klasModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center;">
<div style="background:white;border-radius:12px;padding:1.5rem;max-width:400px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.3);" class="modal-inner-box">
<h2 style="font-size:1.1rem;margin-bottom:.5rem;">📚 Mijn klassen instellen</h2>
<p style="font-size:.85rem;color:var(--gray-400);margin-bottom:1rem;">
Selecteer de klassen waarvoor jij beoordelingen invult.
</p>
<div id="klasCheckboxes" style="display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:250px;overflow-y:auto;"></div>
<div style="display:flex;gap:.5rem;justify-content:flex-end;">
<button id="btnSluitKlas" class="btn btn-secondary">Annuleren</button>
<button id="btnSlaKlas" class="btn btn-primary">Opslaan</button>
</div>
</div>
</div>
<div class="notification" id="notification"></div>
<script nonce="{{ csp_nonce() }}">
// ── State ────────────────────────────────────────────────────────────────────
let currentUser = null;
let currentVakId = null;
let vakData = {}; // cache van geladen vak JSON
let assessments = {};
let opmerkingen = {}; // { goal_id: 'tekst' } // { goal_id: status } voor huidig vak
let doelzinnen = [];
let filteredData = [];
let saveTimeout = null;
// ── Init ─────────────────────────────────────────────────────────────────────
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
else console.warn('Element niet gevonden:', id);
}
document.addEventListener('DOMContentLoaded', async () => {
bind('btnImportJson', 'click', function() { document.getElementById('importJsonFile').click(); });
bind('importJsonFile', 'change', function() { importLegacyJson(this.files[0]); this.value=''; });
bind('btnOpenKlas', 'click', openKlasModal);
bind('btnSluitKlas', 'click', closeKlasModal);
bind('btnSlaKlas', 'click', saveKlassen);
bind('vakSelector', 'change', switchVak);
bind('searchInput', 'input', applyFilters);
bind('statusFilter', 'change', applyFilters);
bind('ebgFilter', 'change', applyFilters);
bind('sectieFilter', 'change', applyFilters);
document.querySelectorAll('.leeftijd-checkboxes input').forEach(cb => cb.addEventListener('change', applyFilters));
try { await loadUser(); } catch(e) { console.error('loadUser fout:', e); }
try { await loadVakken(); } catch(e) { console.error('loadVakken fout:', e); }
});
async function loadUser() {
try {
const res = await fetch('/api/me');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
currentUser = data.user;
if (!currentUser) throw new Error('Geen gebruikersdata');
document.getElementById('userInfo').textContent =
`${currentUser.full_name}${currentUser.school?.name || ''}`;
await loadKlassen();
} catch(e) {
console.error('loadUser fout:', e);
document.getElementById('userInfo').textContent = 'Fout bij laden gebruiker';
}
}
// ── Klassen ───────────────────────────────────────────────────────────────────
let allKlassen = [];
let myKlassen = [];
async function loadKlassen() {
const res = await fetch('/api/my/classes');
if (!res.ok) return;
const data = await res.json();
allKlassen = data.all_classes || [];
myKlassen = data.my_classes || [];
renderKlasChips();
if (allKlassen.length > 0) {
document.getElementById('klasSelector').style.display = 'flex';
}
}
function renderKlasChips() {
const container = document.getElementById('klasChips');
if (!myKlassen.length) {
container.innerHTML = '<span style="font-size:.75rem;color:var(--gray-500);font-style:italic;">Geen klas</span>';
} else {
container.innerHTML = myKlassen.map(c =>
`<span style="background:var(--primary);color:white;padding:.15rem .5rem;border-radius:4px;font-size:.75rem;">${c.name}</span>`
).join('');
}
}
function openKlasModal() {
const container = document.getElementById('klasCheckboxes');
container.innerHTML = allKlassen.map(c => `
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;padding:.4rem;border-radius:4px;">
<input type="checkbox" value="${c.id}"
${myKlassen.some(m => m.id === c.id) ? 'checked' : ''}>
<span>${c.name}</span>
</label>`).join('');
document.getElementById('klasModal').style.display = 'flex';
}
function closeKlasModal() {
document.getElementById('klasModal').style.display = 'none';
}
async function saveKlassen() {
const checked = [...document.querySelectorAll('#klasCheckboxes input:checked')].map(i => parseInt(i.value));
const res = await fetch('/api/my/classes', {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ class_ids: checked })
});
if (!res.ok) { showNotification('Opslaan mislukt', 'error'); return; }
const data = await res.json();
myKlassen = data.my_classes;
renderKlasChips();
closeKlasModal();
showNotification('Klassen opgeslagen', 'success');
}
async function loadVakken() {
const sel = document.getElementById('vakSelector');
try {
const res = await fetch('/api/doelen/index');
if (!res.ok) {
showNotification('Fout bij laden vakken (HTTP ' + res.status + ')', 'error');
return;
}
const data = await res.json();
if (!data.vakken?.length) {
// Geen doelen geüpload — toon duidelijke boodschap in selector
const opt = document.createElement('option');
opt.value = '';
opt.textContent = '⚠️ Geen vakken beschikbaar — beheerder moet doelen uploaden';
opt.disabled = true;
sel.appendChild(opt);
document.getElementById('tableBody').innerHTML =
`<tr><td colspan="6" style="padding:2rem;text-align:center;color:var(--gray-500);">
<strong>Geen leerdoelen beschikbaar.</strong><br><br>
Vraag je beheerder om de doelensets te uploaden via het beheerderspaneel
(<em>Beheer → Leerdoelen bestanden</em>).<br><br>
<span style="font-size:.85rem;">Heb je al beoordelingen uit de vorige versie?
Gebruik de <strong>📥 Vorige beoordelingen importeren</strong> knop hierboven
om ze te migreren zodra de vakken beschikbaar zijn.</span>
</td></tr>`;
return;
}
const sorted = [...data.vakken].sort((a,b) => (a.naam||a.id).localeCompare(b.naam||b.id, 'nl'));
sorted.forEach(v => {
const opt = document.createElement('option');
opt.value = v.id;
opt.textContent = `${v.naam || vakNaam(v.id)} (${v.aantalDoelzinnen} doelen)`;
sel.appendChild(opt);
});
} catch(e) {
console.error('loadVakken fout:', e);
showNotification('Netwerk­fout bij laden vakken', 'error');
}
}
// ── Vak wisselen ─────────────────────────────────────────────────────────────
async function switchVak() {
const vakId = document.getElementById('vakSelector').value;
if (!vakId) {
currentVakId = null; doelzinnen = []; renderEmptyState(); updateStats(); return;
}
currentVakId = vakId;
showLoading();
// Laad vak data (cache)
if (!vakData[vakId]) {
const res = await fetch(`/api/doelen/${vakId}`);
if (!res.ok) { showNotification(`Kon ${vakId} niet laden`, 'error'); return; }
vakData[vakId] = await res.json();
}
// Laad beoordelingen voor dit vak
const res2 = await fetch(`/api/assessments?vak_id=${vakId}`);
const data2 = await res2.json();
assessments = {};
opmerkingen = {};
data2.assessments.forEach(a => {
assessments[a.goal_id] = a.status;
if (a.opmerking) opmerkingen[a.goal_id] = a.opmerking;
});
processVakData(vakId);
populateSectieFilter();
resetFilters();
}
function processVakData(vakId) {
const data = vakData[vakId];
doelzinnen = [];
const sectieLookup = {};
data.rijen.forEach(r => {
if (['onderwerp','rubriek','subrubriek','subthema'].includes(r.type))
sectieLookup[r.id] = r.inhoud;
});
const miaPerDoel = {};
data.rijen.forEach(r => {
if (r.parentDoelzinId && r.type?.startsWith('MIA')) {
(miaPerDoel[r.parentDoelzinId] = miaPerDoel[r.parentDoelzinId] || []).push(r);
}
});
data.rijen.forEach(r => {
if (r.type === 'doelzin' && r.goNr) {
doelzinnen.push({
id: r.goNr, inhoud: r.inhoud, goNr: r.goNr,
kennisverwerking: r.kennisverwerking,
leeftijden: r.leeftijden || [],
sectie: r.parentId ? sectieLookup[r.parentId] : null,
mia: miaPerDoel[r.id] || []
});
}
});
}
// ── Filters ───────────────────────────────────────────────────────────────────
function applyFilters() {
if (!currentVakId) return;
const search = document.getElementById('searchInput').value.toLowerCase();
const status = document.getElementById('statusFilter').value;
const ebg = document.getElementById('ebgFilter').value;
const sectie = document.getElementById('sectieFilter').value;
const leeftijd = [...document.querySelectorAll('.leeftijd-checkboxes input:checked')].map(c => c.value);
filteredData = doelzinnen.filter(d => {
if (search && !`${d.goNr} ${d.inhoud}`.toLowerCase().includes(search)) return false;
const ds = assessments[d.id] || '';
if (status === 'none' && ds) return false;
if (status !== 'all' && status !== 'none' && ds !== status) return false;
if (ebg !== 'all' && (d.kennisverwerking||'').toLowerCase() !== ebg) return false;
if (sectie !== 'all' && d.sectie !== sectie) return false;
if (leeftijd.length > 0 && !leeftijd.some(l => d.leeftijden.includes(l))) return false;
return true;
});
renderTable();
updateStats();
}
function resetFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = 'all';
document.getElementById('ebgFilter').value = 'all';
document.getElementById('sectieFilter').value = 'all';
document.querySelectorAll('.leeftijd-checkboxes input').forEach(c => c.checked = false);
applyFilters();
}
function populateSectieFilter() {
const secties = [...new Set(doelzinnen.map(d => d.sectie).filter(Boolean))].sort();
const sel = document.getElementById('sectieFilter');
sel.innerHTML = '<option value="all">Alle secties</option>';
secties.forEach(s => { const o = document.createElement('option'); o.value = o.textContent = s; sel.appendChild(o); });
}
// ── Render ────────────────────────────────────────────────────────────────────
function renderTable() {
const tbody = document.getElementById('tableBody');
if (!filteredData.length) {
tbody.innerHTML = `<tr><td colspan="7" class="empty-state">${currentVakId ? 'Geen doelen gevonden' : 'Selecteer een vak'}</td></tr>`;
return;
}
tbody.innerHTML = filteredData.map(d => {
const s = assessments[d.id] || '';
const ebg = (d.kennisverwerking||'').toLowerCase();
return `
<tr class="${s ? 'status-'+s : ''}">
<td><button class="status-selector status-${s||'none'}" data-action="cycleStatus" data-id="${d.id}"></button></td>
<td><strong>${d.goNr}</strong></td>
<td>${ebg ? `<span class="ebg-badge ebg-${ebg}">${ebg.charAt(0).toUpperCase()+ebg.slice(1)}</span>` : '-'}</td>
<td><div class="leeftijden">${d.leeftijden.map(l=>`<span class="leeftijd-badge">${l}</span>`).join('')}</div></td>
<td>${d.sectie||'-'}</td>
<td class="beschrijving-cell">
${d.inhoud}
${renderMIA(d.mia)}
</td>
<td class="opm-cell">
<input type="text" class="opm-input" maxlength="150"
value="${escapeHtml(opmerkingen[d.id] || '')}"
data-action="saveOpmerking" data-id="${d.id}"
placeholder="..."
title="${escapeHtml(opmerkingen[d.id] || '')}">
</td>
</tr>`;
}).join('');
}
function renderMIA(items) {
if (!items?.length) return '';
const aankl = items.filter(m => m.type === 'MIA - aanklikbaar');
const niet = items.filter(m => m.type === 'MIA - niet aanklikbaar');
const titels = items.filter(m => m.type === 'MIA - titel');
if (!aankl.length && !niet.length) return '';
return `<div class="mia-container">
${titels.length ? `<strong>${titels.map(t=>t.inhoud).join(' ')}</strong>` : ''}
<div class="mia-items">
${aankl.map(m=>`<span class="mia-item">${m.inhoud}</span>`).join('')}
${niet.map(m=>`<span class="mia-item" style="opacity:.6;font-style:italic">${m.inhoud}</span>`).join('')}
</div></div>`;
}
function showLoading() {
document.getElementById('tableBody').innerHTML =
`<tr><td colspan="7" class="loading"><div class="spinner"></div>Laden...</td></tr>`;
}
function renderEmptyState() {
document.getElementById('tableBody').innerHTML =
`<tr><td colspan="7" class="empty-state">Selecteer een vak om te beginnen</td></tr>`;
}
// ── Status ────────────────────────────────────────────────────────────────────
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
function cycleStatus(goalId) {
const cycle = ['', 'groen', 'oranje', 'roze'];
const cur = assessments[goalId] || '';
const next = cycle[(cycle.indexOf(cur) + 1) % cycle.length];
if (next === '') delete assessments[goalId];
else assessments[goalId] = next;
// Optimistisch de UI updaten
renderTable();
updateStats();
// Debounced save naar API
clearTimeout(saveTimeout);
setSavingIndicator('Opslaan...');
saveTimeout = setTimeout(() => saveToApi(goalId, next), 500);
}
async function saveToApi(goalId, status) {
try {
const res = await fetch('/api/assessments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vak_id: currentVakId, goal_id: goalId, status })
});
if (!res.ok) throw new Error('Opslaan mislukt');
setSavingIndicator('✓ Opgeslagen');
setTimeout(() => setSavingIndicator(''), 2000);
} catch(e) {
showNotification('Opslaan mislukt!', 'error');
setSavingIndicator('⚠ Fout bij opslaan');
}
}
function setSavingIndicator(text) {
document.getElementById('savingIndicator').textContent = text;
}
// ── Stats ─────────────────────────────────────────────────────────────────────
function updateStats() {
const total = doelzinnen.length;
const vals = Object.values(assessments);
const groen = vals.filter(s=>s==='groen').length;
const oranje = vals.filter(s=>s==='oranje').length;
const roze = vals.filter(s=>s==='roze').length;
const pct = total > 0 ? Math.round((groen+oranje+roze)/total*100) : 0;
document.getElementById('statTotal').textContent = total || '-';
document.getElementById('statGroen').textContent = groen || '-';
document.getElementById('statOranje').textContent = oranje || '-';
document.getElementById('statRoze').textContent = roze || '-';
document.getElementById('statBeoordeeld').textContent = total > 0 ? `${pct}%` : '-';
document.getElementById('vakProgress').textContent =
currentVakId ? `${groen+oranje+roze}/${total} beoordeeld` : '';
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function vakNaam(id) {
// Naam komt uit de API (index.json via /api/doelen/index, veld 'naam').
// Deze functie is enkel een fallback als de naam niet beschikbaar is.
return id.replace(/^doelenset-bao-/, '').replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
function showNotification(msg, type='success') {
const el = document.getElementById('notification');
el.textContent = msg;
el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3000);
}
// ── 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 === 'cycleStatus') { cycleStatus(btn.dataset.id); }
});
document.addEventListener('change', function(e) {
const inp = e.target.closest('[data-action="saveOpmerking"]');
if (inp) saveOpmerking(inp.dataset.id, inp.value);
});
// ── Opmerking opslaan ─────────────────────────────────────────────────────────
let opmTimer = null;
function saveOpmerking(goalId, tekst) {
if (!currentVakId) return;
opmerkingen[goalId] = tekst.trim() || undefined;
if (!opmerkingen[goalId]) delete opmerkingen[goalId];
// Debounce: wacht 600ms voor versturen
clearTimeout(opmTimer);
opmTimer = setTimeout(async () => {
try {
await fetch('/api/assessments/opmerking', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ vak_id: currentVakId, goal_id: goalId, opmerking: tekst.trim() })
});
} catch(e) { console.error('Opmerking opslaan mislukt:', e); }
}, 600);
}
// ── Legacy JSON import (uit vorige standalone versie) ────────────────────────
async function importLegacyJson(file) {
if (!file) return;
let data;
try {
const text = await file.text();
data = JSON.parse(text);
// Valideer dat het een herkenbaar formaat is
if (!data.vakken) {
showNotification('Ongeldig bestand — geen vakken gevonden. Verwacht een export uit de Leerdoelen Tracker.', 'error');
return;
}
} catch(e) {
showNotification('Ongeldig JSON bestand', 'error'); return;
}
if (!data.vakken) { showNotification('Geen vakken gevonden in dit bestand', 'error'); return; }
showNotification('Bezig met importeren...', 'info');
try {
const res = await fetch('/api/assessments/bulk-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vakken: data.vakken })
});
const result = await res.json();
if (!res.ok) { showNotification(result.error || 'Import mislukt', 'error'); return; }
showNotification(
`Import klaar: ${result.totaal} beoordelingen geïmporteerd` +
(result.fouten > 0 ? `, ${result.fouten} fouten` : ''),
result.fouten > 0 ? 'warning' : 'success'
);
if (currentVakId) await loadAssessments(currentVakId);
updateStats();
} catch(e) {
showNotification('Netwerkfout tijdens import', 'error');
}
}
</script>
</body>
</html>