Files
Pivot/templates/gopro.html
T
2026-10-14 12:29:29 +02:00

461 lines
32 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 %}GO! Pro{% endblock %}
{% block page_title %}GO! Pro{% endblock %}
{% block loader_text %}Personeel ophalen uit Informat...{% endblock %}
{% block extra_css %}
<style>
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; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
.filter-bar input[type=search] { flex: 1 1 260px; }
td.mail { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .86em; }
input.adres { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .84em; padding: 5px 7px;
min-width: 250px; background: transparent; }
input.adres.aangepast { border-color: var(--primary-color); font-weight: 600; }
/* Kleur per rij volgens Google Workspace */
#personeelBody tr.g-account td { background: #e8f7ee; }
#personeelBody tr.g-alias td { background: #fff1e0; }
#personeelBody tr.g-geen td { background: #fdeaea; }
#personeelBody tr[class*="g-"] input.adres, #personeelBody tr[class*="g-"] select.rol { background: #fff; }
.legende { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; margin: -6px 0 14px; font-size: .88em; color: var(--text-muted); }
.legende button.kleurfilter { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center;
gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid transparent; }
.legende button.kleurfilter:hover { border-color: var(--border); color: var(--text, #1e293b); }
.legende button.kleurfilter:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 1px; }
.legende button.kleurfilter.actief { border-color: var(--primary-color); color: var(--primary-color); font-weight: 600;
background: var(--surface); }
.legende b { font-weight: 600; font-variant-numeric: tabular-nums; }
.legende b:not(:empty)::before { content: "("; } .legende b:not(:empty)::after { content: ")"; }
.legende i { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
.legende i.account { background: #e8f7ee; border: 1px solid #a7dcb9; }
.legende i.alias { background: #fff1e0; border: 1px solid #f3c48d; }
.legende i.geen { background: #fdeaea; border: 1px solid #f0a8a8; }
select.rol { min-width: 150px; max-width: 190px; padding: 5px 6px; }
th.breekt { white-space: normal; min-width: 90px; }
select.rol.anders { border-color: var(--primary-color); font-weight: 600; }
.rol-status { display: inline-block; width: 18px; margin-left: 4px; font-size: .9em; }
td.nowrap { white-space: nowrap; }
button.negeer { all: unset; cursor: pointer; display: inline-grid; place-items: center; width: 24px; height: 24px;
border-radius: 6px; font-size: 13px; opacity: .45; margin-right: 4px; vertical-align: middle; }
button.negeer:hover, button.negeer:focus-visible { opacity: 1; background: #fee2e2; outline: none; }
tr.weg { opacity: .3; transition: opacity .25s; }
button.verplaats { all: unset; cursor: pointer; display: inline-grid; place-items: center; width: 24px; height: 24px;
border-radius: 6px; font-size: 15px; font-weight: 800; color: #16a34a; margin-right: 2px; vertical-align: middle; }
button.verplaats:hover, button.verplaats:focus-visible { background: #dcfce7; outline: none; }
.vp-raster { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
.vp-raster .field { margin-bottom: 12px; }
.vp-raster .breed { grid-column: 1 / -1; }
@media (max-width: 560px) { .vp-raster { grid-template-columns: 1fr; } }
#vpFout { display: none; margin: 4px 0 0; }
.nacht { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; justify-content: space-between;
padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }
.nacht.aan { border-color: var(--primary-color); background: var(--primary-light); }
.nacht strong { display: block; }
.nacht-acties { display: flex; gap: 12px; align-items: center; }
.schakelaar { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-weight: 600; }
.schakelaar .spoor { width: 44px; height: 24px; border-radius: 999px; background: #cbd5e1; position: relative; transition: background .15s; flex-shrink: 0; }
.schakelaar .spoor::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%;
background: #fff; transition: transform .15s; box-shadow: 0 1px 2px rgba(0,0,0,.25); }
.schakelaar.aan .spoor { background: var(--primary-color); }
.schakelaar.aan .spoor::after { transform: translateX(20px); }
.schakelaar button { all: unset; display: inline-flex; align-items: center; gap: 10px; cursor: pointer; }
.schakelaar button:focus-visible .spoor { outline: 2px solid var(--primary-color); outline-offset: 2px; }
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="page-head">
<h2>🪪 Personeel {{ school_naam }}</h2>
<div class="actions">
{% if rijen %}
<form method="POST" action="{{ url_for('gopro.verzamel') }}" style="margin:0"
onsubmit="if (!confirm('Alle groene personeelsleden (account bestaat in Google) van deze school naar de verzameltabel zetten? Ze verdwijnen dan uit deze lijst.')) return false; showLoader('Verzamelen...');">
<button type="submit" class="btn-success btn-sm">➕ Groene naar verzameltabel</button>
</form>
<a class="btn btn-light btn-sm" id="exportKnop" href="{{ url_for('gopro.export') }}">⬇️ CSV voor GO! Pro</a>
{% endif %}
<a class="btn btn-light btn-sm" href="{{ url_for('gopro.verzameltabel') }}"><span id="groenTeller">📚 Verzameltabel ({{ verzameld_totaal }})</span></a>
<a class="btn btn-light btn-sm" href="{{ url_for('gopro.verzameltabel', lijst='rood') }}"><span id="roodTeller">🚫 Rode lijst ({{ genegeerd_totaal }})</span></a>
<a class="btn btn-light btn-sm" href="{{ url_for('gopro.verzameltabel', lijst='verzonden') }}" title="Personeelsleden van {{ school_naam }} die al naar GO! Pro verzonden zijn">📨 Verzonden van deze school ({{ verzonden_school }})</a>
<a class="btn btn-secondary btn-sm" href="{{ url_for('gopro.overzicht', vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
<div class="nacht {{ 'aan' if nacht.aan }}">
<div>
<strong>🌙 Elke nacht automatisch verzamelen</strong>
<span class="muted small">
{% if nacht.aan %}Staat <b>aan</b>: elke nacht tussen 2 en 6 uur worden van alle scholen de groene personeelsleden
naar de verzameltabel gezet (wie op de rode lijst staat niet){% if nacht.door %} · aangezet door {{ nacht.door }}{% endif %}.
{% else %}Staat uit: enkel met de knop ➕ Groene naar verzameltabel, per school.{% endif %}
{% if nacht.bezig %}<br>⏳ Er loopt nu een verzameling voor alle scholen…
{% elif nacht.resultaat %}<br>Laatste keer ({{ nacht.resultaat_op.strftime('%d-%m-%Y %H:%M') if nacht.resultaat_op else '?' }}): {{ nacht.resultaat }}{% endif %}
</span>
</div>
<div class="nacht-acties">
<form method="POST" action="{{ url_for('gopro.nachtelijk') }}" style="margin:0"
onsubmit="if (!confirm('Nu meteen van alle scholen de groene personeelsleden naar de verzameltabel zetten?')) return false;">
<input type="hidden" name="nu" value="1">
<button type="submit" class="btn-light btn-sm" {{ 'disabled' if nacht.bezig }}>▶ Nu voor alle scholen</button>
</form>
<form method="POST" action="{{ url_for('gopro.nachtelijk') }}" style="margin:0">
<input type="hidden" name="aan" value="{{ '0' if nacht.aan else '1' }}">
<span class="schakelaar {{ 'aan' if nacht.aan }}">
<button type="submit" role="switch" aria-checked="{{ 'true' if nacht.aan else 'false' }}">
<span class="spoor"></span> {{ 'Aan' if nacht.aan else 'Uit' }}
</button>
</span>
</form>
</div>
</div>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% else %}
<div class="legende" role="group" aria-label="Legende en filter">
<button type="button" class="kleurfilter actief" data-kleur="" aria-pressed="true">Alle <b data-tel=""></b></button>
<button type="button" class="kleurfilter" data-kleur="account" aria-pressed="false"><i class="account"></i> Account bestaat in Google <b data-tel="account"></b></button>
<button type="button" class="kleurfilter" data-kleur="alias" aria-pressed="false"><i class="alias"></i> Adres is een alias van een ander account <b data-tel="alias"></b></button>
<button type="button" class="kleurfilter" data-kleur="geen" aria-pressed="false"><i class="geen"></i> Geen account gevonden in Google <b data-tel="geen"></b></button>
</div>
{% if not domein %}
<div class="alert alert-warning">Deze school heeft nog geen e-maildomein: vul het in via 🏫 Alle scholen beheren › Bewerken.</div>
{% endif %}
{% if verzameld_hier.groen or verzameld_hier.genegeerd or verzameld_hier.verzonden %}
<div class="alert alert-info">Niet meer getoond:
{% if verzameld_hier.groen %}{{ verzameld_hier.groen }} in de <a href="{{ url_for('gopro.verzameltabel') }}">verzameltabel</a>{% endif %}
{%- if verzameld_hier.genegeerd %}{{ ', ' if verzameld_hier.groen }}{{ verzameld_hier.genegeerd }} op de <a href="{{ url_for('gopro.verzameltabel', lijst='rood') }}">rode lijst</a>{% endif -%}
{%- if verzameld_hier.verzonden %}{{ ', ' if verzameld_hier.groen or verzameld_hier.genegeerd }}{{ verzameld_hier.verzonden }} al <a href="{{ url_for('gopro.verzameltabel', lijst='verzonden') }}">verzonden</a>{% endif -%}
.</div>
{% endif %}
{% if dubbel %}
<div class="alert alert-warning">⚠️ {{ dubbel }} rij(en) met een e-mailadres dat ook bij een ander personeelslid voorkomt (zelfde naam). Die zijn gemarkeerd.</div>
{% endif %}
<div class="filter-bar">
<input type="search" id="zoek" placeholder="Zoek op naam, e-mail, functie of rol..." aria-label="Zoeken">
<span class="muted small">Opgehaald uit Informat op {{ opgehaald }}</span>
</div>
<div class="filter-bar" style="margin-top:-4px">
<span class="muted small" id="googleStatus">Controleren in Google…</span>
<button type="button" class="btn-sm btn-light" id="googleKnop">🔄 Opnieuw controleren</button>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sorteer" data-kol="0">Voornaam <span class="pijl"></span></th>
<th class="sorteer" data-kol="1">Achternaam <span class="pijl"></span></th>
<th class="sorteer" data-kol="2">E-mail <span class="pijl"></span></th>
<th class="sorteer" data-kol="3">UPN <span class="pijl"></span></th>
<th class="sorteer" data-kol="4">Functie <span class="pijl"></span></th>
<th class="sorteer" data-kol="5">Rol <span class="pijl"></span></th>
<th class="sorteer breekt" data-kol="6">Startdatum functie <span class="pijl"></span></th>
<th class="sorteer breekt" data-kol="7">Einddatum functie <span class="pijl"></span></th>
<th class="sorteer" data-kol="8">Telefoon <span class="pijl"></span></th>
</tr>
</thead>
<tbody id="personeelBody">
{% for r in rijen %}
<tr>
<td data-sort="{{ r.voornaam }}" class="nowrap"><button type="button" class="verplaats" data-person="{{ r.person_id }}"
data-voornaam="{{ r.voornaam }}" data-achternaam="{{ r.achternaam }}" data-instelling="{{ r.instellingsnummer }}"
data-start="{{ r.start.isoformat() if r.start else '' }}" data-einde="{{ r.einde.isoformat() if r.einde else '' }}"
data-telefoon="{{ r.telefoon }}"
title="{{ r.voornaam }} {{ r.achternaam }} nakijken en naar de verzameltabel"
aria-label="{{ r.voornaam }} {{ r.achternaam }} naar de verzameltabel">+</button><button type="button" class="negeer" data-person="{{ r.person_id }}"
title="{{ r.voornaam }} {{ r.achternaam }} naar de rode lijst (negeren: niet exporteren en niet meer tonen)"
aria-label="{{ r.voornaam }} {{ r.achternaam }} negeren">🚫</button> {{ r.voornaam }}</td>
<td data-sort="{{ r.achternaam }}"><strong>{{ r.achternaam }}</strong></td>
{% for veld in ('email', 'upn') %}
<td class="nowrap" data-sort="{{ r[veld] }}">
<input type="text" class="adres {{ 'aangepast' if r[veld ~ '_aangepast'] }}" value="{{ r[veld] }}"
data-person="{{ r.person_id }}" data-veld="{{ veld }}" data-oud="{{ r[veld] }}"
aria-label="{{ 'E-mail' if veld == 'email' else 'UPN' }} van {{ r.voornaam }} {{ r.achternaam }}"
title="Leeg maken = terug naar het standaardadres"><span class="rol-status" aria-live="polite"></span>
{% if veld == 'email' and r.dubbel %}<span class="badge badge-warning" title="Dit adres komt meer dan één keer voor">dubbel</span>{% endif %}
</td>
{% endfor %}
{% for veld, label in (('functie', 'Functie'), ('rol', 'Rol')) %}
<td data-sort="{{ r[veld] }}" class="nowrap">
<select class="rol {{ 'anders' if r[veld] != 'Leerkracht' }}" aria-label="{{ label }} van {{ r.voornaam }} {{ r.achternaam }}"
data-person="{{ r.person_id }}" data-veld="{{ veld }}">
{% for rol in rollen %}<option {% if rol == r[veld] %}selected{% endif %}>{{ rol }}</option>{% endfor %}
</select><span class="rol-status" aria-live="polite"></span>
</td>
{% endfor %}
<td class="nowrap" data-sort="{{ r.start.isoformat() if r.start else '' }}">{{ r.start.strftime('%d-%m-%Y') if r.start else '' }}</td>
<td class="nowrap" data-sort="{{ r.einde.isoformat() if r.einde else '' }}">{{ r.einde.strftime('%d-%m-%Y') if r.einde else '' }}</td>
<td class="nowrap" data-sort="{{ r.telefoon }}">{{ r.telefoon }}</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="9">Informat gaf geen actieve personeelsleden terug.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tabel-voet">
<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap">
<div class="muted small" id="teller"></div>
<span id="perPagina"></span>
</div>
<nav class="paginering" id="paginering" aria-label="Pagina's"></nav>
</div>
{% endif %}
</div>
<div id="vpVenster" class="modal" role="dialog" aria-modal="true" aria-labelledby="vpTitel">
<div class="modal-content wide">
<span class="close" id="vpSluit" title="Annuleren">&times;</span>
<h3 id="vpTitel">Naar de verzameltabel</h3>
<form id="vpForm" autocomplete="off">
<input type="hidden" name="person_id">
<div class="vp-raster">
<div class="field"><label for="vpVoornaam">Voornaam</label><input id="vpVoornaam" name="voornaam" required maxlength="100"></div>
<div class="field"><label for="vpAchternaam">Achternaam</label><input id="vpAchternaam" name="achternaam" required maxlength="100"></div>
<div class="field breed"><label for="vpEmail">E-mailadres (aanmelden en communicatie)</label><input id="vpEmail" name="email" type="email" maxlength="255"></div>
<div class="field breed"><label for="vpUpn">UPN</label><input id="vpUpn" name="upn" maxlength="255"></div>
<div class="field"><label for="vpFunctie">Functie</label><select id="vpFunctie" name="functie">{% for rol in rollen %}<option>{{ rol }}</option>{% endfor %}</select></div>
<div class="field"><label for="vpRol">Rol</label><select id="vpRol" name="rol">{% for rol in rollen %}<option>{{ rol }}</option>{% endfor %}</select></div>
<div class="field"><label for="vpStart">Startdatum functie</label><input id="vpStart" name="start" type="date" readonly title="In de verzameltabel heeft iedereen het lopende schooljaar"></div>
<div class="field"><label for="vpEinde">Einddatum functie</label><input id="vpEinde" name="einde" type="date" readonly title="In de verzameltabel heeft iedereen het lopende schooljaar"></div>
<div class="field"><label for="vpInstelling">Onderwijsinstelling nr.</label><input id="vpInstelling" name="instellingsnummer" maxlength="20"></div>
<div class="field"><label for="vpTelefoon">Professioneel telefoonnummer</label><input id="vpTelefoon" name="telefoon" maxlength="50"></div>
</div>
<div class="alert alert-danger" id="vpFout"></div>
<div class="modal-footer">
<button type="button" class="btn-light" id="vpAnnuleer">Annuleren</button>
<button type="submit" class="btn-success" id="vpBewaar">💾 Bewaren en naar verzameltabel</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Welke adressen bestaan al in Google? Die rijen worden lichtgroen.
let googleAdressen = null; // {adres: 'primair' | 'alias'} zodra Google geantwoord heeft
let kleurFilter = ''; // '' = alle, of 'account' / 'alias' / 'geen'
let herteken = () => {}; // wordt hieronder de functie die de tabel opnieuw toont
let hertoon = () => {}; // idem, maar op dezelfde pagina blijven
function kleurRij(tr) {
if (!googleAdressen) return;
const soorten = Array.from(tr.querySelectorAll('input.adres')).map(i => googleAdressen[i.value.trim().toLowerCase()]);
const soort = soorten.includes('primair') ? 'account' : soorten.includes('alias') ? 'alias' : 'geen';
tr.classList.remove('g-account', 'g-alias', 'g-geen');
tr.classList.add('g-' + soort);
tr.title = {account: 'Account bestaat in Google', alias: 'Dit adres is een alias van een ander account in Google',
geen: 'Geen account gevonden in Google'}[soort];
}
function controleerGoogle(vernieuw) {
const status = document.getElementById('googleStatus');
if (!status) return;
status.textContent = 'Controleren in Google…';
const adressen = Array.from(document.querySelectorAll('input.adres')).map(i => i.value.trim().toLowerCase()).filter(Boolean);
fetch("{{ url_for('gopro.google_check') }}", {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({adressen, vernieuw: !!vernieuw})
}).then(r => r.json()).then(res => {
if (!res.ok) { status.textContent = '⚠️ ' + res.fout; return; }
googleAdressen = Object.assign(googleAdressen || {}, res.adressen);
const rijen = Array.from(document.querySelectorAll('#personeelBody tr')).filter(tr => tr.querySelector('input.adres'));
rijen.forEach(kleurRij);
const tel = k => rijen.filter(tr => tr.classList.contains('g-' + k)).length;
status.textContent = `${tel('account')} met account, ${tel('alias')} via een alias, ${tel('geen')} zonder account in Google.`;
document.querySelectorAll('.legende b[data-tel]').forEach(b => { b.textContent = b.dataset.tel ? tel(b.dataset.tel) : rijen.length; });
herteken();
}).catch(() => { status.textContent = '⚠️ Google kon niet bevraagd worden.'; });
}
// 🚫 naast de naam: personeelslid naar de rode lijst (negeren) en uit de tabel halen
function telLegende() {
const rijen = Array.from(document.querySelectorAll('#personeelBody tr')).filter(tr => tr.querySelector('input.adres'));
const tel = k => rijen.filter(tr => tr.classList.contains('g-' + k)).length;
document.querySelectorAll('.legende b[data-tel]').forEach(b => { b.textContent = b.dataset.tel ? tel(b.dataset.tel) : rijen.length; });
const status = document.getElementById('googleStatus');
if (status && googleAdressen) status.textContent = `${tel('account')} met account, ${tel('alias')} via een alias, ${tel('geen')} zonder account in Google.`;
}
// + naast de naam: venster met alle gegevens; bewaren = met de aanpassingen naar de verzameltabel
(function () {
const venster = document.getElementById('vpVenster'), form = document.getElementById('vpForm');
const fout = document.getElementById('vpFout'), bewaar = document.getElementById('vpBewaar');
let huidigeRij = null;
const sluit = () => { venster.style.display = 'none'; huidigeRij = null; };
document.querySelectorAll('button.verplaats').forEach(knop => knop.addEventListener('click', () => {
const tr = knop.closest('tr'), d = knop.dataset;
const veld = v => tr.querySelector(`[data-veld="${v}"]`);
huidigeRij = tr;
form.person_id.value = d.person;
form.voornaam.value = d.voornaam; form.achternaam.value = d.achternaam;
form.email.value = veld('email').value.trim(); form.upn.value = veld('upn').value.trim();
form.functie.value = veld('functie').value; form.rol.value = veld('rol').value;
form.start.value = {{ periode[0].isoformat()|tojson }}; form.einde.value = {{ periode[1].isoformat()|tojson }};
form.instellingsnummer.value = d.instelling; form.telefoon.value = d.telefoon;
document.getElementById('vpTitel').textContent = `${d.voornaam} ${d.achternaam} naar de verzameltabel`;
fout.style.display = 'none'; bewaar.disabled = false;
venster.style.display = 'block';
form.voornaam.focus();
}));
document.getElementById('vpSluit').addEventListener('click', sluit);
document.getElementById('vpAnnuleer').addEventListener('click', sluit);
venster.addEventListener('click', e => { if (e.target === venster) sluit(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && venster.style.display === 'block') sluit(); });
form.addEventListener('submit', e => {
e.preventDefault();
const gegevens = Object.fromEntries(new FormData(form).entries());
bewaar.disabled = true; fout.style.display = 'none';
fetch("{{ url_for('gopro.verplaats') }}", {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(gegevens)
}).then(r => r.json()).then(res => {
if (!res.ok) { fout.textContent = res.fout || 'Mislukt.'; fout.style.display = 'block'; bewaar.disabled = false; return; }
const tr = huidigeRij;
sluit();
if (tr) tr.remove();
const t = document.getElementById('groenTeller'); if (t) t.textContent = `📚 Verzameltabel (${res.totaal})`;
telLegende(); hertoon();
}).catch(() => { fout.textContent = 'Mislukt: geen verbinding met PIVOT.'; fout.style.display = 'block'; bewaar.disabled = false; });
});
})();
document.querySelectorAll('button.negeer').forEach(knop => knop.addEventListener('click', () => {
const tr = knop.closest('tr');
tr.classList.add('weg');
fetch("{{ url_for('gopro.negeer') }}", {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({person_id: knop.dataset.person})
}).then(r => r.json()).then(res => {
if (!res.ok) { tr.classList.remove('weg'); alert(res.fout || 'Mislukt.'); return; }
tr.remove();
const t = document.getElementById('roodTeller'); if (t) t.textContent = `🚫 Rode lijst (${res.totaal})`;
telLegende(); hertoon();
}).catch(() => { tr.classList.remove('weg'); alert('Mislukt: geen verbinding met PIVOT.'); });
}));
const gk = document.getElementById('googleKnop');
if (gk) gk.addEventListener('click', () => controleerGoogle(true));
// E-mail of UPN wijzigen: bewaren bij het verlaten van het veld (of Enter)
document.querySelectorAll('input.adres').forEach(inp => {
inp.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); inp.blur(); } });
inp.addEventListener('change', () => {
const status = inp.nextElementSibling;
status.textContent = '…';
fetch("{{ url_for('gopro.keuze') }}", {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({person_id: inp.dataset.person, veld: inp.dataset.veld, waarde: inp.value})
}).then(r => r.json()).then(res => {
if (!res.ok) {
status.textContent = '⚠️'; status.title = res.fout || 'Niet opgeslagen';
alert(res.fout || 'Niet opgeslagen'); inp.value = inp.dataset.oud; return;
}
const leeg = !inp.value.trim();
if (res.waarde !== undefined && res.waarde !== null) inp.value = res.waarde;
inp.dataset.oud = inp.value;
inp.classList.toggle('aangepast', !leeg);
inp.closest('td').dataset.sort = inp.value;
status.textContent = '✓'; status.title = 'Opgeslagen';
setTimeout(() => { status.textContent = ''; }, 2000);
if (googleAdressen) controleerGoogle(false); // ook het nieuwe adres laten nakijken
}).catch(() => { status.textContent = '⚠️'; status.title = 'Niet opgeslagen'; inp.value = inp.dataset.oud; });
});
});
controleerGoogle(false);
// Functie of rol wijzigen: meteen bewaren
document.querySelectorAll('select.rol').forEach(sel => sel.addEventListener('change', () => {
const status = sel.nextElementSibling;
status.textContent = '…';
fetch("{{ url_for('gopro.keuze') }}", {
method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({person_id: sel.dataset.person, veld: sel.dataset.veld, waarde: sel.value})
}).then(r => r.json()).then(res => {
status.textContent = res.ok ? '✓' : '⚠️';
status.title = res.ok ? 'Opgeslagen' : (res.fout || 'Niet opgeslagen');
sel.classList.toggle('anders', sel.value !== 'Leerkracht');
sel.closest('td').dataset.sort = sel.value;
if (res.ok) setTimeout(() => { status.textContent = ''; }, 2000);
}).catch(() => { status.textContent = '⚠️'; status.title = 'Niet opgeslagen'; });
}));
// Zoeken, sorteren en pagineren in de browser
(function () {
const body = document.getElementById('personeelBody');
if (!body || body.querySelector('.empty-row')) return;
let PER_PAGINA = perPagina('gopro');
let pagina = 1, kol = null, op = true, zoek = '';
const tekst = tr => Array.from(tr.cells).map(td => td.dataset.sort || td.textContent).join(' ').toLowerCase();
function zichtbaar() {
return Array.from(body.rows).filter(tr => (!zoek || tekst(tr).includes(zoek))
&& (!kleurFilter || tr.classList.contains('g-' + kleurFilter)));
}
function toon() {
const rijen = zichtbaar();
const grootte = PER_PAGINA || Infinity;
const 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} rij(en)` : 'Geen resultaten';
const nav = document.getElementById('paginering');
nav.innerHTML = '';
if (paginas <= 1) return;
const knop = (label, p, actief, titel) => {
const b = document.createElement('button');
b.type = 'button'; b.textContent = label; b.className = 'btn-sm ' + (actief ? 'btn-secondary' : 'btn-light');
if (titel) b.title = titel;
b.onclick = () => { pagina = p; toon(); body.closest('.table-wrap').scrollIntoView({ block: 'start' }); };
nav.appendChild(b);
};
if (pagina > 1) { knop('«', 1, false, 'Eerste pagina'); 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, false, 'Laatste pagina'); }
const info = document.createElement('span');
info.className = 'muted small'; info.textContent = `pagina ${pagina} van ${paginas}`;
nav.appendChild(info);
}
document.getElementById('zoek').addEventListener('input', e => { zoek = e.target.value.trim().toLowerCase(); pagina = 1; toon(); });
document.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
const k = +th.dataset.kol;
op = kol === k ? !op : true; 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'), 'gopro', n => { PER_PAGINA = n; pagina = 1; toon(); });
herteken = () => { pagina = 1; toon(); };
hertoon = () => toon();
document.querySelectorAll('button.kleurfilter').forEach(b => b.addEventListener('click', () => {
kleurFilter = (b.dataset.kleur && kleurFilter !== b.dataset.kleur) ? b.dataset.kleur : '';
const exp = document.getElementById('exportKnop'); // export volgt de kleurkeuze
if (exp) exp.href = "{{ url_for('gopro.export') }}" + (kleurFilter ? '?kleur=' + kleurFilter : '');
document.querySelectorAll('button.kleurfilter').forEach(x => {
const aan = x.dataset.kleur === kleurFilter;
x.classList.toggle('actief', aan); x.setAttribute('aria-pressed', String(aan));
});
herteken();
}));
toon();
})();
</script>
{% endblock %}