From 30818dc5f2264a663517361071229ca9592f5ad2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 17 Jul 2026 21:21:05 +0200 Subject: [PATCH] fix: gebruikers gegroepeerd per school in alle-scholen-weergave + zichtbare verwijderfouten (v0.3.94-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In de "alle scholen"-weergave stonden gebruikers op rol gegroepeerd, waardoor je niet zag bij welke school ze horen (Ramon/Rubi/leerlingb1b leken los te staan). Ze worden nu per school gegroepeerd (inklapbaar), met systeemmanagers, ouders en losse accounts apart eronder - je ziet meteen wie bij De Ziep hoort - Bij één gekozen school blijft de indeling op rol/klas; die rolgroepen zijn nu ook inklapbaar - Verwijderfouten (en andere actie-fouten in het detailpaneel) verschijnen nu in het detailpaneel zelf i.p.v. alleen bovenaan (buiten beeld op mobiel). Als een verwijdering serverzijdig faalt, zie je nu de reden - het lijkt niet langer of "niets gebeurt" - Verwijderen zelf is end-to-end geverifieerd (DELETE verstuurd, gebruiker verdwijnt); een gesimuleerde 409 toont de melding in het paneel en laat de gebruiker staan - 7 nieuwe Playwright-checks (school-groepering, zichtbare fout, geslaagde verwijdering) + volledige regressie van 8 suites; 94 servertests groen Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF --- VERSION | 2 +- public/js/admin.js | 106 +++++++++++++++++++++++---------------------- public/js/core.js | 2 +- 3 files changed, 57 insertions(+), 53 deletions(-) diff --git a/VERSION b/VERSION index 171c2a7..9c8a451 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.93-beta +0.3.94-beta diff --git a/public/js/admin.js b/public/js/admin.js index 541e432..a8c8749 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -359,7 +359,8 @@ /* het bewerkformulier van één gebruiker; wordt gebruikt door het detailpaneel (gebruikers) én inline door het Systeem-tabblad (subrijen) */ - function userTools(u){ + function userTools(u, note){ + note = note || msg; const tools = h("div","am-tools"); const actions = h("div","am-actions"); const isStaff = ["teacher","admin","super"].includes(u.role); @@ -373,7 +374,7 @@ ss.value = u.schoolId ?? ""; ss.addEventListener("change", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{ schoolId: ss.value ? +ss.value : null }}); reload(); } - catch(e){ msg(e.message); ss.value = u.schoolId ?? ""; } + catch(e){ note(e.message); ss.value = u.schoolId ?? ""; } }); tools.appendChild(field("amFieldSchool", ss)); } @@ -385,7 +386,7 @@ rs.value = u.role; rs.addEventListener("change", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{role: rs.value}}); reload(); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); tools.appendChild(field("amFieldRole", rs)); } @@ -403,7 +404,7 @@ x.title = T("amClassRemove"); x.addEventListener("click", async ()=>{ try{ await api("/admin/classes/"+cid+"/teachers", {body:{ userId: u.id, remove:true }}); reload(); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); chip.appendChild(x); wrap.appendChild(chip); @@ -416,7 +417,7 @@ addSel.addEventListener("change", async ()=>{ if(!addSel.value) return; try{ await api("/admin/classes/"+addSel.value+"/teachers", {body:{ userId: u.id }}); reload(); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); wrap.appendChild(addSel); } @@ -427,7 +428,7 @@ const cs = classSel(u.classId); cs.addEventListener("change", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{classId: cs.value ? +cs.value : null}}); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); tools.appendChild(field("amFieldClass", cs)); /* wachtwoord inzien en aanpassen - alleen zichtbaar zolang de leerling @@ -440,7 +441,7 @@ pw.addEventListener("change", async ()=>{ if(pw.value.length < 4) return; try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{password: pw.value}}); reload(); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); pwWrap.appendChild(pw); const pwCopy = h("button","am-btn", T("amCopy")); @@ -456,7 +457,7 @@ rowResults.set(u.id, { label: T("amResultPw"), value: password }); selectedUserKey = "u:"+u.id; reload(); - }catch(e){ msg(e.message); } + }catch(e){ note(e.message); } })); } /* eenmalige ouder-code waarmee een ouder dit kind aan het eigen @@ -467,7 +468,7 @@ rowResults.set(u.id, { label: T("amResultParentCode"), value: r.parentCode }); selectedUserKey = "u:"+u.id; reload(); - }catch(e){ msg(e.message); } + }catch(e){ note(e.message); } })); }else{ if(u.pending && u.linkCode){ @@ -480,7 +481,7 @@ rowResults.set(u.id, { label: T("amResultCode"), value: r.linkCode }); selectedUserKey = "u:"+u.id; reload(); - }catch(e){ msg(e.message); } + }catch(e){ note(e.message); } })); /* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */ actions.appendChild(dangerBtn(T("amNewPw"), "amNewPwWarn", async ()=>{ @@ -489,14 +490,14 @@ rowResults.set(u.id, { label: T("amResultPw"), value: r.password }); selectedUserKey = "u:"+u.id; reload(); - }catch(e){ msg(e.message); } + }catch(e){ note(e.message); } })); } } if(u.id !== currentUser.id){ const del = dangerBtn(T("amDelete"), "amDeleteWarn", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"DELETE"}); selectedUserKey = null; reload(); } - catch(e){ msg(e.message); } + catch(e){ note(e.message); } }); actions.appendChild(del); } @@ -580,7 +581,11 @@ detailHost.appendChild(detailHead(u.displayName, T(roleKey(u.role)) + (u.displayName!==u.username ? ` · ${u.username}` : ""))); const badge = resultBadge(u); if(badge) detailHost.appendChild(badge); - detailHost.appendChild(userTools(u)); + /* fouten (bv. verwijderen mislukt) tonen we hier, waar de beheerder kijkt, + niet alleen bovenaan het paneel (daar valt het buiten beeld) */ + const note = h("div","am-detail-msg formMsg"); + detailHost.appendChild(note); + detailHost.appendChild(userTools(u, (t)=>{ note.textContent = t || ""; })); return; } if(selectedUserKey && selectedUserKey.startsWith("req:")){ @@ -1297,25 +1302,10 @@ master.appendChild(thead); const list = h("div","am-list am-userlist"); - /* losse accounts (ooit aan een school gekoppeld, nu zonder): alleen - relevant voor de systeemmanager in de alle-scholen-weergave */ - const showLoose = currentUser.role==="super" && !selSchool; - const isLoose = u=>showLoose && ["teacher","admin","pupil"].includes(u.role) && u.schoolId==null; - const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]]; - staffGroups.forEach(([role, label])=>{ - const us = USERS.filter(u=>u.role===role && !isLoose(u)); - if(!us.length) return; - list.appendChild(h("div","am-group", `${label} (${us.length})`)); - us.forEach(u=>list.appendChild(userCells(u))); - }); - - const pupils = USERS.filter(u=>u.role==="pupil" && !isLoose(u)); - const schoolChosen = schoolChosenTop; - - /* 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)=>{ + /* inklapbare groepskop; standaard dicht tenzij defaultOpen (dan onthoudt + closedGroups een bewuste dichtklap). Zoeken kijkt door alle groepen heen. */ + const group = (key, label, us, defaultOpen)=>{ + if(defaultOpen && !closedGroups.has(key)) openGroups.add(key); const g = h("div","am-group am-group-toggle"); g.dataset.collapsible = "1"; g.dataset.gkey = key; @@ -1331,27 +1321,41 @@ us.forEach(u=>list.appendChild(userCells(u))); }; - /* per-klas indeling heeft alleen zin met één gekozen school (CLASSES is dan - betrouwbaar); zonder gekozen school (super, alle scholen) of voor groepsleiding - éé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) pupilGroup("pupils", T("amPupils"), pupils); - }else{ - CLASSES.forEach(c=>{ - const us = pupils.filter(u=>u.classId===c.id); - /* met een gekozen school staan de klasgroepen standaard open - de - schoolkeuze wás al het filter; dichtklappen wordt onthouden */ - if(us.length && !closedGroups.has("class:"+c.id)) openGroups.add("class:"+c.id); - if(us.length) pupilGroup("class:"+c.id, c.name, us); + if(currentUser.role==="super" && !selSchool){ + /* ALLE SCHOLEN: groepeer per school zodat je meteen ziet wie waar hoort; + daarna systeemmanagers, ouders en losse accounts apart */ + const roleOrder = { admin:0, teacher:1, pupil:2 }; + SCHOOLS.forEach(sc=>{ + const us = USERS.filter(u=>Number(u.schoolId)===Number(sc.id)) + .sort((a,b)=>(roleOrder[a.role]??9)-(roleOrder[b.role]??9) || a.displayName.localeCompare(b.displayName)); + if(us.length) group("gschool:"+sc.id, sc.name, us); }); - const noClass = pupils.filter(u=>!u.classId); - if(noClass.length) pupilGroup("class:none", T("amNoClass"), noClass); + const supers = USERS.filter(u=>u.role==="super"); + if(supers.length) group("gsys", T("amSuperGroup"), supers); + const parents = USERS.filter(u=>u.role==="parent"); + if(parents.length) group("gparents", T("amParents"), parents); + const loose = USERS.filter(u=>["teacher","admin","pupil"].includes(u.role) && u.schoolId==null); + if(loose.length) group("gnoschool", T("amNoSchoolGroup"), loose); + }else{ + /* één school gekozen: groepeer op rol, leerlingen per klas */ + [["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]].forEach(([role, label])=>{ + const us = USERS.filter(u=>u.role===role); + if(us.length) group("role:"+role, label, us, true); + }); + const pupils = USERS.filter(u=>u.role==="pupil"); + if(currentUser.role==="teacher"){ + if(pupils.length) group("pupils", T("amPupils"), pupils, true); + }else{ + CLASSES.forEach(c=>{ + const us = pupils.filter(u=>u.classId===c.id); + if(us.length) group("class:"+c.id, c.name, us, true); + }); + const noClass = pupils.filter(u=>!u.classId); + if(noClass.length) group("class:none", T("amNoClass"), noClass, true); + } + const parents = USERS.filter(u=>u.role==="parent"); + if(parents.length) group("parents", T("amParents"), parents); } - const parents = USERS.filter(u=>u.role==="parent"); - if(parents.length) pupilGroup("parents", T("amParents"), parents); - const loose = USERS.filter(isLoose); - if(loose.length) pupilGroup("noschool", T("amNoSchoolGroup"), loose); master.appendChild(list); split.appendChild(master); split.appendChild(detail); diff --git a/public/js/core.js b/public/js/core.js index 3276f55..4000aec 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.93-beta"; +const VERSION = "0.3.94-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION;