/* teach - beheerpaneel (👥): scholen, klassen en gebruikers als één drill-down navigator (tegel-overzicht → gefocust detail, i.p.v. alles tegelijk uitgeklapt op één lange pagina). 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 de schooltegels op "alle scholen" */ let SYS_ADMINS = []; /* huidige klas-toewijzingen (leerling-omgeving), per klas-id - opnieuw opgehaald bij elke reload() zoals CLASSES/USERS hierboven */ let ASSIGNMENTS = {}; /* openstaande aanmeldverzoeken van ouders (thuiskind -> deze school) */ let LINK_REQUESTS = []; /* 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 = "school"; /* navigatie binnen "School": welk niveau van de boom je ziet. {type:"overview"} - tegels (scholen op "alle scholen", anders klassen+platte groepen van de gekozen school) {type:"class", id} - gefocust detail van één klas {type:"flat", kind} - gefocust detail van één platte groep (admins/supers/parents/noschool/unassigned/requests) Blijft behouden over her-renders, reset bij het wisselen van school en bij in-/uitloggen - anders belandt een systeemmanager na het kiezen van een andere school midden in de klas van de vórige school. */ let navLevel = { type:"overview" }; /* welke sheet (persoon of aanmelding) open staat; blijft ook over reload() heen zodat een actie ín de sheet (nieuw wachtwoord, klas koppelen) de sheet niet laat verdwijnen maar met verse data laat herladen - zelfde reden als waarom rowResults/resetResults hieronder blijven staan. */ let openSheet = null; /* uitgeklapt "⋯"-actiepaneel (klas- of schoolbeheer) op het huidige niveau; hoort bij navLevel, dus reset zodra dat wijzigt */ let kebabOpen = false; /* systeembrede zoekopdracht: vult, op het tegel-niveau, het scherm met een platte resultatenlijst i.p.v. tegels */ let searchQuery = ""; /* 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-btn", T("amCopy")); b.type = "button"; 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 sheet (met kopieerknop). */ 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; } /* ---- groepskaart: één kader om wat bij elkaar hoort ------------------------ Nog gebruikt door assignmentRow (Toewijzingen) en de Opslag-tab (image-catalog.js) - niet meer door dit bestand zelf. */ function groupCard(opts){ const openSet = opts.openSet || new Set(); const editSet = opts.editSet || new Set(); const card = h("div","am-card"); const head = h("div","am-card-head"); card.appendChild(head); if(opts.icon) head.appendChild(h("span","am-ico", opts.icon)); head.appendChild(h("span","am-card-title", opts.title)); const sum = h("span","am-card-sum"); head.appendChild(sum); let tools = null; if(opts.edit){ tools = h("div","am-card-tools"); tools.appendChild(opts.edit.tools); const btn = h("button","am-edit", T("amEdit")); btn.type = "button"; const ekey = opts.edit.key; const esync = ()=>{ const open = editSet.has(ekey); tools.style.display = open ? "" : "none"; btn.classList.toggle("on", open); }; btn.addEventListener("click", ()=>{ if(editSet.has(ekey)) editSet.delete(ekey); else editSet.add(ekey); esync(); }); esync(); head.appendChild(btn); card.appendChild(tools); } if(opts.subs && opts.subs.length){ const subHost = h("div","am-card-subs"); opts.subs.forEach(el=>subHost.appendChild(el)); card.appendChild(subHost); sum.classList.add("am-card-toggle"); const sync = ()=>{ const open = openSet.has(opts.key); sum.textContent = `${opts.summary} ${open ? "▾" : "▸"}`; subHost.style.display = open ? "" : "none"; }; sum.addEventListener("click", ()=>{ if(openSet.has(opts.key)) openSet.delete(opts.key); else openSet.add(opts.key); sync(); }); sync(); }else if(opts.summary){ sum.textContent = opts.summary; } return card; } window.amGroupCard = groupCard; /* ---- beheer-secties in het instellingen-register (settings.js) ------------ Elke sectie meldt zichzelf aan met rol-zichtbaarheid, een activate() en zoekgegevens; de schil bouwt daar navigatie + zoeken uit. Scholen, klassen en gebruikers zijn hier bewust ÉÉN sectie ("school") die intern een eigen drill-down navigator opbouwt (zie renderSchoolNavigator). */ const ADMIN_SECTIONS = [ { tab:"school", icon:"🏫", group:"school", order:10, labelKey:"amSchoolMenu", subKey:"stSchoolMenuSub", show:()=>true, keywords:()=>[T("amPupils"), T("roleTeacher"), T("roleAdmin"), T("amParents"), T("amLinkRequests"), T("amNewPw"), T("amParentCode"), "leerling","pupil","ouder","parent","wachtwoord","password", "koppelcode","aanmelding","enrolment", T("amNewClass"), T("amResetClass"), "klas","class","groep","group", T("amNewSchool"), T("amNewSuper"), "school","systeemmanager","system manager"], search:q=>[ ...USERS .filter(u=>`${u.displayName} ${u.username}`.toLowerCase().includes(q)) .map(u=>({ icon: u.role==="pupil" ? "🧒" : u.role==="parent" ? "👪" : "🧑‍🏫", label:u.displayName, sub:T(roleKey(u.role)), go:()=>goSettingsSection("school", { openPerson:u.id }) })), ...CLASSES .filter(c=>c.name.toLowerCase().includes(q)) .map(c=>({ icon:"🏷", label:c.name, sub:T("amClasses"), go:()=>{ navLevel={type:"class", id:c.id}; kebabOpen=false; goSettingsSection("school"); } })), ...SCHOOLS .filter(s=>s.name.toLowerCase().includes(q)) .map(s=>({ icon:"🏫", label:s.name, sub:T("amSchools"), go:()=>{ selSchool=String(s.id); navLevel={type:"overview"}; goSettingsSection("school"); } })), ] }, { tab:"toewijzingen", icon:"🎯", group:"school", order:12, labelKey:"amAssignments", subKey:"stToewijzSub", show:()=>can("assignments.manage"), keywords:()=>[T("amModeWatch"), T("amModeWork"), "bord","board","delen","share","toewijzen","assign","widget"] }, { tab:"voortgang", icon:"📈", group:"school", order:13, labelKey:"amProgress", subKey:"stVoortgangSub", show:()=>can("progress.view"), keywords:()=>["voortgang","progress","score","resultaten","results","tijdlijn","timeline"] }, { tab:"opslag", icon:"🖼", group:"system", order:32, labelKey:"stOpslag", subKey:"stOpslagSub", show:()=>currentUser.role==="super", keywords:()=>["opslag","storage","afbeeldingen","images","quota"] }, ]; const adminTabDefs = ()=>ADMIN_SECTIONS.filter(d=>d.show()).map(d=>[d.tab, T(d.labelKey)]); /* data één keer per bezoek aan Instellingen laden; secties onderling hergebruiken de lading (in-flight promise voorkomt dubbel laden) */ let loadPromise = null; function ensureLoaded(force){ if(force) loadPromise = null; if(!loadPromise){ loadPromise = (async ()=>{ /* super: selSchool null = nog niet gekozen (reload() zet dan "alle scholen" als default). Andere rollen: eigen school. */ if(currentUser.role!=="super") selSchool = currentUser.schoolId; await reload(); })(); } return loadPromise; } /* settings.js roept dit bij het openen aan: verse data + zoekindex vullen */ window.adminPrefetch = ()=>{ ensureLoaded(true).catch(()=>{}); }; ADMIN_SECTIONS.forEach(d=>{ registerSettingsSection({ id:d.tab, icon:d.icon, group:d.group, order:d.order, label:()=>T(d.labelKey), sub:()=>T(d.subKey), visible:()=>!!currentUser && can("admin.access") && d.show(), async activate(payload){ await ensureLoaded(); if(d.tab==="school" && payload && payload.openPerson !== undefined) openSheet = { type:"person", id:payload.openPerson }; showAdminHost(); switchAdminTab(d.tab); if(d.tab==="school") render(); }, keywords:d.keywords, searchItems:d.search, }); }); document.addEventListener("userchange", ()=>{ if(!currentUser) resetResults = null; loadPromise = null; selSchool = null; /* nieuwe sessie: laat reload() opnieuw defaulten */ navLevel = { type:"overview" }; openSheet = null; kebabOpen = false; searchQuery = ""; }); async function reload(){ let reloadError = null; try{ if(currentUser.role==="super"){ SCHOOLS = await api("/schools"); /* eerste bezoek: standaard "alle scholen" (het scholenoverzicht) i.p.v. meteen een school te kiezen - de systeemmanager beheert immers over scholen heen; een specifieke school kiezen is een bewuste stap. */ if(selSchool == null) selSchool = ""; } const q = currentUser.role==="super" && selSchool ? "?school="+selSchool : ""; USERS = (await api("/admin/users"+q)).users; /* schooltegels op "alle scholen" tonen schoolbeheerders van élke school; 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 : ""; 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; }); } LINK_REQUESTS = []; if(currentUser.role!=="super" || selSchool){ const lq = currentUser.role==="super" ? "?school="+selSchool : ""; LINK_REQUESTS = ((await api("/admin/link-requests"+lq).catch(()=>({}))).requests) || []; } }catch(e){ USERS = []; CLASSES = []; ASSIGNMENTS = {}; LINK_REQUESTS = []; reloadError = e; } render(); /* render() bouwt de modal (incl. .am-msg) helemaal opnieuw op, dus de melding pas ná render() zetten - anders wordt hij meteen weer gewist. Zonder dit bleef het paneel bij een fout stilletjes helemaal leeg staan zonder enige aanwijzing waarom. */ if(reloadError) msg(T("amLoadError") + ": " + reloadError.message); } 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 avatar(nameStr, big){ const parts = String(nameStr || "?").trim().split(/\s+/); const ini = ((parts[0][0] || "?") + (parts[1] ? parts[1][0] : "")).toUpperCase(); return h("span", "am-avatar" + (big ? " big" : ""), ini); } /* status compact als glyph met uitleg-title: ● in gebruik, ○ wacht nog op eerste login/activatie. */ function userStatusGlyph(u){ const waiting = u.pending || (u.role==="pupil" && !u.everLoggedIn); const label = u.pending ? T("amPending") : (u.role==="pupil" && !u.everLoggedIn) ? T("amNeverLoggedIn") : T("amStatusActive"); const g = h("span","am-glyph" + (waiting ? "" : " ok"), waiting ? "○" : "●"); g.title = label; g.setAttribute("aria-label", label); return g; } /* het bewerkformulier van één gebruiker; wordt in de sheet getoond zodra iemand op een personRow tikt. */ function userTools(u, note){ note = note || msg; const tools = h("div","am-tools"); const actions = h("div","am-actions"); const isStaff = ["teacher","admin","super"].includes(u.role); /* school koppelen/losmaken (systeemmanager): losse accounts terugzetten naar hun school, of bewust losmaken - super/ouder zijn per definitie schoolloos en vallen hierbuiten */ if(currentUser.role==="super" && ["teacher","admin","pupil"].includes(u.role)){ const ss = h("select","am-sel"); ss.appendChild(new Option(T("amNoSchool"), "")); SCHOOLS.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id))); ss.value = u.schoolId ?? ""; ss.addEventListener("change", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{ schoolId: ss.value ? +ss.value : null }}); reload(); } catch(e){ note(e.message); ss.value = u.schoolId ?? ""; } }); tools.appendChild(field("amFieldSchool", ss)); } /* rolwijziging: alleen tussen groepsleiding/schoolbeheerder onderling. Systeemmanager is hier bewust nooit een optie - die rol ontstaat alleen via de aparte "Nieuwe systeemmanager"-aanmaak (met bevestiging van het eigen wachtwoord), nooit door een bestaand account te promoveren. Een bestaande systeemmanager toont dit veld daarom ook niet: er is geen veilige weg om die rol via dit simpele dropdown-veld te wijzigen. */ if(isStaff && u.role!=="super" && can("users.role.change") && u.id!==currentUser.id){ const rs = h("select","am-sel"); ["teacher","admin"].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){ note(e.message); } }); tools.appendChild(field("amFieldRole", rs)); } /* elke staf-rol eenvoudig aan klassen koppelen (groepsleiding, schoolbeheerder én systeemmanager): huidige klassen als chips met ✕, en een select om een klas toe te voegen */ if(isStaff && 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){ note(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){ note(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}}); reload(); } catch(e){ note(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){ note(e.message); } }); pwWrap.appendChild(pw); const pwCopy = h("button","am-btn", T("amCopy")); pwCopy.type = "button"; 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 }); reload(); }catch(e){ note(e.message); } })); } /* eenmalige ouder-code waarmee een ouder dit kind aan het eigen ouderaccount koppelt (ouderportaal); een eerdere code vervalt */ actions.appendChild(dangerBtn(T("amParentCode"), "amParentCodeWarn", async ()=>{ try{ const r = await api("/admin/users/"+u.id+"/parent-code", { body:{} }); rowResults.set(u.id, { label: T("amResultParentCode"), value: r.parentCode }); reload(); }catch(e){ note(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 }); reload(); }catch(e){ note(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 }); reload(); }catch(e){ note(e.message); } })); } } if(u.id !== currentUser.id){ const del = dangerBtn(T("amDelete"), "amDeleteWarn", async ()=>{ try{ await api("/admin/users/"+u.id, {method:"DELETE"}); openSheet = null; reload(); } catch(e){ note(e.message); } }); actions.appendChild(del); } if(actions.childNodes.length) tools.appendChild(field("amFieldActions", actions)); return tools; } /* 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"; /* het formulier zelf herhaalt wat je aan het toevoegen bent, zodat het nooit met een bestaande-gebruikersrij te verwarren is zodra de knoptekst erboven uit beeld is (zie ook .am-add's eigen kader in css) */ f.appendChild(h("div","am-add-title", label)); 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", T("amGenerate")); gen.type = "button"; 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)); } /* een systeemmanager aanmaken is de meest ingrijpende actie in dit beheer (school-onafhankelijk, alle rechten overal) - vereist daarom als extra stap het eigen wachtwoord van de aanmaker zelf, ter bevestiging. De server controleert dit ook; dit is niet louter cosmetisch. */ let confirmInp = null; if(role==="super"){ confirmInp = h("input","am-inp"); confirmInp.type = "password"; confirmInp.autocomplete = "current-password"; confirmInp.placeholder = T("amOwnPasswordPh"); confirmInp.addEventListener("keydown", ev=>ev.stopPropagation()); f.appendChild(field("amOwnPassword", confirmInp)); } const go = h("button","tbtn", label); go.type = "button"; go.disabled = true; const sync = ()=>{ go.disabled = name.value.trim().length < 2 || (confirmInp && !confirmInp.value); }; name.addEventListener("input", sync); if(confirmInp) confirmInp.addEventListener("input", sync); /* 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; if(confirmInp) body.confirmPassword = confirmInp.value; try{ const r = await api("/admin/users", { body }); /* groepsleiding die vanuit een klasrij wordt aangemaakt, hoort ook meteen aan die klas gekoppeld te zijn - anders staat de nieuwe leerkracht wel in "Groepsleiding" toegevoegd, maar (nog) niet echt bij die klas. Hergebruikt de al bestaande koppel-route. */ if(role==="teacher" && presetClassId){ await api("/admin/classes/"+presetClassId+"/teachers", { body:{ userId: r.user.id } }); } name.value = ""; if(pwInp) pwInp.value = ""; if(confirmInp) confirmInp.value = ""; rowResults.set(r.user.id, { label: r.user.password ? T("amResultPw") : T("amResultCode"), value: r.user.password || r.user.linkCode, }); 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; } /* 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 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)); 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); } }); /* kop toont compact de huidige stand; de keuzes klappen uit via Bewerken */ const tools = h("div","am-tools"); const inline = h("div","am-inline"); inline.appendChild(sel); inline.appendChild(widgetSel); inline.appendChild(modeSel); inline.appendChild(apply); tools.appendChild(inline); tools.appendChild(info); return groupCard({ title: target.label, summary: describeCurrent(current) || T("amNoAssignment"), edit: { key: "assign:"+target.type+":"+target.id, tools }, }); } /* vult een