Files
Pivot/templates/internen.html
2026-10-14 13:34:37 +02:00

218 lines
14 KiB
HTML
Raw Permalink 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.
{% extends "base.html" %}
{% block title %}Internen{% endblock %}
{% block page_title %}Internen{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Internen ophalen uit Informat...{% endblock %}
{% block extra_css %}
<style>
.keuze { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0; }
.keuze { flex-wrap: nowrap; }
.keuze select, .keuze input { width: auto; flex: 0 0 auto; }
.keuze input[type=date] { max-width: 165px; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.filter-bar input[type=search] { flex: 0 1 320px; }
.filter-bar select { width: auto; min-width: 0; }
.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; }
td.nowrap { white-space: nowrap; }
td.ss { text-align: center; }
.ss-teken { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 6px; font-weight: 700; }
.ss-teken.ja { background: #dcfce7; color: #15803d; }
.ss-teken.nee { background: #fee2e2; color: #b91c1c; }
.ss-teken.bezig { color: var(--text-muted, #64748b); }
.ouder { display: block; white-space: nowrap; font-size: .9em; }
.ouder .muted { font-size: .92em; }
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; }
.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>🛏️ Internen · {{ school_naam }}</h2>
<div class="actions">
<form method="GET" action="{{ url_for('internaat.internen') }}" class="keuze" onchange="showLoader(); this.submit()">
<select name="schooljaar" aria-label="Schooljaar">
{% for j in jaren %}<option value="{{ j }}" {{ 'selected' if j == schooljaar }}>Schooljaar {{ j }}</option>{% endfor %}
</select>
<label class="muted small" for="peildatum">op</label>
<input type="date" id="peildatum" name="peildatum" value="{{ peildatum.isoformat() }}" aria-label="Peildatum">
</form>
{% if rijen %}<a class="btn btn-light btn-sm" id="exportKnop" href="{{ url_for('internaat.export', schooljaar=schooljaar, peildatum=peildatum.isoformat()) }}">⬇️ CSV</a>{% endif %}
<a class="btn btn-secondary btn-sm" href="{{ url_for('internaat.internen', schooljaar=schooljaar, peildatum=peildatum.isoformat(), vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% elif not rijen %}
<p class="muted">Geen internen ingeschreven op {{ peildatum.strftime('%d-%m-%Y') }} (schooljaar {{ schooljaar }}).</p>
{% else %}
<div class="tellers">
<span>Internen <b id="tAantal">{{ rijen|length }}</b></span>
<span>Jongens <b id="tM">{{ rijen|selectattr('geslacht', 'equalto', 'M')|list|length }}</b></span>
<span>Meisjes <b id="tV">{{ rijen|selectattr('geslacht', 'equalto', 'V')|list|length }}</b></span>
<span id="ssTeller" title="Internen waarvoor geen Smartschool-gebruiker gevonden werd">Niet in Smartschool <b id="tSS">…</b></span>
<span id="ssFout" class="muted small" hidden></span>
</div>
<div class="filter-bar">
<input type="search" id="zoek" placeholder="Zoek op naam, kamer, stamnummer of woonplaats..." aria-label="Zoeken">
{% if internaten|length > 1 %}
<select id="internaat" aria-label="Internaat">
<option value="">Alle internaten</option>
{% for i in internaten %}<option>{{ i }}</option>{% endfor %}
</select>
{% endif %}
<span id="perPagina"></span>
<span class="muted small">Opgehaald uit Informat op {{ opgehaald }}</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sorteer" data-tekst>Naam <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Voornaam <span class="pijl"></span></th>
<th class="sorteer" data-tekst title="Bestaat de intern als gebruiker in Smartschool (gebruikersnaam: voornaam achternaam)?">Smartschool <span class="pijl"></span></th>
<th class="sorteer">Geboren <span class="pijl"></span></th>
<th class="sorteer" data-tekst>M/V <span class="pijl"></span></th>
{% if internaten|length > 1 %}<th class="sorteer" data-tekst>Internaat <span class="pijl"></span></th>{% endif %}
<th class="sorteer" data-tekst>Vestiging <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Kamer <span class="pijl"></span></th>
<th class="sorteer">Ingeschreven <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Woonplaats <span class="pijl"></span></th>
<th>Contactpersonen</th>
</tr>
</thead>
<tbody id="body">
{% for r in rijen %}
<tr data-internaat="{{ r.internaat }}" data-geslacht="{{ r.geslacht }}">
<td data-sort="{{ r.naam }}"><strong>{{ r.naam }}</strong>{% if r.stamnummer %}<br><span class="muted small" style="white-space:nowrap">stamnr. {{ r.stamnummer }}</span>{% endif %}</td>
<td data-sort="{{ r.voornaam }}">{{ r.voornaam }}</td>
<td class="ss" data-sort="" data-pid="{{ r.id }}" data-voornaam="{{ r.voornaam }}" data-naam="{{ r.naam }}"><span class="ss-teken bezig" title="Controleren in Smartschool…">…</span></td>
<td class="nowrap" data-sort="{{ r.geboortedatum.isoformat() if r.geboortedatum else '' }}">{{ r.geboortedatum.strftime('%d-%m-%Y') if r.geboortedatum else '' }}</td>
<td data-sort="{{ r.geslacht }}">{{ r.geslacht }}</td>
{% if internaten|length > 1 %}<td data-sort="{{ r.internaat }}">{{ r.internaat }}</td>{% endif %}
<td data-sort="{{ r.vestiging }}">{{ r.vestiging }}</td>
<td class="nowrap" data-sort="{{ r.kamer_sort }}">{{ r.kamer or '–' }}</td>
<td class="nowrap" data-sort="{{ r.begin.isoformat() if r.begin else '' }}">{{ r.begin.strftime('%d-%m-%Y') if r.begin else '' }}{% if r.einde %}<br><span class="muted small">tot {{ r.einde.strftime('%d-%m-%Y') }}</span>{% endif %}
{% if r.financiering %}<br><span class="muted small">{{ r.financiering }}</span>{% endif %}</td>
<td data-sort="{{ r.gemeente }}">{{ r.gemeente }}</td>
<td>
{% for o in r.ouders[:3] %}
<span class="ouder">{{ o.naam }}{% if o.type %} <span class="muted">({{ o.type|lower }})</span>{% endif %}
{% if o.gsm %}· <a href="tel:{{ o.gsm|replace(' ', '') }}">{{ o.gsm }}</a>{% endif %}
{% if o.email %}· <a href="mailto:{{ o.email }}" title="{{ o.email }}">✉️</a>{% endif %}</span>
{% endfor %}
</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>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
const body = document.getElementById('body');
if (!body) return;
const zoekveld = document.getElementById('zoek'), internaatSel = document.getElementById('internaat');
const exp = document.getElementById('exportKnop'), basisExport = exp ? exp.href : '';
let PER_PAGINA = perPagina('internen'), pagina = 1, zoek = '', internaat = '';
let ssKlaar = false;
function toon() {
const rijen = Array.from(body.rows).filter(tr => (!internaat || tr.dataset.internaat === internaat)
&& (!zoek || tr.textContent.toLowerCase().includes(zoek)));
document.getElementById('tAantal').textContent = rijen.length;
document.getElementById('tM').textContent = rijen.filter(tr => tr.dataset.geslacht === 'M').length;
document.getElementById('tV').textContent = rijen.filter(tr => tr.dataset.geslacht === 'V').length;
if (ssKlaar) document.getElementById('tSS').textContent = rijen.filter(tr => tr.querySelector('td.ss').dataset.sort === '0').length;
const grootte = PER_PAGINA || Infinity, paginas = Math.max(1, Math.ceil(rijen.length / grootte));
pagina = Math.min(Math.max(1, pagina), paginas);
const van = PER_PAGINA ? (pagina - 1) * PER_PAGINA : 0, tot = van + grootte;
Array.from(body.rows).forEach(tr => tr.style.display = 'none');
rijen.forEach((tr, i) => tr.style.display = (i >= van && i < tot) ? '' : 'none');
document.getElementById('teller').textContent = rijen.length
? `${van + 1}–${Math.min(tot, rijen.length)} van ${rijen.length}` : 'Niets gevonden';
if (exp) {
const u = new URL(basisExport);
if (internaat) u.searchParams.set('internaat', internaat); else u.searchParams.delete('internaat');
if (zoek) u.searchParams.set('zoek', zoek); else u.searchParams.delete('zoek');
exp.href = u.toString();
}
const nav = document.getElementById('paginering');
nav.innerHTML = '';
if (paginas <= 1) return;
const knop = (label, p, actief) => {
const b = document.createElement('button');
b.type = 'button'; b.textContent = label; b.className = 'btn-sm ' + (actief ? 'btn-secondary' : 'btn-light');
b.onclick = () => { pagina = p; toon(); };
nav.appendChild(b);
};
if (pagina > 1) { knop('«', 1); knop('‹ Vorige', 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('Volgende ›', pagina + 1); knop('»', paginas); }
}
zoekveld.addEventListener('input', () => { zoek = zoekveld.value.trim().toLowerCase(); pagina = 1; toon(); });
if (internaatSel) internaatSel.addEventListener('change', () => { internaat = internaatSel.value; 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 : 'tekst' in th.dataset;
kol = k;
document.querySelectorAll('th.sorteer').forEach(x => x.classList.remove('op', 'af'));
th.classList.add(op ? 'op' : 'af');
Array.from(body.rows).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();
}));
perPaginaKiezer(document.getElementById('perPagina'), 'internen', n => { PER_PAGINA = n; pagina = 1; toon(); });
toon();
// Smartschool: bestaat er een gebruiker "voornaam achternaam"? (op de achtergrond, 1 uur bewaard)
const cellen = Array.from(body.querySelectorAll('td.ss'));
fetch({{ url_for('internaat.internen_smartschool')|tojson }}, {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({personen: cellen.map(td => ({pid: td.dataset.pid, voornaam: td.dataset.voornaam, naam: td.dataset.naam}))})
}).then(r => r.json()).then(res => {
if (!res.ok) {
cellen.forEach(td => td.innerHTML = '<span class="muted">–</span>');
const f = document.getElementById('ssFout'); f.textContent = '⚠️ ' + res.fout; f.hidden = false;
document.getElementById('ssTeller').hidden = true;
return;
}
let nee = 0;
cellen.forEach(td => {
const gebruiker = res.status[td.dataset.pid];
td.dataset.sort = gebruiker ? '1' : '0';
td.innerHTML = gebruiker
? `<span class="ss-teken ja" title="Smartschool-gebruiker: ${gebruiker}">✓</span>`
: '<span class="ss-teken nee" title="Geen Smartschool-gebruiker gevonden">✕</span>';
if (!gebruiker) nee++;
});
ssKlaar = true; toon();
}).catch(() => cellen.forEach(td => td.innerHTML = '<span class="muted">?</span>'));
})();
</script>
{% endblock %}