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

321 lines
22 KiB
HTML
Raw Permalink 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 %}Gedeelde drives{% endblock %}
{% block page_title %}Gedeelde drives{% endblock %}
{% block main_class %}wide{% endblock %}
{% block loader_text %}Gedeelde drives ophalen bij Google...{% endblock %}
{% block extra_css %}
<style>
[hidden] { display: none !important; }
.balk { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 12px 0; }
.balk input[type=search] { flex: 0 1 320px; margin: 0; }
.filters { display: flex; flex-wrap: wrap; gap: 8px; }
.filters button { width: auto; margin: 0; padding: 4px 12px; border-radius: 999px; background: var(--surface-muted);
border: 1px solid var(--border); font-size: .88em; color: inherit; font-weight: 400; cursor: pointer; }
.filters button[aria-pressed="true"] { border-color: var(--primary-color); background: var(--primary-light); color: var(--primary-color); font-weight: 600; }
.filters button.let { background: #fef3c7; border-color: #fcd34d; }
.filters button.let[aria-pressed="true"] { background: #fde68a; color: #92400e; }
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; }
td.getal { text-align: right; white-space: nowrap; }
tr.klik { cursor: pointer; }
tr.klik:hover td { background: var(--primary-light); }
.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; }
.toevoegen { display: grid; grid-template-columns: minmax(220px, 2fr) minmax(160px, 1fr) auto auto; gap: 10px; align-items: end; }
.toevoegen .field { margin: 0; }
@media (max-width: 800px) { .toevoegen { grid-template-columns: 1fr; } }
td.acties { white-space: nowrap; text-align: right; }
td.acties form { display: inline; margin: 0; }
td.acties select { width: auto; margin: 0; padding: 4px 8px; font-size: 13px; }
.type { font-size: .8em; padding: 1px 8px; border-radius: 999px; background: var(--surface-muted); }
</style>
{% endblock %}
{% block content %}
{% if lid_van is not none %}
{# ======================= Waar is iemand lid van? ======================= #}
<div class="card">
<div class="page-head">
<h2>🔎 {{ lid }}</h2>
<div class="actions"><a class="btn btn-light btn-sm" href="{{ url_for('gedeelde_drives.overzicht') }}">← Alle gedeelde drives</a></div>
</div>
{% if fout %}<div class="alert alert-danger">{{ fout }}</div>
{% elif not lid_van %}
<p class="muted">{{ lid }} is op geen enkele gedeelde drive lid (rechtstreeks; lidmaatschap via een Google-groep telt hier niet mee).</p>
{% else %}
<p class="muted small" style="margin-top:-6px">Lid van {{ lid_van|length }} gedeelde drive(s) (rechtstreeks; lidmaatschap via een Google-groep telt hier niet mee).</p>
<div class="table-wrap"><table>
{% set meerdere = (lid_van|map(attribute='email')|unique|list|length) > 1 or '@' not in lid %}
<thead><tr><th>Drive</th>{% if meerdere %}<th>Lid</th>{% endif %}<th>Rol</th><th></th></tr></thead>
<tbody>{% for d in lid_van %}
<tr><td><a href="{{ url_for('gedeelde_drives.overzicht', drive=d.drive) }}" onclick="showLoader()"><strong>{{ d.naam }}</strong></a></td>
{% if meerdere %}<td class="small">{{ d.wie }}{% if d.wie != d.email %}<br><span class="muted">{{ d.email }}</span>{% endif %}</td>{% endif %}
<td>{{ d.rol_naam }}</td>
<td class="acties"><a class="btn btn-light btn-sm" href="https://drive.google.com/drive/folders/{{ d.drive }}" target="_blank" rel="noopener">Openen in Drive ↗</a></td></tr>
{% endfor %}</tbody>
</table></div>
{% if '@' in lid %}
<form class="balk" method="GET" action="{{ url_for('gedeelde_drives.overzicht') }}" onsubmit="showLoader()" style="margin-top:14px">
<input type="hidden" name="oud" value="{{ lid }}">
<span>🔁 Op al deze drives vervangen door</span>
<input type="email" name="nieuw" required placeholder="nieuwe gebruiker (e-mail)" style="flex:0 1 280px;margin:0">
<button type="submit" class="btn-light btn-sm" style="margin:0">Verder</button>
</form>
{% endif %}
{% endif %}
</div>
{% elif vervang %}
{# ======================= Gebruiker vervangen ======================= #}
<div class="card">
<div class="page-head">
<h2>🔁 {{ vervang.oud }} → {{ vervang.nieuw or '?' }}</h2>
<div class="actions"><a class="btn btn-light btn-sm" href="{{ url_for('gedeelde_drives.overzicht') }}">← Alle gedeelde drives</a></div>
</div>
{% if fout %}<div class="alert alert-danger">{{ fout }}</div>
{% elif resultaat is not none %}
{% set ok = resultaat|selectattr('ok')|list|length %}
<div class="alert {{ 'alert-success' if ok == resultaat|length else 'alert-warning' }}">{{ ok }} van {{ resultaat|length }} drive(s) aangepast.</div>
<div class="table-wrap"><table>
<thead><tr><th>Drive</th><th></th><th>Resultaat</th></tr></thead>
<tbody>{% for r in resultaat %}<tr><td><strong>{{ r.naam }}</strong></td><td>{{ '✅' if r.ok else '⚠️' }}</td><td class="small">{{ r.melding }}</td></tr>{% endfor %}</tbody>
</table></div>
{% elif not vervang.drives %}
<p class="muted">{{ vervang.oud }} is op geen enkele gedeelde drive lid.</p>
{% else %}
<p class="muted small" style="margin-top:-6px">{{ vervang.oud }} is lid van {{ vervang.drives|length }} gedeelde drive(s).
{{ vervang.nieuw }} krijgt op elke aangevinkte drive exact dezelfde rol (ook als die er al lid van was).</p>
<form method="POST" action="{{ url_for('gedeelde_drives.vervangen') }}"
onsubmit="if (!this.querySelector('input[name=drive]:checked')) { alert('Vink minstens één drive aan.'); return false; }
if (!confirm({{ (vervang.oud ~ ' vervangen door ' ~ vervang.nieuw ~ ' op de aangevinkte drives?')|tojson|forceescape }})) return false; showLoader('Vervangen...');">
<input type="hidden" name="oud" value="{{ vervang.oud }}"><input type="hidden" name="nieuw" value="{{ vervang.nieuw }}">
<div class="table-wrap"><table>
<thead><tr><th style="width:28px"><input type="checkbox" checked style="width:auto;margin:0" onchange="this.closest('table').querySelectorAll('input[name=drive]').forEach(x => x.checked = this.checked)"></th>
<th>Drive</th><th>Rol van {{ vervang.oud }}</th></tr></thead>
<tbody>{% for d in vervang.drives %}
<tr><td><input type="checkbox" name="drive" value="{{ d.drive }}" checked style="width:auto;margin:0"></td>
<td><a href="{{ url_for('gedeelde_drives.overzicht', drive=d.drive) }}">{{ d.naam }}</a></td><td>{{ d.rol_naam }}</td></tr>
{% endfor %}</tbody>
</table></div>
<div class="balk">
<label style="display:flex;gap:6px;align-items:center;margin:0;font-weight:400"><input type="checkbox" name="verwijderen" value="1" checked style="width:auto;margin:0"> {{ vervang.oud }} daarna verwijderen</label>
<label style="display:flex;gap:6px;align-items:center;margin:0;font-weight:400"><input type="checkbox" name="melding" value="1" style="width:auto;margin:0"> e-mail sturen naar {{ vervang.nieuw }}</label>
<button type="submit" class="btn-success" style="margin:0 0 0 auto">🔁 Vervangen</button>
</div>
</form>
{% endif %}
</div>
{% elif gekozen %}
{# ======================= Detail van één drive ======================= #}
<div class="card">
<div class="page-head">
<h2>🗂️ {{ gekozen.naam }}</h2>
<div class="actions">
<a class="btn btn-light btn-sm" href="{{ url_for('gedeelde_drives.overzicht') }}">← Alle gedeelde drives</a>
<a class="btn btn-light btn-sm" href="https://drive.google.com/drive/folders/{{ gekozen.id }}" target="_blank" rel="noopener">Openen in Drive ↗</a>
</div>
</div>
<p class="muted small" style="margin-top:-6px">{{ gekozen.leden }} lid/leden · {{ gekozen.beheerders }} beheerder(s)
{% if gekozen.aangemaakt %} · aangemaakt {{ gekozen.aangemaakt }}{% endif %}</p>
{% if not gekozen.beheerders %}<div class="alert alert-warning">⚠️ Deze drive heeft geen beheerder. Voeg hieronder iemand toe als <strong>Beheerder</strong>.</div>{% endif %}
{% if gekozen.extern %}<div class="alert alert-warning">🌍 {{ gekozen.extern }} lid/leden van buiten de school.</div>{% endif %}
<form class="toevoegen" method="POST" action="{{ url_for('gedeelde_drives.lid_toevoegen') }}" onsubmit="showLoader('Toevoegen...')">
<input type="hidden" name="drive" value="{{ gekozen.id }}"><input type="hidden" name="naam" value="{{ gekozen.naam }}">
<div class="field"><label for="email">Persoon of groep (e-mailadres)</label>
<input type="email" id="email" name="email" required placeholder="bv. jan.peeters@school.be of leerkrachten@school.be"></div>
<div class="field"><label for="rol">Rol</label>
<select id="rol" name="rol">{% for r, n in rollen %}<option value="{{ r }}" {{ 'selected' if r == 'writer' }}>{{ n }}</option>{% endfor %}</select></div>
<label style="display:flex;gap:6px;align-items:center;margin:0 0 10px;font-weight:400"><input type="checkbox" name="melding" value="1" style="width:auto;margin:0"> e-mail sturen</label>
<button type="submit" class="btn-success" style="margin:0 0 2px">➕ Toevoegen</button>
</form>
<div class="table-wrap" style="margin-top:16px">
<table>
<thead><tr><th>Naam</th><th>E-mail</th><th>Soort</th><th>Rol</th><th></th></tr></thead>
<tbody>
{% for l in leden %}
<tr>
<td><strong>{{ l.naam or '—' }}</strong>{% if l.extern %} <span class="badge badge-warning" title="Buiten de school">extern</span>{% endif %}
{% if l.verwijderd %} <span class="badge badge-danger">account verwijderd</span>{% endif %}</td>
<td class="small">{{ l.email }}</td>
<td><span class="type">{{ {'user': 'persoon', 'group': 'groep', 'domain': 'domein', 'anyone': 'iedereen met link'}.get(l.type, l.type) }}</span></td>
<td class="acties" style="text-align:left">
<form method="POST" action="{{ url_for('gedeelde_drives.lid_wijzigen') }}">
<input type="hidden" name="drive" value="{{ gekozen.id }}"><input type="hidden" name="naam" value="{{ gekozen.naam }}">
<input type="hidden" name="perm" value="{{ l.id }}"><input type="hidden" name="email" value="{{ l.email or l.naam }}">
<select name="rol" onchange="if (confirm({{ ('Rol van ' ~ (l.email or l.naam) ~ ' wijzigen?')|tojson|forceescape }})) { showLoader(); this.form.submit(); } else { this.value = {{ l.rol|tojson|forceescape }}; }">
{% for r, n in rollen %}<option value="{{ r }}" {{ 'selected' if r == l.rol }}>{{ n }}</option>{% endfor %}
</select>
</form>
</td>
<td class="acties">
<form method="POST" action="{{ url_for('gedeelde_drives.lid_verwijderen') }}"
onsubmit="if (!confirm({{ ((l.email or l.naam) ~ ' uit ' ~ gekozen.naam ~ ' verwijderen?')|tojson|forceescape }})) return false; showLoader();">
<input type="hidden" name="drive" value="{{ gekozen.id }}"><input type="hidden" name="naam" value="{{ gekozen.naam }}">
<input type="hidden" name="perm" value="{{ l.id }}"><input type="hidden" name="email" value="{{ l.email or l.naam }}">
<button type="submit" class="btn-sm btn-ghost" title="Verwijderen">🗑️</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Deze drive heeft geen leden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="field-hint" style="margin-top:12px"><strong>Beheerder</strong>: alles, ook leden beheren · <strong>Inhoudsbeheerder</strong>:
bestanden toevoegen, bewerken, verplaatsen en verwijderen · <strong>Bijdrager</strong>: toevoegen en bewerken ·
<strong>Reageerder</strong>: bekijken en reageren · <strong>Lezer</strong>: enkel bekijken.</p>
</div>
{% else %}
{# ======================= Overzicht ======================= #}
<div class="card">
<div class="page-head">
<h2>🗂️ Gedeelde drives <span class="muted small">({{ drives|length }})</span></h2>
<div class="actions">
<button type="button" class="btn-success btn-sm" onclick="document.getElementById('nieuwVenster').style.display='block'">➕ Nieuwe gedeelde drive</button>
<a class="btn btn-secondary btn-sm" href="{{ url_for('gedeelde_drives.overzicht', vernieuw=1) }}" onclick="showLoader()">🔄 Opnieuw ophalen</a>
</div>
</div>
<form class="balk" method="GET" action="{{ url_for('gedeelde_drives.overzicht') }}" onsubmit="showLoader('Drives van deze gebruiker zoeken...')"
style="padding:12px 14px;border:1px solid var(--border);border-radius:var(--radius);margin:0 0 10px">
<strong style="white-space:nowrap">🔎 Waar is iemand lid van?</strong>
<input type="text" name="lid" required placeholder="e-mailadres, of een deel van de naam of het adres" style="flex:1 1 300px;margin:0">
<button type="submit" class="btn-light btn-sm" style="margin:0">Zoeken</button>
</form>
<form class="balk" method="GET" action="{{ url_for('gedeelde_drives.overzicht') }}" onsubmit="showLoader('Drives van deze gebruiker zoeken...')"
style="padding:12px 14px;border:1px solid var(--border);border-radius:var(--radius);margin-top:0">
<strong style="white-space:nowrap">🔁 Gebruiker vervangen</strong>
<input type="email" name="oud" required placeholder="oude gebruiker (e-mail)" style="flex:1 1 220px;margin:0">
<span>→</span>
<input type="email" name="nieuw" required placeholder="nieuwe gebruiker (e-mail)" style="flex:1 1 220px;margin:0">
<button type="submit" class="btn-light btn-sm" style="margin:0">Drives zoeken</button>
</form>
{% if fout %}
<div class="alert alert-danger">{{ fout }}</div>
{% elif not drives %}
<p class="muted">Er zijn nog geen gedeelde drives in dit domein.</p>
{% else %}
{% set zonder = drives|selectattr('beheerders', 'equalto', 0)|list|length %}
{% set ext = drives|selectattr('extern')|list|length %}
<div class="balk">
<input type="search" id="zoek" placeholder="Zoek een drive..." aria-label="Zoeken">
<div class="filters" id="filters">
<button type="button" data-filter="" aria-pressed="true">Alle <b>{{ drives|length }}</b></button>
{% if zonder %}<button type="button" class="let" data-filter="zonder" aria-pressed="false">Zonder beheerder <b>{{ zonder }}</b></button>{% endif %}
{% if ext %}<button type="button" class="let" data-filter="extern" aria-pressed="false">Met externe leden <b>{{ ext }}</b></button>{% endif %}
</div>
<span id="perPagina"></span>
<span class="muted small">Opgehaald op {{ opgehaald }}</span>
</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="sorteer" data-tekst>Naam <span class="pijl"></span></th>
<th class="sorteer">Leden <span class="pijl"></span></th>
<th class="sorteer">Beheerders <span class="pijl"></span></th>
<th class="sorteer">Groepen <span class="pijl"></span></th>
<th class="sorteer">Extern <span class="pijl"></span></th>
<th class="sorteer">Aangemaakt <span class="pijl"></span></th>
</tr></thead>
<tbody id="body">
{% for d in drives %}
<tr class="klik" data-href="{{ url_for('gedeelde_drives.overzicht', drive=d.id) }}"
data-filters="{{ 'zonder' if not d.beheerders }} {{ 'extern' if d.extern }}">
<td data-sort="{{ d.naam }}"><strong>{{ d.naam }}</strong>{% if d.fout %} <span class="badge badge-danger" title="{{ d.fout }}">leden niet op te halen</span>{% endif %}</td>
<td class="getal" data-sort="{{ d.leden }}">{{ d.leden }}</td>
<td class="getal" data-sort="{{ d.beheerders }}">{% if d.beheerders %}{{ d.beheerders }}{% else %}<span class="badge badge-danger">geen</span>{% endif %}</td>
<td class="getal" data-sort="{{ d.groepen }}">{{ d.groepen or '' }}</td>
<td class="getal" data-sort="{{ d.extern }}">{% if d.extern %}<span class="badge badge-warning">{{ d.extern }}</span>{% endif %}</td>
<td data-sort="{{ d.aangemaakt }}" class="small">{{ d.aangemaakt }}</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 %}
<p class="field-hint" style="margin-top:14px">Vereist in de domeinbrede delegatie: <code>https://www.googleapis.com/auth/drive</code>,
en de <strong>Google Drive API</strong> moet aanstaan in het Cloud-project van het serviceaccount.</p>
</div>
{% endif %}
{% endblock %}
{% block modals %}
<div id="nieuwVenster" class="modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('nieuwVenster').style.display='none'">&times;</span>
<h3>➕ Nieuwe gedeelde drive</h3>
<form method="POST" action="{{ url_for('gedeelde_drives.nieuw') }}" onsubmit="showLoader('Aanmaken...')">
<div class="field"><label>Naam</label><input type="text" name="naam" required maxlength="100"></div>
<div class="field"><label>Beheerder (optioneel)</label><input type="email" name="beheerder" placeholder="e-mailadres van persoon of groep">
<p class="field-hint">De beheerder van PIVOT wordt automatisch ook beheerder van de nieuwe drive.</p></div>
<button type="submit" class="btn-block">Aanmaken</button>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
window.addEventListener('click', e => { if (e.target.id === 'nieuwVenster') e.target.style.display = 'none'; });
(function () {
const body = document.getElementById('body');
if (!body) return;
const rijen = Array.from(body.rows), zoek = document.getElementById('zoek');
let PER = perPagina('gedeelde-drives', 25), pagina = 1, filter = '';
body.addEventListener('click', e => { const tr = e.target.closest('tr.klik'); if (tr) { showLoader(); location.href = tr.dataset.href; } });
document.querySelectorAll('#filters button').forEach(b => b.addEventListener('click', () => {
filter = b.dataset.filter;
document.querySelectorAll('#filters button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
pagina = 1; toon();
}));
let kol = null, op = true;
document.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
const k = th.cellIndex;
op = kol === k ? !op : 'tekst' in th.dataset; kol = k;
document.querySelectorAll('th.sorteer').forEach(x => x.classList.remove('op', 'af'));
th.classList.add(op ? 'op' : 'af');
rijen.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();
}));
function toon() {
const z = zoek.value.trim().toLowerCase();
const lijst = rijen.filter(tr => (!z || tr.cells[0].textContent.toLowerCase().includes(z))
&& (!filter || tr.dataset.filters.split(' ').includes(filter)));
const grootte = PER || Infinity, paginas = Math.max(1, Math.ceil(lijst.length / grootte));
pagina = Math.min(Math.max(1, pagina), paginas);
const van = PER ? (pagina - 1) * PER : 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 = (t, p, a) => { const b = document.createElement('button'); b.type = 'button'; b.textContent = t;
b.className = 'btn-sm ' + (a ? '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); }
}
}
zoek.addEventListener('input', () => { pagina = 1; toon(); });
perPaginaKiezer(document.getElementById('perPagina'), 'gedeelde-drives', n => { PER = n; pagina = 1; toon(); }, 25);
toon();
})();
</script>
{% endblock %}