Files
Pivot/templates/inventaris.html
T
2026-10-14 12:29:29 +02:00

311 lines
20 KiB
HTML
Raw 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 %}Eigendom{% endblock %}
{% block page_title %}Eigendom{% endblock %}
{% block main_class %}wide{% endblock %}
{% block extra_css %}
<style>
.col-gebruikersnaam, .col-ssnummer { display: none; }
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; }
/* Actieknoppen in een raster van 2 kolommen, zodat de hele regel zonder horizontaal schuiven zichtbaar is */
td.actions { width: 1%; }
.knoppen { display: grid; grid-template-columns: repeat(2, auto); gap: 6px; justify-content: end; }
.knoppen > *, .knoppen form button { white-space: nowrap; margin: 0; width: 100%; text-align: center; }
.knoppen form { margin: 0; }
td.col-naam { min-width: 120px; }
.toestelfilter { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; margin: 4px 0 10px; }
.toestelfilter button { border: 0; border-radius: 0; margin: 0; width: auto; padding: 6px 14px; font-size: .9em;
background: var(--surface, #fff); color: inherit; font-weight: 500; cursor: pointer; }
.toestelfilter button + button { border-left: 1px solid var(--border); }
.toestelfilter button[aria-pressed="true"] { background: var(--primary-color); color: #fff; }
.toestelfilter b { margin-left: 4px; }
tr.geen-toestel td.col-kamxm { color: var(--text-muted, #64748b); font-style: italic; }
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="page-head"><h2>🏷️ Toestellen bij leerlingen</h2></div>
<form id="search-form" class="search-bar" method="POST" action="{{ url_for('devices.inventaris_eigendom') }}">
<input type="text" name="query" placeholder="Zoek op naam, internnummer, toestelnummer of serienummer..." value="{{ query or '' }}">
<button type="submit">🔍 Zoeken</button>
</form>
{% if message %}
<div class="alert">{{ message }}</div>
{% endif %}
{% if results %}
{% set met = results|selectattr('heeft_toestel')|list|length %}
<div class="toestelfilter" role="group" aria-label="Filter op toestel">
<button type="button" data-filter="" aria-pressed="true">Alle <b>{{ results|length }}</b></button>
<button type="button" data-filter="1" aria-pressed="false">✅ Met toestel <b>{{ met }}</b></button>
<button type="button" data-filter="0" aria-pressed="false">⛔ Zonder toestel <b>{{ results|length - met }}</b></button>
</div>
<div class="column-toggles" id="column-toggles">
<label><input type="checkbox" id="toggle-internnummer" checked onchange="toggleColumn('col-internnummer')"> Internnummer</label>
<label><input type="checkbox" id="toggle-naam" checked onchange="toggleColumn('col-naam')"> Naam</label>
<label><input type="checkbox" id="toggle-gebruikersnaam" onchange="toggleColumn('col-gebruikersnaam')"> Gebruikersnaam</label>
<label><input type="checkbox" id="toggle-ssnummer" onchange="toggleColumn('col-ssnummer')"> SS_nummer</label>
<label><input type="checkbox" id="toggle-kamxm" checked onchange="toggleColumn('col-kamxm')"> Toestelnummer</label>
<label><input type="checkbox" id="toggle-serie" checked onchange="toggleColumn('col-serie')"> Serienummer</label>
<label><input type="checkbox" id="toggle-sinds" checked onchange="toggleColumn('col-sinds')"> Sinds</label>
<label><input type="checkbox" id="toggle-status" checked onchange="toggleColumn('col-status')"> Status</label>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-internnummer sorteer" data-kol="0">Internnummer <span class="pijl"></span></th>
<th class="col-naam sorteer" data-kol="1">Naam <span class="pijl"></span></th>
<th class="col-gebruikersnaam sorteer" data-kol="2">Gebruikersnaam <span class="pijl"></span></th>
<th class="col-ssnummer sorteer" data-kol="3">SS_nummer <span class="pijl"></span></th>
<th class="col-kamxm sorteer" data-kol="4">Toestelnummer <span class="pijl"></span></th>
<th class="col-serie sorteer" data-kol="5">Serienummer <span class="pijl"></span></th>
<th class="col-sinds sorteer" data-kol="6">Sinds <span class="pijl"></span></th>
<th class="col-status sorteer" data-kol="7">Status <span class="pijl"></span></th>
<th class="actions" aria-label="Acties"></th>
</tr>
</thead>
<tbody id="eigendomBody">
{% for row in results %}
<tr data-toestel="{{ 1 if row['heeft_toestel'] else 0 }}" class="{{ '' if row['heeft_toestel'] else 'geen-toestel' }}">
<td class="col-internnummer" data-sort="{{ row['internnummer'] }}"><strong>{{ row['internnummer'] }}</strong></td>
<td class="col-naam" data-sort="{{ row['Naam'] or '' }} {{ row['Voornaam'] or '' }}">{{ row['Voornaam'] or '' }} {{ row['Naam'] or '' }}</td>
<td class="col-gebruikersnaam" data-sort="{{ row['Gebruikersnaam'] or '' }}">{{ row['Gebruikersnaam'] or '' }}</td>
<td class="col-ssnummer" data-sort="{{ row['SS_nummer'] or '' }}">{{ row['SS_nummer'] or '' }}</td>
<td class="col-kamxm" data-sort="{{ row['kamxm_nummer'] or '' }}">{{ row['kamxm_nummer'] or ('' if row['heeft_toestel'] else 'geen toestel') }}</td>
<td class="col-serie" data-sort="{{ row['Serienummer'] or '' }}">{{ row['Serienummer'] or '' }}</td>
<td class="col-sinds nowrap" data-sort="{{ row['Datum_toekenning'] or '' }}">{{ row['Datum_toekenning'].strftime('%d-%m-%Y') if row['Datum_toekenning'] and row['Datum_toekenning'].strftime is defined else (row['Datum_toekenning'] or '') }}</td>
<td class="col-status" data-sort="{{ row['locked'] | int }}">
{% if not row['heeft_toestel'] %}<span class="badge">—</span>
{% elif row['locked'] | int == 1 %}<span class="badge badge-danger">🔒 Geblokkeerd</span>
{% else %}<span class="badge badge-success">🔓 Actief</span>
{% endif %}
</td>
<td class="actions">
<span class="knoppen">
{% if mag('toestel_blokkeren') and row['heeft_toestel'] %}
<form class="action-form" method="POST" action="{{ url_for('devices.toggle_lock') }}" style="margin:0">
<input type="hidden" name="internnummer" value="{{ row['internnummer'] }}">
<input type="hidden" name="query" value="{{ query }}">
{% if row['locked'] | int == 1 %}
<button type="submit" class="btn-sm btn-success">🔓 Deblokkeren</button>
{% else %}
<button type="submit" class="btn-sm btn-danger">🔒 Blokkeren</button>
{% endif %}
</form>
{% endif %}
{% if mag('toestellen_uitdelen') and row['heeft_toestel'] %}
<button type="button" class="btn-sm btn-warning"
data-id="{{ row['Eigendom_ID'] }}" data-intern="{{ row['internnummer'] }}"
data-toestel="{{ row['kamxm_nummer'] or row['Serienummer'] or '' }}"
data-naam="{{ row['Voornaam'] or '' }} {{ row['Naam'] or '' }}"
onclick="openEigendomBewerken(this)">✏️ Bewerken</button>
{% endif %}
{% if mag('toestellen_uitdelen') %}
{% if row['heeft_toestel'] %}
<a class="btn btn-sm btn-light" title="Naar het inleverformulier, al ingevuld"
href="{{ url_for('devices.inleveren_page', internnummer=row['internnummer'], toestel=row['kamxm_nummer'] or row['Serienummer'] or '', naam=((row['Voornaam'] or '') ~ ' ' ~ (row['Naam'] or ''))|trim) }}">📥 Inleveren</a>
{% else %}
<a class="btn btn-sm btn-success" title="Naar het uitdeelformulier met deze leerling ingevuld"
href="{{ url_for('devices.uitdelen_page', internnummer=row['internnummer'], naam=((row['Voornaam'] or '') ~ ' ' ~ (row['Naam'] or ''))|trim) }}">📤 Uitdelen</a>
{% endif %}
{% endif %}
<button type="button" class="btn-sm btn-light" data-intern="{{ row['internnummer'] }}"
data-naam="{{ row['Voornaam'] or '' }} {{ row['Naam'] or '' }}"
onclick="openHistorie(this)" title="Alle toestellen die deze leerling gehad heeft">🕑 Historie</button>
</span>
</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="eigendomTeller"></div>
<span id="eigendomPerPagina"></span>
</div>
<nav class="paginering" id="eigendomPaginering" aria-label="Pagina's"></nav>
</div>
{% elif query %}
<p class="muted">Geen resultaten gevonden voor "{{ query }}".</p>
{% else %}
<p class="muted">Er zijn nog geen leerlingen.</p>
{% endif %}
</div>
{% endblock %}
{% block modals %}
<div id="historieModal" class="modal">
<div class="modal-content" style="max-width:900px">
<span class="close" onclick="document.getElementById('historieModal').style.display='none'">&times;</span>
<h3>🕑 Toestellen van <span id="hNaam"></span></h3>
<p class="muted small" id="hSub" style="margin-top:-8px"></p>
<div id="hInhoud"><p class="muted">Laden...</p></div>
</div>
</div>
<div id="eigendomModal" class="modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('eigendomModal').style.display='none'">&times;</span>
<h3>✏️ Eigendom aanpassen</h3>
<p class="muted small" id="emHuidig" style="margin-top:-8px"></p>
<form method="POST" action="{{ url_for('devices.eigendom_bewerken') }}" id="eigendomForm" autocomplete="off">
<input type="hidden" name="eigendom_id" id="emId">
<input type="hidden" name="query" value="{{ query or '' }}">
<div class="field zoek-wrap">
<label for="internnummer">Internnummer</label>
<input type="text" id="internnummer" name="internnummer" required autocomplete="off"
placeholder="Zoek op naam of typ internnummer..." role="combobox"
aria-autocomplete="list" aria-controls="leerlingSuggesties" aria-expanded="false">
<ul class="suggesties" id="leerlingSuggesties" role="listbox"></ul>
</div>
<div class="field zoek-wrap">
<label for="serienummer">Toestelnummer</label>
<input type="text" id="serienummer" name="toestelnummer" required autocomplete="off"
placeholder="Zoek toestelnummer (of serienummer)..." role="combobox"
aria-autocomplete="list" aria-controls="toestelSuggesties" aria-expanded="false">
<ul class="suggesties" id="toestelSuggesties" role="listbox"></ul>
</div>
<p class="field-hint">Kies je een ander toestel, dan komt het vorige toestel weer op <em>Beschikbaar</em>.
Een toestel dat al bij een andere leerling is, kan je hier niet kiezen: lever het eerst in.</p>
<button type="submit" class="btn-block">💾 Opslaan</button>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
{% if mag('toestellen_uitdelen') %}
{% set toestel_voorkeur = 'beschikbaar' %}{% include '_toestel_zoekveld.html' %}
<script>
function openEigendomBewerken(knop) {
document.getElementById('emId').value = knop.dataset.id;
document.getElementById('internnummer').value = knop.dataset.intern;
document.getElementById('serienummer').value = knop.dataset.toestel;
document.getElementById('emHuidig').textContent =
`Nu: ${knop.dataset.toestel} bij ${knop.dataset.naam.trim() || knop.dataset.intern} (${knop.dataset.intern})`;
document.getElementById('eigendomModal').style.display = 'block';
document.getElementById('internnummer').focus();
}
window.addEventListener('click', e => { if (e.target.id === 'eigendomModal') e.target.style.display = 'none'; });
</script>
{% endif %}
<script>
const HISTORIE_URL = {{ url_for('devices.eigendom_historie', internnummer='__I__')|tojson }};
const esc = t => String(t ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
function duur(dagen) {
if (dagen == null) return '';
if (dagen < 31) return dagen + ' dag' + (dagen === 1 ? '' : 'en');
const m = Math.round(dagen / 30.44);
return m < 12 ? m + ' maand' + (m === 1 ? '' : 'en') : (dagen / 365.25).toFixed(1).replace('.', ',') + ' jaar';
}
function openHistorie(knop) {
const venster = document.getElementById('historieModal'), inhoud = document.getElementById('hInhoud');
document.getElementById('hNaam').textContent = knop.dataset.naam.trim() || knop.dataset.intern;
document.getElementById('hSub').textContent = 'Internnummer ' + knop.dataset.intern;
inhoud.innerHTML = '<p class="muted">Laden...</p>';
venster.style.display = 'block';
fetch(HISTORIE_URL.replace('__I__', encodeURIComponent(knop.dataset.intern)), { credentials: 'same-origin' })
.then(r => r.json()).then(res => {
const t = res.toestellen || [];
if (res.gebruikersnaam) document.getElementById('hSub').textContent += ' · ' + res.gebruikersnaam;
if (!t.length) { inhoud.innerHTML = '<p class="muted">Geen toestellen gevonden.</p>'; return; }
inhoud.innerHTML = `<p class="muted small">${t.length} toestel(len) in totaal</p>
<div class="table-wrap"><table><thead><tr><th>Toestelnummer</th><th>Serienummer</th><th>Type</th>
<th>Gekregen</th><th>Ingeleverd</th><th>Duur</th><th>Opmerkingen</th></tr></thead><tbody>`
+ t.map(x => `<tr${x.huidig ? ' style="background:var(--primary-light)"' : ''}>
<td><strong>${esc(x.toestel)}</strong></td><td>${esc(x.serienummer)}</td><td>${esc(x.type)}</td>
<td class="nowrap">${esc(x.van)}</td>
<td class="nowrap">${x.huidig ? '<span class="badge badge-success">nog in gebruik</span>' : esc(x.tot)}${x.geblokkeerd && x.huidig ? ' <span class="badge badge-danger">🔒</span>' : ''}</td>
<td class="nowrap">${duur(x.dagen)}</td><td class="small">${esc(x.opmerkingen)}</td></tr>`).join('')
+ '</tbody></table></div>';
}).catch(() => { inhoud.innerHTML = '<div class="alert alert-danger">Kon de historie niet ophalen.</div>'; });
}
window.addEventListener('click', e => { if (e.target.id === 'historieModal') e.target.style.display = 'none'; });
// Sorteren (klik op kolomkop) en 10 per pagina — in de browser
(function () {
const body = document.getElementById('eigendomBody');
if (!body) return;
let PER_PAGINA = perPagina('eigendom');
let pagina = 1, kol = null, op = true, filter = '';
// Filter: alle leerlingen / met toestel / zonder toestel
document.querySelectorAll('.toestelfilter button').forEach(b => b.addEventListener('click', () => {
filter = b.dataset.filter;
document.querySelectorAll('.toestelfilter button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
pagina = 1; toon();
}));
function toon() {
const zichtbaar = Array.from(body.rows).filter(tr => !filter || tr.dataset.toestel === filter);
const totaal = zichtbaar.length;
const grootte = PER_PAGINA || Infinity;
const paginas = Math.max(1, Math.ceil(totaal / grootte));
pagina = Math.min(Math.max(1, pagina), paginas);
const van = PER_PAGINA ? (pagina - 1) * PER_PAGINA : 0, tot = van + grootte; // 0 = alles
Array.from(body.rows).forEach(tr => tr.style.display = 'none');
zichtbaar.forEach((tr, i) => tr.style.display = (i >= van && i < tot) ? '' : 'none');
document.getElementById('eigendomTeller').textContent = totaal
? `${van + 1}–${Math.min(tot, totaal)} van ${totaal} leerling(en)` : 'Geen leerlingen';
const nav = document.getElementById('eigendomPaginering');
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(); };
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.querySelectorAll('th.sorteer').forEach(th => th.addEventListener('click', () => {
const k = +th.dataset.kol;
op = kol === k ? !op : true; kol = k;
document.querySelectorAll('th.sorteer').forEach(x => x.classList.remove('op', 'af'));
th.classList.add(op ? 'op' : 'af');
const rijen = Array.from(body.rows);
rijen.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;
});
rijen.forEach(tr => body.appendChild(tr));
pagina = 1; toon();
}));
perPaginaKiezer(document.getElementById('eigendomPerPagina'), 'eigendom', n => { PER_PAGINA = n; pagina = 1; toon(); });
toon();
})();
function toggleColumn(columnClass) {
const checkbox = document.getElementById('toggle-' + columnClass.replace('col-', ''));
if (!checkbox) return;
const displayStyle = checkbox.checked ? 'table-cell' : 'none';
document.querySelectorAll('.' + columnClass).forEach(cell => { cell.style.display = displayStyle; });
}
// Laadscherm tonen en dubbelklikken voorkomen bij elk formulier
document.addEventListener('submit', function (e) {
if (e.target.closest('.app-header')) return; // uitloggen niet blokkeren
showLoader();
document.querySelectorAll('button[type="submit"]').forEach(btn => { btn.disabled = true; });
});
</script>
{% endblock %}