/* 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"); let SCHOOLS = [], USERS = [], CLASSES = [], selSchool = null; /* schoolbeheerders van alle scholen, voor het Systeem-tabblad (super) */ let SYS_ADMINS = []; /* huidige klas-toewijzingen (leerling-omgeving), per klas-id - opnieuw opgehaald bij elke reload() zoals CLASSES/USERS hierboven */ let ASSIGNMENTS = {}; /* 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 = ""; /* uitgeklapte leerling-groepen en gebruikersrijen (standaard alles dicht voor een rustig overzicht), blijven behouden over her-renders */ const openGroups = new Set(); const openRows = new Set(); /* 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(/{ 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)); }; /* ---- één interactiesysteem voor het hele beheer --------------------------- field(): elk invoerveld/select krijgt een zichtbaar mini-label erboven (een placeholder alleen is geen toelichting). dangerBtn(): acties met gevolgen (wachtwoord/koppelcode vernieuwen, verwijderen, resetten) zien er anders uit dan gewone knoppen en vragen een tweede klik als bevestiging (zelfde arm-then-confirm als elders in de app); wapening loopt vanzelf af. Geen native confirm()-dialogen meer. rowResults: nieuw gegenereerde wachtwoorden/codes verschijnen direct bij de gebruiker in de rij (met kopieerknop), niet bovenaan. */ function field(labelKey, control){ const w = h("label","am-field"); w.appendChild(h("span","am-field-lbl", T(labelKey))); w.appendChild(control); return w; } function dangerBtn(label, warnKey, fn){ const b = h("button","am-btn am-danger", label); b.type = "button"; if(warnKey) b.title = T(warnKey); let timer = null; b.addEventListener("click", async ()=>{ if(!timer){ b.classList.add("armed"); b.textContent = "⚠ " + T("amSure"); timer = setTimeout(()=>{ timer = null; b.classList.remove("armed"); b.textContent = label; }, 4000); return; } clearTimeout(timer); timer = null; b.classList.remove("armed"); b.textContent = label; await fn(b); }); return b; } /* per gebruiker het laatste credential-resultaat, overleeft de re-render na de actie zodat het bij de rij zichtbaar is tot het paneel sluit */ const rowResults = new Map(); document.addEventListener("userchange", ()=>rowResults.clear()); function resultBadge(u){ const r = rowResults.get(u.id); if(!r) return null; const badge = h("div","am-result"); badge.appendChild(h("span", null, r.label + ":")); const code = h("code", null, r.value); badge.appendChild(code); badge.appendChild(copyBtn(r.value)); return badge; } /* de beheer-tabs staan als losse knoppen in de ene instellingen-tabbalk (#adminTabsSlot in core.js/index.html) - geen geneste tabbalk meer */ const adminTabDefs = ()=>{ const defs = []; if(currentUser && currentUser.role==="super") defs.push(["scholen", T("amOverarching")]); defs.push(["klassen", T("amClasses")]); defs.push(["gebruikers", T("amUsers")]); defs.push(["toewijzingen", T("amAssignments")]); defs.push(["voortgang", T("amProgress")]); return defs; }; function buildAdminTabs(){ const slot = document.getElementById("adminTabsSlot"); slot.innerHTML = ""; if(!currentUser || !can("admin.access")) return; adminTabDefs().forEach(([t, label])=>{ const b = h("button", null, label); b.type = "button"; b.dataset.tab = "admin"; b.dataset.admintab = t; b.addEventListener("click", async ()=>{ adminTab = t; /* data (opnieuw) laden bij binnenkomst in het beheer-deel; wisselen tussen beheer-tabs onderling hergebruikt de geladen data */ const adminVisible = document.getElementById("settingsAdminPanel").style.display !== "none"; if(!adminVisible){ selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId; await reload(); } switchSettingsTab("admin"); switchAdminTab(t); }); slot.appendChild(b); }); } document.addEventListener("userchange", ()=>{ if(!currentUser) resetResults = null; buildAdminTabs(); }); document.addEventListener("langchange", buildAdminTabs); 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; /* het Systeem-tabblad toont schoolbeheerders van álle scholen, ook als de contextbalk op één school staat; zonder schoolfilter is USERS al compleet en is de extra call overbodig */ if(currentUser.role==="super"){ SYS_ADMINS = q ? ((await api("/admin/users")).users || []).filter(u=>u.role==="admin") : USERS.filter(u=>u.role==="admin"); } const cq = currentUser.role==="super" ? (selSchool ? "?school="+selSchool : "?school=0") : ""; CLASSES = (await api("/admin/classes"+cq)).classes || []; ASSIGNMENTS = {}; if(CLASSES.length){ const results = await Promise.all(CLASSES.map(c=>api("/assignments/class/"+c.id).catch(()=>({assignment:null})))); CLASSES.forEach((c,i)=>{ ASSIGNMENTS[c.id] = results[i].assignment; }); } }catch(e){ USERS = []; CLASSES = []; ASSIGNMENTS = {}; } 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, opts){ const row = h("div","am-row" + (opts && opts.sub ? " am-row-sub" : "")); /* rustig standaardbeeld: alleen naam + rol/klas; alle knoppen en velden zitten achter het ✏-knopje in een uitklapbaar gereedschapsblok */ const tools = h("div","am-row-tools"); const actions = h("div","am-actions"); const icon = opts && opts.icon ? opts.icon + " " : ""; row.appendChild(h("span","am-name", icon + u.displayName + (u.displayName!==u.username ? ` (${u.username})` : ""))); if(u.role==="pupil"){ const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId)); row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : ""))); }else if(u.role==="teacher"){ const names = (u.classIds||[]) .map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name) .filter(Boolean); row.appendChild(h("span","am-role", T(roleKey(u.role)) + (names.length ? ` · ${names.join(", ")}` : "") + (u.pending ? ` · ${T("amPending")}` : ""))); }else{ row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` · ${T("amPending")}` : ""))); } if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){ /* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super); het aparte extra-functies-systeem is uit de UI gehaald */ 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); } }); tools.appendChild(field("amFieldRole", rs)); } /* groepsleiding eenvoudig aan klassen koppelen: huidige klassen als chips met ✕, en een select om een klas toe te voegen */ if(u.role==="teacher" && can("classes.manage") && CLASSES.length){ const wrap = h("div","am-classes"); (u.classIds||[]).forEach(cid=>{ const c = CLASSES.find(x=>Number(x.id)===Number(cid)); if(!c) return; const chip = h("span","am-chip on", c.name); const x = h("button","am-chipx","✕"); x.type = "button"; x.title = T("amClassRemove"); x.addEventListener("click", async ()=>{ try{ await api("/admin/classes/"+cid+"/teachers", {body:{ userId: u.id, remove:true }}); reload(); } catch(e){ msg(e.message); } }); chip.appendChild(x); wrap.appendChild(chip); }); const rest = CLASSES.filter(c=>!(u.classIds||[]).some(id=>Number(id)===Number(c.id))); if(rest.length){ const addSel = h("select","am-sel"); addSel.appendChild(new Option(T("amClassAdd"), "")); rest.forEach(c=>addSel.appendChild(new Option(c.name, c.id))); addSel.addEventListener("change", async ()=>{ if(!addSel.value) return; try{ await api("/admin/classes/"+addSel.value+"/teachers", {body:{ userId: u.id }}); reload(); } catch(e){ msg(e.message); } }); wrap.appendChild(addSel); } tools.appendChild(field("amFieldClasses", wrap)); } 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); } }); tools.appendChild(field("amFieldClass", cs)); /* wachtwoord inzien en aanpassen - alleen zichtbaar zolang de leerling nog niet is ingelogd; daarna kan alleen een nieuw wachtwoord gezet worden */ const pwWrap = h("div","am-inline"); const pw = h("input","am-pw"); pw.value = u.password || ""; pw.placeholder = u.everLoggedIn ? T("amPwHidden") : T("amPwTypePh"); 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); } }); pwWrap.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); }); pwWrap.appendChild(pwCopy); tools.appendChild(field("amFieldPw", pwWrap)); if(u.everLoggedIn){ actions.appendChild(dangerBtn("🎲 " + T("amNewPw"), "amNewPwWarn", async ()=>{ try{ const { password } = await api("/admin/genpw?role=pupil"); await api("/admin/users/"+u.id, {method:"PATCH", body:{password}}); rowResults.set(u.id, { label: T("amResultPw"), value: password }); openRows.add(u.id); reload(); }catch(e){ msg(e.message); } })); } }else{ if(u.pending && u.linkCode){ tools.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode)); } if(can("users.staffCredentials") && u.role!=="pupil" && u.id!==currentUser.id){ actions.appendChild(dangerBtn("🔑 " + T("amNewCode"), "amNewCodeWarn", async ()=>{ try{ const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{newLinkCode:true}}); rowResults.set(u.id, { label: T("amResultCode"), value: r.linkCode }); openRows.add(u.id); reload(); }catch(e){ msg(e.message); } })); /* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */ actions.appendChild(dangerBtn("🎲 " + T("amNewPw"), "amNewPwWarn", async ()=>{ try{ const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{generatePassword:true}}); rowResults.set(u.id, { label: T("amResultPw"), value: r.password }); openRows.add(u.id); reload(); }catch(e){ msg(e.message); } })); } } if(u.id !== currentUser.id){ const del = dangerBtn("🗑 " + T("amDelete"), "amDeleteWarn", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); } catch(e){ msg(e.message); } }); actions.appendChild(del); } const badge = resultBadge(u); if(badge) tools.appendChild(badge); if(actions.childNodes.length) tools.appendChild(field("amFieldActions", actions)); if(tools.childNodes.length){ const edit = h("button","am-edit","✏"); edit.type = "button"; edit.title = T("amEdit"); const sync = ()=>{ const open = openRows.has(u.id); tools.style.display = open ? "" : "none"; edit.classList.toggle("on", open); }; edit.addEventListener("click", ()=>{ if(openRows.has(u.id)) openRows.delete(u.id); else openRows.add(u.id); sync(); }); sync(); row.appendChild(edit); row.appendChild(tools); } 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(field("amFieldName", name)); let cs = null; if(role==="pupil"){ cs = classSel(presetClassId ?? ""); f.appendChild(field("amFieldClass", 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) */ const pwWrap = h("div","am-inline"); pwInp = h("input","am-inp am-pwinp"); pwInp.type = "password"; pwInp.autocomplete = "new-password"; pwInp.placeholder = T("amPwOptional"); pwInp.addEventListener("keydown", ev=>ev.stopPropagation()); pwWrap.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); } }); pwWrap.appendChild(gen); f.appendChild(field("amFieldPw", pwWrap)); } const go = h("button","tbtn", label); go.type = "button"; go.disabled = true; name.addEventListener("input", ()=>{ go.disabled = name.value.trim().length < 2; }); /* fouten horen bij het formulier zelf, niet ergens bovenaan het paneel */ const errEl = h("div","am-form-err"); go.addEventListener("click", async ()=>{ errEl.textContent = ""; 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 = ""; /* het wachtwoord of de koppelcode verschijnt direct bij de nieuwe gebruiker in de lijst (rij en groep klappen vanzelf open) */ rowResults.set(r.user.id, { label: r.user.password ? T("amResultPw") : T("amResultCode"), value: r.user.password || r.user.linkCode, }); openRows.add(r.user.id); if(role==="pupil"){ openGroups.add(r.user.classId ? "class:"+r.user.classId : "class:none"); openGroups.add("pupils"); } reload(); }catch(e){ errEl.textContent = e.message; } }); f.appendChild(go); f.appendChild(errEl); 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; } /* systeem: overzicht van alle scholen + nieuwe school + systeemmanagers. De schoolkiezer-contextbalk is hier verborgen (zie switchAdminTab), want niets op dit tabblad hangt van een gekozen school af. Toevoegen zit achter een bewuste extra stap (+-knop klapt het formulier uit, zoals bij gebruikers) en bevestigt daarna nog met de naam erin. */ function renderScholenPanel(panel){ const list = h("div","am-list"); SCHOOLS.forEach(s=>{ const admins = SYS_ADMINS.filter(a=>Number(a.schoolId)===Number(s.id)); const row = h("div","am-row am-row-school"); row.appendChild(h("span","am-name", "🏫 " + s.name)); row.appendChild(h("span","am-role", `${admins.length} ${(admins.length===1 ? T("amAdminOne") : T("amAdmins")).toLowerCase()}`)); /* zelfde ✏-systeem als bij gebruikers: hernoemen en verwijderen zitten achter het gereedschapsblok, de rij zelf blijft rustig */ const tools = h("div","am-row-tools"); const nameInp = h("input","am-inp"); nameInp.value = s.name; nameInp.maxLength = 80; nameInp.addEventListener("keydown", ev=>ev.stopPropagation()); nameInp.addEventListener("change", async ()=>{ const name = nameInp.value.trim(); if(name.length < 2 || name === s.name) return; try{ await api("/admin/schools/"+s.id, {method:"PATCH", body:{ name }}); reload(); } catch(e){ msg(e.message); } }); tools.appendChild(field("amFieldName", nameInp)); const acts = h("div","am-actions"); acts.appendChild(dangerBtn("🗑 " + T("amDelete"), "amDeleteSchoolWarn", async ()=>{ try{ await api("/admin/schools/"+s.id, {method:"DELETE"}); if(String(selSchool)===String(s.id)) selSchool = ""; reload(); }catch(e){ msg(e.message); } })); tools.appendChild(field("amFieldActions", acts)); const edit = h("button","am-edit","✏"); edit.type = "button"; edit.title = T("amEdit"); const key = "school:"+s.id; const sync = ()=>{ const open = openRows.has(key); tools.style.display = open ? "" : "none"; edit.classList.toggle("on", open); }; edit.addEventListener("click", ()=>{ if(openRows.has(key)) openRows.delete(key); else openRows.add(key); sync(); }); sync(); row.appendChild(edit); row.appendChild(tools); list.appendChild(row); /* de schoolbeheerders van deze school, duidelijk onderscheiden van de schoolrij (🛡-icoon, ingesprongen) maar met hetzelfde ✏-gereedschap */ admins.forEach(a=>list.appendChild(userRow(a, { icon:"🛡", sub:true }))); }); panel.appendChild(list); const wrap = h("div","am-add-wrap"); const toggle = h("button","tbtn ghost", T("amNewSchool")); toggle.type = "button"; wrap.appendChild(toggle); const f = h("div","am-add"); f.style.display = "none"; const inp = h("input","am-inp"); inp.placeholder = T("amSchoolNamePh"); inp.maxLength = 80; inp.addEventListener("keydown", ev=>ev.stopPropagation()); f.appendChild(field("amFieldName", inp)); const add = h("button","tbtn", T("amNewSchool")); add.type = "button"; add.disabled = true; let armTimer = null; const disarm = ()=>{ clearTimeout(armTimer); armTimer = null; add.classList.remove("danger"); add.textContent = T("amNewSchool"); }; inp.addEventListener("input", ()=>{ add.disabled = inp.value.trim().length < 2; disarm(); }); add.addEventListener("click", async ()=>{ const name = inp.value.trim(); if(name.length < 2) return; if(!armTimer){ add.classList.add("danger"); add.textContent = `${T("amNewSchoolConfirm")} “${name}”?`; armTimer = setTimeout(disarm, 5000); return; } disarm(); try{ const r = await api("/admin/schools", { body:{ name } }); selSchool = r.school.id; inp.value = ""; reload(); }catch(e){ msg(e.message); } }); f.appendChild(add); wrap.appendChild(f); toggle.addEventListener("click", ()=>{ toggle.style.display = "none"; f.style.display = "flex"; inp.focus(); }); panel.appendChild(wrap); /* systeemmanagers 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"); /* klassen als volwaardige rijen (zelfde maat als de gebruikersvakken), met de gevaarlijke acties als aparte, bevestigde knoppen */ const list = h("div","am-list"); CLASSES.forEach(c=>{ const row = h("div","am-row"); const pupilCount = USERS.filter(u=>u.role==="pupil" && Number(u.classId)===Number(c.id)).length; row.appendChild(h("span","am-name", c.name)); row.appendChild(h("span","am-role", `${pupilCount} ${(pupilCount===1 ? T("amPupilOne") : T("amPupils")).toLowerCase()}`)); if(canResetPw){ const acts = h("div","am-actions"); acts.appendChild(dangerBtn("🔄 " + T("amResetClass"), "amConfirmReset", async ()=>{ 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); } })); acts.appendChild(dangerBtn("🗑 " + T("amDelete"), "amDeleteClassWarn", async ()=>{ try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); } catch(e){ msg(e.message); } })); row.appendChild(acts); } list.appendChild(row); }); panel.appendChild(list); if(canResetPw){ const f = h("div","am-add"); const inp = h("input","am-inp"); inp.placeholder = T("amClassNamePh"); inp.maxLength = 80; inp.addEventListener("keydown", ev=>ev.stopPropagation()); f.appendChild(field("amFieldName", inp)); const go = h("button","tbtn", T("amNewClass")); go.type = "button"; go.disabled = true; inp.addEventListener("input", ()=>{ go.disabled = inp.value.trim().length < 2; }); go.addEventListener("click", async ()=>{ if(inp.value.trim().length < 2) 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 */ panel.appendChild(dangerBtn("🔄 " + T("amResetSchool"), "amConfirmResetSchool", async ()=>{ const schoolId = currentUser.role==="super" ? selSchool : currentUser.schoolId; if(!schoolId) 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); } })); } } /* toewijzingsrij: kies een van je eigen borden (uit BS, board.js) voor een klas of een individuele leerling. Geen momentopname - de leerling-pagina zoekt het bord live op, dus nieuwe woorden die je toevoegt komen vanzelf door zonder opnieuw toe te wijzen. Een expliciete "Toewijzen"-knop i.p.v. direct bij het kiezen van een bord opslaan, zodat een klas-brede wijziging niet per ongeluk gebeurt. */ /* widgets van een eigen bord (voor de één-widget-keuze in assignmentRow); widgets zonder wid komen uit borden van vóór v0.3.13 en verschijnen pas na de eerstvolgende save van dat bord */ function boardWidgets(boardId){ let found = null; BS.folders.forEach(fo=>fo.boards.forEach(bo=>{ if(bo.id===boardId) found = bo; })); return ((found && found.data && found.data.widgets) || []).filter(w=>w.wid); } function describeCurrent(current){ if(!current) return ""; let s = `${T("amAssignedAs")} “${current.boardName || current.boardId}”`; if(current.widgetId){ const wName = current.widgetType ? T("wg_"+current.widgetType) : current.widgetId; s += ` › ${wName} (${current.mode==="kijken" ? T("amModeWatch") : T("amModeWork")})`; } return s; } function assignmentRow(target, boardOptions, current){ const row = h("div","am-row"); row.appendChild(h("span","am-name", target.label)); const sel = h("select","am-sel"); sel.appendChild(new Option(T("amNoAssignment"), "")); boardOptions.forEach(b=>sel.appendChild(new Option(b.label, b.id))); sel.value = current ? current.boardId : ""; /* keuze: heel bord (default, bestaand gedrag) of één widget + modus */ const widgetSel = h("select","am-sel am-widget-sel"); const modeSel = h("select","am-sel am-mode-sel"); modeSel.appendChild(new Option(T("amModeWork"), "werken")); modeSel.appendChild(new Option(T("amModeWatch"), "kijken")); const syncWidgetSel = ()=>{ widgetSel.innerHTML = ""; widgetSel.appendChild(new Option(T("amWholeBoard"), "")); boardWidgets(sel.value).forEach(w=>widgetSel.appendChild(new Option(T("wg_"+w.id), w.wid))); widgetSel.value = (current && current.boardId===sel.value && current.widgetId) ? current.widgetId : ""; widgetSel.style.display = sel.value ? "" : "none"; syncModeSel(); }; const syncModeSel = ()=>{ modeSel.style.display = (sel.value && widgetSel.value) ? "" : "none"; }; modeSel.value = current && current.mode ? current.mode : "werken"; sel.addEventListener("change", syncWidgetSel); widgetSel.addEventListener("change", syncModeSel); const info = h("span","am-code", describeCurrent(current)); row.appendChild(sel); row.appendChild(widgetSel); row.appendChild(modeSel); row.appendChild(info); syncWidgetSel(); const apply = h("button","am-btn", T("amAssign")); apply.type = "button"; apply.addEventListener("click", async ()=>{ try{ if(sel.value){ const body = { boardId: sel.value }; if(widgetSel.value){ body.widgetId = widgetSel.value; body.mode = modeSel.value; } await api(`/assignments/${target.type}/${target.id}`, {method:"PUT", body}); } else await api(`/assignments/${target.type}/${target.id}`, {method:"DELETE"}); if(target.type==="class"){ await reload(); } else{ const a = (await api("/assignments/pupil/"+target.id)).assignment; info.textContent = describeCurrent(a); msg(T("amAssignSaved")); } }catch(e){ msg(e.message); } }); row.appendChild(apply); return row; } /* vult een