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

283 lines
15 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 %}Co-accounts{% endblock %}
{% block page_title %}Co-accounts{% endblock %}
{% block extra_css %}
<style>
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 16px; margin-bottom: 20px; }
.stat { padding: 18px 20px; }
.stat + .stat { margin-top: 0; }
.stat .val { font-size: 28px; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat .lbl { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; margin-top: 4px; }
.stat .sub { font-size: 13px; color: var(--text-muted); }
.sync-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.progress { flex: 1 1 200px; height: 8px; background: var(--surface-muted); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.progress > span { display: block; height: 100%; width: 0; background: var(--primary-color); transition: width .4s; }
.filterbalk { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 12px; }
.filterbalk input[type=search] { flex: 1 1 280px; }
.filterbalk label.check-box { margin: 0; }
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.nowrap { white-space: nowrap; }
</style>
{% endblock %}
{% block content %}
<div class="stats">
<div class="card stat">
<div class="val">{{ nooit }}</div>
<div class="lbl">Nog nooit aangemeld</div>
{% if rows %}<div class="sub">van {{ rows|length }} co-accounts</div>{% endif %}
</div>
<div class="card stat">
<div class="val">{{ leerlingen }}</div>
<div class="lbl">Leerlingen met co-accounts</div>
</div>
<div class="card stat">
<div class="val" style="font-size:18px; padding-top:6px">{% if laatst %}{{ laatst.strftime('%d-%m-%Y %H:%M') }}{% else %}Nog nooit{% endif %}</div>
<div class="lbl">Opgehaald uit Smartschool</div>
<div class="sub">Automatisch één keer per dag</div>
</div>
</div>
{% if fout %}<div class="flash-message error"><span>{{ fout }}</span></div>{% endif %}
{% if not ingesteld %}
<div class="alert alert-warning">Smartschool is nog niet ingesteld voor deze school (🔌 Koppelingen › Smartschool).</div>
{% endif %}
<div class="card">
<div class="sync-bar">
<form method="POST" action="{{ url_for('smartschool.coaccount_ophalen') }}" style="margin:0">
<button type="submit" class="btn-secondary" id="syncBtn" {% if status.bezig or not ingesteld %}disabled{% endif %}>🔄 Nu ophalen uit Smartschool</button>
</form>
{% if rows %}<a class="btn btn-light" id="exportBtn" href="{{ url_for('smartschool.coaccount_export', nooit=1) }}">⬇️ CSV-export</a>{% endif %}
<div class="progress {% if not status.bezig %}hidden{% endif %}" id="syncProgress"><span id="syncProgressBar"></span></div>
<span class="muted small" id="syncText">
{% if status.bezig %}Bezig met ophalen…{% elif status.fout %}⚠️ {{ status.fout }}{% elif status.resultaat %}{{ status.resultaat }}{% endif %}
</span>
</div>
</div>
<div class="card">
<div class="filterbalk">
<input type="search" id="zoek" placeholder="Zoek op leerling, klas of co-account..." aria-label="Zoeken">
<label class="check-box"><input type="checkbox" id="enkelNooit" checked style="width:auto"> <span>Enkel nog nooit aangemeld</span></label>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sorteer" data-kol="0">Klas <span class="pijl"></span></th>
<th class="sorteer" data-kol="1">Leerling <span class="pijl"></span></th>
<th class="sorteer" data-kol="2">Co-account <span class="pijl"></span></th>
<th class="sorteer" data-kol="3">Type <span class="pijl"></span></th>
<th class="sorteer" data-kol="4">E-mail <span class="pijl"></span></th>
<th class="sorteer" data-kol="5">Laatste aanmelding <span class="pijl"></span></th>
<th class="actions">Actie</th>
</tr>
</thead>
<tbody id="coBody">
{% for r in rows %}
<tr data-nooit="{{ '1' if not r.Laatste_login else '0' }}"
data-zoek="{{ [r.Klas, r.Leerling_voornaam, r.Leerling_naam, r.Gebruikersnaam, r.Internnummer, r.Voornaam, r.Naam, r.Email]|select|join(' ')|lower }}">
<td class="nowrap" data-sort="{{ r.Klas or '' }}">{{ r.Klas or '–' }}</td>
<td data-sort="{{ (r.Leerling_naam or '') ~ ' ' ~ (r.Leerling_voornaam or '') }}"><strong>{{ r.Leerling_naam }}</strong> {{ r.Leerling_voornaam }}</td>
<td data-sort="{{ (r.Naam or '') ~ ' ' ~ (r.Voornaam or '') }}">{{ r.Voornaam }} {{ r.Naam }}</td>
<td data-sort="{{ r.Type or '' }}">{{ r.Type or '' }}</td>
<td data-sort="{{ r.Email or '' }}">{{ r.Email or '' }}</td>
<td class="nowrap" data-sort="{{ r.Laatste_login.isoformat() if r.Laatste_login else '' }}">
{% if r.Laatste_login %}{{ r.Laatste_login.strftime('%d-%m-%Y %H:%M') }}{% else %}<span class="badge badge-warning">Nooit</span>{% endif %}
</td>
<td class="actions">
<button type="button" class="btn-email btn-sm"
data-voornaam="{{ r.Voornaam or '' }}" data-naam="{{ r.Naam or '' }}" data-email="{{ r.Email or '' }}"
onclick="openEmailModal(this.dataset.voornaam, this.dataset.naam, this.dataset.email)">✉️ Bericht opstellen</button>
</td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="7">{% if status.bezig %}De co-accounts worden opgehaald uit Smartschool…{% else %}Nog geen co-accounts opgehaald.{% endif %}</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>
</div>
{% endblock %}
{% block modals %}
<div id="emailModal" class="modal">
<div class="modal-content wide">
<span class="close" onclick="closeModal()">&times;</span>
<h3>E-mailherinnering bewerken</h3>
<p>Ontvanger: <strong id="displayEmail"></strong></p>
<div class="field">
<label for="emailSubject">Onderwerp</label>
<input type="text" id="emailSubject">
</div>
<div class="field">
<label for="emailBody">Bericht</label>
<textarea id="emailBody" style="min-height: 220px;"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn-ghost" onclick="closeModal()">Annuleren</button>
<button type="button" class="btn-email" id="confirmSendBtn" onclick="executeSend()">✉️ Nu verzenden</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentTargetData = {};
function openEmailModal(voornaam, naam, email) {
if (!email || email.trim() === "" || email === "None") {
alert("Dit co-account heeft geen e-mailadres ingesteld in Smartschool.");
return;
}
currentTargetData = { voornaam, naam, email };
document.getElementById('displayEmail').innerText = email;
document.getElementById('emailSubject').value = "Belangrijk: Uw Smartschool co-account";
document.getElementById('emailBody').value = `Beste ${voornaam} ${naam},\n\nWe merkten dat u al enige tijd niet meer bent ingelogd op uw Smartschool co-account. \n\nVia dit account kunt u de resultaten, agenda en afwezigheden van uw kind op de voet volgen. Wij raden u aan om regelmatig in te loggen om op de hoogte te blijven.\n\nIndien u problemen heeft met inloggen, kunt u contact opnemen met de ICT-dienst van de school.\n\nMet vriendelijke groeten,\nHet schoolteam`;
document.getElementById('emailModal').style.display = 'block';
}
function closeModal() { document.getElementById('emailModal').style.display = 'none'; }
window.onclick = function (event) {
if (event.target == document.getElementById('emailModal')) closeModal();
};
function executeSend() {
const subject = document.getElementById('emailSubject').value;
const message = document.getElementById('emailBody').value;
if (!subject || !message) {
alert("Onderwerp en bericht mogen niet leeg zijn.");
return;
}
showLoader();
fetch("{{ url_for('smartschool.send_reminder') }}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: currentTargetData.email,
voornaam: currentTargetData.voornaam,
naam: currentTargetData.naam,
subject: subject,
message: message
})
})
.then(async response => {
const isJson = response.headers.get('content-type')?.includes('application/json');
const data = isJson ? await response.json() : null;
if (!response.ok) throw new Error((data && data.message) || response.statusText);
return data;
})
.then(() => {
hideLoader();
closeModal();
alert("E-mail succesvol verzonden!");
})
.catch(error => {
hideLoader();
console.error("Fetch error:", error);
alert("Er is een fout opgetreden: " + error.message);
});
}
// Zoeken, filter "nooit aangemeld", sorteren en pagineren in de browser
(function () {
const body = document.getElementById('coBody');
if (!body || body.querySelector('.empty-row')) return;
let PER_PAGINA = perPagina('coaccounts');
let pagina = 1, kol = null, op = true, zoek = '';
const nooit = document.getElementById('enkelNooit');
const zichtbaar = () => Array.from(body.rows).filter(tr =>
(!nooit.checked || tr.dataset.nooit === '1') && (!zoek || tr.dataset.zoek.includes(zoek)));
function toon() {
const exp = document.getElementById('exportBtn');
if (exp) exp.href = "{{ url_for('smartschool.coaccount_export') }}?" + new URLSearchParams({nooit: nooit.checked ? '1' : '0', zoek});
const rijen = zichtbaar();
const 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} co-account(s)` : 'Geen co-accounts gevonden';
const nav = document.getElementById('paginering');
nav.innerHTML = '';
if (paginas <= 1) return;
const knop = (label, p, actief, titel) => {
const b = document.createElement('button');
b.type = 'button'; b.textContent = label; b.className = 'btn-sm ' + (actief ? 'btn-secondary' : 'btn-light');
if (titel) b.title = titel;
b.onclick = () => { pagina = p; toon(); body.closest('.table-wrap').scrollIntoView({ block: 'start' }); };
nav.appendChild(b);
};
if (pagina > 1) { knop('«', 1, false, 'Eerste pagina'); 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, false, 'Laatste pagina'); }
const info = document.createElement('span');
info.className = 'muted small'; info.textContent = `pagina ${pagina} van ${paginas}`;
nav.appendChild(info);
}
document.getElementById('zoek').addEventListener('input', e => { zoek = e.target.value.trim().toLowerCase(); pagina = 1; toon(); });
nooit.addEventListener('change', () => { pagina = 1; toon(); });
document.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
const k = +th.dataset.kol;
op = kol === k ? !op : (k !== 5); // laatste aanmelding: eerst de recentste
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 x = a.cells[k].dataset.sort || '', y = b.cells[k].dataset.sort || '';
if (!x !== !y) return x ? -1 : 1;
const c = x.localeCompare(y, 'nl', { numeric: true, sensitivity: 'base' });
return op ? c : -c;
}).forEach(tr => body.appendChild(tr));
pagina = 1; toon();
}));
perPaginaKiezer(document.getElementById('perPagina'), 'coaccounts', n => { PER_PAGINA = n; pagina = 1; toon(); });
toon();
})();
// Voortgang van een lopende ophaling volgen; daarna de pagina vernieuwen
(function () {
const box = document.getElementById('syncProgress'), bar = document.getElementById('syncProgressBar');
const txt = document.getElementById('syncText'), btn = document.getElementById('syncBtn');
let bezig = {{ 'true' if status.bezig else 'false' }};
function tick() {
fetch("{{ url_for('smartschool.coaccount_status') }}").then(r => r.json()).then(s => {
if (s.bezig) {
box.classList.remove('hidden'); btn.disabled = true;
bar.style.width = (s.totaal ? Math.round(s.voortgang / s.totaal * 100) : 0) + '%';
txt.textContent = s.totaal ? `Bezig: ${s.voortgang} van ${s.totaal}…` : 'Bezig met ophalen…';
setTimeout(tick, 2000);
} else {
window.location.reload();
}
}).catch(() => {});
}
if (bezig) tick();
})();
</script>
{% endblock %}