PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -0,0 +1,217 @@
|
||||
{% 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 %}
|
||||
Reference in new issue
Block a user