From 1b6960b7ba2b17c0b897408334162b1d8ac305c1 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 13 Jul 2026 21:03:13 +0200 Subject: [PATCH] v0.2.17-beta: beheerpaneel opgesplitst in tabs (Scholen/Klassen/Gebruikers) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - render() bouwde tot nu toe alles in één lange, doorlopende lijst; nu drie tabbladen die hetzelfde ed-tabs-patroon hergebruiken als de login- en Instellingen-tabs - "Scholen" alleen zichtbaar voor super, "Gebruikers" is het standaard-tabblad - Actief tabblad blijft behouden na acties (nieuwe klas/gebruiker toevoegen springt niet meer terug naar het eerste tabblad) - Zuiver een frontend-herstructurering: geen gedragswijziging aan de onderliggende functionaliteit (reload/msg/classSel/userRow/addForm/resetPanel ongewijzigd) Co-Authored-By: Claude Sonnet 5 --- public/js/admin.js | 221 ++++++++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 93 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 92e2ce2..de3ad98 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -11,6 +11,8 @@ totdat de beheerder het venster zelf sluit, zodat wachtwoorden niet meteen weer verdwijnen na een reload() */ let resetResults = null; + /* actief tabblad, blijft behouden over her-renders heen (na elke actie) */ + let adminTab = "gebruikers"; const esc = s => String(s ?? "").replace(/&/g,"&").replace(/{ @@ -264,107 +266,100 @@ return box; } - function render(){ - modal.innerHTML = ""; + /* super: schoolkiezer + nieuwe school */ + 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()); + bar.appendChild(inp); + const add = h("button","tbtn", T("amNewSchool")); + add.addEventListener("click", async ()=>{ + if(!inp.value.trim()) return; + try{ + const r = await api("/admin/schools", { body:{ name: inp.value.trim() } }); + selSchool = r.school.id; inp.value = ""; + reload(); + }catch(e){ msg(e.message); } + }); + bar.appendChild(add); + panel.appendChild(bar); + } - /* super: schoolkiezer + nieuwe school */ - if(currentUser.role==="super"){ - 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()); - bar.appendChild(inp); - const add = h("button","tbtn", T("amNewSchool")); - add.addEventListener("click", async ()=>{ - if(!inp.value.trim()) return; - try{ - const r = await api("/admin/schools", { body:{ name: inp.value.trim() } }); - selSchool = r.school.id; inp.value = ""; - reload(); - }catch(e){ msg(e.message); } - }); - bar.appendChild(add); - modal.appendChild(bar); - } - - modal.appendChild(h("div","am-msg")); - const rp = resetPanel(); - if(rp) modal.appendChild(rp); - - /* klassen (admin/super binnen een school) */ + /* klassen (admin/super binnen een school) */ + function renderKlassenPanel(panel){ const schoolChosen = currentUser.role!=="super" || !!selSchool; + if(!schoolChosen) return; const canResetPw = can("schools.resetPasswords"); - if(schoolChosen){ - modal.appendChild(h("h3","am-h3", T("amClasses"))); - const cwrapEl = h("div","am-classes"); - CLASSES.forEach(c=>{ - const chip = h("span","am-chip", c.name); - if(canResetPw){ - const rs = h("button","am-chipx","🔄"); - rs.title = T("amResetClass"); - rs.addEventListener("click", async ()=>{ - if(!confirm(`${T("amConfirmReset")} ${c.name}?`)) return; - try{ - const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"}); - await reload(); - resetResults = { title: `${T("amResetClass")}: ${c.name}`, entries: r.users }; - render(); - }catch(e){ msg(e.message); } - }); - chip.appendChild(rs); - const x = h("button","am-chipx","✕"); - x.addEventListener("click", async ()=>{ - try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); } - catch(e){ msg(e.message); } - }); - chip.appendChild(x); - } - cwrapEl.appendChild(chip); - }); - modal.appendChild(cwrapEl); + const cwrapEl = h("div","am-classes"); + CLASSES.forEach(c=>{ + const chip = h("span","am-chip", c.name); if(canResetPw){ - const f = h("div","am-add"); - const inp = h("input","am-inp"); - inp.placeholder = T("amNamePh"); - inp.addEventListener("keydown", ev=>ev.stopPropagation()); - f.appendChild(inp); - const go = h("button","tbtn", T("amNewClass")); - go.addEventListener("click", async ()=>{ - if(!inp.value.trim()) return; + const rs = h("button","am-chipx","🔄"); + rs.title = T("amResetClass"); + rs.addEventListener("click", async ()=>{ + if(!confirm(`${T("amConfirmReset")} ${c.name}?`)) return; try{ - await api("/admin/classes", { body:{ name: inp.value.trim(), school: selSchool ? +selSchool : undefined } }); - reload(); - }catch(e){ msg(e.message); } - }); - f.appendChild(go); - modal.appendChild(f); - - /* multireset: alle leerlingwachtwoorden van de hele school in één keer */ - const rsAll = h("button","am-btn","🔄 "+T("amResetSchool")); - rsAll.type = "button"; - rsAll.addEventListener("click", async ()=>{ - const schoolId = currentUser.role==="super" ? selSchool : currentUser.schoolId; - if(!schoolId) return; - if(!confirm(T("amConfirmResetSchool"))) return; - try{ - const r = await api("/admin/schools/"+schoolId+"/reset-passwords", {method:"POST"}); + const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"}); await reload(); - resetResults = { title: T("amResetSchool"), entries: r.users }; + resetResults = { title: `${T("amResetClass")}: ${c.name}`, entries: r.users }; render(); }catch(e){ msg(e.message); } }); - modal.appendChild(rsAll); + chip.appendChild(rs); + const x = h("button","am-chipx","✕"); + x.addEventListener("click", async ()=>{ + try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); } + catch(e){ msg(e.message); } + }); + chip.appendChild(x); } - } + cwrapEl.appendChild(chip); + }); + panel.appendChild(cwrapEl); + if(canResetPw){ + const f = h("div","am-add"); + const inp = h("input","am-inp"); + inp.placeholder = T("amNamePh"); + inp.addEventListener("keydown", ev=>ev.stopPropagation()); + f.appendChild(inp); + const go = h("button","tbtn", T("amNewClass")); + go.addEventListener("click", async ()=>{ + if(!inp.value.trim()) return; + try{ + await api("/admin/classes", { body:{ name: inp.value.trim(), school: selSchool ? +selSchool : undefined } }); + reload(); + }catch(e){ msg(e.message); } + }); + f.appendChild(go); + panel.appendChild(f); - /* gebruikers */ - modal.appendChild(h("h3","am-h3", T("amUsers"))); + /* multireset: alle leerlingwachtwoorden van de hele school in één keer */ + const rsAll = h("button","am-btn","🔄 "+T("amResetSchool")); + rsAll.type = "button"; + rsAll.addEventListener("click", async ()=>{ + const schoolId = currentUser.role==="super" ? selSchool : currentUser.schoolId; + if(!schoolId) return; + if(!confirm(T("amConfirmResetSchool"))) return; + try{ + const r = await api("/admin/schools/"+schoolId+"/reset-passwords", {method:"POST"}); + await reload(); + resetResults = { title: T("amResetSchool"), entries: r.users }; + render(); + }catch(e){ msg(e.message); } + }); + panel.appendChild(rsAll); + } + } + + /* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren */ + 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])=>{ @@ -373,13 +368,53 @@ list.appendChild(h("div","am-group", label)); us.forEach(u=>list.appendChild(userRow(u))); }); - modal.appendChild(list); + panel.appendChild(list); /* toevoegen - welke rollen deze gebruiker mag aanmaken (CREATABLE_ROLES) */ + const schoolChosen = currentUser.role!=="super" || !!selSchool; const creatable = CREATABLE_ROLES[currentUser.role]; if(schoolChosen){ - ["pupil","teacher","admin"].forEach(r=>{ if(creatable.includes(r)) modal.appendChild(addForm(r)); }); + ["pupil","teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); }); } - if(creatable.includes("super")) modal.appendChild(addForm("super")); + if(creatable.includes("super")) panel.appendChild(addForm("super")); + } + + function switchAdminTab(tab){ + adminTab = tab; + modal.querySelectorAll(".ed-tabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab)); + modal.querySelectorAll(".am-tabpanel").forEach(p=>{ p.style.display = p.dataset.panel===tab ? "block" : "none"; }); + } + + function render(){ + modal.innerHTML = ""; + modal.appendChild(h("div","am-msg")); + const rp = resetPanel(); + if(rp) modal.appendChild(rp); + + const tabDefs = []; + if(currentUser.role==="super") tabDefs.push(["scholen", T("amSchools")]); + tabDefs.push(["klassen", T("amClasses")]); + tabDefs.push(["gebruikers", T("amUsers")]); + if(!tabDefs.some(([t])=>t===adminTab)) adminTab = tabDefs[0][0]; + + const tabsBar = h("div","ed-tabs"); + tabDefs.forEach(([t, label])=>{ + const b = h("button", null, label); + b.type = "button"; + b.dataset.tab = t; + b.addEventListener("click", ()=>switchAdminTab(t)); + tabsBar.appendChild(b); + }); + modal.appendChild(tabsBar); + + const panels = { scholen: renderScholenPanel, klassen: renderKlassenPanel, gebruikers: renderGebruikersPanel }; + tabDefs.forEach(([t])=>{ + const panel = h("div","am-tabpanel"); + panel.dataset.panel = t; + panels[t](panel); + modal.appendChild(panel); + }); + + switchAdminTab(adminTab); } })();