All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
- Kopieerknop (📋) bij elk getoond wachtwoord/koppelcode, zodat het niet
meer verdwijnt voordat het overgetypt kan worden
- Bugfix: getoond wachtwoord werd bijna direct overschreven doordat de
lijst opnieuw werd opgebouwd vlak nadat het bericht getoond werd (race
condition tussen msg() en reload())
- Nieuw: alle leerlingwachtwoorden van een klas of hele school in één
keer resetten, alleen voor schoolbeheerder en overkoepelend beheerder
- Resultatenpaneel na een multireset met "kopieer alles"-knop (blijft
zichtbaar tot de beheerder het zelf sluit)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
377 lines
15 KiB
JavaScript
377 lines
15 KiB
JavaScript
/* teach - beheerpaneel (👥): gebruikers, klassen en scholen.
|
|
Zichtbaar voor super (alles), admin (eigen school) en teacher (leerlingen). */
|
|
"use strict";
|
|
(function(){
|
|
const wrap = document.getElementById("adminWrap");
|
|
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;
|
|
|
|
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(!currentUser || currentUser.role==="pupil") return;
|
|
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
|
await reload();
|
|
wrap.classList.add("open");
|
|
});
|
|
wrap.addEventListener("click", e=>{ if(e.target===wrap) wrap.classList.remove("open"); });
|
|
document.addEventListener("userchange", ()=>{ if(!currentUser){ wrap.classList.remove("open"); 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");
|
|
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
|
|
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); });
|
|
row.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); }
|
|
});
|
|
row.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(["super","admin"].includes(currentUser.role) && 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); }
|
|
});
|
|
row.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); }
|
|
});
|
|
row.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); }
|
|
});
|
|
row.appendChild(del);
|
|
}
|
|
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;
|
|
}
|
|
|
|
function render(){
|
|
modal.innerHTML = "";
|
|
const head = h("div","am-head");
|
|
head.appendChild(h("h2", null, T("beheer")));
|
|
const close = h("button","wclose","✕");
|
|
close.addEventListener("click", ()=>wrap.classList.remove("open"));
|
|
head.appendChild(close);
|
|
modal.appendChild(head);
|
|
|
|
/* 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) */
|
|
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
|
const canResetPw = ["super","admin"].includes(currentUser.role);
|
|
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);
|
|
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);
|
|
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"});
|
|
await reload();
|
|
resetResults = { title: T("amResetSchool"), entries: r.users };
|
|
render();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
modal.appendChild(rsAll);
|
|
}
|
|
}
|
|
|
|
/* gebruikers */
|
|
modal.appendChild(h("h3","am-h3", T("amUsers")));
|
|
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)));
|
|
});
|
|
modal.appendChild(list);
|
|
|
|
/* toevoegen */
|
|
if(schoolChosen){
|
|
if(["super","admin","teacher"].includes(currentUser.role)) modal.appendChild(addForm("pupil"));
|
|
if(["super","admin"].includes(currentUser.role)){
|
|
modal.appendChild(addForm("teacher"));
|
|
modal.appendChild(addForm("admin"));
|
|
}
|
|
}
|
|
if(currentUser.role==="super") modal.appendChild(addForm("super"));
|
|
}
|
|
})();
|