All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 7s
- Nieuwe user_roles-koppeltabel (db/004_user_roles.sql): staf kan voortaan naast de hoofdrol extra functies krijgen (bv. groepsleiding + schoolbeheerder tegelijk); leerling blijft altijd exclusief
- can()/PERMISSIONS accepteren nu een array van rollen i.p.v. alleen een losse string (backward compatible), CREATABLE_ROLES-opzoeking wordt een unie over alle rollen van een gebruiker
- Rechten-checks in src/api.js gebruiken voortaan req.user.allRoles (hoofd- + extra rollen); identiteits-/scoping-logica (leerling-filters, aanmaken, groepsleiding-klas-scope) blijft bewust op de hoofdrol staan
- PATCH /admin/users/:id accepteert een nieuw veld extraRoles (super-only, alleen voor staf)
- Beheerpaneel: toggle-knoppen per stafgebruiker om extra rollen aan/uit te zetten; Instellingen toont voortaan alle rollen samengevoegd ("Schoolbeheerder + Groepsleiding")
- Zoekveld boven de gebruikerslijst (live filter op naam, client-side, behoudt focus tijdens typen) omdat die lijst in een grote school lang kan worden
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
536 lines
22 KiB
JavaScript
536 lines
22 KiB
JavaScript
/* teach - beheerpaneel (👥): gebruikers, klassen en scholen.
|
||
Zichtbaar voor super (alles), admin (eigen school) en teacher (leerlingen). */
|
||
"use strict";
|
||
(function(){
|
||
/* het beheerpaneel is een tabblad binnen Instellingen (settingsWrap/settingsModal),
|
||
geen los venster meer - modal is alleen nog de content-container voor render() */
|
||
const modal = document.getElementById("adminModal");
|
||
const btn = document.getElementById("btnAdmin");
|
||
let SCHOOLS = [], USERS = [], CLASSES = [], selSchool = null;
|
||
/* resultaat van een laatste multireset (klas/school): blijft zichtbaar
|
||
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";
|
||
/* live zoekfilter voor de gebruikerslijst, blijft ook behouden over her-renders */
|
||
let userFilter = "";
|
||
|
||
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
|
||
const creatableRoles = () =>
|
||
[...new Set([currentUser.role, ...(currentUser.extraRoles||[])].flatMap(r => CREATABLE_ROLES[r] || []))];
|
||
|
||
const esc = s => String(s ?? "").replace(/&/g,"&").replace(/</g,"<");
|
||
const h = (tag, cls, text)=>{
|
||
const el = document.createElement(tag);
|
||
if(cls) el.className = cls;
|
||
if(text != null) el.textContent = text;
|
||
return el;
|
||
};
|
||
/* wachtwoorden/koppelcodes zijn maar eenmalig zichtbaar - een kopieerknop
|
||
zorgt dat ze veilig bewaard kunnen worden voordat het scherm ververst */
|
||
async function copyText(btn, text){
|
||
try{ await navigator.clipboard.writeText(text); }
|
||
catch(e){
|
||
const ta = document.createElement("textarea");
|
||
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
|
||
document.body.appendChild(ta); ta.select();
|
||
try{ document.execCommand("copy"); }catch(_){ /* niets meer te doen */ }
|
||
document.body.removeChild(ta);
|
||
}
|
||
const orig = btn.textContent;
|
||
btn.textContent = "✅";
|
||
setTimeout(()=>{ btn.textContent = orig; }, 1200);
|
||
}
|
||
function copyBtn(text, cls){
|
||
const b = h("button", cls || "am-copy", "📋");
|
||
b.type = "button";
|
||
b.title = T("amCopy");
|
||
b.addEventListener("click", e=>{ e.preventDefault(); if(text) copyText(b, text); });
|
||
return b;
|
||
}
|
||
const msg = (t, copyValue) => {
|
||
const m = modal.querySelector(".am-msg");
|
||
if(!m) return;
|
||
m.innerHTML = "";
|
||
if(!t) return;
|
||
m.appendChild(document.createTextNode(t));
|
||
if(copyValue) m.appendChild(copyBtn(copyValue));
|
||
};
|
||
|
||
btn.addEventListener("click", async ()=>{
|
||
if(!can("admin.access")) return;
|
||
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
||
await reload();
|
||
switchSettingsTab("admin");
|
||
});
|
||
document.addEventListener("userchange", ()=>{ if(!currentUser) resetResults = null; });
|
||
|
||
async function reload(){
|
||
try{
|
||
if(currentUser.role==="super") SCHOOLS = await api("/schools");
|
||
const q = currentUser.role==="super" && selSchool ? "?school="+selSchool : "";
|
||
USERS = (await api("/admin/users"+q)).users;
|
||
const cq = currentUser.role==="super" ? (selSchool ? "?school="+selSchool : "?school=0") : "";
|
||
CLASSES = (await api("/admin/classes"+cq)).classes || [];
|
||
}catch(e){ USERS = []; CLASSES = []; }
|
||
render();
|
||
}
|
||
|
||
function classSel(current){
|
||
const sel = h("select","am-sel");
|
||
sel.appendChild(new Option(T("amNoClass"), ""));
|
||
CLASSES.forEach(c=>sel.appendChild(new Option(c.name, c.id)));
|
||
sel.value = current ?? "";
|
||
return sel;
|
||
}
|
||
|
||
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 */
|
||
const actions = h("div","am-actions");
|
||
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
|
||
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");
|
||
["teacher","admin","super"].forEach(r=>rs.appendChild(new Option(T(roleKey(r)), r)));
|
||
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); }
|
||
});
|
||
row.appendChild(rs);
|
||
|
||
/* extra functies naast de hoofdrol (bv. een teacher die ook beheerdersrechten
|
||
krijgt) - los van de hoofdrol hierboven, direct toggelbaar per rol */
|
||
const extraWrap = h("div","am-classes");
|
||
["teacher","admin","super"].filter(r=>r!==u.role).forEach(r=>{
|
||
const has = (u.extraRoles||[]).includes(r);
|
||
const chip = h("button","am-chip", (has?"✓ ":"+ ")+T(roleKey(r)));
|
||
chip.type = "button";
|
||
chip.classList.toggle("on", has);
|
||
chip.addEventListener("click", async ()=>{
|
||
const next = has ? (u.extraRoles||[]).filter(x=>x!==r) : [...(u.extraRoles||[]), r];
|
||
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{extraRoles: next}}); reload(); }
|
||
catch(e){ msg(e.message); }
|
||
});
|
||
extraWrap.appendChild(chip);
|
||
});
|
||
row.appendChild(extraWrap);
|
||
}else{
|
||
row.appendChild(h("span","am-role", T(roleKey(u.role))));
|
||
}
|
||
if(u.role==="pupil"){
|
||
/* klas (uitwisselen met andere klassen) */
|
||
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); }
|
||
});
|
||
row.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");
|
||
pw.value = u.password || "";
|
||
pw.placeholder = u.everLoggedIn ? T("amPwHidden") : "";
|
||
pw.title = T("amPw");
|
||
pw.addEventListener("keydown", ev=>ev.stopPropagation());
|
||
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); }
|
||
});
|
||
row.appendChild(pw);
|
||
const pwCopy = h("button","am-copy","📋");
|
||
pwCopy.type = "button";
|
||
pwCopy.title = T("amCopy");
|
||
pwCopy.addEventListener("click", e=>{ e.preventDefault(); if(pw.value) copyText(pwCopy, pw.value); });
|
||
actions.appendChild(pwCopy);
|
||
if(u.everLoggedIn){
|
||
const gen = h("button","am-btn","🎲");
|
||
gen.title = T("amGenerate");
|
||
gen.addEventListener("click", async ()=>{
|
||
try{
|
||
const { password } = await api("/admin/genpw?role=pupil");
|
||
await api("/admin/users/"+u.id, {method:"PATCH", body:{password}});
|
||
await reload();
|
||
msg(`${T("amPwSet")} ${password}`, password);
|
||
}catch(e){ msg(e.message); }
|
||
});
|
||
actions.appendChild(gen);
|
||
}
|
||
}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")));
|
||
}
|
||
if(can("users.staffCredentials") && u.role!=="pupil" && u.id!==currentUser.id){
|
||
const nc = h("button","am-btn", T("amNewCode"));
|
||
nc.addEventListener("click", async ()=>{
|
||
try{
|
||
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{newLinkCode:true}});
|
||
await reload();
|
||
msg(`${T("amCreated")} ${r.linkCode}`, r.linkCode);
|
||
}catch(e){ msg(e.message); }
|
||
});
|
||
actions.appendChild(nc);
|
||
/* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */
|
||
const np = h("button","am-btn", T("amNewPw"));
|
||
np.addEventListener("click", async ()=>{
|
||
try{
|
||
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{generatePassword:true}});
|
||
await reload();
|
||
msg(`${T("amPwSet")} ${r.password}`, r.password);
|
||
}catch(e){ msg(e.message); }
|
||
});
|
||
actions.appendChild(np);
|
||
}
|
||
}
|
||
if(u.id !== currentUser.id){
|
||
const del = h("button","am-del","🗑");
|
||
del.title = T("amDelete") || "";
|
||
del.addEventListener("click", async ()=>{
|
||
if(!confirm(T("amConfirmDel"))) return;
|
||
try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); }
|
||
catch(e){ msg(e.message); }
|
||
});
|
||
actions.appendChild(del);
|
||
}
|
||
if(actions.childNodes.length) row.appendChild(actions);
|
||
return row;
|
||
}
|
||
|
||
/* 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(presetClassId ?? ""); f.appendChild(cs); }
|
||
let pwInp = null;
|
||
if(role!=="pupil"){
|
||
/* optioneel: meteen zelf een wachtwoord instellen i.p.v. een koppelcode
|
||
(die de gebruiker normaal zelf gebruikt om een wachtwoord te kiezen) */
|
||
pwInp = h("input","am-inp am-pwinp");
|
||
pwInp.type = "password";
|
||
pwInp.autocomplete = "new-password";
|
||
pwInp.placeholder = T("amPwOptional");
|
||
pwInp.addEventListener("keydown", ev=>ev.stopPropagation());
|
||
f.appendChild(pwInp);
|
||
const gen = h("button","am-btn","🎲");
|
||
gen.type = "button";
|
||
gen.title = T("amGenerate");
|
||
gen.addEventListener("click", async ()=>{
|
||
try{
|
||
const { password } = await api("/admin/genpw");
|
||
pwInp.type = "text";
|
||
pwInp.value = password;
|
||
}catch(e){ msg(e.message); }
|
||
});
|
||
f.appendChild(gen);
|
||
}
|
||
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;
|
||
if(pwInp && pwInp.value.trim()) body.password = pwInp.value.trim();
|
||
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
||
try{
|
||
const r = await api("/admin/users", { body });
|
||
name.value = "";
|
||
if(pwInp) pwInp.value = "";
|
||
await reload();
|
||
msg(role==="pupil"
|
||
? `${T("amPupilMade")} ${r.user.password}`
|
||
: r.user.password
|
||
? `${T("amPwSet")} ${r.user.password}`
|
||
: `${T("amCreated")} ${r.user.linkCode}`,
|
||
r.user.password || r.user.linkCode);
|
||
}catch(e){ msg(e.message); }
|
||
});
|
||
f.appendChild(go);
|
||
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
|
||
nieuwe leerlingwachtwoorden die stuk voor stuk of allemaal ineens
|
||
gekopieerd kunnen worden. Blijft staan tot de beheerder ✕ klikt. */
|
||
function resetPanel(){
|
||
if(!resetResults) return null;
|
||
const box = h("div","am-reset");
|
||
const head = h("div","am-reset-head");
|
||
head.appendChild(h("strong",null, resetResults.title));
|
||
const close = h("button","wclose","✕");
|
||
close.addEventListener("click", ()=>{ resetResults = null; render(); });
|
||
head.appendChild(close);
|
||
box.appendChild(head);
|
||
if(!resetResults.entries.length){
|
||
box.appendChild(h("div","am-msg", T("amResetNone")));
|
||
return box;
|
||
}
|
||
const all = resetResults.entries.map(en=>`${en.displayName}\t${en.password}`).join("\n");
|
||
const copyAll = h("button","am-btn","📋 "+T("amCopyAll"));
|
||
copyAll.type = "button";
|
||
copyAll.addEventListener("click", e=>{ e.preventDefault(); copyText(copyAll, all); });
|
||
box.appendChild(copyAll);
|
||
const list = h("div","am-reset-list");
|
||
resetResults.entries.forEach(en=>{
|
||
const row = h("div","am-reset-row");
|
||
row.appendChild(h("span","am-name", en.displayName));
|
||
row.appendChild(h("span","am-reset-pw", en.password));
|
||
row.appendChild(copyBtn(`${en.displayName}: ${en.password}`));
|
||
list.appendChild(row);
|
||
});
|
||
box.appendChild(list);
|
||
return box;
|
||
}
|
||
|
||
/* overkoepelend: schoolkiezer + nieuwe school + overkoepelend-beheerders beheren.
|
||
Dit tabblad is waar een super zaken regelt die niet aan één school vastzitten. */
|
||
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);
|
||
|
||
/* overkoepelend beheerders horen niet bij één school, dus het aanmaken ervan
|
||
staat hier i.p.v. tussen de school-gebonden gebruikers in de Gebruikers-tab
|
||
(de lijst van bestaande supers blijft wel gewoon in Gebruikers staan) */
|
||
if(creatableRoles().includes("super")) panel.appendChild(addForm("super"));
|
||
}
|
||
|
||
/* klassen (admin/super binnen een school) */
|
||
function renderKlassenPanel(panel){
|
||
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
||
if(!schoolChosen){
|
||
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
|
||
return;
|
||
}
|
||
const canResetPw = can("schools.resetPasswords");
|
||
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);
|
||
});
|
||
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);
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
|
||
/* 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) */
|
||
function applyUserFilter(panel){
|
||
const q = userFilter.trim().toLowerCase();
|
||
const list = panel.querySelector(".am-list");
|
||
if(!list) return;
|
||
let pendingGroup = null, groupVisible = false;
|
||
[...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")){
|
||
const name = el.querySelector(".am-name");
|
||
const match = !q || (name && name.textContent.toLowerCase().includes(q));
|
||
el.style.display = match ? "" : "none";
|
||
if(match) groupVisible = true;
|
||
}
|
||
/* .am-add-wrap (toevoegrij) blijft altijd zichtbaar, ongeacht het filter */
|
||
});
|
||
if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none";
|
||
}
|
||
|
||
/* 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 filterBar = h("div","am-add");
|
||
const filterInp = h("input","am-inp am-filter");
|
||
filterInp.type = "search";
|
||
filterInp.placeholder = T("amSearchPh");
|
||
filterInp.value = userFilter;
|
||
filterInp.addEventListener("keydown", ev=>ev.stopPropagation());
|
||
filterInp.addEventListener("input", ()=>{ userFilter = filterInp.value; applyUserFilter(panel); });
|
||
filterBar.appendChild(filterInp);
|
||
panel.appendChild(filterBar);
|
||
|
||
const list = h("div","am-list");
|
||
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)));
|
||
});
|
||
|
||
const pupils = USERS.filter(u=>u.role==="pupil");
|
||
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
||
const creatable = creatableRoles();
|
||
const canAddPupil = schoolChosen && creatable.includes("pupil");
|
||
|
||
/* 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
|
||
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(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).
|
||
"super" aanmaken staat in het Overkoepelend-tabblad, niet hier (die rol hoort niet
|
||
bij één school). */
|
||
if(schoolChosen){
|
||
["teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); });
|
||
}
|
||
applyUserFilter(panel);
|
||
}
|
||
|
||
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("amOverarching")]);
|
||
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);
|
||
}
|
||
})();
|