Files
leerdoelen_tracker/backend/templates/directeur.html
Sam 51c0755d67
All checks were successful
Build & Push / Build & Push image (push) Successful in 39s
Refactor styles for stat cards and update PDF export functionality
2026-03-03 10:06:21 +01:00

1352 lines
66 KiB
HTML
Raw 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>Directeur Dashboard - Leerdoelen Tracker</title>
<style>
: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-500: #6b7280; --gray-600: #4b5563;
--gray-700: #374151; --gray-800: #1f2937;
--status-groen: #10b981; --status-oranje: #f59e0b; --status-roze: #ec4899;
}
* { 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: 1600px; margin: 0 auto; padding: 1rem; }
.header { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
.header h1 { font-size: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; }
.btn-light { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); }
.btn-light:hover { background: rgba(255,255,255,0.3); }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: var(--gray-200); color: var(--gray-700); }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.75rem; margin-bottom: 0; }
.stat-card {
background: white; border-radius: 10px; padding: 1.1rem 1rem;
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
border-top: 3px solid transparent; transition: transform 0.15s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card.highlight { border-top-color: var(--primary); }
.stat-card.accent-groen { border-top-color: var(--status-groen); }
.stat-card.accent-oranje { border-top-color: var(--status-oranje); }
.stat-card.accent-roze { border-top-color: var(--status-roze); }
.stat-value { font-size: 2rem; font-weight: 700; line-height: 1.1; }
.stat-card.accent-groen .stat-value { color: var(--status-groen); }
.stat-card.accent-oranje .stat-value { color: var(--status-oranje); }
.stat-card.accent-roze .stat-value { color: var(--status-roze); }
.stat-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gray-500); margin-top: 0.3rem; }
.section { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.section h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; }
.status-selector { width: 22px; height: 22px; border-radius: 5px; border: 2px solid var(--gray-300); display: inline-flex; align-items: center; justify-content: center; font-size: .85rem; font-weight: bold; flex-shrink: 0; }
.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: '!'; }
.status-selector.status-none { background: white; border-color: var(--gray-300); color: var(--gray-400); }
.status-selector.status-none::after { content: '○'; }
.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; white-space: nowrap; }
.legend-color { width: 18px; height: 18px; border-radius: 4px; flex-shrink: 0; }
.legend-divider { width: 1px; height: 28px; background: var(--gray-300); margin: 0 .5rem; }
.filters-bar { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: 1rem; }
.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 select, .filter-group input { padding: 0.5rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.9rem; min-width: 150px; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
thead { background: var(--gray-50); }
th { padding: 0.75rem 0.5rem; text-align: center; font-weight: 600; color: var(--gray-600); border-bottom: 2px solid var(--gray-200); white-space: nowrap; }
th.goal-header { text-align: left; min-width: 250px; }
th.teacher-header { min-width: 80px; font-size: 0.75rem; }
td { padding: 0.5rem; border-bottom: 1px solid var(--gray-100); text-align: center; }
td.goal-cell { text-align: left; }
tr:hover { background: var(--gray-50); }
.goal-code { font-weight: 600; color: var(--gray-700); }
.goal-desc { font-size: 0.8rem; color: var(--gray-500); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-indicator { width: 24px; height: 24px; border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; }
.status-indicator.groen { background: var(--status-groen); color: white; }
.status-indicator.oranje { background: var(--status-oranje); color: white; }
.status-indicator.roze { background: var(--status-roze); color: white; }
.status-indicator.none { background: var(--gray-200); color: var(--gray-400); }
.progress-bar { height: 8px; background: var(--gray-200); border-radius: 4px; overflow: hidden; min-width: 60px; }
.progress-bar-inner { height: 100%; display: flex; }
.p-groen { background: var(--status-groen); }
.p-oranje { background: var(--status-oranje); }
.p-roze { background: var(--status-roze); }
.leeftijd-badge { font-size: 0.7rem; padding: 0.15rem 0.35rem; background: var(--gray-200); border-radius: 3px; }
.empty-state { text-align: center; padding: 3rem; color: var(--gray-500); }
.loading { text-align: center; padding: 3rem; color: var(--gray-500); }
.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); }
/* Leerkrachten beheer */
.teacher-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
.teacher-chip { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.75rem; background: var(--gray-100); border-radius: 9999px; font-size: 0.85rem; }
.teacher-chip .name { font-weight: 500; }
.teacher-chip .klas { color: var(--gray-500); }
.teacher-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* Leerkrachten sectie */
.teachers-section { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.teachers-section h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.action-bar { display: flex; gap: 0.5rem; flex-wrap: wrap; }
/* Statistieken */
.stats-overview { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stats-overview h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; }
/* Vak statistieken */
.vak-stats h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; }
.vak-card { background: var(--gray-50); border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; }
.vak-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
.vak-card-header h3 { font-size: 1rem; color: var(--gray-800); }
.vak-card-header .percentage { font-weight: 600; color: var(--primary); }
.vak-card-stats { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--gray-600); }
.vak-card-stats span { display: flex; align-items: center; gap: 0.25rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.dot-groen { background: var(--status-groen); }
.dot-oranje { background: var(--status-oranje); }
.dot-roze { background: var(--status-roze); }
/* Progress bars vak stats */
.progress-groen { background: var(--status-groen); }
.progress-oranje { background: var(--status-oranje); }
.progress-roze { background: var(--status-roze); }
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,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.5rem; max-width: 450px; width: 90%; }
.modal h2 { margin-bottom: 1rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-size: 0.85rem; font-weight: 600; color: var(--gray-700); margin-bottom: 0.35rem; }
.form-group input { width: 100%; padding: 0.6rem 0.75rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.9rem; }
.modal-buttons { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
@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; }
.stat-card.accent-groen .stat-value { color: #34d399 !important; }
.stat-card.accent-oranje .stat-value { color: #fbbf24 !important; }
.stat-card.accent-roze .stat-value { color: #f472b6 !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 */ }
/* Nieuwe layout blokken */
.teachers-section { background: #1e293b !important; }
.stats-overview { background: #1e293b !important; }
.teacher-chip { background: #334155 !important; color: #e2e8f0 !important; }
.teacher-chip .klas { color: #94a3b8 !important; }
.vak-card-header h3 { color: #e2e8f0 !important; }
.vak-card-stats { color: #94a3b8 !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; }
/* Legend container */
.legend-container { background: #1e293b !important; }
.legend-title { color: #94a3b8 !important; }
.legend-divider { background: #334155 !important; }
.status-selector.status-none { background: #1e293b !important; border-color: #475569 !important; 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div>
<h1>🏫 Directeur Dashboard <span class="version-badge">v4.0</span></h1>
<div style="opacity:0.85;font-size:0.85rem;margin-top:0.25rem;" id="schoolInfo">Schooloverzicht van alle leerdoelen en leerkrachten</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.7rem;opacity:0.75;text-transform:uppercase;letter-spacing:0.05em;">Schooljaar</label>
<select id="jaarSelector"
style="padding:0.35rem 0.6rem;border:1px solid rgba(255,255,255,0.3);border-radius:6px;background:rgba(255,255,255,0.15);color:white;font-size:0.85rem;cursor:pointer;">
<option value="">Laden...</option>
</select>
</div>
<button id="btnVernieuw" class="btn btn-light">↻ Vernieuwen</button>
<a href="/auth/logout" class="btn btn-light">Uitloggen</a>
</div>
</div>
<!-- Schoolbrede statistieken -->
<div class="stats-overview" id="statsOverview">
<h2>📊 Schoolbrede statistieken</h2>
<div class="stats-grid" id="statsGrid">
<div class="stat-card highlight"><div class="stat-value" id="statTeachers">-</div><div class="stat-label">Leerkrachten</div></div>
<div class="stat-card"><div class="stat-value" id="statVakken">-</div><div class="stat-label">Vakken</div></div>
<div class="stat-card"><div class="stat-value" id="statBeoordeeld">-</div><div class="stat-label">Beoordelingen</div></div>
<div class="stat-card accent-groen"><div class="stat-value" id="statGroen">-</div><div class="stat-label">Groen</div></div>
<div class="stat-card accent-oranje"><div class="stat-value" id="statOranje">-</div><div class="stat-label">Oranje</div></div>
<div class="stat-card accent-roze"><div class="stat-value" id="statRoze">-</div><div class="stat-label">Roze</div></div>
</div>
</div>
<!-- Leerkrachten sectie -->
<div class="teachers-section" id="teachersSection">
<h2>
<svg width="20" height="20" 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"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
Leerkrachten
</h2>
<div class="teacher-chips" id="teacherChips"></div>
<div class="action-bar" style="margin-top:1rem;">
<button id="btnAddTeacher" class="btn btn-primary">+ Leerkracht toevoegen</button>
<button id="btnExportCSV" class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Exporteer CSV
</button>
<button id="btnExportPDF" class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Exporteer PDF
</button>
</div>
</div>
<!-- Statistieken per vak -->
<div class="vak-stats section" id="vakStats" style="display:none;">
<h2>📚 Statistieken per vak</h2>
<div id="vakStatsContent"></div>
</div>
<!-- Tab navigatie -->
<div style="display:flex;gap:.25rem;margin-bottom:.5rem;">
<button class="tab-btn active" id="tab-doelen">📋 Doelen</button>
<button class="tab-btn" id="tab-klassen">🏫 Klasoverzicht</button>
<button class="tab-btn" id="tab-vergelijk">⚖️ Klasvergelijking</button>
</div>
<!-- Tab: Doelen -->
<div id="panel-doelen" class="section">
<!-- 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="pointer-events:none;flex-shrink:0;"></div>
<span>Doen we al</span>
</div>
<div class="legend-item">
<div class="status-selector status-oranje" style="pointer-events:none;flex-shrink:0;"></div>
<span>Doen we ongeveer</span>
</div>
<div class="legend-item">
<div class="status-selector status-roze" style="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="pointer-events:none;flex-shrink:0;"></div>
<span>Niet beoordeeld</span>
</div>
</div>
<div class="legend-divider"></div>
<div class="legend-section">
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-groen);flex-shrink:0;"></div><span>Groen = consensus</span></div>
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-oranje);flex-shrink:0;"></div><span>Oranje = gedeeltelijk</span></div>
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-roze);flex-shrink:0;"></div><span>Roze = nog te doen</span></div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filters-bar">
<div class="filter-group">
<label>Vak</label>
<select id="filterVak">
<option value="all">Alle vakken</option>
</select>
</div>
<div class="filter-group">
<label>Leerkracht</label>
<select id="filterTeacher">
<option value="all">Alle leerkrachten</option>
</select>
</div>
<div class="filter-group">
<label>Klas</label>
<select id="filterKlas">
<option value="all">Alle klassen</option>
</select>
</div>
<div class="filter-group">
<label>Status</label>
<select id="filterStatus">
<option value="all">Alle statussen</option>
<option value="groen">✓ Groen (minstens 1)</option>
<option value="oranje">~ Oranje (minstens 1)</option>
<option value="roze">! Roze (minstens 1)</option>
<option value="verschil">⚠ Verschillen</option>
<option value="niemand">○ Geen status</option>
</select>
</div>
<div class="filter-group">
<label>Sectie</label>
<select id="filterSectie">
<option value="all">Alle secties</option>
</select>
</div>
<div class="filter-group">
<label>Zoeken</label>
<input type="text" id="filterSearch" placeholder="Code of beschrijving...">
</div>
<div class="filter-group" style="min-width:unset;">
<label>Leeftijd</label>
<div style="display:flex;flex-wrap:wrap;gap:.3rem;">
<label class="leeftijd-checkbox"><input type="checkbox" value="2,5-4"><span>2,5-4</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="4-5"><span>4-5</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="5-6"><span>5-6</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="6-7"><span>6-7</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="7-8"><span>7-8</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="8-9"><span>8-9</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="9-10"><span>9-10</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="10-11"><span>10-11</span></label>
<label class="leeftijd-checkbox"><input type="checkbox" value="11-12"><span>11-12</span></label>
</div>
</div>
</div>
<!-- Tabel -->
<div class="table-scroll">
<table>
<thead id="tableHead">
<tr><th class="goal-header">Doel</th><th>Samenvatting</th></tr>
</thead>
<tbody id="tableBody">
<tr><td colspan="3" class="loading">Laden...</td></tr>
</tbody>
</table>
</div>
<!-- Legenda tabel onderaan -->
<div class="legend" style="padding:1rem 1.5rem;border-top:1px solid var(--gray-200);display:flex;gap:1.5rem;flex-wrap:wrap;font-size:0.8rem;color:var(--gray-600);">
<div class="legend-item"><div class="status-indicator groen"></div><span>Doen we al</span></div>
<div class="legend-item"><div class="status-indicator oranje">~</div><span>Doen we ongeveer</span></div>
<div class="legend-item"><div class="status-indicator roze">!</div><span>Nieuw</span></div>
<div class="legend-item"><div class="status-indicator none"></div><span>Niet beoordeeld</span></div>
</div>
</div><!-- /panel-doelen -->
<!-- Tab: Klasoverzicht -->
<div id="panel-klassen" class="section" style="display:none;">
<div id="klasOverzichtContent">
<p style="color:var(--gray-400);font-style:italic;">Laden...</p>
</div>
</div>
<!-- Tab: Klasvergelijking -->
<div id="panel-vergelijk" class="section" style="display:none;">
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;">
<div class="filter-group">
<label>Klas A</label>
<select id="vergelijkKlasA"><option value="">-- Kies klas --</option></select>
</div>
<div class="filter-group">
<label>Klas B</label>
<select id="vergelijkKlasB"><option value="">-- Kies klas --</option></select>
</div>
<div class="filter-group">
<label>Vak</label>
<select id="vergelijkVak"><option value="">Alle vakken</option></select>
</div>
</div>
<div id="vergelijkContent">
<p style="color:var(--gray-400);font-style:italic;">Selecteer twee klassen om te vergelijken.</p>
</div>
</div>
<!-- Empty state -->
<div class="empty-state" id="emptyState" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="80" height="80" style="opacity:0.4;margin-bottom:1rem;">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 style="font-size:1.1rem;margin-bottom:0.5rem;color:var(--gray-700);">Nog geen data beschikbaar</h3>
<p>Nog geen leerkrachten hebben doelen beoordeeld.</p>
</div>
<footer class="footer">💡 Gegevens worden automatisch geladen van de server.</footer>
</div><!-- /container -->
<!-- Modal: leerkracht toevoegen -->
<div class="modal-overlay" id="addTeacherModal">
<div class="modal">
<script nonce="{{ csp_nonce() }}">
function bind(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
}
let teachers = [];
let allGoals = {};
let vakData = {};
let overviewData = null;
let activeYearId = null; // null = huidig actief jaar
document.addEventListener('DOMContentLoaded', async () => {
bind('jaarSelector', 'change', switchJaar);
bind('btnVernieuw', 'click', loadOverview);
bind('btnAddTeacher', 'click', openAddTeacher);
bind('btnExportCSV', 'click', exportToCSV);
bind('btnExportPDF', 'click', exportToPDF);
bind('tab-doelen', 'click', () => switchTab('doelen'));
bind('tab-klassen', 'click', () => switchTab('klassen'));
document.getElementById('tab-vergelijk') && bind('tab-vergelijk', 'click', () => switchTab('vergelijk'));
bind('btnCancelTeacher', 'click', closeModal);
bind('btnConfirmTeacher', 'click', addTeacher);
bind('filterVak', 'change', applyFilters);
bind('filterTeacher', 'change', applyFilters);
document.getElementById('filterKlas') && bind('filterKlas', 'change', applyFilters);
bind('filterStatus', 'change', applyFilters);
bind('filterSectie', 'change', applyFilters);
bind('filterSearch', 'input', applyFilters);
document.querySelectorAll('.leeftijd-checkbox input').forEach(cb => cb.addEventListener('change', applyFilters));
await loadUser();
await loadJaren();
await loadTeachers();
await loadOverview();
});
async function loadUser() {
const res = await fetch('/api/me');
const data = await res.json();
document.getElementById('schoolInfo').textContent =
`${data.user.full_name}${data.user.school?.name || ''}`;
}
async function loadJaren() {
const res = await fetch('/api/school/years');
if (!res.ok) return;
const data = await res.json();
const sel = document.getElementById('jaarSelector');
sel.innerHTML = '';
data.years.forEach(y => {
const opt = document.createElement('option');
opt.value = y.id;
opt.textContent = y.label + (y.is_active ? ' (huidig)' : '');
if (y.is_active) { opt.selected = true; activeYearId = y.id; }
sel.appendChild(opt);
});
if (!activeYearId && data.years.length) activeYearId = data.years[0].id;
}
async function switchJaar() {
activeYearId = document.getElementById('jaarSelector').value || null;
await loadOverview();
}
async function loadTeachers() {
const res = await fetch('/api/users');
const data = await res.json();
teachers = data.users.filter(u => u.role === 'teacher');
renderTeacherList();
populateTeacherFilter();
populateKlasFilter();
}
function populateKlasFilter() {
const allKlassen = [...new Set(
teachers.flatMap(t => (t.classes || []).map(c => c.name))
)].sort();
const sel = document.getElementById('filterKlas');
sel.innerHTML = '<option value="all">Alle klassen</option>';
allKlassen.forEach(k => {
const o = document.createElement('option');
o.value = o.textContent = k;
sel.appendChild(o);
});
}
function renderTeacherList() {
// Nieuwe chips in teacherChips (nieuwe layout)
const chips = document.getElementById('teacherChips');
if (chips) {
chips.innerHTML = !teachers.length
? '<em style="color:var(--gray-500)">Nog geen leerkrachten</em>'
: teachers.map(t => `
<div class="teacher-chip">
<span class="name">${t.full_name}</span>
<span class="klas">${(t.classes||[]).map(c=>c.name).join(', ') || ''}</span>
<button data-action="removeTeacher" data-id="${t.id}"
style="width:18px;height:18px;border-radius:50%;border:none;background:var(--gray-300);cursor:pointer;font-size:0.7rem;"
title="Verwijderen">×</button>
</div>`).join('');
}
// Toon/verberg de sectie
const section = document.getElementById('teachersSection');
if (section) section.style.display = teachers.length ? 'block' : 'none';
}
function updateVakStats() {
if (!overviewData) return;
const container = document.getElementById('vakStatsContent');
const vakStats = document.getElementById('vakStats');
if (!container || !vakStats) return;
const stats = {};
Object.values(overviewData.assessments_by_teacher || {}).forEach(tv => {
Object.entries(tv).forEach(([vakId, goals]) => {
if (!stats[vakId]) stats[vakId] = { groen:0, oranje:0, roze:0, total:0 };
Object.values(goals).forEach(s => {
stats[vakId].total++;
if (s === 'groen') stats[vakId].groen++;
if (s === 'oranje') stats[vakId].oranje++;
if (s === 'roze') stats[vakId].roze++;
});
});
});
const sorted = Object.entries(stats).sort((a,b) => vakNaam(a[0]).localeCompare(vakNaam(b[0]),'nl'));
if (!sorted.length) { vakStats.style.display = 'none'; return; }
vakStats.style.display = 'block';
container.innerHTML = sorted.map(([vakId, s]) => {
const total = s.total;
const gPct = total ? s.groen / total * 100 : 0;
const oPct = total ? s.oranje / total * 100 : 0;
const rPct = total ? s.roze / total * 100 : 0;
return `
<div class="vak-card">
<div class="vak-card-header">
<h3>${vakNaam(vakId)}</h3>
<span class="percentage">${total} beoordelingen</span>
</div>
<div class="progress-bar">
<div class="progress-bar-inner">
<div class="progress-groen" style="width:${gPct}%"></div>
<div class="progress-oranje" style="width:${oPct}%"></div>
<div class="progress-roze" style="width:${rPct}%"></div>
</div>
</div>
<div class="vak-card-stats">
<span><div class="dot dot-groen"></div> ${s.groen} groen</span>
<span><div class="dot dot-oranje"></div> ${s.oranje} oranje</span>
<span><div class="dot dot-roze"></div> ${s.roze} roze</span>
</div>
</div>`;
}).join('');
}
async function loadOverview() {
const vakFilter = document.getElementById('filterVak').value || 'all';
const params = new URLSearchParams();
if (vakFilter !== 'all') params.set('vak_id', vakFilter);
if (activeYearId) params.set('year_id', activeYearId);
const url = `/api/school/overview?${params.toString()}`;
const res = await fetch(url);
if (!res.ok) { showNotification('Kon overzicht niet laden', 'error'); return; }
overviewData = await res.json();
// Laad vak data voor doelomschrijvingen
await loadVakData();
updateStats();
updateVakStats();
populateVakFilter();
populateTeacherFilter();
populateSectieFilter();
applyFilters();
}
async function loadVakData() {
// Verzamel unieke vakken uit assessments
const vakIds = new Set();
Object.values(overviewData.assessments_by_teacher).forEach(teacherVakken => {
Object.keys(teacherVakken).forEach(v => vakIds.add(v));
});
for (const vakId of vakIds) {
if (!vakData[vakId]) {
const res = await fetch(`/api/doelen/${vakId}`);
if (res.ok) {
vakData[vakId] = await res.json();
processVakGoals(vakId);
}
}
}
}
function processVakGoals(vakId) {
const data = vakData[vakId];
if (!data?.rijen) return;
const sectieLookup = {};
data.rijen.forEach(r => {
if (['onderwerp','rubriek','subrubriek','subthema'].includes(r.type))
sectieLookup[r.id] = r.inhoud;
});
allGoals[vakId] = data.rijen
.filter(r => r.type === 'doelzin' && r.goNr)
.map(r => ({
id: r.goNr, goNr: r.goNr, inhoud: r.inhoud,
sectie: r.parentId ? sectieLookup[r.parentId] : null,
leeftijden: r.leeftijden || []
}));
}
function updateStats() {
const byTeacher = overviewData.assessments_by_teacher;
const vakken = new Set();
let groen=0, oranje=0, roze=0;
Object.values(byTeacher).forEach(tv => {
Object.entries(tv).forEach(([vakId, goals]) => {
vakken.add(vakId);
Object.values(goals).forEach(s => {
if (s==='groen') groen++;
else if (s==='oranje') oranje++;
else if (s==='roze') roze++;
});
});
});
document.getElementById('statTeachers').textContent = overviewData.teachers.length;
document.getElementById('statVakken').textContent = vakken.size;
document.getElementById('statBeoordeeld').textContent = groen+oranje+roze;
document.getElementById('statGroen').textContent = groen;
document.getElementById('statOranje').textContent = oranje;
document.getElementById('statRoze').textContent = roze;
}
function populateVakFilter() {
const vakIds = new Set(Object.keys(allGoals));
const sel = document.getElementById('filterVak');
const cur = sel.value;
sel.innerHTML = '<option value="all">Alle vakken</option>';
[...vakIds].sort((a,b) => vakNaam(a).localeCompare(vakNaam(b),'nl')).forEach(v => {
const o = document.createElement('option');
o.value = v; o.textContent = vakNaam(v);
sel.appendChild(o);
});
sel.value = cur;
}
function populateTeacherFilter() {
const sel = document.getElementById('filterTeacher');
sel.innerHTML = '<option value="all">Alle leerkrachten</option>';
teachers.forEach(t => {
const o = document.createElement('option');
o.value = t.id; o.textContent = t.full_name;
sel.appendChild(o);
});
}
function populateSectieFilter() {
const sel = document.getElementById('filterSectie');
if (!sel) return;
const cur = sel.value;
const secties = new Set();
Object.values(allGoals).forEach(goals => {
goals.forEach(g => { if (g.sectie) secties.add(g.sectie); });
});
sel.innerHTML = '<option value="all">Alle secties</option>';
[...secties].sort((a, b) => a.localeCompare(b, 'nl')).forEach(s => {
const o = document.createElement('option');
o.value = o.textContent = s;
sel.appendChild(o);
});
sel.value = cur || 'all';
}
function applyFilters() {
if (!overviewData) return;
const vakFilter = document.getElementById('filterVak').value;
const teacherFilter = document.getElementById('filterTeacher').value;
const statusFilter = document.getElementById('filterStatus').value;
const sectieFilter = document.getElementById('filterSectie')?.value || 'all';
const search = document.getElementById('filterSearch').value.toLowerCase();
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
const shownTeachers = teacherFilter === 'all'
? overviewData.teachers
: overviewData.teachers.filter(t => t.id == teacherFilter);
const shownVakken = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
// Build header
let hdr = `<tr><th class="goal-header">Doel</th><th>Leeftijden</th>`;
shownTeachers.forEach(t => {
hdr += `<th class="teacher-header">${t.full_name}</th>`;
});
hdr += '<th>Samenvatting</th></tr>';
document.getElementById('tableHead').innerHTML = hdr;
// Build rows
const rows = [];
shownVakken.forEach(vakId => {
(allGoals[vakId] || []).forEach(goal => {
if (search && !`${goal.goNr} ${goal.inhoud}`.toLowerCase().includes(search)) return;
if (leeftijdFilter.length > 0 && !leeftijdFilter.some(l => (goal.leeftijden||[]).includes(l))) return;
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
const statussen = shownTeachers.map(t => {
return overviewData.assessments_by_teacher[t.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 === 'niemand' && statussen.some(s => s)) return;
if (statusFilter === 'verschil') {
const filled = statussen.filter(s => s);
if (filled.length <= 1 || new Set(filled).size <= 1) return;
}
rows.push({ vakId, goal, statussen });
});
});
if (!rows.length) {
document.getElementById('tableBody').innerHTML =
`<tr><td colspan="${shownTeachers.length+3}" class="empty-state">Geen doelen gevonden</td></tr>`;
return;
}
document.getElementById('tableBody').innerHTML = rows.map(({ goal, statussen }) => {
const groen = statussen.filter(s=>s==='groen').length;
const oranje = statussen.filter(s=>s==='oranje').length;
const roze = statussen.filter(s=>s==='roze').length;
const total = statussen.length || 1;
let row = `<tr>
<td class="goal-cell">
<div class="goal-code">${goal.goNr}</div>
<div class="goal-desc" title="${goal.inhoud}">${goal.inhoud}</div>
</td>
<td>${goal.leeftijden.map(l=>`<span class="leeftijd-badge">${l}</span>`).join(' ')}</td>`;
statussen.forEach(s => {
const sym = s==='groen'?'✓':s==='oranje'?'~':s==='roze'?'!':'○';
row += `<td><div class="status-indicator ${s||'none'}">${sym}</div></td>`;
});
row += `<td>
<div class="progress-bar">
<div class="progress-bar-inner">
<div class="p-groen" style="width:${groen/total*100}%"></div>
<div class="p-oranje" style="width:${oranje/total*100}%"></div>
<div class="p-roze" style="width:${roze/total*100}%"></div>
</div>
</div>
</td></tr>`;
return row;
}).join('');
}
// ── Leerkrachten beheer ───────────────────────────────────────────────────────
function openAddTeacher() {
document.getElementById('addTeacherModal').classList.add('active');
}
function closeModal() {
document.getElementById('addTeacherModal').classList.remove('active');
}
async function addTeacher() {
const errorEl = document.getElementById('addTeacherError');
errorEl.style.display = 'none';
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: document.getElementById('newFirstName').value,
last_name: document.getElementById('newLastName').value,
email: document.getElementById('newEmail').value,
password: document.getElementById('newPassword').value,
})
});
const data = await res.json();
if (!res.ok) {
errorEl.textContent = data.error;
errorEl.style.display = 'block';
return;
}
closeModal();
showNotification(`${data.user.full_name} toegevoegd!`, 'success');
await loadTeachers();
await loadOverview();
}
async function removeTeacher(userId) {
if (!confirm('Leerkracht deactiveren?')) return;
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
showNotification('Leerkracht verwijderd', 'success');
await loadTeachers();
await loadOverview();
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function vakNaam(id) {
return id.replace(/^doelenset-bao-/, '').replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
function exportToCSV() {
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
const shownVakken = Object.keys(allGoals);
const shownTeachers = overviewData.teachers;
let csv = 'Vak,Doelnummer,Omschrijving,Leeftijden';
shownTeachers.forEach(t => { csv += `,"${t.full_name}"`; });
csv += ',Groen,Oranje,Roze\n';
shownVakken.forEach(vakId => {
(allGoals[vakId] || []).forEach(goal => {
const statussen = shownTeachers.map(t =>
overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '');
const groen = statussen.filter(s => s === 'groen').length;
const oranje = statussen.filter(s => s === 'oranje').length;
const roze = statussen.filter(s => s === 'roze').length;
const desc = (goal.inhoud || '').replace(/"/g, '""');
csv += `"${vakNaam(vakId)}","${goal.goNr}","${desc}","${(goal.leeftijden||[]).join(', ')}"`;
statussen.forEach(s => { csv += `,"${s || '-'}"`; });
csv += `,${groen},${oranje},${roze}\n`;
});
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Leerdoelen_Export_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a); a.click();
document.body.removeChild(a); URL.revokeObjectURL(url);
showNotification('CSV geëxporteerd!', 'success');
}
function exportToPDF() {
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l', 'mm', 'a4');
const vakFilter = document.getElementById('filterVak').value;
const teacherFilter = document.getElementById('filterTeacher').value;
const statusFilter = document.getElementById('filterStatus').value;
const sectieFilter = document.getElementById('filterSectie')?.value || 'all';
const search = document.getElementById('filterSearch').value.toLowerCase();
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
const shownVakken = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
const shownTeachers = teacherFilter === 'all'
? overviewData.teachers
: overviewData.teachers.filter(t => t.id == teacherFilter);
// Titel
doc.setFontSize(16);
doc.text('Leerdoelen Overzicht — Directeur', 14, 15);
doc.setFontSize(9);
doc.setTextColor(100);
doc.text(`Export: ${new Date().toLocaleDateString('nl-BE')}`, 14, 21);
// Headers
const headers = ['Vak', 'Code', 'Omschrijving', 'Leeftijd',
...shownTeachers.map(t => t.full_name), '✓', '~', '!'];
// Rijen
const rows = [];
shownVakken.forEach(vakId => {
(allGoals[vakId] || []).forEach(goal => {
if (search && !`${goal.goNr} ${goal.inhoud} ${goal.sectie||''}`.toLowerCase().includes(search)) return;
if (leeftijdFilter.length && !leeftijdFilter.some(l => (goal.leeftijden||[]).includes(l))) return;
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
const statussen = shownTeachers.map(t =>
overviewData.assessments_by_teacher[t.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 === 'niemand' && statussen.some(s => s)) return;
if (statusFilter === 'verschil') {
const filled = statussen.filter(s => s);
if (filled.length <= 1 || new Set(filled).size <= 1) return;
}
const groen = statussen.filter(s => s === 'groen').length;
const oranje = statussen.filter(s => s === 'oranje').length;
const roze = statussen.filter(s => s === 'roze').length;
rows.push([
vakNaam(vakId),
goal.goNr,
(goal.inhoud || '').substring(0, 70) + (goal.inhoud?.length > 70 ? '…' : ''),
(goal.leeftijden || []).join(', '),
...statussen.map(s => s === 'groen' ? '✓' : s === 'oranje' ? '~' : s === 'roze' ? '!' : '-'),
groen, oranje, roze
]);
});
});
doc.autoTable({
head: [headers],
body: rows,
startY: 26,
styles: { fontSize: 7, cellPadding: 2, overflow: 'linebreak' },
headStyles: { fillColor: [79, 70, 229], textColor: 255, fontSize: 7 },
columnStyles: {
0: { cellWidth: 22 },
1: { cellWidth: 14 },
2: { cellWidth: 'auto' },
3: { cellWidth: 18 },
},
margin: { left: 10, right: 10 },
didParseCell: (data) => {
// Kleur de status cellen
if (data.section === 'body' && data.column.index >= 4) {
const v = data.cell.raw;
if (v === '✓') { data.cell.styles.textColor = [16, 185, 129]; data.cell.styles.fontStyle = 'bold'; }
if (v === '~') { data.cell.styles.textColor = [245, 158, 11]; data.cell.styles.fontStyle = 'bold'; }
if (v === '!') { data.cell.styles.textColor = [236, 72, 153]; data.cell.styles.fontStyle = 'bold'; }
}
}
});
doc.save(`Leerdoelen_Directeur_${new Date().toISOString().split('T')[0]}.pdf`);
showNotification('PDF geëxporteerd!', 'success');
}
function showNotification(msg, type='success') {
const el = document.getElementById('notification');
el.textContent = msg;
el.className = `notification ${type} show`;
setTimeout(() => el.classList.remove('show'), 3000);
}
// ── Tab navigatie ─────────────────────────────────────────────────────────────
function switchTab(tab) {
['doelen', 'klassen', 'vergelijk'].forEach(t => {
document.getElementById(`panel-${t}`).style.display = t === tab ? 'block' : 'none';
document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
});
if (tab === 'klassen') renderKlasOverzicht();
if (tab === 'vergelijk') setupVergelijking();
}
// ── Klasoverzicht ─────────────────────────────────────────────────────────────
function renderKlasOverzicht() {
if (!overviewData) return;
const container = document.getElementById('klasOverzichtContent');
// Groepeer leerkrachten per klas
const klasMap = {}; // { klasNaam: [teacher, ...] }
const noKlas = [];
overviewData.teachers.forEach(t => {
const klassen = t.classes || [];
if (!klassen.length) {
noKlas.push(t);
} else {
klassen.forEach(c => {
(klasMap[c.name] = klasMap[c.name] || []).push(t);
});
}
});
if (!Object.keys(klasMap).length && !noKlas.length) {
container.innerHTML = '<p style="color:var(--gray-400);font-style:italic;">Geen klassen gevonden. Koppel leerkrachten aan klassen via het ICT-beheerscherm.</p>';
return;
}
const byTeacher = overviewData.assessments_by_teacher;
function teacherStats(t) {
const vakken = byTeacher[t.id] || {};
let groen=0, oranje=0, roze=0, total=0;
Object.values(vakken).forEach(goals => {
Object.values(goals).forEach(s => {
total++;
if (s==='groen') groen++;
else if (s==='oranje') oranje++;
else if (s==='roze') roze++;
});
});
return { groen, oranje, roze, total, beoordeeld: groen+oranje+roze };
}
function klasCard(naam, leraren) {
const totaalDoelen = Object.keys(allGoals).reduce((sum, v) => sum + (allGoals[v]?.length || 0), 0);
const rows = leraren.map(t => {
const s = teacherStats(t);
const pct = totaalDoelen > 0 ? Math.round(s.beoordeeld / totaalDoelen * 100) : 0;
return `
<div class="klas-progress-row">
<span class="klas-label" title="${t.email}">${t.full_name.split(' ')[0]}</span>
<div class="klas-progress-bar">
<div style="width:${totaalDoelen>0?s.groen/totaalDoelen*100:0}%;background:var(--status-groen)"></div>
<div style="width:${totaalDoelen>0?s.oranje/totaalDoelen*100:0}%;background:var(--status-oranje)"></div>
<div style="width:${totaalDoelen>0?s.roze/totaalDoelen*100:0}%;background:var(--status-roze)"></div>
</div>
<span class="klas-pct">${pct}%</span>
</div>`;
}).join('');
const heeftBeoordeeld = leraren.filter(t => teacherStats(t).beoordeeld > 0).length;
const badge = heeftBeoordeeld === leraren.length
? `<span style="background:#d1fae5;color:#065f46;font-size:.72rem;padding:.15rem .5rem;border-radius:4px;">✓ Iedereen actief</span>`
: heeftBeoordeeld > 0
? `<span style="background:#fef3c7;color:#92400e;font-size:.72rem;padding:.15rem .5rem;border-radius:4px;">${heeftBeoordeeld}/${leraren.length} actief</span>`
: `<span style="background:#fee2e2;color:#991b1b;font-size:.72rem;padding:.15rem .5rem;border-radius:4px;">○ Nog niemand</span>`;
return `
<div style="background:var(--gray-50);border-radius:8px;padding:1rem;margin-bottom:.75rem;">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem;">
<strong style="font-size:1rem;">🏫 ${naam}</strong>
<span style="color:var(--gray-400);font-size:.82rem;">${leraren.length} leerkracht${leraren.length!==1?'en':''}</span>
${badge}
</div>
${rows}
</div>`;
}
let html = Object.entries(klasMap).sort((a,b) => a[0].localeCompare(b[0],'nl'))
.map(([naam, leraren]) => klasCard(naam, leraren)).join('');
if (noKlas.length) {
html += `<div style="border:1px dashed var(--gray-300);border-radius:8px;padding:1rem;">
<strong style="color:var(--gray-500);font-size:.9rem;">Geen klas toegewezen (${noKlas.length})</strong>
<div style="margin-top:.5rem;">${noKlas.map(t => `<span style="font-size:.82rem;color:var(--gray-500);margin-right:.5rem;">${t.full_name}</span>`).join('')}</div>
</div>`;
}
container.innerHTML = html;
}
// ── Klasvergelijking ──────────────────────────────────────────────────────────
function setupVergelijking() {
// Vul de klas dropdowns
const allKlassen = [...new Set(
overviewData?.teachers.flatMap(t => (t.classes||[]).map(c => c.name)) || []
)].sort();
['vergelijkKlasA', 'vergelijkKlasB', 'vergelijkVak'].forEach(id => {
const sel = document.getElementById(id);
if (id.startsWith('vergelijkKlas')) {
const cur = sel.value;
sel.innerHTML = '<option value="">— Kies klas —</option>';
allKlassen.forEach(k => {
const o = document.createElement('option');
o.value = o.textContent = k;
sel.appendChild(o);
});
sel.value = cur;
} else {
// Vak select
const cur = sel.value;
sel.innerHTML = '<option value="all">Alle vakken</option>';
Object.keys(allGoals).sort((a,b) => vakNaam(a).localeCompare(vakNaam(b),'nl')).forEach(v => {
const o = document.createElement('option');
o.value = v; o.textContent = vakNaam(v);
sel.appendChild(o);
});
sel.value = cur;
}
});
renderVergelijking();
}
function renderVergelijking() {
const klasA = document.getElementById('vergelijkKlasA').value;
const klasB = document.getElementById('vergelijkKlasB').value;
const vakSel = document.getElementById('vergelijkVak').value;
const container = document.getElementById('vergelijkContent');
if (!klasA || !klasB) {
container.innerHTML = '<p style="color:var(--gray-400);font-style:italic;">Selecteer twee klassen om te vergelijken.</p>';
return;
}
if (klasA === klasB) {
container.innerHTML = '<p style="color:var(--gray-400);font-style:italic;">Kies twee verschillende klassen.</p>';
return;
}
const byTeacher = overviewData.assessments_by_teacher;
function leraarenVanKlas(naam) {
return overviewData.teachers.filter(t => (t.classes||[]).some(c => c.name === naam));
}
// Aggregeer statussen per goal per klas (gemiddeld over alle leerkrachten)
function klasStatussen(leraren, vakFilter) {
const result = {}; // { goalId: { groen: n, oranje: n, roze: n, total: n } }
const vakken = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
vakken.forEach(vakId => {
(allGoals[vakId] || []).forEach(goal => {
const key = `${vakId}:${goal.id}`;
result[key] = { label: goal.goNr, desc: goal.inhoud, vakId, goalId: goal.id, groen:0, oranje:0, roze:0, total:leraren.length };
leraren.forEach(t => {
const s = byTeacher[t.id]?.[vakId]?.[goal.id] || '';
if (s==='groen') result[key].groen++;
else if (s==='oranje') result[key].oranje++;
else if (s==='roze') result[key].roze++;
});
});
});
return result;
}
const lerarenA = leraarenVanKlas(klasA);
const lerarenB = leraarenVanKlas(klasB);
if (!lerarenA.length || !lerarenB.length) {
container.innerHTML = `<p style="color:var(--gray-400);">Een van de geselecteerde klassen heeft geen leerkrachten.</p>`;
return;
}
const statA = klasStatussen(lerarenA, vakSel);
const statB = klasStatussen(lerarenB, vakSel);
const allKeys = [...new Set([...Object.keys(statA), ...Object.keys(statB)])];
// Bereken score per goal (% beoordeeld als groen+oranje+roze)
function score(stat) {
return stat.total > 0 ? (stat.groen + stat.oranje + stat.roze) / stat.total : 0;
}
// Toon enkel goals met verschil of beide beoordeeld
const rows = allKeys.map(k => {
const a = statA[k] || { groen:0, oranje:0, roze:0, total: lerarenA.length, label: k };
const b = statB[k] || { groen:0, oranje:0, roze:0, total: lerarenB.length, label: k };
const pctA = Math.round(score(a) * 100);
const pctB = Math.round(score(b) * 100);
const diff = Math.abs(pctA - pctB);
return { key: k, a, b, pctA, pctB, diff };
}).filter(r => r.pctA > 0 || r.pctB > 0)
.sort((x, y) => y.diff - x.diff); // grootste verschil eerst
if (!rows.length) {
container.innerHTML = '<p style="color:var(--gray-400);font-style:italic;">Nog geen beoordelingen in de geselecteerde klassen.</p>';
return;
}
const diffColor = diff => diff >= 50 ? '#ef4444' : diff >= 25 ? '#f59e0b' : '#10b981';
container.innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem;">
<div style="text-align:center;padding:.75rem;background:var(--gray-50);border-radius:8px;">
<div style="font-size:1.5rem;font-weight:700;color:var(--primary);">${lerarenA.length}</div>
<div style="font-size:.8rem;color:var(--gray-500);">Leerkrachten klas ${klasA}</div>
</div>
<div style="text-align:center;padding:.75rem;background:var(--gray-50);border-radius:8px;">
<div style="font-size:1.5rem;font-weight:700;color:var(--primary);">${lerarenB.length}</div>
<div style="font-size:.8rem;color:var(--gray-500);">Leerkrachten klas ${klasB}</div>
</div>
</div>
<p style="font-size:.82rem;color:var(--gray-400);margin-bottom:.75rem;">
${rows.length} beoordeelde doelen — gesorteerd op grootste verschil. Balk toont % leerkrachten dat een status gaf.
</p>
<div style="overflow-x:auto;">
<table style="width:100%;font-size:.82rem;border-collapse:collapse;">
<thead style="background:var(--gray-50);">
<tr>
<th style="padding:.5rem;text-align:left;border-bottom:2px solid var(--gray-200);">Doel</th>
<th style="padding:.5rem;text-align:center;border-bottom:2px solid var(--gray-200);min-width:130px;">${klasA}</th>
<th style="padding:.5rem;text-align:center;border-bottom:2px solid var(--gray-200);min-width:50px;">Δ</th>
<th style="padding:.5rem;text-align:center;border-bottom:2px solid var(--gray-200);min-width:130px;">${klasB}</th>
</tr>
</thead>
<tbody>
${rows.map(r => `
<tr style="border-bottom:1px solid var(--gray-100);">
<td style="padding:.4rem .5rem;">
<strong>${r.a.label || r.key.split(':')[1]}</strong>
<div style="color:var(--gray-400);font-size:.75rem;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
title="${r.a.desc || ''}">${r.a.desc || ''}</div>
</td>
<td style="padding:.4rem .5rem;">
<div class="vergelijk-bar-wrap">
<div style="height:100%;display:flex;">
<div style="width:${r.a.total>0?r.a.groen/r.a.total*100:0}%;background:var(--status-groen)"></div>
<div style="width:${r.a.total>0?r.a.oranje/r.a.total*100:0}%;background:var(--status-oranje)"></div>
<div style="width:${r.a.total>0?r.a.roze/r.a.total*100:0}%;background:var(--status-roze)"></div>
</div>
</div>
<div style="text-align:center;font-size:.75rem;color:var(--gray-500);">${r.pctA}%</div>
</td>
<td style="text-align:center;padding:.4rem;font-weight:700;font-size:.85rem;color:${diffColor(r.diff)};">
${r.diff > 0 ? r.diff + '%' : '='}
</td>
<td style="padding:.4rem .5rem;">
<div class="vergelijk-bar-wrap">
<div style="height:100%;display:flex;">
<div style="width:${r.b.total>0?r.b.groen/r.b.total*100:0}%;background:var(--status-groen)"></div>
<div style="width:${r.b.total>0?r.b.oranje/r.b.total*100:0}%;background:var(--status-oranje)"></div>
<div style="width:${r.b.total>0?r.b.roze/r.b.total*100:0}%;background:var(--status-roze)"></div>
</div>
</div>
<div style="text-align:center;font-size:.75rem;color:var(--gray-500);">${r.pctB}%</div>
</td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
// ── 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 === 'removeTeacher') { removeTeacher(btn.dataset.id); }
});
</script>
</body>
</html>