feat: role filter in results grid + role-scoped Excel and Art.30 exports

- New Role dropdown in filter bar (All / Ansatte / Elever) — filters the
  results grid client-side via applyFilters() and clearFilters().

- Exports respect the active role: exportExcel() and exportArticle30()
  append ?role=student|staff to the fetch URL when a role is selected.

- _build_excel_bytes(role='') and _build_article30_docx(role='') filter
  to a local _items list at the top; all internal sheets (Summary, GPS,
  External transfers, Art.30 staff/student tables) see only the filtered
  subset. Filenames get _elever or _ansatte suffix.

- i18n: m365_filter_all_roles / m365_filter_staff / m365_filter_student
  added to en/da/de.json.

- CLAUDE.md, README.md, CHANGELOG.md, MANUAL-EN.md, MANUAL-DA.md updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
StyxX65andClaude Sonnet 4.6 committed 2026-04-12 09:02:52 +02:00
1 parent 28c9effd17
commit 0c35a7a83d
11 files changed
+68 -18

No files matched your search

+9 -2
View File
@@ -669,6 +669,7 @@ function applyFilters() {
const dispVal = document.getElementById('filterDisposition')?.value || '';
const transferVal = document.getElementById('filterTransfer')?.value || '';
const specialVal = document.getElementById('filterSpecial')?.value || '';
const roleVal = document.getElementById('filterRole')?.value || '';
S.filteredData = S.flaggedData.filter(f => {
if (search && !f.name.toLowerCase().includes(search)) return false;
if (srcVal && f.source_type !== srcVal) return false;
@@ -676,6 +677,8 @@ function applyFilters() {
if (transferVal && (f.transfer_risk || '') !== transferVal) return false;
if (specialVal === '1' && !(f.special_category && f.special_category.length)) return false;
if (specialVal === 'photo' && !(f.face_count > 0)) return false;
if (roleVal === 'student' && f.user_role !== 'student') return false;
if (roleVal === 'staff' && f.user_role === 'student') return false;
return true;
});
const grid = document.getElementById('grid');
@@ -721,7 +724,8 @@ async function exportExcel() {
return;
}
// Browser / localhost fallback: fetch as blob and trigger download
const r = await fetch('/api/export_excel');
const _roleParam = document.getElementById('filterRole')?.value || '';
const r = await fetch('/api/export_excel' + (_roleParam ? '?role=' + encodeURIComponent(_roleParam) : ''));
if (!r.ok) {
const err = await r.json().catch(() => ({error: 'Export failed'}));
log('Export error: ' + (err.error || r.status), 'err');
@@ -762,7 +766,8 @@ async function exportArticle30() {
const btn = document.getElementById('exportA30Btn');
if (btn) { btn.disabled = true; btn.textContent = '⏳'; }
try {
const r = await fetch('/api/export_article30');
const _roleParam30 = document.getElementById('filterRole')?.value || '';
const r = await fetch('/api/export_article30' + (_roleParam30 ? '?role=' + encodeURIComponent(_roleParam30) : ''));
if (!r.ok) {
const err = await r.json().catch(() => ({error: 'Export failed'}));
log('Article 30 export error: ' + (err.error || r.status), 'err');
@@ -796,6 +801,8 @@ function clearFilters() {
if (ft) ft.value = '';
const fs = document.getElementById('filterSpecial');
if (fs) fs.value = '';
const fr = document.getElementById('filterRole');
if (fr) fr.value = '';
applyFilters();
}