Initial commit

This commit is contained in:
Kristof committed 2026-10-14 12:29:29 +02:00
commit 536ff43ce4
158 files changed
+30355

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
{% extends "base.html" %}
{% block title %}Bingel{% endblock %}
{% block page_title %}Bingel{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Leerlingen ophalen uit Informat...{% endblock %}
{% block extra_css %}
<style>
.balk { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.balk input[type=search] { flex: 0 1 300px; margin: 0; }
.balk select { width: auto; min-width: 180px; margin: 0; }
.tellers { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 6px; }
.tellers button { width: auto; margin: 0; padding: 4px 12px; border-radius: 999px; background: var(--surface-muted);
border: 1px solid var(--border); font-size: .88em; color: inherit; font-weight: 400; cursor: pointer; }
.tellers button:hover { border-color: var(--primary-color); }
.tellers button[aria-pressed="true"] { border-color: var(--primary-color); background: var(--primary-light); color: var(--primary-color); font-weight: 600; }
.tellers b { margin-left: 4px; }
.tellers .let { background: #fef3c7; border-color: #fcd34d; }
th.sorteer { cursor: pointer; user-select: none; white-space: nowrap; }
th.sorteer:hover { color: var(--primary-color); }
th.sorteer .pijl::after { content: "↕"; opacity: .35; margin-left: 2px; }
th.sorteer.op .pijl::after { content: "▲"; opacity: 1; }
th.sorteer.af .pijl::after { content: "▼"; opacity: 1; }
td.leeg { background: #fff7ed; }
.klaskeuze { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; margin: 10px 0; }
.klaskeuze-kop { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 8px; }
.klaskeuze-kop button { margin: 0; }
.klasvinken { display: flex; flex-wrap: wrap; gap: 6px 10px; }
.klasvinken label { display: inline-flex; gap: 6px; align-items: center; margin: 0; padding: 4px 10px; font-weight: 600;
border: 1px solid var(--border); border-radius: 999px; cursor: pointer; }
.klasvinken label:has(input:checked) { border-color: var(--primary-color); background: var(--primary-light); }
.klasvinken input { width: auto; margin: 0; }
.tabel-voet { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; margin-top: 12px; }
.paginering { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="page-head">
<h2>📗 Bingel · {{ school_naam }}</h2>
<div class="actions">
{% if rijen %}<a class="btn btn-success btn-sm" id="exportKnop" href="{{ url_for('basisonderwijs.bingel_export') }}">⬇️ Excel alle klassen</a>{% endif %}
<a class="btn btn-secondary btn-sm" href="{{ url_for('basisonderwijs.bingel', vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
<p class="field-hint" style="margin-top:-6px">Alle leerlingen uit Informat, klaar voor de import in Bingel. Kolommen in de Excel:
{{ kolommen|join(', ') }}. Het wachtwoord blijft leeg; het klasnummer is alfabetisch binnen de klas (familienaam, voornaam).</p>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% elif not rijen %}
<p class="muted">Informat gaf geen leerlingen terug.</p>
{% else %}
{% if waarschuwing %}<div class="alert alert-warning">{{ waarschuwing }}</div>{% endif %}
<div class="tellers" id="tellers">
<button type="button" data-filter="" aria-pressed="true" title="Alle leerlingen tonen">Leerlingen <b>{{ rijen|length }}</b></button>
<button type="button" data-filter="" title="Alle leerlingen tonen">Klassen <b>{{ klassen|length }}</b></button>
{% if zonder_klas %}<button type="button" class="let" data-filter="klas" aria-pressed="false" title="Enkel leerlingen zonder klas">Zonder klas <b>{{ zonder_klas }}</b></button>{% endif %}
{% if zonder_geslacht %}<button type="button" class="let" data-filter="geslacht" aria-pressed="false" title="Enkel leerlingen zonder geslacht M/V">Geslacht niet M/V <b>{{ zonder_geslacht }}</b></button>{% endif %}
</div>
<div class="klaskeuze">
<div class="klaskeuze-kop">
<strong>Klassen in de lijst</strong> <span class="muted small" id="klasTeller"></span>
<button type="button" class="btn-sm btn-light" id="allesAan">Alles aanvinken</button>
<button type="button" class="btn-sm btn-light" id="allesUit">Niets</button>
</div>
<div class="klasvinken" id="klasVinken">
{% for k in klassen %}
<label><input type="checkbox" value="{{ k }}" checked> {{ k or '(geen klas)' }}
<span class="muted small">{{ rijen|selectattr('klas', 'equalto', k)|list|length }}</span></label>
{% endfor %}
</div>
</div>
<div class="balk">
<input type="search" id="zoek" placeholder="Zoek op naam..." aria-label="Zoeken">
<span id="perPagina"></span>
</div>
<div class="table-wrap">
<table>
<thead><tr>{% for k in kolommen %}<th class="{{ 'sorteer' if k != 'Wachtwoord' }}" {{ 'data-tekst' if k not in ('Klasnummer',) }}>{{ k }}{% if k != 'Wachtwoord' %} <span class="pijl"></span>{% endif %}</th>{% endfor %}</tr></thead>
<tbody id="body">
{% for r in rijen %}
<tr data-klas="{{ r.klas }}" data-geen="{{ ' '.join([('klas' if not r.klas else ''), ('geslacht' if r.geslacht not in ('M', 'V') else '')]) }}">
<td class="{{ 'leeg' if not r.klas }}" data-sort="{{ r.klas }}"><strong>{{ r.klas }}</strong></td>
<td data-sort="{{ r.voornaam }}">{{ r.voornaam }}</td>
<td data-sort="{{ r.naam }}">{{ r.naam }}</td>
<td class="muted"></td>
<td data-sort="{{ r.klasnummer if r.klasnummer is not none else '' }}">{{ r.klasnummer if r.klasnummer is not none else '' }}</td>
<td class="{{ 'leeg' if r.geslacht not in ('M', 'V') }}" data-sort="{{ r.geslacht }}">{{ r.geslacht }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tabel-voet">
<div class="muted small" id="teller"></div>
<nav class="paginering" id="paginering" aria-label="Pagina's"></nav>
</div>
{% if (zonder_klas or zonder_nummer or zonder_geslacht) and voorbeeld %}
<details style="margin-top:14px">
<summary class="muted small">Velden die Informat teruggeeft (om na te kijken waarom iets leeg blijft)</summary>
<p class="small" style="margin-top:6px"><strong>Leerling:</strong> <code>{{ voorbeeld.leerling|join(', ') }}</code></p>
<p class="small"><strong>Inschrijving:</strong> <code>{{ voorbeeld.inschrijving|join(', ') or '— geen inschrijvingen ontvangen —' }}</code></p>
{% if voorbeeld.klas_item %}<p class="small"><strong>Voorbeeld van een klas (inschrKlassen):</strong> <code>{{ voorbeeld.klas_item }}</code></p>{% endif %}
</details>
{% endif %}
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
const body = document.getElementById('body');
if (!body) return;
const rijen = Array.from(body.rows), zoekVeld = document.getElementById('zoek');
const vinken = Array.from(document.querySelectorAll('#klasVinken input'));
const gekozen = () => new Set(vinken.filter(v => v.checked).map(v => v.value));
const exp = document.getElementById('exportKnop'), basis = exp.href;
let PER = perPagina('bingel', 25), pagina = 1, filter = '';
document.querySelectorAll('#tellers button').forEach(b => b.addEventListener('click', () => {
filter = b.dataset.filter;
document.querySelectorAll('#tellers button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
pagina = 1; toon();
}));
let kol = null, op = true;
document.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
const k = th.cellIndex;
op = kol === k ? !op : true; kol = k;
document.querySelectorAll('th.sorteer').forEach(x => x.classList.remove('op', 'af'));
th.classList.add(op ? 'op' : 'af');
rijen.sort((a, b) => {
const x = a.cells[k].dataset.sort || '', y = b.cells[k].dataset.sort || '';
if (!x !== !y) return x ? -1 : 1;
const c = x.localeCompare(y, 'nl', { numeric: true, sensitivity: 'base' });
return op ? c : -c;
}).forEach(tr => body.appendChild(tr));
pagina = 1; toon();
}));
function toon() {
const k = gekozen(), z = zoekVeld.value.trim().toLowerCase();
const lijst = rijen.filter(tr => k.has(tr.dataset.klas) && (!z || tr.textContent.toLowerCase().includes(z))
&& (!filter || tr.dataset.geen.split(' ').includes(filter)));
const grootte = PER || Infinity, paginas = Math.max(1, Math.ceil(lijst.length / grootte));
pagina = Math.min(Math.max(1, pagina), paginas);
const van = PER ? (pagina - 1) * PER : 0, tot = van + grootte;
rijen.forEach(tr => tr.style.display = 'none');
lijst.forEach((tr, i) => tr.style.display = i >= van && i < tot ? '' : 'none');
document.getElementById('teller').textContent = lijst.length ? `${van + 1}–${Math.min(tot, lijst.length)} van ${lijst.length}` : 'Niets gevonden';
const nav = document.getElementById('paginering'); nav.innerHTML = '';
if (paginas > 1) {
const knop = (t, p, a) => { const b = document.createElement('button'); b.type = 'button'; b.textContent = t;
b.className = 'btn-sm ' + (a ? 'btn-secondary' : 'btn-light'); b.onclick = () => { pagina = p; toon(); }; nav.appendChild(b); };
if (pagina > 1) { knop('«', 1); knop('‹', pagina - 1); }
for (let p = Math.max(1, pagina - 2); p <= Math.min(paginas, pagina + 2); p++) knop(String(p), p, p === pagina);
if (pagina < paginas) { knop('›', pagina + 1); knop('»', paginas); }
}
}
function klassenGewijzigd() {
const k = gekozen(), alle = k.size === vinken.length;
exp.href = alle ? basis : basis + '?' + Array.from(k).map(x => 'klas=' + encodeURIComponent(x)).join('&');
exp.textContent = alle ? '⬇️ Excel alle klassen' : (k.size === 1 ? '⬇️ Excel ' + Array.from(k)[0] : `⬇️ Excel ${k.size} klassen`);
exp.classList.toggle('disabled', !k.size);
exp.style.pointerEvents = k.size ? '' : 'none';
exp.style.opacity = k.size ? '' : '.5';
document.getElementById('klasTeller').textContent = `${k.size} van ${vinken.length} aangevinkt`;
pagina = 1; toon();
}
vinken.forEach(v => v.addEventListener('change', klassenGewijzigd));
document.getElementById('allesAan').onclick = () => { vinken.forEach(v => v.checked = true); klassenGewijzigd(); };
document.getElementById('allesUit').onclick = () => { vinken.forEach(v => v.checked = false); klassenGewijzigd(); };
zoekVeld.addEventListener('input', () => { pagina = 1; toon(); });
perPaginaKiezer(document.getElementById('perPagina'), 'bingel', n => { PER = n; pagina = 1; toon(); }, 25);
klassenGewijzigd();
})();
</script>
{% endblock %}