PIVOT 1.1.0

This commit is contained in:
marijn committed 2026-10-14 13:34:37 +02:00
commit 8a53234eb6
161 files changed
+31109

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}SGR groepen{% endblock %}
{% block page_title %}SGR groepen{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Groepen ophalen uit Smartschool...{% endblock %}
{% block extra_css %}
<style>
.keuze { display: flex; gap: 8px; align-items: center; margin: 0; }
.keuze input { width: 240px; }
.tellers { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 6px; }
.tellers span { padding: 4px 12px; border-radius: 999px; background: var(--surface-muted); border: 1px solid var(--border); font-size: .88em; }
.tellers b { margin-left: 4px; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.filter-bar input[type=search] { flex: 0 1 340px; }
.groepen { display: grid; gap: 8px; }
details.groep { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface, #fff); }
details.groep[open] { border-color: var(--primary-color); }
details.groep > summary { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px;
cursor: pointer; list-style: none; }
details.groep > summary::-webkit-details-marker { display: none; }
details.groep > summary::before { content: "▸"; color: var(--text-muted, #64748b); transition: transform .15s; }
details.groep[open] > summary::before { transform: rotate(90deg); }
.groep .gnaam { font-weight: 600; }
.groep .pad { font-size: .85em; color: var(--text-muted, #64748b); }
.groep .aantal { margin-left: auto; padding: 2px 10px; border-radius: 999px; background: var(--primary-light);
color: var(--primary-color); font-weight: 700; font-size: .88em; white-space: nowrap; }
.groep .aantal.leeg { background: var(--surface-muted); color: var(--text-muted, #64748b); font-weight: 400; }
.groep .inhoud { padding: 0 14px 12px; }
.groep table { margin: 0; }
.rol { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .85em; background: var(--surface-muted); }
.rol.Leerling { background: #dbeafe; color: #1d4ed8; }
.rol.Leerkracht { background: #dcfce7; color: #15803d; }
mark { background: #fef08a; padding: 0; }
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="page-head">
<h2>👥 SGR groep in Smartschool</h2>
<div class="actions">
<form method="GET" action="{{ url_for('sgr_groepen.overzicht') }}" class="keuze" onsubmit="showLoader()">
<input type="text" name="groep" value="{{ groep }}" aria-label="Hoofdgroep" title="Naam of code van de Smartschool-groep">
<button type="submit" class="btn-light btn-sm">Tonen</button>
</form>
{% if groepen %}<a class="btn btn-light btn-sm" href="{{ url_for('sgr_groepen.export', groep=groep if groep != standaard else None) }}">⬇️ CSV</a>{% endif %}
<a class="btn btn-secondary btn-sm" href="{{ url_for('sgr_groepen.overzicht', groep=groep if groep != standaard else None, vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% elif not groepen %}
<p class="muted">De groep <strong>{{ hoofd.naam }}</strong> bestaat in Smartschool, maar heeft geen subgroepen.</p>
{% else %}
<div class="tellers">
<span>Subgroepen <b>{{ groepen|length }}</b></span>
<span title="Elk lidmaatschap apart geteld">Lidmaatschappen <b>{{ aantal_leden }}</b></span>
<span title="Wie in meer dan één groep zit, telt één keer">Verschillende personen <b>{{ uniek }}</b></span>
{% set leeg = groepen|selectattr('leden', 'equalto', [])|selectattr('fout', 'none')|list|length %}
{% if leeg %}<span>Zonder leden <b>{{ leeg }}</b></span>{% endif %}
</div>
<div class="filter-bar">
<input type="search" id="zoek" placeholder="Zoek een groep of een lid (naam, gebruikersnaam)..." aria-label="Zoeken">
<button type="button" class="btn-light btn-sm" id="allesOpen">Alles openklappen</button>
<button type="button" class="btn-light btn-sm" id="allesDicht">Alles dichtklappen</button>
<span class="muted small" id="zoekTeller"></span>
<span class="muted small">Opgehaald uit Smartschool op {{ opgehaald }}</span>
</div>
<div class="groepen" id="groepen">
{% for g in groepen %}
<details class="groep" style="margin-left: {{ g.diepte * 24 }}px" data-groep="{{ (g.naam ~ ' ' ~ g.code ~ ' ' ~ g.omschrijving)|lower }}">
<summary>
<span class="gnaam">{{ g.naam }}</span>
{% if g.code and g.code != g.naam %}<span class="muted small">{{ g.code }}</span>{% endif %}
{% if g.pad %}<span class="pad">in {{ g.pad|join(' › ') }}</span>{% endif %}
{% if g.fout %}
<span class="badge badge-danger" title="{{ g.fout }}">leden niet op te halen</span>
{% else %}
<span class="aantal {{ 'leeg' if not g.leden }}">{{ g.leden|length }} {{ 'lid' if g.leden|length == 1 else 'leden' }}{% if g.aantal_sub %} · {{ g.aantal_sub }} subgroep{{ 'en' if g.aantal_sub != 1 }}{% endif %}</span>
{% endif %}
</summary>
<div class="inhoud">
{% if g.omschrijving and g.omschrijving != g.naam %}<p class="muted small" style="margin:0 0 8px">{{ g.omschrijving }}</p>{% endif %}
{% if g.fout %}
<div class="alert alert-danger" style="margin:0">Leden konden niet opgehaald worden: {{ g.fout }}</div>
{% elif not g.leden %}
<p class="muted small" style="margin:0">Deze groep heeft momenteel geen rechtstreekse leden.</p>
{% else %}
<div class="table-wrap">
<table>
<thead><tr><th>Naam</th><th>Voornaam</th><th>Gebruikersnaam</th><th>Rol</th><th>E-mail</th></tr></thead>
<tbody>
{% for l in g.leden %}
<tr class="lid" data-tekst="{{ (l.naam ~ ' ' ~ l.voornaam ~ ' ' ~ l.voornaam ~ ' ' ~ l.naam ~ ' ' ~ l.gebruikersnaam ~ ' ' ~ l.email)|lower }}">
<td><strong>{{ l.naam }}</strong></td>
<td>{{ l.voornaam }}</td>
<td>{{ l.gebruikersnaam }}</td>
<td>{% if l.rol %}<span class="rol {{ l.rol }}">{{ l.rol }}</span>{% endif %}</td>
<td class="small">{{ l.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</details>
{% endfor %}
</div>
{% endif %}
<p class="field-hint" style="margin-top:14px">Toont alle groepen onder <strong>{{ groep }}</strong> in Smartschool met hun huidige,
rechtstreekse leden. De standaard hoofdgroep ({{ standaard }}) stel je in via 🔌 Koppelingen › Smartschool.
Gegevens worden 10 minuten bewaard.</p>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
const lijst = document.getElementById('groepen');
if (!lijst) return;
const groepen = Array.from(lijst.querySelectorAll('details.groep'));
const zoekveld = document.getElementById('zoek'), teller = document.getElementById('zoekTeller');
document.getElementById('allesOpen').onclick = () => groepen.forEach(d => { if (d.style.display !== 'none') d.open = true; });
document.getElementById('allesDicht').onclick = () => groepen.forEach(d => d.open = false);
zoekveld.addEventListener('input', () => {
const z = zoekveld.value.trim().toLowerCase();
let treffers = 0, zichtbaar = 0;
groepen.forEach(d => {
const naamTreffer = !z || d.dataset.groep.includes(z);
let lidTreffer = 0;
d.querySelectorAll('tr.lid').forEach(tr => {
const ok = !z || naamTreffer || tr.dataset.tekst.includes(z);
tr.style.display = ok ? '' : 'none';
if (z && tr.dataset.tekst.includes(z)) lidTreffer++;
});
const toon = naamTreffer || lidTreffer > 0;
d.style.display = toon ? '' : 'none';
if (toon) zichtbaar++;
treffers += lidTreffer;
if (z) d.open = lidTreffer > 0;
});
teller.textContent = z ? `${zichtbaar} groep(en)` + (treffers ? `, ${treffers} lid/leden gevonden` : '') : '';
});
})();
</script>
{% endblock %}