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

214 lines
14 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.
{% extends "base.html" %}
{% block title %}Leerlingfacturatie{% endblock %}
{% block page_title %}Leerlingfacturatie{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Facturen ophalen uit Informat...{% endblock %}
{% block extra_css %}
<style>
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.filter-bar input[type=search] { flex: 0 1 300px; }
.filter-bar select { width: auto; min-width: 0; }
.statusfilter { display: flex; flex-wrap: wrap; gap: 6px; }
.statusfilter button { all: unset; box-sizing: border-box; cursor: pointer; padding: 5px 12px; border-radius: 999px;
border: 1px solid var(--border); font-size: .88em; color: var(--text-muted, #64748b); }
.statusfilter button b { margin-left: 4px; color: var(--text, #1e293b); }
.statusfilter button.actief { border-color: var(--primary-color); color: var(--primary-color); background: var(--primary-light); font-weight: 600; }
.totalen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 6px 0 14px; }
.totalen div { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; }
.totalen span { display: block; font-size: .8em; color: var(--text-muted, #64748b); text-transform: uppercase; letter-spacing: .04em; }
.totalen strong { font-size: 1.25rem; font-variant-numeric: tabular-nums; }
.totalen .open strong { color: #b91c1c; }
@media (max-width: 700px) { .totalen { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
td.bedrag, th.bedrag { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
td.nowrap { white-space: nowrap; }
td.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .86em; white-space: nowrap; }
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>🧾 Facturen leerlingen · {{ school_naam }}</h2>
<div class="actions">
<form method="GET" action="{{ url_for('facturatie.overzicht') }}" style="margin:0" 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>
</form>
{% if rijen %}<a class="btn btn-light btn-sm" id="exportKnop" href="{{ url_for('facturatie.export', schooljaar=schooljaar) }}">⬇️ CSV</a>{% endif %}
<a class="btn btn-secondary btn-sm" href="{{ url_for('facturatie.overzicht', schooljaar=schooljaar, 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 geboekte facturen gevonden voor schooljaar {{ schooljaar }}{% if groepen %} ({{ groepen|length }} factuurgroep(en) voor leerlingen){% endif %}.</p>
{% else %}
<div class="totalen" aria-live="polite">
<div><span>Facturen</span><strong id="tAantal">0</strong></div>
<div><span>Totaal</span><strong id="tTotaal">€ 0,00</strong></div>
<div><span>Betaald</span><strong id="tBetaald">€ 0,00</strong></div>
<div class="open"><span>Nog te betalen</span><strong id="tOpen">€ 0,00</strong></div>
</div>
<div class="filter-bar">
<div class="statusfilter" role="group" aria-label="Status">
<button type="button" class="actief" data-status="">Alle <b data-tel=""></b></button>
<button type="button" data-status="onbetaald">Nog te betalen <b data-tel="onbetaald"></b></button>
<button type="button" data-status="betaald">Betaald <b data-tel="betaald"></b></button>
<button type="button" data-status="credit">Creditnota's <b data-tel="credit"></b></button>
</div>
</div>
<div class="filter-bar">
<input type="search" id="zoek" placeholder="Zoek op leerling, klas, stamnummer of mededeling..." aria-label="Zoeken">
{% if groepen|length > 1 %}
<select id="groep" aria-label="Factuurgroep">
<option value="">Alle factuurgroepen</option>
{% for g in groepen %}<option value="{{ g.id }}">{{ g.naam }}</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>Leerling <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Klas <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Factuurgroep <span class="pijl"></span></th>
<th class="sorteer">Datum <span class="pijl"></span></th>
<th class="sorteer">Vervaldag <span class="pijl"></span></th>
<th class="sorteer bedrag">Totaal <span class="pijl"></span></th>
<th class="sorteer bedrag">Betaald <span class="pijl"></span></th>
<th class="sorteer bedrag">Open <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Status <span class="pijl"></span></th>
<th class="sorteer" data-tekst>Mededeling <span class="pijl"></span></th>
<th class="actions"></th>
</tr>
</thead>
<tbody id="body">
{% for r in rijen %}
{% set vervallen = r.status in ('open', 'gedeeltelijk') and r.vervaldag and r.vervaldag < today %}
<tr data-status="{{ r.status }}" data-groep="{{ r.groep_id }}" data-totaal="{{ r.totaal * r.teken }}" data-betaald="{{ r.betaald * r.teken }}" data-open="{{ r.open * r.teken }}">
<td data-sort="{{ r.naam }} {{ r.voornaam }}"><strong>{{ r.naam }}</strong> {{ r.voornaam }}{% if r.stamnummer %}<br><span class="muted small">stamnr. {{ r.stamnummer }}</span>{% endif %}</td>
<td class="nowrap" data-sort="{{ r.klas }} {{ '%03d'|format(r.klasnummer or 0) }}">{{ r.klas }}{% if r.klasnummer %} <span class="muted small">nr. {{ r.klasnummer }}</span>{% endif %}</td>
<td data-sort="{{ r.groep }}">{{ r.groep }}</td>
<td class="nowrap" data-sort="{{ r.datum.isoformat() if r.datum else '' }}">{{ r.datum.strftime('%d-%m-%Y') if r.datum else '' }}</td>
<td class="nowrap" data-sort="{{ r.vervaldag.isoformat() if r.vervaldag else '' }}">{{ r.vervaldag.strftime('%d-%m-%Y') if r.vervaldag else '' }}</td>
<td class="bedrag" data-sort="{{ '%012.2f'|format(r.totaal * r.teken + 1000000) }}">{{ '− ' if r.teken < 0 }}{{ '€ %.2f'|format(r.totaal)|replace('.', ',') }}</td>
<td class="bedrag" data-sort="{{ '%012.2f'|format(r.betaald + 1000000) }}">{{ '€ %.2f'|format(r.betaald)|replace('.', ',') }}</td>
<td class="bedrag" data-sort="{{ '%012.2f'|format(r.open + 1000000) }}">{% if r.open %}<strong>{{ '€ %.2f'|format(r.open)|replace('.', ',') }}</strong>{% else %}€ 0,00{% endif %}</td>
<td data-sort="{{ labels[r.status] }}">
{% if r.status == 'betaald' %}<span class="badge badge-success">Betaald</span>
{% elif r.status == 'credit' %}<span class="badge badge-info">Creditnota</span>
{% elif vervallen %}<span class="badge badge-danger" title="Vervaldag {{ r.vervaldag.strftime('%d-%m-%Y') }} is voorbij">Vervallen</span>
{% elif r.status == 'gedeeltelijk' %}<span class="badge badge-warning">Gedeeltelijk</span>
{% else %}<span class="badge badge-warning">Open</span>{% endif %}
</td>
<td class="mono" data-sort="{{ r.ogm }}">{% if r.ogm|length == 12 %}+++{{ r.ogm[:3] }}/{{ r.ogm[3:7] }}/{{ r.ogm[7:] }}+++{% else %}{{ r.ogm }}{% endif %}
{%- if r.ogm_factuur %}<br><span class="muted small" title="Mededeling van de factuur waarbij deze creditnota hoort">bij {{ r.ogm_factuur }}</span>{% endif %}</td>
<td class="actions">
<a class="btn btn-sm btn-light" target="_blank" rel="noopener"
href="{{ url_for('facturatie.pdf', toegangsgroep=r.toegangsgroep, invoice_id=r.id, schooljaar=schooljaar, naam=('Factuur ' ~ r.naam ~ ' ' ~ r.voornaam ~ ' ' ~ (r.datum.isoformat() if r.datum else ''))|trim) }}"
title="PDF openen">📄 PDF</a>
</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'), groepSel = document.getElementById('groep');
const exp = document.getElementById('exportKnop'), basisExport = exp ? exp.href : '';
let PER_PAGINA = perPagina('facturatie'), pagina = 1, zoek = '', status = '', groep = '';
const euro = n => '€ ' + n.toLocaleString('nl-BE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const past = (tr, st) => !st || (st === 'onbetaald' ? ['open', 'gedeeltelijk'].includes(tr.dataset.status) : tr.dataset.status === st);
const basis = tr => (!groep || tr.dataset.groep === groep) && (!zoek || tr.textContent.toLowerCase().includes(zoek));
function toon() {
const kandidaten = Array.from(body.rows).filter(basis);
document.querySelectorAll('.statusfilter b[data-tel]').forEach(b => b.textContent = kandidaten.filter(tr => past(tr, b.dataset.tel)).length);
const rijen = kandidaten.filter(tr => past(tr, status));
const som = k => rijen.reduce((t, tr) => t + parseFloat(tr.dataset[k] || 0), 0);
document.getElementById('tAantal').textContent = rijen.length;
document.getElementById('tTotaal').textContent = euro(som('totaal'));
document.getElementById('tBetaald').textContent = euro(som('betaald'));
document.getElementById('tOpen').textContent = euro(som('open'));
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';
const nav = document.getElementById('paginering');
nav.innerHTML = '';
if (exp) {
const p = new URLSearchParams(new URL(basisExport).search);
if (groep) p.set('groep', groep); if (status) p.set('status', status); if (zoek) p.set('zoek', zoek);
exp.href = basisExport.split('?')[0] + '?' + p.toString();
}
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 (groepSel) groepSel.addEventListener('change', () => { groep = groepSel.value; pagina = 1; toon(); });
document.querySelectorAll('.statusfilter button').forEach(b => b.addEventListener('click', () => {
status = b.dataset.status;
document.querySelectorAll('.statusfilter button').forEach(x => x.classList.toggle('actief', 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 : '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'), 'facturatie', n => { PER_PAGINA = n; pagina = 1; toon(); });
toon();
})();
</script>
{% endblock %}