PIVOT 1.1.0

This commit is contained in:
marijn committed 2026-10-14 13:34:37 +02:00
commit 8a53234eb6
161 files changed
+31109

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
{% extends "base.html" %}
{% block title %}Verzendlijsten{% endblock %}
{% block page_title %}Verzendlijsten{% endblock %}
{% block loader_text %}Verzendlijsten ophalen bij Google...{% endblock %}
{% block extra_css %}
<style>
.nieuw-grid { display: grid; grid-template-columns: 1.4fr 1.2fr 1.6fr; gap: 12px; align-items: end; }
.nieuw-grid .breed { grid-column: 1 / -1; }
.nieuw-grid .field { margin: 0; }
.adres-veld { display: flex; align-items: stretch; }
.adres-veld input { border-top-right-radius: 0; border-bottom-right-radius: 0; flex: 1; min-width: 0; }
.adres-veld select, .adres-veld .at { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.adres-veld .at { display: grid; place-items: center; padding: 0 8px; background: var(--surface-muted);
border: 1px solid var(--border); border-left: 0; border-right: 0; color: var(--text-muted); }
.adres-veld select { width: auto; max-width: 55%; }
@media (max-width: 760px) { .nieuw-grid { grid-template-columns: 1fr; } }
details.nieuw > summary { cursor: pointer; list-style: none; display: inline-flex; }
details.nieuw > summary::-webkit-details-marker { display: none; }
details.nieuw[open] > summary { margin-bottom: 14px; }
.zoekbalk { display: flex; gap: 10px; margin-bottom: 12px; }
.zoekbalk input { flex: 1; }
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; }
td.mail { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em; }
</style>
{% endblock %}
{% block content %}
<div class="card">
<details class="nieuw" {% if not lijsten and not fout %}open{% endif %}>
<summary class="btn btn-success">➕ Nieuwe verzendlijst</summary>
<form method="POST" action="{{ url_for('verzendlijsten.nieuw') }}" class="nieuw-grid" onsubmit="showLoader('Verzendlijst aanmaken...')">
<div class="field">
<label for="adres">E-mailadres</label>
<div class="adres-veld">
<input type="text" id="adres" name="adres" required placeholder="bv. leerkrachten-1ste-graad" autocomplete="off">
{% if domeinen %}
<span class="at">@</span>
<select name="domein" aria-label="Domein">
{% for d in domeinen %}<option>{{ d }}</option>{% endfor %}
</select>
{% endif %}
</div>
</div>
<div class="field">
<label for="naam">Naam</label>
<input type="text" id="naam" name="naam" required placeholder="bv. Leerkrachten 1ste graad">
</div>
<div class="field">
<label for="wie_mailt_n">Wie mag mailen naar de lijst?</label>
<select id="wie_mailt_n" name="wie_mailt">
<option value="">Standaard van Google</option>
{% for w, l in wie_mailt %}<option value="{{ w }}">{{ l }}</option>{% endfor %}
</select>
</div>
<div class="field breed">
<label for="beschrijving">Beschrijving</label>
<input type="text" id="beschrijving" name="beschrijving" placeholder="Waarvoor dient deze lijst? (optioneel)">
</div>
<div class="breed"><button type="submit">💾 Verzendlijst aanmaken</button></div>
</form>
</details>
</div>
<div class="card">
<div class="page-head">
<h2>📮 Verzendlijsten</h2>
<a class="btn btn-light btn-sm" href="{{ url_for('verzendlijsten.overzicht') }}" onclick="showLoader()">🔄 Vernieuwen</a>
</div>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% else %}
<div class="zoekbalk">
<input type="search" id="zoek" placeholder="Zoek op naam, adres of beschrijving..." aria-label="Zoeken">
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sorteer" data-kol="0">Naam <span class="pijl"></span></th>
<th class="sorteer" data-kol="1">E-mailadres <span class="pijl"></span></th>
<th class="sorteer text-right" data-kol="2">Leden <span class="pijl"></span></th>
<th class="sorteer" data-kol="3">Beschrijving <span class="pijl"></span></th>
<th class="actions"></th>
</tr>
</thead>
<tbody id="lijstBody">
{% for g in lijsten %}
<tr data-zoek="{{ [g.naam, g.email, g.beschrijving]|join(' ')|lower }}">
<td data-sort="{{ g.naam }}"><strong>{{ g.naam }}</strong></td>
<td class="mail" data-sort="{{ g.email }}">{{ g.email }}{% if g.aliassen %}<br><span class="muted small">ook: {{ g.aliassen|join(', ') }}</span>{% endif %}</td>
<td class="text-right" data-sort="{{ '%06d'|format(g.leden) }}">{{ g.leden }}</td>
<td class="muted small" data-sort="{{ g.beschrijving }}">{{ g.beschrijving }}</td>
<td class="actions"><a class="btn btn-sm btn-secondary" href="{{ url_for('verzendlijsten.detail', email=g.email) }}" onclick="showLoader('Leden ophalen...')">✏️ Openen</a></td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="5">Er zijn nog geen verzendlijsten in Google.</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>
{% endblock %}
{% block scripts %}
<script>
(function () {
const body = document.getElementById('lijstBody');
if (!body || body.querySelector('.empty-row')) return;
let PER_PAGINA = perPagina('verzendlijsten'), pagina = 1, kol = null, op = true, zoek = '';
const zichtbaar = () => Array.from(body.rows).filter(tr => !zoek || tr.dataset.zoek.includes(zoek));
function toon() {
const rijen = zichtbaar(), 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} verzendlijst(en)` : 'Geen verzendlijsten gevonden';
const nav = document.getElementById('paginering');
nav.innerHTML = '';
if (paginas <= 1) return;
const knop = (label, p, actief) => {
const b = document.createElement('button');
b.type = 'button'; b.textContent = label; b.className = 'btn-sm ' + (actief ? 'btn-secondary' : 'btn-light');
b.onclick = () => { pagina = p; toon(); };
nav.appendChild(b);
};
if (pagina > 1) { knop('«', 1); knop('‹ Vorige', pagina - 1); }
for (let p = Math.max(1, pagina - 2); p <= Math.min(paginas, pagina + 2); p++) knop(String(p), p, p === pagina);
if (pagina < paginas) { knop('Volgende ›', pagina + 1); knop('»', paginas); }
}
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 : (k !== 2); 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 c = (a.cells[k].dataset.sort || '').localeCompare(b.cells[k].dataset.sort || '', 'nl', { numeric: true, sensitivity: 'base' });
return op ? c : -c;
}).forEach(tr => body.appendChild(tr));
pagina = 1; toon();
}));
perPaginaKiezer(document.getElementById('perPagina'), 'verzendlijsten', n => { PER_PAGINA = n; pagina = 1; toon(); });
toon();
})();
</script>
{% endblock %}