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