Initial commit

This commit is contained in:
Kristof committed 2026-10-14 12:29:29 +02:00
commit 536ff43ce4
158 files changed
+30355

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
{# Zoekvelden met suggesties voor uitdelen / inleveren:
- Internnummer (<input id="internnummer"> + <ul id="leerlingSuggesties">): zoeken op internnummer of naam
- Toestelnummer (<input id="serienummer"> + <ul id="toestelSuggesties">): zoeken op toestel- of serienummer
Opties: toestel_voorkeur = 'beschikbaar' | 'uitgeleend' (die eerst tonen),
toestel_vul_intern = bij inleveren het andere veld automatisch invullen. #}
<style>
.zoek-wrap { position: relative; }
.suggesties { display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 20; margin: 2px 0 0; padding: 4px;
list-style: none; background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius);
box-shadow: var(--shadow-lg); max-height: 320px; overflow-y: auto; }
.suggesties.open { display: block; }
.suggesties li { padding: 8px 10px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; gap: 10px; }
.suggesties li.actief, .suggesties li:hover { background: var(--primary-light); }
.suggesties .nr { font-weight: 700; }
.suggesties .sub { color: var(--text-muted); font-size: .85em; }
.suggesties .leeg { color: var(--text-muted); cursor: default; }
</style>
<script>
const zoekEsc = t => String(t ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function zoekFlits(el) { el.style.backgroundColor = '#d1fae5'; setTimeout(() => el.style.backgroundColor = '', 600); }
// Algemeen zoekveld met suggestielijst (pijltjes, Enter, Escape, muis). Enter zonder keuze = gewoon indienen (scanner).
function zoekveld({ veldId, lijstId, url, regel, kies, leeg }) {
const veld = document.getElementById(veldId), lijst = document.getElementById(lijstId);
if (!veld || !lijst) return;
let timer, items = [], actief = -1;
const sluit = () => { lijst.classList.remove('open'); veld.setAttribute('aria-expanded', 'false'); actief = -1; };
const neem = i => { const t = items[i]; if (!t) return; kies(t, veld); sluit(); };
const teken = () => {
lijst.innerHTML = items.length
? items.map((t, i) => `<li role="option" data-i="${i}" class="${i === actief ? 'actief' : ''}">${regel(t)}</li>`).join('')
: `<li class="leeg">${leeg}</li>`;
lijst.classList.add('open'); veld.setAttribute('aria-expanded', 'true');
};
veld.addEventListener('input', () => {
clearTimeout(timer);
const q = veld.value.trim();
if (!q) { sluit(); return; }
timer = setTimeout(() => {
fetch(url + encodeURIComponent(q), { credentials: 'same-origin' })
.then(r => r.json()).then(d => { items = d; actief = -1; teken(); }).catch(sluit);
}, 200);
});
veld.addEventListener('keydown', e => {
if (!lijst.classList.contains('open') || !items.length) return;
if (e.key === 'ArrowDown') { actief = (actief + 1) % items.length; teken(); e.preventDefault(); }
else if (e.key === 'ArrowUp') { actief = (actief - 1 + items.length) % items.length; teken(); e.preventDefault(); }
else if (e.key === 'Enter' && actief >= 0) { neem(actief); e.preventDefault(); }
else if (e.key === 'Escape') sluit();
});
lijst.addEventListener('mousedown', e => { const li = e.target.closest('li[data-i]'); if (li) { e.preventDefault(); neem(+li.dataset.i); } });
veld.addEventListener('blur', () => setTimeout(sluit, 150));
}
// Internnummer: zoeken op internnummer of naam; het internnummer komt in het veld
zoekveld({
veldId: 'internnummer', lijstId: 'leerlingSuggesties', leeg: 'Geen leerling gevonden',
url: "{{ url_for('devices.zoek_leerlingen') }}?q=",
regel: l => `<span><span class="nr">${zoekEsc(l.naam || '(geen naam)')}</span>
<span class="sub"> · ${zoekEsc(l.intern)}${l.gebruiker ? ' · ' + zoekEsc(l.gebruiker) : ''}</span></span>
<span class="sub">${l.toestellen ? '💻 ' + zoekEsc(l.toestellen) : 'geen toestel'}</span>`,
kies: (l, veld) => {
veld.value = l.intern;
{% if toestel_vul_intern %}
// Inleveren: het toestel van deze leerling meteen invullen (als het nog leeg is en hij er juist één heeft)
const toestel = document.getElementById('serienummer');
if (toestel && !toestel.value.trim() && l.toestellen && !l.toestellen.includes(',')) { toestel.value = l.toestellen; zoekFlits(toestel); }
{% endif %}
document.getElementById('serienummer')?.focus();
},
});
// Toestelnummer: zoeken op toestel- of serienummer
zoekveld({
veldId: 'serienummer', lijstId: 'toestelSuggesties', leeg: 'Geen toestel gevonden',
url: "{{ url_for('devices.zoek_toestellen') }}?voorkeur={{ toestel_voorkeur|default('') }}&q=",
regel: t => `<span><span class="nr">${zoekEsc(t.nummer || '(geen toestelnummer)')}</span>
<span class="sub"> · ${zoekEsc(t.serie)}${t.type ? ' · ' + zoekEsc(t.type) : ''}</span></span>
<span class="sub">${t.bij ? 'uitgeleend aan ' + zoekEsc(t.bij) : zoekEsc(t.status || 'Beschikbaar')}</span>`,
kies: (t, veld) => {
veld.value = t.nummer || t.serie;
{% if toestel_vul_intern %}
// Inleveren: internnummer van de leerling die het toestel heeft meteen invullen (als het nog leeg is)
const intern = document.getElementById('internnummer');
if (intern && !intern.value.trim() && t.bij) { intern.value = t.bij; zoekFlits(intern); }
{% endif %}
veld.focus();
},
});
</script>