PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -0,0 +1,273 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mailbox-delegatie{% endblock %}
|
||||
{% block page_title %}Mailbox-delegatie{% endblock %}
|
||||
{% block main_class %}medium{% endblock %}
|
||||
{% block loader_text %}Bezig met Google...{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.mailbox-head { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
|
||||
.avatar { width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center;
|
||||
background: var(--primary-light); color: var(--primary-color); font-weight: 800; font-size: 18px; flex-shrink: 0; }
|
||||
.mailbox-head .who { flex: 1; min-width: 200px; }
|
||||
.mailbox-head .who strong { display: block; font-size: 1.1rem; }
|
||||
.counter { font-size: 13px; color: var(--text-muted); }
|
||||
.add-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.add-form input[type=email] { flex: 1 1 260px; width: auto; }
|
||||
.pick-list { list-style: none; margin: 0; padding: 0; }
|
||||
.pick-list a { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: var(--radius-sm);
|
||||
text-decoration: none; color: var(--text); }
|
||||
.pick-list a:hover { background: var(--primary-light); }
|
||||
.pick-list .ou { color: var(--text-muted); font-size: 13px; }
|
||||
.ov-kop { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.ov-kop .card-title { margin: 0; }
|
||||
.ov-balk { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; margin: 14px 0 10px; }
|
||||
.ov-balk input[type=search] { flex: 0 1 340px; }
|
||||
.persoon strong { display: block; font-weight: 600; }
|
||||
.persoon span { color: var(--text-muted); font-size: 13px; }
|
||||
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; }
|
||||
.voortgang { height: 8px; border-radius: 999px; background: var(--surface-muted); overflow: hidden; margin-top: 8px; }
|
||||
.voortgang div { height: 100%; width: 0; background: var(--primary-color); transition: width .4s; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# ---------- Zoeken ---------- #}
|
||||
<div class="card">
|
||||
<h2 class="card-title">✉️ Welke mailbox?</h2>
|
||||
<form class="search-bar" method="GET" action="{{ url_for('gmail_delegatie.overzicht') }}" onsubmit="showLoader()" style="margin:0">
|
||||
<input type="text" name="zoek" placeholder="Naam of e-mailadres van de mailbox, bv. secretariaat@..." value="{{ zoek or (mailbox.email if mailbox else '') }}" autofocus>
|
||||
<button type="submit">🔍 Zoeken</button>
|
||||
</form>
|
||||
|
||||
{% if zoekresultaten %}
|
||||
<p class="muted small" style="margin:14px 0 6px">Kies een mailbox:</p>
|
||||
<ul class="pick-list">
|
||||
{% for u in zoekresultaten %}
|
||||
<li><a href="{{ url_for('gmail_delegatie.overzicht', mailbox=u.email) }}" onclick="showLoader()">
|
||||
<span><strong>{{ u.naam }}</strong> · {{ u.email }}</span><span class="ou">{{ u.ou }}</span>
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if fout %}
|
||||
<div class="flash-message error" style="margin-top:20px"><span>{{ fout }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
{# ---------- Gedelegeerden van de gekozen mailbox ---------- #}
|
||||
{% if mailbox %}
|
||||
<div class="card">
|
||||
<div class="mailbox-head">
|
||||
<span class="avatar">{{ (mailbox.naam or mailbox.email)[:1]|upper }}</span>
|
||||
<div class="who">
|
||||
<strong>{{ mailbox.naam or mailbox.email }}</strong>
|
||||
<span class="muted">{{ mailbox.email }}{% if mailbox.ou %} · {{ mailbox.ou }}{% endif %}</span>
|
||||
{% if mailbox.geschorst %}<span class="badge badge-warning">Geschorst</span>{% endif %}
|
||||
</div>
|
||||
<span class="counter">{{ delegates|length }} / {{ max_delegates }} gedelegeerden</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Heeft toegang</th><th>Status</th><th class="actions">Actie</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in delegates %}
|
||||
<tr>
|
||||
<td><strong>{{ d.email }}</strong></td>
|
||||
<td>
|
||||
{% if d.status == 'accepted' %}<span class="badge badge-success">{{ d.status_label }}</span>
|
||||
{% elif d.status == 'pending' %}<span class="badge badge-warning">{{ d.status_label }}</span>
|
||||
{% else %}<span class="badge">{{ d.status_label }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<form method="POST" action="{{ url_for('gmail_delegatie.verwijderen') }}"
|
||||
onsubmit="if (!confirm('Toegang van {{ d.email }} tot {{ mailbox.email }} intrekken?')) return false; showLoader();">
|
||||
<input type="hidden" name="mailbox" value="{{ mailbox.email }}">
|
||||
<input type="hidden" name="delegate" value="{{ d.email }}">
|
||||
<button type="submit" class="btn-sm btn-danger">✕ Intrekken</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="empty-row"><td colspan="3">Niemand anders heeft toegang tot deze mailbox.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if delegates|length < max_delegates %}
|
||||
<form class="add-form" method="POST" action="{{ url_for('gmail_delegatie.toevoegen') }}" onsubmit="showLoader()">
|
||||
<input type="hidden" name="mailbox" value="{{ mailbox.email }}">
|
||||
<input type="email" name="delegate" required placeholder="E-mailadres van de collega die toegang krijgt">
|
||||
<button type="submit" class="btn-success">➕ Toegang geven</button>
|
||||
</form>
|
||||
<p class="field-hint">Enkel accounts van jullie eigen Google Workspace. De collega ziet de mailbox binnen ongeveer een minuut via het profielmenu in Gmail.</p>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" style="margin-top:16px">Deze mailbox heeft het maximum van {{ max_delegates }} gedelegeerden bereikt.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ---------- Alle lopende machtigingen ---------- #}
|
||||
<div class="card" id="overzicht">
|
||||
<div class="ov-kop">
|
||||
<h2 class="card-title">📋 Lopende machtigingen <span class="muted small">({{ machtigingen|length }})</span></h2>
|
||||
<form method="POST" action="{{ url_for('gmail_delegatie.overzicht_vernieuwen') }}" style="margin:0">
|
||||
<button type="submit" class="btn-light btn-sm" {{ 'disabled' if ov_status.bezig }}>🔄 Vernieuwen</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="muted small" style="margin:6px 0 0">
|
||||
{% if laatst and laatst.Opgehaald %}Alle mailboxen nagekeken op {{ laatst.Opgehaald.strftime('%d-%m-%Y om %H:%M') }}
|
||||
({{ laatst.Mailboxen }} mailboxen{% if laatst.Fouten %}, {{ laatst.Fouten }} niet bevraagbaar{% endif %}).
|
||||
Wijzigingen via PIVOT staan er meteen in.
|
||||
{% else %}Nog niet opgehaald.{% endif %}
|
||||
</p>
|
||||
{% if ov_fout %}<div class="alert alert-danger" style="margin-top:12px">{{ ov_fout }}</div>{% endif %}
|
||||
{% if ov_status.fout and not ov_status.bezig %}<div class="alert alert-danger" style="margin-top:12px">Ophalen mislukt: {{ ov_status.fout }}</div>{% endif %}
|
||||
<div id="ovBezig" {{ '' if ov_status.bezig else 'hidden' }} style="margin-top:12px">
|
||||
<span class="muted small" id="ovTekst">Mailboxen nakijken...</span>
|
||||
<div class="voortgang"><div id="ovBalk"></div></div>
|
||||
</div>
|
||||
|
||||
{% if machtigingen %}
|
||||
<div class="ov-balk">
|
||||
<input type="search" id="ovZoek" placeholder="Zoek een mailbox of persoon..." aria-label="Zoeken in de machtigingen">
|
||||
<span class="per-pagina-plek"></span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="ovTabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sorteer">Mailbox <span class="pijl"></span></th>
|
||||
<th class="sorteer">Heeft toegang <span class="pijl"></span></th>
|
||||
<th class="sorteer">Status <span class="pijl"></span></th>
|
||||
<th class="actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in machtigingen %}
|
||||
<tr>
|
||||
<td class="persoon" data-sort="{{ (m.Mailbox_naam or m.Mailbox)|lower }}"><strong>{{ m.Mailbox_naam or m.Mailbox }}</strong>{% if m.Mailbox_naam %}<span>{{ m.Mailbox }}</span>{% endif %}</td>
|
||||
<td class="persoon" data-sort="{{ (m.Gedelegeerde_naam or m.Gedelegeerde)|lower }}"><strong>{{ m.Gedelegeerde_naam or m.Gedelegeerde }}</strong>{% if m.Gedelegeerde_naam %}<span>{{ m.Gedelegeerde }}</span>{% endif %}</td>
|
||||
<td data-sort="{{ m.Status or '' }}">
|
||||
{% if m.Status == 'accepted' %}<span class="badge badge-success">Actief</span>
|
||||
{% elif m.Status == 'pending' %}<span class="badge badge-warning">In afwachting</span>
|
||||
{% elif m.Status == 'rejected' %}<span class="badge">Geweigerd</span>
|
||||
{% elif m.Status == 'expired' %}<span class="badge">Verlopen</span>
|
||||
{% else %}<span class="badge">{{ m.Status or '?' }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="actions"><a href="{{ url_for('gmail_delegatie.overzicht', mailbox=m.Mailbox) }}" class="btn btn-sm btn-light" onclick="showLoader()">Openen</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tabel-voet">
|
||||
<div class="muted small" id="ovTeller"></div>
|
||||
<nav class="paginering" id="ovPaginas" aria-label="Pagina's"></nav>
|
||||
</div>
|
||||
{% elif laatst and laatst.Opgehaald %}
|
||||
<p class="muted" style="margin-top:12px">Er zijn geen lopende machtigingen.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ---------- Logboek ---------- #}
|
||||
{% if geschiedenis %}
|
||||
<div class="card">
|
||||
<h3 class="card-title">🕑 {{ 'Wijzigingen voor deze mailbox' if mailbox else 'Recente wijzigingen' }}</h3>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Tijdstip</th><th>Door</th><th>Actie</th><th>Mailbox</th><th>Gedelegeerde</th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in geschiedenis %}
|
||||
<tr>
|
||||
<td class="nowrap">{{ h.Tijdstip.strftime('%d-%m-%Y %H:%M') if h.Tijdstip else '' }}</td>
|
||||
<td>{{ h.Gebruiker or '?' }}</td>
|
||||
<td>{% if h.Actie == 'toegevoegd' %}<span class="badge badge-success">Toegevoegd</span>{% else %}<span class="badge badge-danger">Ingetrokken</span>{% endif %}</td>
|
||||
<td>{{ h.Mailbox }}</td>
|
||||
<td>{{ h.Gedelegeerde }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Ophaling op de achtergrond: voortgang tonen en daarna de pagina vernieuwen
|
||||
(function () {
|
||||
const blok = document.getElementById('ovBezig');
|
||||
if (!blok || blok.hidden) return;
|
||||
const tekst = document.getElementById('ovTekst'), balk = document.getElementById('ovBalk');
|
||||
const kijk = () => fetch({{ url_for('gmail_delegatie.overzicht_status')|tojson }}).then(r => r.json()).then(s => {
|
||||
if (s.totaal) {
|
||||
tekst.textContent = `Mailboxen nakijken: ${s.voortgang} van ${s.totaal}...`;
|
||||
balk.style.width = Math.round(100 * s.voortgang / s.totaal) + '%';
|
||||
}
|
||||
if (s.bezig) setTimeout(kijk, 2000);
|
||||
else location.reload();
|
||||
}).catch(() => setTimeout(kijk, 5000));
|
||||
kijk();
|
||||
})();
|
||||
|
||||
// Tabel: zoeken, sorteren en pagineren
|
||||
(function () {
|
||||
const tabel = document.getElementById('ovTabel');
|
||||
if (!tabel) return;
|
||||
const body = tabel.tBodies[0], zoekveld = document.getElementById('ovZoek');
|
||||
let PER_PAGINA = perPagina('gmail-machtigingen'), pagina = 1, zoek = '';
|
||||
function toon() {
|
||||
const rijen = Array.from(body.rows).filter(tr => !zoek || tr.textContent.toLowerCase().includes(zoek));
|
||||
const grootte = PER_PAGINA || Infinity, 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('ovTeller').textContent = rijen.length
|
||||
? `${van + 1}–${Math.min(tot, rijen.length)} van ${rijen.length}` : 'Niets gevonden';
|
||||
const nav = document.getElementById('ovPaginas');
|
||||
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); }
|
||||
}
|
||||
zoekveld.addEventListener('input', () => { zoek = zoekveld.value.trim().toLowerCase(); pagina = 1; toon(); });
|
||||
let kol = null, op = true;
|
||||
tabel.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
|
||||
const k = th.cellIndex;
|
||||
op = kol === k ? !op : true;
|
||||
kol = k;
|
||||
tabel.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.querySelector('#overzicht .per-pagina-plek'), 'gmail-machtigingen', n => { PER_PAGINA = n; pagina = 1; toon(); });
|
||||
toon();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user