From 2ceeec2de2badc4cda881037b66ff9aab18c7603 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 13 Jul 2026 22:04:20 +0200 Subject: [PATCH] v0.2.19-beta: leerlingen per klas in het beheerpaneel + groepsleiding-scope + extra stap bij toevoegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /admin/users: groepsleiding (teacher) ziet voortaan alleen leerlingen uit de eigen klas(sen) (via class_teachers), i.p.v. de hele school - staf blijft zichtbaar zoals voorheen - Gebruikers-tab: leerlingen worden voor schoolbeheerder/super gegroepeerd per klas getoond (met een "geen klas"-groep), i.p.v. één platte lijst los van de Klassen-tab; groepsleiding houdt een platte lijst (nu vanzelf al beperkt tot eigen klas door de backend-scope) - Alle "gebruiker toevoegen"-formulieren (staf + leerling, incl. de nieuwe per-klas toevoegrij) starten ingeklapt als alleen een "+ …"-knop; pas na een klik verschijnen de invoervelden - Verwijderen van een gebruiker had al een bevestigingsvraag - ongewijzigd, bevestigd dat dit blijft werken - Koppelcode-functie bewust ongewijzigd gelaten (privacy-optie naast direct-wachtwoord-aanmaken) Co-Authored-By: Claude Sonnet 5 --- public/js/admin.js | 65 +++++++++++++++++++++++++++++++++++++++------- src/api.js | 10 ++++++- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index de3ad98..d738cfc 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -180,14 +180,24 @@ return row; } - function addForm(role){ + /* toevoegen vereist een bewuste extra stap: begint ingeklapt als alleen de "+ …"-knop, + pas na een klik verschijnen de invoervelden. presetClassId zet de klas-select alvast op + de klas waar deze toevoegrij bij hoort (leerkracht/beheerder kan dit nog wijzigen). */ + function addForm(role, presetClassId){ + const label = T(role==="pupil" ? "amNewPupil" : role==="teacher" ? "amNewTeacher" : role==="admin" ? "amNewAdmin" : "amNewSuper"); + const wrap = h("div","am-add-wrap"); + const toggle = h("button","tbtn ghost", label); + toggle.type = "button"; + wrap.appendChild(toggle); + const f = h("div","am-add"); + f.style.display = "none"; const name = h("input","am-inp"); name.placeholder = T("amUserPh"); name.addEventListener("keydown", ev=>ev.stopPropagation()); f.appendChild(name); let cs = null; - if(role==="pupil"){ cs = classSel(""); f.appendChild(cs); } + if(role==="pupil"){ cs = classSel(presetClassId ?? ""); f.appendChild(cs); } let pwInp = null; if(role!=="pupil"){ /* optioneel: meteen zelf een wachtwoord instellen i.p.v. een koppelcode @@ -210,7 +220,7 @@ }); f.appendChild(gen); } - const go = h("button","tbtn", T(role==="pupil" ? "amNewPupil" : role==="teacher" ? "amNewTeacher" : role==="admin" ? "amNewAdmin" : "amNewSuper")); + const go = h("button","tbtn", label); go.addEventListener("click", async ()=>{ const body = { role, username: name.value.trim(), displayName: name.value.trim() }; if(role==="pupil" && cs && cs.value) body.classId = +cs.value; @@ -230,7 +240,14 @@ }catch(e){ msg(e.message); } }); f.appendChild(go); - return f; + wrap.appendChild(f); + + toggle.addEventListener("click", ()=>{ + toggle.style.display = "none"; + f.style.display = "flex"; + name.focus(); + }); + return wrap; } /* paneel met het resultaat van een multireset (klas/school): een lijst @@ -358,23 +375,51 @@ } } - /* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren */ + /* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren. + Leerlingen krijgen een aparte behandeling: voor schoolbeheerder/super gegroepeerd + per klas (met een inline toevoegrij per klas), voor groepsleiding een platte lijst + (die dankzij de server-side scope toch al beperkt is tot de eigen klas(sen)). */ function renderGebruikersPanel(panel){ const list = h("div","am-list"); - const groups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")], ["pupil", T("rolePupil")]]; - groups.forEach(([role, label])=>{ + const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]]; + staffGroups.forEach(([role, label])=>{ const us = USERS.filter(u=>u.role===role); if(!us.length) return; list.appendChild(h("div","am-group", label)); us.forEach(u=>list.appendChild(userRow(u))); }); - panel.appendChild(list); - /* toevoegen - welke rollen deze gebruiker mag aanmaken (CREATABLE_ROLES) */ + const pupils = USERS.filter(u=>u.role==="pupil"); const schoolChosen = currentUser.role!=="super" || !!selSchool; const creatable = CREATABLE_ROLES[currentUser.role]; + const canAddPupil = schoolChosen && creatable.includes("pupil"); + + if(currentUser.role==="teacher"){ + if(pupils.length){ + list.appendChild(h("div","am-group", T("rolePupil"))); + pupils.forEach(u=>list.appendChild(userRow(u))); + } + 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)); + }); + 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")); + } + } + panel.appendChild(list); + + /* staf-aanmaakformulieren - welke rollen deze gebruiker mag aanmaken (CREATABLE_ROLES) */ if(schoolChosen){ - ["pupil","teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); }); + ["teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); }); } if(creatable.includes("super")) panel.appendChild(addForm("super")); } diff --git a/src/api.js b/src/api.js index 247908d..ca7b9f2 100644 --- a/src/api.js +++ b/src/api.js @@ -217,7 +217,8 @@ export default async function api(app) { }); // ---- gebruikers --------------------------------------------------------------- - // Lijst: super ziet alles (optioneel per school), admin/teacher de eigen school. + // Lijst: super ziet alles (optioneel per school), admin de eigen school, + // teacher de eigen school maar leerlingen alleen uit de eigen klas(sen). app.get('/admin/users', async (req, reply) => { need(req, reply, PERMISSIONS['users.manage']); let rows; @@ -225,6 +226,13 @@ export default async function api(app) { rows = (req.query.school ? await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.query.school]) : await pool.query('SELECT * FROM users ORDER BY school_id NULLS FIRST, role, lower(username)')).rows; + } else if (req.user.role === 'teacher') { + rows = (await pool.query( + `SELECT * FROM users + WHERE school_id = $1 + AND (role != 'pupil' OR class_id IN (SELECT class_id FROM class_teachers WHERE user_id = $2)) + ORDER BY role, lower(username)`, + [req.user.school_id, req.user.id])).rows; } else { rows = (await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.user.school_id])).rows; }