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

507 lines
35 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 %}Handtekeningen{% endblock %}
{% block page_title %}Handtekeningen{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Bezig met Google...{% endblock %}
{% block extra_css %}
<style>
[hidden] { display: none !important; }
.ht-raster { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 18px; align-items: start; }
@media (max-width: 1000px) { .ht-raster { grid-template-columns: 1fr; } }
.werkbalk { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; padding: 6px; border: 1px solid var(--border);
border-bottom: 0; border-radius: var(--radius) var(--radius) 0 0; background: var(--surface-muted); }
.werkbalk button, .werkbalk select, .werkbalk label.kleur { width: auto; margin: 0; padding: 4px 9px; font-size: 13px; min-width: 30px;
background: var(--surface, #fff); color: inherit; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight: 600; }
.werkbalk button:hover { border-color: var(--primary-color); }
.werkbalk button.aan { background: var(--primary-color); color: #fff; border-color: var(--primary-color); }
.werkbalk .scheiding { width: 1px; align-self: stretch; background: var(--border); margin: 0 4px; }
.werkbalk label.kleur { display: inline-flex; align-items: center; gap: 4px; }
.werkbalk input[type=color] { width: 22px; height: 18px; padding: 0; border: 0; background: none; }
.werkbalk .rechts { margin-left: auto; display: flex; gap: 4px; }
#editor, #broncode { min-height: 260px; border: 1px solid var(--border); border-radius: 0 0 var(--radius) var(--radius);
padding: 14px; background: #fff; color: #111; overflow: auto; }
#editor:focus, #broncode:focus { outline: 2px solid var(--primary-light); border-color: var(--primary-color); }
#broncode { width: 100%; font-family: ui-monospace, Consolas, monospace; font-size: 12.5px; line-height: 1.45; resize: vertical; margin: 0; }
.tekens { font-size: 12px; color: var(--text-muted); text-align: right; margin-top: 4px; }
.tekens.te-lang { color: #b91c1c; font-weight: 700; }
.voorbeeld-kader { border: 1px solid var(--border); border-radius: var(--radius); background: #fff; color: #111; }
.voorbeeld-kop { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between;
padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--surface-muted); border-radius: var(--radius) var(--radius) 0 0; }
.voorbeeld-kop select { width: auto; max-width: 260px; margin: 0; }
.mail-nep { padding: 16px; font-family: Arial, Helvetica, sans-serif; font-size: 13px; }
.mail-nep .tekst { color: #6b7280; margin-bottom: 14px; }
.mail-nep .streep { color: #9ca3af; }
.velden { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.velden button { width: auto; margin: 0; padding: 3px 9px; font-size: 12px; border-radius: 999px; font-weight: 600;
background: var(--primary-light); color: var(--primary-color); border: 1px solid transparent; cursor: pointer; }
.velden button:hover { border-color: var(--primary-color); }
.instel { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin-top: 18px; }
.instel .field { margin: 0; }
.logo-rij { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.logo-rij img { max-height: 60px; max-width: 180px; border: 1px solid var(--border); border-radius: 6px; padding: 4px; background: #fff; }
.ou-kader { border: 1px solid var(--border); border-radius: var(--radius); max-height: 230px; overflow: auto; padding: 6px 10px; background: var(--surface, #fff); }
.ou-kader label { display: flex; gap: 8px; align-items: center; margin: 0; padding: 3px 0; font-weight: 400; cursor: pointer; }
.ou-kader input { width: auto; margin: 0; }
.ou-kader label.gekozen { font-weight: 600; color: var(--primary-color); }
.gekozen-ous { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.gekozen-ous span { padding: 2px 10px; border-radius: 999px; background: var(--primary-light); color: var(--primary-color); font-size: 12px; font-weight: 600; }
.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: 14px; }
.nacht.aan { border-color: var(--primary-color); background: var(--primary-light); }
.nacht strong { display: block; }
.balk { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.balk input[type=search] { flex: 0 1 300px; }
.balk .rechts { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
td.inv input { margin: 0; padding: 5px 8px; font-size: 13px; min-width: 150px; }
td.inv input.gewijzigd { border-color: #d97706; background: #fffbeb; }
td.nowrap { white-space: nowrap; }
td.acties { white-space: nowrap; text-align: right; }
td.acties button { margin: 0 0 0 4px; }
.st { display: inline-grid; place-items: center; min-width: 24px; height: 24px; border-radius: 6px; font-weight: 700; padding: 0 6px; font-size: 12px; }
.st.ok { background: #dcfce7; color: #15803d; } .st.fout { background: #fee2e2; color: #b91c1c; } .st.nooit { color: var(--text-muted); }
.voortgang { height: 8px; border-radius: 999px; background: var(--surface-muted); overflow: hidden; margin: 8px 0; }
.voortgang div { height: 100%; width: 0; background: var(--primary-color); transition: width .4s; }
.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 %}
{# ===================== Sjabloon ===================== #}
<form class="card" method="POST" action="{{ url_for('handtekeningen.sjabloon') }}" enctype="multipart/form-data" id="sjabloonForm"
onsubmit="return voorBewaren()">
<div class="page-head" style="margin-bottom:6px">
<h2>✍️ Algemene handtekening</h2>
<div class="actions">
<button type="submit" class="btn-success btn-sm">💾 Bewaren</button>
</div>
</div>
<p class="field-hint" style="margin-top:0">Eén handtekening voor iedereen in de gekozen OU's. Klik op een veld hieronder om het in te
voegen; PIVOT vult het per persoon in. Zet een stuk tussen <code>{{ '{#functie}' }}</code> en <code>{{ '{/functie}' }}</code> (of
<code>telefoon</code>, ...) om het weg te laten als dat veld leeg is.</p>
<div class="ht-raster">
<div>
<div class="werkbalk" id="werkbalk">
<button type="button" data-cmd="bold" title="Vet"><b>B</b></button>
<button type="button" data-cmd="italic" title="Cursief"><i>I</i></button>
<button type="button" data-cmd="underline" title="Onderstrepen"><u>U</u></button>
<span class="scheiding"></span>
<select id="lettergrootte" title="Lettergrootte">
<option value="">Grootte</option><option value="1">Klein</option><option value="2">Normaal</option>
<option value="3">Groter</option><option value="4">Groot</option><option value="5">Zeer groot</option>
</select>
<select id="lettertype" title="Lettertype">
<option value="">Lettertype</option><option>Arial</option><option>Verdana</option><option>Tahoma</option>
<option>Georgia</option><option>Trebuchet MS</option><option>Calibri</option>
</select>
<label class="kleur" title="Tekstkleur">A <input type="color" id="tekstkleur" value="#1d6fa5"></label>
<span class="scheiding"></span>
<button type="button" data-cmd="justifyLeft" title="Links uitlijnen">⇤</button>
<button type="button" data-cmd="justifyCenter" title="Centreren">≡</button>
<button type="button" id="linkKnop" title="Link toevoegen">🔗</button>
<button type="button" data-cmd="unlink" title="Link weghalen">⛓️‍💥</button>
<button type="button" data-cmd="insertHorizontalRule" title="Lijn">―</button>
<button type="button" data-cmd="removeFormat" title="Opmaak wissen">⌫</button>
<span class="rechts">
<button type="button" id="standaardKnop" title="Het standaardsjabloon van PIVOT terugzetten">↺ Standaard</button>
<button type="button" id="htmlKnop" title="HTML-broncode bewerken">&lt;/&gt; HTML</button>
</span>
</div>
<div id="editor" contenteditable="true" spellcheck="false"></div>
<textarea id="broncode" name="sjabloon" hidden spellcheck="false">{{ inst.Sjabloon or standaard_sjabloon }}</textarea>
<div class="velden" id="velden">
{% for k, uitleg in velden %}<button type="button" data-veld="{{ k }}" title="{{ uitleg }}">{{ '{' ~ k ~ '}' }}</button>{% endfor %}
</div>
</div>
<div>
<div class="voorbeeld-kader">
<div class="voorbeeld-kop">
<strong class="small">👁 Voorbeeld</strong>
<select id="voorbeeldVoor" aria-label="Voorbeeld voor">
<option value="">Fictieve persoon</option>
{% for u in gebruikers %}<option value="{{ loop.index0 }}">{{ u.voornaam }} {{ u.achternaam }}</option>{% endfor %}
</select>
</div>
<div class="mail-nep">
<div class="tekst">Beste collega,<br><br>Zo ziet je e-mail eruit met de handtekening.<br><br>Met vriendelijke groeten,</div>
<div class="streep">--</div>
<div id="voorbeeld"></div>
</div>
</div>
<div class="tekens" id="tekens"></div>
</div>
</div>
<div class="instel">
<div class="field">
<label>Logo</label>
<div class="logo-rij">
{% if inst.heeft_logo and inst.Logo_sleutel %}
<img src="{{ url_for('handtekening_logo.logo', code=school.code, sleutel=inst.Logo_sleutel, ext='png' if inst.Logo_type == 'image/png' else ('jpg' if inst.Logo_type == 'image/jpeg' else 'gif')) }}" alt="Logo">
<label style="font-weight:400;display:flex;gap:6px;align-items:center;margin:0"><input type="checkbox" name="logo_wissen" value="1" style="width:auto;margin:0"> wissen</label>
{% endif %}
<input type="file" name="logo" accept="image/png,image/jpeg,image/gif" style="flex:1 1 200px">
</div>
<p class="field-hint">PNG, JPG of GIF, max. 512 kB. Of geef hieronder een eigen publieke link (bv. van de schoolwebsite).</p>
<input type="url" name="logo_url" id="logoUrl" value="{{ inst.Logo_url or '' }}" placeholder="https://... (laat leeg om het geüploade logo te gebruiken)">
<div style="display:flex;gap:8px;align-items:center;margin-top:8px">
<label for="logoBreedte" style="margin:0;font-weight:400">Breedte</label>
<input type="number" name="logo_breedte" id="logoBreedte" min="20" max="600" value="{{ inst.Logo_breedte or 120 }}" style="width:90px;margin:0"> <span class="muted small">pixels</span>
</div>
</div>
<div class="field">
<label for="publiekAdres">Publiek adres van PIVOT</label>
<input type="url" name="publiek_adres" id="publiekAdres" value="{{ inst.Publiek_adres or publiek_voorstel }}">
<p class="field-hint">Gmail haalt een geüpload logo op via <code>{{ (inst.Publiek_adres or publiek_voorstel) }}/handtekening-logo/...</code>.
Dat adres moet van buitenaf (internet) bereikbaar zijn; de IP-blokkering geldt niet voor het logo.
Is PIVOT enkel intern bereikbaar, gebruik dan een eigen link naar het logo.</p>
{% if inst.logo_link %}<p class="field-hint">Huidige logolink: <a href="{{ inst.logo_link }}" target="_blank" rel="noopener">{{ inst.logo_link }}</a></p>{% endif %}
<label style="margin-top:12px">Gegevens van de school</label>
<p class="field-hint" style="margin-top:0"><strong>{{ school_gegevens.schoolnaam }}</strong><br>{{ school_gegevens.schooladres or '— geen adres ingevuld —' }}<br>
Aan te passen via 🏫 Alle scholen beheren › Bewerken › Adres.</p>
</div>
<div class="field">
<label>OU's</label>
{% if fout_ou %}<div class="alert alert-danger" style="margin:0 0 8px">{{ fout_ou }}</div>{% endif %}
<input type="search" id="ouZoek" placeholder="Zoek een OU..." style="margin-bottom:6px">
<div class="ou-kader" id="ouKader">
{% for ou in ous %}
<label class="{{ 'gekozen' if ou in inst.ous }}"><input type="checkbox" name="ou" value="{{ ou }}" {{ 'checked' if ou in inst.ous }}> {{ ou }}</label>
{% endfor %}
</div>
<label style="font-weight:400;display:flex;gap:8px;align-items:center;margin-top:8px">
<input type="checkbox" name="met_sub" value="1" {{ 'checked' if inst.Met_sub_ous }} style="width:auto;margin:0"> Ook de onderliggende OU's
</label>
<div class="gekozen-ous" id="gekozenOus"></div>
</div>
</div>
{% if inst.Bijgewerkt %}<p class="muted small" style="margin:14px 0 0">Laatst bewaard op {{ inst.Bijgewerkt.strftime('%d-%m-%Y %H:%M') }}{% if inst.Door %} door {{ inst.Door }}{% endif %}.</p>{% endif %}
</form>
{# ===================== Gebruikers ===================== #}
<div class="card">
<div class="page-head" style="margin-bottom:10px">
<h2>👥 Gebruikers <span class="muted small">({{ gebruikers|length }})</span></h2>
<div class="actions">
<a class="btn btn-light btn-sm" href="{{ url_for('handtekeningen.overzicht', vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
<div class="nacht {{ 'aan' if inst.Nachtelijk }}">
<div>
<strong>🌙 Elke nacht opnieuw toepassen</strong>
<span class="muted small">{% if inst.Nachtelijk %}Staat <b>aan</b>: elke nacht (tussen 3 en 6 uur) zet PIVOT de handtekening
opnieuw bij wie ze in Gmail zelf gewijzigd heeft, of bij wie de gegevens veranderd zijn.{% else %}Staat uit: de handtekening
wordt enkel gezet met de knoppen hieronder. Gebruikers kunnen ze in Gmail zelf nog aanpassen.{% endif %}
{% if inst.Nacht_resultaat %}<br>Laatste keer: {{ inst.Nacht_resultaat }}{% endif %}</span>
</div>
<form method="POST" action="{{ url_for('handtekeningen.nachtelijk') }}" style="margin:0">
<input type="hidden" name="aan" value="{{ '0' if inst.Nachtelijk else '1' }}">
<label class="switch" title="{{ 'Uitzetten' if inst.Nachtelijk else 'Aanzetten' }}">
<input type="checkbox" {{ 'checked' if inst.Nachtelijk }} onchange="this.form.submit()"><span class="slider"></span>
</label>
</form>
</div>
{% if not inst.ous %}
<p class="muted">Kies hierboven één of meer OU's en klik op 💾 Bewaren.</p>
{% elif fout_gebruikers %}
<div class="alert alert-danger">{{ fout_gebruikers }}</div>
{% elif not gebruikers %}
<p class="muted">Geen (actieve) gebruikers in de gekozen OU's.</p>
{% else %}
<p class="field-hint" style="margin-top:0">Functie en telefoon komen uit Google. Pas je ze hier aan, dan worden ze ook in Google
bewaard (en staan ze in het adresboek van de organisatie).</p>
<div class="balk">
<input type="search" id="zoek" placeholder="Zoek op naam, e-mail, functie of OU..." aria-label="Zoeken">
<span id="perPagina"></span>
<div class="rechts">
<button type="button" class="btn-sm btn-warning" id="alleBewaren" hidden>💾 Wijzigingen in Google bewaren (<span id="aantalGewijzigd">0</span>)</button>
<button type="button" class="btn-sm btn-light" id="geselecteerdToepassen" disabled>▶ Toepassen op geselecteerden (<span id="aantalGekozen">0</span>)</button>
<button type="button" class="btn-sm btn-success" id="iedereenToepassen">▶ Toepassen op iedereen</button>
</div>
</div>
<div id="voortgangKaart" {{ '' if status.bezig or status.resultaat or status.fout else 'hidden' }} style="margin-bottom:12px">
<div class="voortgang"><div id="balk"></div></div>
<div class="small"><b id="tVoortgang">{{ status.voortgang }}</b> / <b id="tTotaal">{{ status.totaal }}</b> ·
✅ <b id="tGelukt">{{ status.gelukt }}</b> · ⏭️ <b id="tOngewijzigd">{{ status.ongewijzigd }}</b> · ⚠️ <b id="tFout">{{ status.fouten }}</b></div>
<div class="alert alert-success" id="resultaat" style="margin:8px 0 0" {{ '' if status.resultaat and not status.bezig else 'hidden' }}>{{ status.resultaat or '' }}</div>
<div class="alert alert-danger" id="foutmelding" style="margin:8px 0 0" {{ '' if status.fout else 'hidden' }}>{{ status.fout or '' }}</div>
<ul class="muted small" id="foutvoorbeelden"></ul>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:28px"><input type="checkbox" id="allesKiezen" title="Alles (op deze pagina) selecteren" style="width:auto;margin:0"></th>
<th>Naam</th><th>E-mail</th><th>OU</th><th>Functie</th><th>Telefoon</th>
<th title="Laatste keer toegepast">Gmail</th><th></th>
</tr>
</thead>
<tbody id="body">
{% for u in gebruikers %}
{% set l = logboek.get(u.email) %}
<tr data-i="{{ loop.index0 }}" data-email="{{ u.email }}">
<td><input type="checkbox" class="kies" style="width:auto;margin:0"></td>
<td class="nowrap"><strong>{{ u.achternaam }}</strong> {{ u.voornaam }}</td>
<td class="small">{{ u.email }}</td>
<td class="small muted">{{ u.ou }}</td>
<td class="inv"><input type="text" class="functie" value="{{ u.functie }}" data-oud="{{ u.functie }}" placeholder="Functie" maxlength="100"></td>
<td class="inv"><input type="tel" class="telefoon" value="{{ u.telefoon }}" data-oud="{{ u.telefoon }}" placeholder="Telefoon" maxlength="40" style="min-width:130px"></td>
<td class="nowrap status">
{% if not l %}<span class="st nooit" title="Nog niet toegepast">—</span>
{% elif l.Ok %}<span class="st ok" title="Toegepast op {{ l.Toegepast.strftime('%d-%m-%Y %H:%M') }}{{ ' door ' ~ l.Door if l.Door }}">✓</span>
{% else %}<span class="st fout" title="{{ l.Melding }}">✕</span>{% endif %}
</td>
<td class="acties">
<button type="button" class="btn-sm btn-warning bewaar" hidden title="Functie en telefoon in Google bewaren">💾</button>
<button type="button" class="btn-sm btn-light toon" title="Voorbeeld van deze persoon">👁</button>
<button type="button" class="btn-sm btn-light pasToe" title="Handtekening nu instellen in Gmail">▶</button>
</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>
<p class="field-hint">Eenmalig nodig in de Google Admin-console (Beveiliging › API-beheer › Domeinbrede delegatie, bij het serviceaccount
van PIVOT): <code>https://www.googleapis.com/auth/gmail.settings.basic</code> (handtekening zetten) en
<code>https://www.googleapis.com/auth/admin.directory.user</code> (functie en telefoon).</p>
{% endblock %}
{% block scripts %}
<script>
const GEBRUIKERS = {{ gebruikers|tojson }};
const MAX_TEKENS = {{ max_tekens }};
const STANDAARD = {{ standaard_sjabloon|tojson }};
const URL_VOORBEELD = {{ url_for('handtekeningen.voorbeeld')|tojson }};
const URL_GEBRUIKER = {{ url_for('handtekeningen.gebruiker')|tojson }};
const URL_TOEPASSEN = {{ url_for('handtekeningen.toepassen')|tojson }};
const URL_STATUS = {{ url_for('handtekeningen.status')|tojson }};
const FICTIEF = { voornaam: 'An', achternaam: 'Voorbeeld', email: 'an.voorbeeld@school.be', functie: 'Leerkracht wiskunde', telefoon: '03 123 45 67' };
const post = (url, data) => fetch(url, { method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json());
// ---------------- Editor (opmaak ↔ HTML) ----------------
const editor = document.getElementById('editor'), bron = document.getElementById('broncode');
const htmlKnop = document.getElementById('htmlKnop');
let htmlModus = false, kiesBereik = null;
editor.innerHTML = bron.value;
function sjabloon() { return htmlModus ? bron.value : editor.innerHTML; }
function zetModus(html) {
if (html === htmlModus) return;
if (html) { bron.value = editor.innerHTML; } else { editor.innerHTML = bron.value; }
htmlModus = html;
bron.hidden = !html; editor.hidden = html;
htmlKnop.classList.toggle('aan', html);
document.querySelectorAll('#werkbalk [data-cmd], #lettergrootte, #lettertype, #tekstkleur, #linkKnop')
.forEach(x => x.disabled = html);
(html ? bron : editor).focus();
}
htmlKnop.onclick = () => zetModus(!htmlModus);
editor.addEventListener('keyup', bewaarSelectie); editor.addEventListener('mouseup', bewaarSelectie);
function bewaarSelectie() { const s = window.getSelection(); if (s.rangeCount && editor.contains(s.anchorNode)) kiesBereik = s.getRangeAt(0); }
function herstelSelectie() { editor.focus(); if (kiesBereik) { const s = window.getSelection(); s.removeAllRanges(); s.addRange(kiesBereik); } }
function opdracht(cmd, waarde) { herstelSelectie(); document.execCommand('styleWithCSS', false, true); document.execCommand(cmd, false, waarde ?? null); bewaarSelectie(); vernieuw(); }
document.querySelectorAll('#werkbalk [data-cmd]').forEach(b => b.addEventListener('mousedown', e => { e.preventDefault(); opdracht(b.dataset.cmd); }));
document.getElementById('lettergrootte').onchange = e => { if (e.target.value) opdracht('fontSize', e.target.value); e.target.value = ''; };
document.getElementById('lettertype').onchange = e => { if (e.target.value) opdracht('fontName', e.target.value); e.target.value = ''; };
document.getElementById('tekstkleur').oninput = e => opdracht('foreColor', e.target.value);
document.getElementById('linkKnop').onclick = () => { const url = prompt('Link (https://... of mailto:...)', 'https://'); if (url) opdracht('createLink', url); };
document.getElementById('standaardKnop').onclick = () => {
if (!confirm('Het standaardsjabloon van PIVOT terugzetten? Je huidige handtekening in de editor gaat verloren (pas na 💾 Bewaren definitief).')) return;
if (htmlModus) bron.value = STANDAARD; else editor.innerHTML = STANDAARD;
vernieuw();
};
// Veld invoegen op de cursor
document.querySelectorAll('#velden [data-veld]').forEach(b => b.addEventListener('mousedown', e => {
e.preventDefault();
const tekst = '{' + b.dataset.veld + '}';
if (htmlModus) {
const a = bron.selectionStart, z = bron.selectionEnd;
bron.value = bron.value.slice(0, a) + tekst + bron.value.slice(z);
bron.selectionStart = bron.selectionEnd = a + tekst.length; bron.focus();
} else { opdracht('insertText', tekst); }
vernieuw();
}));
function voorBewaren() {
bron.value = sjabloon();
if (!bron.value.trim()) { alert('De handtekening is leeg.'); return false; }
showLoader('Bewaren...');
return true;
}
// ---------------- Live voorbeeld ----------------
const voorbeeldVoor = document.getElementById('voorbeeldVoor');
let timer = null;
function persoon(i) {
if (i === '' || i == null) return FICTIEF;
const u = Object.assign({}, GEBRUIKERS[+i]);
const tr = document.querySelector(`#body tr[data-i="${i}"]`);
if (tr) { u.functie = tr.querySelector('.functie').value; u.telefoon = tr.querySelector('.telefoon').value; }
return u;
}
function vernieuw() {
clearTimeout(timer);
timer = setTimeout(() => post(URL_VOORBEELD, {
sjabloon: sjabloon(), gebruiker: persoon(voorbeeldVoor.value),
logo_url: document.getElementById('logoUrl').value, logo_breedte: document.getElementById('logoBreedte').value,
publiek_adres: document.getElementById('publiekAdres').value
}).then(r => {
document.getElementById('voorbeeld').innerHTML = r.html || '';
const t = document.getElementById('tekens');
t.textContent = `${r.tekens} van max. ${MAX_TEKENS} tekens (voor deze persoon)`;
t.classList.toggle('te-lang', r.tekens > MAX_TEKENS);
}).catch(() => {}), 250);
}
editor.addEventListener('input', vernieuw); bron.addEventListener('input', vernieuw);
voorbeeldVoor.addEventListener('change', vernieuw);
['logoUrl', 'logoBreedte', 'publiekAdres'].forEach(id => document.getElementById(id).addEventListener('input', vernieuw));
vernieuw();
// ---------------- OU's ----------------
const ouKader = document.getElementById('ouKader');
function toonGekozen() {
const gekozen = Array.from(ouKader.querySelectorAll('input:checked')).map(x => x.value);
ouKader.querySelectorAll('label').forEach(l => l.classList.toggle('gekozen', l.querySelector('input').checked));
document.getElementById('gekozenOus').innerHTML = gekozen.map(o => `<span>${o.replace(/</g, '&lt;')}</span>`).join('')
|| '<span class="muted small" style="background:none;color:inherit;font-weight:400">Nog geen OU gekozen</span>';
}
ouKader.addEventListener('change', toonGekozen);
document.getElementById('ouZoek').addEventListener('input', e => {
const z = e.target.value.trim().toLowerCase();
ouKader.querySelectorAll('label').forEach(l => l.style.display = !z || l.textContent.toLowerCase().includes(z) ? '' : 'none');
});
toonGekozen();
// ---------------- Gebruikers ----------------
(function () {
const body = document.getElementById('body');
if (!body) return;
const rijen = Array.from(body.rows);
let PER_PAGINA = perPagina('handtekeningen', 25), pagina = 1, zoek = '';
const zichtbaar = () => rijen.filter(tr => !zoek || (tr.textContent + ' ' + tr.querySelector('.functie').value).toLowerCase().includes(zoek));
function toon() {
const lijst = zichtbaar(), grootte = PER_PAGINA || Infinity, paginas = Math.max(1, Math.ceil(lijst.length / grootte));
pagina = Math.min(Math.max(1, pagina), paginas);
const van = PER_PAGINA ? (pagina - 1) * PER_PAGINA : 0, tot = van + grootte;
rijen.forEach(tr => tr.style.display = 'none');
lijst.forEach((tr, i) => tr.style.display = i >= van && i < tot ? '' : 'none');
document.getElementById('teller').textContent = lijst.length ? `${van + 1}–${Math.min(tot, lijst.length)} van ${lijst.length}` : 'Niets gevonden';
const nav = document.getElementById('paginering'); nav.innerHTML = '';
if (paginas > 1) {
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('‹', 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('›', pagina + 1); knop('»', paginas); }
}
document.getElementById('allesKiezen').checked = false;
}
document.getElementById('zoek').addEventListener('input', e => { zoek = e.target.value.trim().toLowerCase(); pagina = 1; toon(); });
perPaginaKiezer(document.getElementById('perPagina'), 'handtekeningen', n => { PER_PAGINA = n; pagina = 1; toon(); }, 25);
// Selectie
const gekozen = () => rijen.filter(tr => tr.querySelector('.kies').checked);
function telKeuze() {
const n = gekozen().length;
document.getElementById('aantalGekozen').textContent = n;
document.getElementById('geselecteerdToepassen').disabled = !n;
}
body.addEventListener('change', e => { if (e.target.classList.contains('kies')) telKeuze(); });
document.getElementById('allesKiezen').addEventListener('change', e => {
rijen.filter(tr => tr.style.display !== 'none').forEach(tr => tr.querySelector('.kies').checked = e.target.checked);
telKeuze();
});
// Functie / telefoon wijzigen
const gewijzigd = tr => ['functie', 'telefoon'].some(k => { const i = tr.querySelector('.' + k); return i.value.trim() !== i.dataset.oud; });
function telWijzigingen() {
const n = rijen.filter(gewijzigd).length;
document.getElementById('aantalGewijzigd').textContent = n;
document.getElementById('alleBewaren').hidden = !n;
}
body.addEventListener('input', e => {
const tr = e.target.closest('tr');
if (!tr || !e.target.matches('.functie, .telefoon')) return;
e.target.classList.toggle('gewijzigd', e.target.value.trim() !== e.target.dataset.oud);
tr.querySelector('.bewaar').hidden = !gewijzigd(tr);
telWijzigingen();
if (voorbeeldVoor.value === tr.dataset.i) vernieuw();
});
function bewaarRij(tr) {
const f = tr.querySelector('.functie'), t = tr.querySelector('.telefoon'), knop = tr.querySelector('.bewaar');
knop.disabled = true; knop.textContent = '⏳';
return post(URL_GEBRUIKER, { email: tr.dataset.email, functie: f.value.trim(), telefoon: t.value.trim() }).then(r => {
knop.disabled = false; knop.textContent = '💾';
if (!r.ok) { alert(r.fout || 'Bewaren in Google mislukt.'); return false; }
[f, t].forEach(i => { i.value = i.value.trim(); i.dataset.oud = i.value; i.classList.remove('gewijzigd'); });
GEBRUIKERS[+tr.dataset.i].functie = f.value; GEBRUIKERS[+tr.dataset.i].telefoon = t.value;
knop.hidden = true; telWijzigingen();
return true;
});
}
body.addEventListener('keydown', e => { if (e.key === 'Enter' && e.target.matches('.functie, .telefoon')) { e.preventDefault(); const tr = e.target.closest('tr'); if (gewijzigd(tr)) bewaarRij(tr); } });
document.getElementById('alleBewaren').onclick = async () => {
for (const tr of rijen.filter(gewijzigd)) { if (!await bewaarRij(tr)) break; }
};
body.addEventListener('click', e => {
const tr = e.target.closest('tr');
if (!tr) return;
if (e.target.closest('.bewaar')) bewaarRij(tr);
if (e.target.closest('.toon')) { voorbeeldVoor.value = tr.dataset.i; vernieuw(); document.getElementById('sjabloonForm').scrollIntoView({ behavior: 'smooth' }); }
if (e.target.closest('.pasToe')) start([tr.dataset.email], tr);
});
// Toepassen
function start(emails, tr) {
const niet = rijen.filter(gewijzigd);
if (niet.length && !confirm(`${niet.length} gebruiker(s) hebben nog niet-bewaarde wijzigingen aan functie/telefoon. Die worden NIET gebruikt. Toch doorgaan?`)) return;
const wie = emails ? (emails.length === 1 ? emails[0] : `${emails.length} gebruikers`) : `alle ${rijen.length} gebruikers`;
if (!confirm(`De handtekening in Gmail instellen voor ${wie}? Een bestaande handtekening wordt vervangen.`)) return;
post(URL_TOEPASSEN, emails ? { emails } : { alle: true }).then(r => {
if (!r.ok) { alert(r.fout || 'Starten mislukt.'); return; }
document.getElementById('voortgangKaart').hidden = false;
kijk();
});
}
document.getElementById('geselecteerdToepassen').onclick = () => start(gekozen().map(tr => tr.dataset.email));
document.getElementById('iedereenToepassen').onclick = () => start(null);
const zet = (id, w) => document.getElementById(id).textContent = w;
function kijk() {
fetch(URL_STATUS, { credentials: 'same-origin' }).then(r => r.json()).then(s => {
zet('tVoortgang', s.voortgang); zet('tTotaal', s.totaal); zet('tGelukt', s.gelukt); zet('tOngewijzigd', s.ongewijzigd); zet('tFout', s.fouten);
document.getElementById('balk').style.width = (s.totaal ? Math.round(100 * s.voortgang / s.totaal) : (s.bezig ? 3 : 100)) + '%';
const res = document.getElementById('resultaat'), fout = document.getElementById('foutmelding');
res.hidden = !(s.resultaat && !s.bezig); res.textContent = s.resultaat || '';
fout.hidden = !s.fout; fout.textContent = s.fout || '';
document.getElementById('foutvoorbeelden').innerHTML = (s.foutvoorbeelden || []).map(f => `<li>${f.replace(/</g, '&lt;')}</li>`).join('');
['iedereenToepassen', 'geselecteerdToepassen'].forEach(id => document.getElementById(id).disabled = s.bezig || (id === 'geselecteerdToepassen' && !gekozen().length));
if (s.bezig) { setTimeout(kijk, 1500); return; }
const log = s.logboek || {};
rijen.forEach(tr => {
const l = log[tr.dataset.email]; if (!l) return;
tr.querySelector('.status').innerHTML = l.ok ? `<span class="st ok" title="Toegepast op ${l.op}">✓</span>`
: `<span class="st fout" title="${l.melding.replace(/"/g, '&quot;')}">✕</span>`;
});
}).catch(() => setTimeout(kijk, 4000));
}
{% if status.bezig %}kijk();{% endif %}
toon(); telKeuze();
})();
</script>
{% endblock %}