PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -0,0 +1,506 @@
|
||||
{% 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"></> 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, '<')}</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, '<')}</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, '"')}">✕</span>`;
|
||||
});
|
||||
}).catch(() => setTimeout(kijk, 4000));
|
||||
}
|
||||
{% if status.bezig %}kijk();{% endif %}
|
||||
toon(); telKeuze();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user