/* 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"; const esc = s => String(s ?? "").replace(/&/g,"&").replace(/{ 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); }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; } function addForm(role){ const f = h("div","am-add"); 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); } 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", T(role==="pupil" ? "amNewPupil" : role==="teacher" ? "amNewTeacher" : role==="admin" ? "amNewAdmin" : "amNewSuper")); 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); return f; } /* 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; } /* 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); } /* klassen (admin/super binnen een school) */ function renderKlassenPanel(panel){ const schoolChosen = currentUser.role!=="super" || !!selSchool; if(!schoolChosen) 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); } } /* 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])=>{ 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 schoolChosen = currentUser.role!=="super" || !!selSchool; const creatable = CREATABLE_ROLES[currentUser.role]; if(schoolChosen){ ["pupil","teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); }); } 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); } })();