From 75244cde147ff8c5f7d3a63c1712a54073012977 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 15 Jul 2026 05:03:30 +0200 Subject: [PATCH] v0.3.33-beta: overzichtelijker beheerpaneel zonder tab-afhankelijkheid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vaste schoolkiezer-balk (🏫) boven alle beheer-tabs voor de systeem- manager: geen omweg meer via het Systeem-tabblad om een school te kiezen; de kiezer is uit het Systeem-tabblad verdwenen en de hint verwijst naar de balk erboven - Gebruikers-tab: leerlingen staan onder inklapbare groepskoppen per klas (standaard dicht, met aantal en ▸/▾-pijl); zoeken kijkt altijd door alle groepen heen zodat een treffer nooit verstopt zit - Gebruikersrijen tonen standaard alleen naam en rol/klas; alle knoppen en velden (rol wijzigen, extra functies, klas, wachtwoord, koppelcode, verwijderen) zitten achter een ✏-knopje per rij; uitgeklapte staat blijft behouden over her-renders --- VERSION | 2 +- public/css/teach.css | 21 +++++++ public/js/admin.js | 146 ++++++++++++++++++++++++++++++------------- public/js/core.js | 8 ++- 4 files changed, 130 insertions(+), 47 deletions(-) diff --git a/VERSION b/VERSION index 3f1bfa5..53a40fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.32-beta +0.3.33-beta diff --git a/public/css/teach.css b/public/css/teach.css index 0573948..eb2d4d4 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1129,6 +1129,13 @@ min-height:20px; font-size:13.5px; font-weight:700; color:var(--green); margin:4px 0; display:flex; align-items:center; gap:8px; flex-wrap:wrap; } +/* vaste schoolkiezer voor de systeemmanager, boven alle beheer-tabs */ +.am-schoolbar{ + display:flex; align-items:center; gap:8px; margin:0 0 10px; + padding:8px 10px; background:var(--surface-2); border-radius:12px; +} +.am-schoolbar-lbl{font-size:16px;} +.am-schoolbar .am-sel{flex:1; min-width:0;} .am-add{display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0;} .am-inp,.am-sel,.am-pw{ border:1px solid var(--line); border-radius:var(--radius); height:var(--control-h); @@ -1143,6 +1150,20 @@ .am-pw{width:120px;} .am-list{display:flex; flex-direction:column; gap:5px;} .am-group{font-weight:700; font-size:13px; margin-top:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px;} +/* inklapbare groepskop (leerlingen per klas): klikbaar, pijltje in de tekst */ +.am-group-toggle{cursor:pointer; user-select:none;} +.am-group-toggle:hover{color:var(--ink);} +/* uitklapbaar gereedschapsblok per gebruikersrij (achter het ✏-knopje) */ +.am-row-tools{ + display:flex; gap:8px; align-items:center; flex-wrap:wrap; width:100%; + margin-top:6px; padding-top:8px; border-top:1px dashed var(--line); +} +.am-edit{ + border:none; background:transparent; border-radius:8px; cursor:pointer; + font-size:14px; padding:4px 8px; opacity:.6; +} +.am-edit:hover{opacity:1; background:var(--surface-3);} +.am-edit.on{opacity:1; background:var(--accent-soft);} .am-row{ display:flex; gap:8px; align-items:center; background:var(--surface-2); border-radius:var(--radius); padding:7px 10px; flex-wrap:wrap; diff --git a/public/js/admin.js b/public/js/admin.js index d4f87df..d1bec9a 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -17,6 +17,10 @@ let adminTab = "gebruikers"; /* live zoekfilter voor de gebruikerslijst, blijft ook behouden over her-renders */ let userFilter = ""; + /* uitgeklapte leerling-groepen en gebruikersrijen (standaard alles dicht + voor een rustig overzicht), blijven behouden over her-renders */ + const openGroups = new Set(); + const openRows = new Set(); /* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */ const creatableRoles = () => @@ -127,11 +131,17 @@ function userRow(u){ const row = h("div","am-row"); - /* actieknoppen (kopieer/genereer/verwijder) samen in één groep, zodat - ze op smalle schermen als blok rechts blijven staan i.p.v. los - tussen de andere rij-elementen te wrappen */ + /* rustig standaardbeeld: alleen naam + rol/klas; alle knoppen en velden + zitten achter het ✏-knopje in een uitklapbaar gereedschapsblok */ + const tools = h("div","am-row-tools"); const actions = h("div","am-actions"); row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : ""))); + if(u.role==="pupil"){ + const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId)); + row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : ""))); + }else{ + row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` · ${T("amPending")}` : ""))); + } if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){ /* super mag de rol van andere stafaccounts wijzigen - leerlingen blijven altijd leerling */ const rs = h("select","am-sel"); @@ -141,7 +151,7 @@ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{role: rs.value}}); reload(); } catch(e){ msg(e.message); } }); - row.appendChild(rs); + tools.appendChild(rs); /* extra functies naast de hoofdrol (bv. een teacher die ook beheerdersrechten krijgt) - los van de hoofdrol hierboven, direct toggelbaar per rol */ @@ -158,9 +168,7 @@ }); extraWrap.appendChild(chip); }); - row.appendChild(extraWrap); - }else{ - row.appendChild(h("span","am-role", T(roleKey(u.role)))); + tools.appendChild(extraWrap); } if(u.role==="pupil"){ /* klas (uitwisselen met andere klassen) */ @@ -169,7 +177,7 @@ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{classId: cs.value ? +cs.value : null}}); } catch(e){ msg(e.message); } }); - row.appendChild(cs); + tools.appendChild(cs); /* wachtwoord inzien en aanpassen - alleen zichtbaar zolang de leerling nog niet is ingelogd; daarna kan alleen een nieuw wachtwoord gezet worden */ const pw = h("input","am-pw"); @@ -182,7 +190,7 @@ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{password: pw.value}}); reload(); } catch(e){ msg(e.message); } }); - row.appendChild(pw); + tools.appendChild(pw); const pwCopy = h("button","am-copy","📋"); pwCopy.type = "button"; pwCopy.title = T("amCopy"); @@ -203,9 +211,7 @@ } }else{ if(u.pending && u.linkCode){ - row.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode)); - }else if(u.pending){ - row.appendChild(h("span","am-code", T("amPending"))); + tools.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode)); } if(can("users.staffCredentials") && u.role!=="pupil" && u.id!==currentUser.id){ const nc = h("button","am-btn", T("amNewCode")); @@ -239,7 +245,24 @@ }); actions.appendChild(del); } - if(actions.childNodes.length) row.appendChild(actions); + if(actions.childNodes.length) tools.appendChild(actions); + if(tools.childNodes.length){ + const edit = h("button","am-edit","✏"); + edit.type = "button"; + edit.title = T("amEdit"); + const sync = ()=>{ + const open = openRows.has(u.id); + tools.style.display = open ? "" : "none"; + edit.classList.toggle("on", open); + }; + edit.addEventListener("click", ()=>{ + if(openRows.has(u.id)) openRows.delete(u.id); else openRows.add(u.id); + sync(); + }); + sync(); + row.appendChild(edit); + row.appendChild(tools); + } return row; } @@ -346,16 +369,12 @@ return box; } - /* systeem: schoolkiezer + nieuwe school + systeemmanagers beheren. - Dit tabblad is waar een super zaken regelt die niet aan één school vastzitten. */ + /* systeem: nieuwe school + systeemmanagers beheren. Dit tabblad is waar een + super zaken regelt die niet aan één school vastzitten; de schoolkiezer + zelf staat als vaste contextbalk boven álle beheer-tabs (schoolContextBar), + zodat geen enkel tabblad meer afhankelijk is van een ander tabblad. */ function renderScholenPanel(panel){ const bar = h("div","am-add"); - const sel = h("select","am-sel"); - sel.appendChild(new Option(T("amAllSchools"), "")); - SCHOOLS.forEach(s=>sel.appendChild(new Option(s.name, s.id))); - sel.value = selSchool ?? ""; - sel.addEventListener("change", ()=>{ selSchool = sel.value; reload(); }); - bar.appendChild(sel); const inp = h("input","am-inp"); inp.placeholder = T("amNamePh"); inp.addEventListener("keydown", ev=>ev.stopPropagation()); @@ -614,27 +633,37 @@ }); } - /* zoekfilter voor de gebruikerslijst: verbergt niet-matchende rijen en lege groepen - via style.display i.p.v. een her-render, zodat het zoekveld focus houdt tijdens - het typen (een volledige render() zou het veld elke toets opnieuw aanmaken) */ + /* zoekfilter + inklapbare groepen voor de gebruikerslijst: één plek die de + zichtbaarheid bepaalt (style.display i.p.v. her-render, zodat het zoekveld + focus houdt). Ingeklapte groepen verbergen hun rijen; zodra er gezocht + wordt tellen alle groepen mee zodat een treffer nooit verstopt zit. */ function applyUserFilter(panel){ const q = userFilter.trim().toLowerCase(); const list = panel.querySelector(".am-list"); if(!list) return; - let pendingGroup = null, groupVisible = false; + let curGroup = null, groupVisible = false; + const closeGroup = ()=>{ + if(curGroup) curGroup.style.display = (groupVisible || curGroup.dataset.collapsible) ? "" : "none"; + }; [...list.children].forEach(el=>{ if(el.classList.contains("am-group")){ - if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none"; - pendingGroup = el; groupVisible = false; - }else if(el.classList.contains("am-row")){ + closeGroup(); + curGroup = el; groupVisible = false; + return; + } + const collapsed = curGroup && curGroup.dataset.collapsible + && !openGroups.has(curGroup.dataset.gkey) && !q; + if(el.classList.contains("am-row")){ const name = el.querySelector(".am-name"); const match = !q || (name && name.textContent.toLowerCase().includes(q)); - el.style.display = match ? "" : "none"; + el.style.display = (match && !collapsed) ? "" : "none"; if(match) groupVisible = true; + }else{ + /* toevoegrij (am-add-wrap): volgt de ingeklapte staat van zijn groep */ + el.style.display = collapsed ? "none" : ""; } - /* .am-add-wrap (toevoegrij) blijft altijd zichtbaar, ongeacht het filter */ }); - if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none"; + closeGroup(); } /* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren. @@ -657,7 +686,7 @@ staffGroups.forEach(([role, label])=>{ const us = USERS.filter(u=>u.role===role); if(!us.length) return; - list.appendChild(h("div","am-group", label)); + list.appendChild(h("div","am-group", `${label} (${us.length})`)); us.forEach(u=>list.appendChild(userRow(u))); }); @@ -666,29 +695,42 @@ const creatable = creatableRoles(); const canAddPupil = schoolChosen && creatable.includes("pupil"); + /* leerlingen staan onder inklapbare groepskoppen (standaard dicht): zo + blijft de lijst overzichtelijk bij grote scholen; zoeken kijkt altijd + door alle groepen heen (zie applyUserFilter) */ + const pupilGroup = (key, label, us, formEl)=>{ + const g = h("div","am-group am-group-toggle"); + g.dataset.collapsible = "1"; + g.dataset.gkey = key; + const sync = ()=>{ g.textContent = `${openGroups.has(key) ? "▾" : "▸"} ${label} (${us.length})`; }; + sync(); + g.addEventListener("click", ()=>{ + if(openGroups.has(key)) openGroups.delete(key); else openGroups.add(key); + sync(); + applyUserFilter(panel); + }); + list.appendChild(g); + us.forEach(u=>list.appendChild(userRow(u))); + if(formEl) list.appendChild(formEl); + }; + /* per-klas indeling heeft alleen zin met één gekozen school (CLASSES is dan betrouwbaar); zonder gekozen school (super, alle scholen) of voor groepsleiding - gewoon een platte lijst - anders vallen leerlingen met een classId die niet in + één groep - anders vallen leerlingen met een classId die niet in de (dan lege) CLASSES-lijst voorkomt onterecht helemaal weg */ if(currentUser.role==="teacher" || !schoolChosen){ - if(pupils.length){ - list.appendChild(h("div","am-group", T("rolePupil"))); - pupils.forEach(u=>list.appendChild(userRow(u))); + if(pupils.length || canAddPupil){ + pupilGroup("pupils", T("amPupils"), pupils, canAddPupil ? addForm("pupil") : null); } - if(canAddPupil) list.appendChild(addForm("pupil")); }else{ CLASSES.forEach(c=>{ const us = pupils.filter(u=>u.classId===c.id); if(!us.length && !canAddPupil) return; - list.appendChild(h("div","am-group", c.name)); - us.forEach(u=>list.appendChild(userRow(u))); - if(canAddPupil) list.appendChild(addForm("pupil", c.id)); + pupilGroup("class:"+c.id, c.name, us, canAddPupil ? addForm("pupil", c.id) : null); }); const noClass = pupils.filter(u=>!u.classId); if(noClass.length || canAddPupil){ - list.appendChild(h("div","am-group", T("amNoClass"))); - noClass.forEach(u=>list.appendChild(userRow(u))); - if(canAddPupil) list.appendChild(addForm("pupil")); + pupilGroup("class:none", T("amNoClass"), noClass, canAddPupil ? addForm("pupil") : null); } } panel.appendChild(list); @@ -708,8 +750,26 @@ modal.querySelectorAll(".am-tabpanel").forEach(p=>{ p.style.display = p.dataset.panel===tab ? "block" : "none"; }); } + /* vaste contextbalk voor de systeemmanager: op elk beheer-tabblad direct + de school kiezen, zonder eerst naar het Systeem-tabblad te moeten */ + function schoolContextBar(){ + if(currentUser.role !== "super") return null; + const bar = h("div","am-schoolbar"); + bar.appendChild(h("span","am-schoolbar-lbl","🏫")); + const sel = h("select","am-sel"); + sel.appendChild(new Option(T("amAllSchools"), "")); + SCHOOLS.forEach(s=>sel.appendChild(new Option(s.name, s.id))); + sel.value = selSchool ?? ""; + sel.setAttribute("aria-label", T("amPickSchool")); + sel.addEventListener("change", ()=>{ selSchool = sel.value; reload(); }); + bar.appendChild(sel); + return bar; + } + function render(){ modal.innerHTML = ""; + const sb = schoolContextBar(); + if(sb) modal.appendChild(sb); modal.appendChild(h("div","am-msg")); const rp = resetPanel(); if(rp) modal.appendChild(rp); diff --git a/public/js/core.js b/public/js/core.js index a9d056a..1867ef5 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.3.32-beta"; +const VERSION = "0.3.33-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -181,7 +181,8 @@ const I18N = { pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.", pupilWatching:"meekijken", amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem", - amPickSchoolHint:"Kies eerst een school bij het tabblad “Systeem”.", + amPickSchoolHint:"Kies hierboven eerst een school (🏫).", amPickSchool:"Kies een school", + amPupils:"Leerlingen", amEdit:"Bewerken", amNewSchool:"+ School", amNewClass:"+ Klas", amNewTeacher:"+ Groepsleiding", amNewAdmin:"+ Schoolbeheerder", amNewSuper:"+ Systeemmanager", amNewPupil:"+ Leerling", amUserPh:"gebruikersnaam…", amNamePh:"naam…", @@ -365,7 +366,8 @@ const I18N = { pupilEmpty:"Your teacher hasn't set up anything for you yet.", pupilWatching:"watching", amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System", - amPickSchoolHint:"First pick a school in the “System” tab.", + amPickSchoolHint:"First pick a school above (🏫).", amPickSchool:"Pick a school", + amPupils:"Pupils", amEdit:"Edit", amNewSchool:"+ School", amNewClass:"+ Class", amNewTeacher:"+ Group leader", amNewAdmin:"+ School admin", amNewSuper:"+ System manager", amNewPupil:"+ Pupil", amUserPh:"username…", amNamePh:"name…",