PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -0,0 +1,310 @@
|
||||
{% 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'">×</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'">×</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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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 %}
|
||||
Reference in new issue
Block a user