diff --git a/VERSION b/VERSION index c6e4500..3336225 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.85-beta +0.3.86-beta diff --git a/public/css/teach.css b/public/css/teach.css index 15f31e6..b5adbbe 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1944,3 +1944,63 @@ body.hc #settingsModal .am-row{border-bottom:1.5px solid #000;} body.hc #settingsModal .am-list, body.hc #settingsModal .am-reset, body.hc #settingsModal .am-btn, body.hc #settingsModal .tbtn, body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000;} + +/* ---- gebruikers: master-detail (tabel links, bewerkpaneel rechts) ---- */ +#settingsModal .am-toolbar{display:flex; gap:8px; margin:0 0 10px; align-items:center;} +#settingsModal .am-toolbar .am-filter{flex:1; min-width:0;} +#settingsModal .am-btn.am-primary{background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600;} +#settingsModal .am-btn.am-primary:hover{background:var(--accent); opacity:.9;} +#settingsModal .am-split{display:flex; align-items:flex-start;} +#settingsModal .am-master{flex:1; min-width:0;} +#settingsModal .am-detail{ + width:330px; flex:none; position:sticky; top:0; margin-left:16px; padding-left:16px; + border-left:1px solid var(--line); display:flex; flex-direction:column; gap:10px; + max-height:calc(min(700px,92vh) - 220px); overflow-y:auto; +} +#settingsModal .am-detail .am-field{width:100%;} +#settingsModal .am-detail .am-tools{display:flex; flex-direction:column; gap:10px; width:100%;} +#settingsModal .am-detail-empty{color:var(--muted); font-size:12.5px; line-height:1.55; padding:10px 2px;} +#settingsModal .am-detail-head{display:flex; gap:10px; align-items:center;} +#settingsModal .am-detail-head h4{margin:0; font-size:13.5px; font-weight:650;} +#settingsModal .am-detail-head .am-role{display:block; font-size:11.5px;} +#settingsModal .am-detail-title{min-width:0; flex:1;} +#settingsModal .am-detail-close{flex:none;} +/* de tabel: per-rij grid met vaste kolomverhoudingen lijnt over rijen heen uit */ +#settingsModal .am-tr{ + display:grid; grid-template-columns:minmax(0,1.8fr) .9fr 1fr .9fr; + gap:10px; align-items:center; cursor:pointer; +} +#settingsModal .am-thead{ + display:grid; grid-template-columns:minmax(0,1.8fr) .9fr 1fr .9fr; gap:10px; + padding:0 10px 5px; font-size:10px; font-weight:700; letter-spacing:.6px; + text-transform:uppercase; color:var(--muted); +} +#settingsModal .am-tr.sel{background:var(--surface-2); box-shadow:inset 3px 0 0 var(--accent);} +#settingsModal .am-tr .am-name{display:flex; gap:8px; align-items:center; min-width:0;} +#settingsModal .am-name-txt{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;} +#settingsModal .am-username{color:var(--muted); font-weight:500; font-size:11px;} +#settingsModal .am-cell{ + font-size:12.5px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; +} +#settingsModal .am-avatar{ + width:24px; height:24px; border-radius:50%; flex:none; + background:var(--surface-3); color:var(--muted); font-size:10px; font-weight:700; + display:inline-flex; align-items:center; justify-content:center; letter-spacing:.3px; +} +#settingsModal .am-avatar.big{width:34px; height:34px; font-size:12.5px;} +#settingsModal .am-status .am-chip{background:transparent; border:1px solid var(--line); color:var(--muted); font-size:11px;} +#settingsModal .am-status .am-chip.on{border-color:var(--accent); color:var(--accent-ink);} +/* smal scherm: het detailpaneel wordt een overlay Γ³ver de tabel (onder de + tabel stapelen betekent bij grote scholen eindeloos scrollen) */ +@media (max-width:760px){ + #settingsModal .am-thead{display:none;} + #settingsModal .am-tr{grid-template-columns:minmax(0,1fr) auto;} + #settingsModal .am-tr .am-cell{display:none;} + #settingsModal .am-tr .am-cell:nth-of-type(2){display:block; text-align:right;} + #settingsModal .am-detail{display:none;} + #settingsModal .am-split.has-sel .am-detail{ + display:flex; position:fixed; inset:10% 4% 4%; z-index:6500; margin:0; + background:var(--surface); border:1px solid var(--line); border-radius:10px; + padding:14px; max-height:none; box-shadow:var(--shadow-2); + } +} diff --git a/public/js/admin.js b/public/js/admin.js index ed7399c..2f396f0 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -145,13 +145,13 @@ { tab:"voortgang", icon:"πŸ“ˆ", group:"school", order:13, labelKey:"amProgress", subKey:"stVoortgangSub", show:()=>can("progress.view"), keywords:()=>["voortgang","progress","score","resultaten","results","tijdlijn","timeline"] }, - { tab:"scholen", icon:"🏫", group:"system", order:30, labelKey:"amOverarching", subKey:"stScholenSub", + { tab:"scholen", icon:"🏫", group:"system", order:30, labelKey:"amSchools", subKey:"stScholenSub", show:()=>currentUser.role==="super", keywords:()=>[T("amNewSchool"), T("amNewSuper"), "school","systeemmanager","system manager"], search:q=>SCHOOLS .filter(s=>s.name.toLowerCase().includes(q)) .map(s=>({ icon:"🏫", label:s.name, sub:T("amSchools"), go:()=>goSettingsSection("scholen") })) }, - { tab:"opslag", icon:"πŸ–Ό", group:"system", order:31, labelKey:"stOpslag", subKey:"stOpslagSub", + { tab:"opslag", icon:"πŸ–Ό", group:"system", order:32, labelKey:"stOpslag", subKey:"stOpslagSub", show:()=>currentUser.role==="super", keywords:()=>["opslag","storage","afbeeldingen","images","quota"] }, ]; @@ -194,6 +194,7 @@ document.addEventListener("userchange", ()=>{ if(!currentUser) resetResults = null; loadPromise = null; + selectedUserKey = null; }); async function reload(){ @@ -232,29 +233,64 @@ 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})` : ""))); + /* ---- gebruikers: master-detail --------------------------------------------- + Links een compacte tabel (naam | functie | klas | status), rechts een vast + detailpaneel waarin de geselecteerde gebruiker bewerkt wordt. De selectie + overleeft reload(); credential-acties selecteren de gebruiker zodat het + eenmalige wachtwoord/de code direct in het paneel zichtbaar is. */ + let selectedUserKey = null; /* "u:" | "req:" | "new" | null */ + let masterHost = null, detailHost = null; + + 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); + } + function userKlasText(u){ 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==="parent"){ - /* de school ziet bij elke ouder direct om welke kinderen het gaat */ - const kids = (u.childNames||[]).join(", "); - row.appendChild(h("span","am-role", T(roleKey(u.role)) + (kids ? ` Β· πŸ§’ ${kids}` : ""))); - }else{ - 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")}` : ""))); + return cls ? cls.name : ""; } + if(u.role==="parent") return (u.childNames||[]).join(", "); + return (u.classIds||[]) + .map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name) + .filter(Boolean).join(", "); + } + function userStatus(u){ + if(u.pending) return T("amPending"); + if(u.role==="pupil" && !u.everLoggedIn) return T("amNeverLoggedIn"); + return ""; + } + function nameCellFor(display, username){ + const cell = h("span","am-name"); + cell.appendChild(avatar(display)); + const nm = h("span","am-name-txt", display); + if(username && display!==username) nm.appendChild(h("small","am-username"," Β· "+username)); + cell.appendChild(nm); + return cell; + } + /* tabelrij in de gebruikerslijst: klik selecteert (geen her-render, zodat + het zoekveld focus houdt); bewerken gebeurt in het detailpaneel */ + function userCells(u){ + const row = h("div","am-row am-tr"); + row.dataset.selkey = "u:"+u.id; + row.appendChild(nameCellFor(u.displayName, u.username)); + row.appendChild(h("span","am-cell", T(roleKey(u.role)))); + row.appendChild(h("span","am-cell", userKlasText(u) || "β€”")); + const stCell = h("span","am-cell am-status"); + const st = userStatus(u); + if(st) stCell.appendChild(h("span","am-chip", st)); + else stCell.textContent = "β€”"; + row.appendChild(stCell); + row.addEventListener("click", ()=>{ selectedUserKey = "u:"+u.id; syncSelection(); }); + return row; + } + + /* het bewerkformulier van één gebruiker; wordt gebruikt door het + detailpaneel (gebruikers) Γ©n inline door het Systeem-tabblad (subrijen) */ + function userTools(u){ + const tools = h("div","am-tools"); + const actions = h("div","am-actions"); const isStaff = ["teacher","admin","super"].includes(u.role); if(isStaff && can("users.role.change") && u.id!==currentUser.id){ /* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super); @@ -322,30 +358,29 @@ catch(e){ msg(e.message); } }); pwWrap.appendChild(pw); - const pwCopy = h("button","am-copy","πŸ“‹"); + const pwCopy = h("button","am-btn", T("amCopy")); 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 ()=>{ + 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); + selectedUserKey = "u:"+u.id; reload(); }catch(e){ msg(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 ()=>{ + 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 }); - openRows.add(u.id); + selectedUserKey = "u:"+u.id; reload(); }catch(e){ msg(e.message); } })); @@ -354,55 +389,115 @@ 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 ()=>{ + 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); + selectedUserKey = "u:"+u.id; reload(); }catch(e){ msg(e.message); } })); /* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */ - actions.appendChild(dangerBtn("🎲 " + T("amNewPw"), "amNewPwWarn", async ()=>{ + 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); + selectedUserKey = "u:"+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(); } + const del = dangerBtn(T("amDelete"), "amDeleteWarn", async ()=>{ + try{ await api("/admin/users/"+u.id, {method:"DELETE"}); selectedUserKey = null; reload(); } catch(e){ msg(e.message); } }); actions.appendChild(del); } + if(actions.childNodes.length) tools.appendChild(field("amFieldActions", actions)); + return tools; + } + + /* inline-variant voor het Systeem-tabblad (schoolbeheerder-subrijen): + naam + rol met een "Bewerken"-tekstknop die het formulier uitklapt */ + function userRow(u, opts){ + const row = h("div","am-row" + (opts && opts.sub ? " am-row-sub" : "")); + row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : ""))); + row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` Β· ${T("amPending")}` : ""))); + const tools = h("div","am-row-tools"); 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(); - }); + tools.appendChild(userTools(u)); + const edit = h("button","am-edit", T("amEdit")); + edit.type = "button"; + 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(); - row.appendChild(edit); - row.appendChild(tools); - } + }); + sync(); + row.appendChild(edit); + row.appendChild(tools); return row; } + /* het detailpaneel rechts: leeg advies, gebruiker bewerken, aanmelding + afhandelen of een nieuwe gebruiker aanmaken - afhankelijk van de selectie */ + function syncSelection(){ + if(!masterHost || !detailHost) return; + /* selectie valideren tegen de actuele data (na reload kan hij weg zijn) */ + if(selectedUserKey && selectedUserKey.startsWith("u:") + && !USERS.some(u=>"u:"+u.id===selectedUserKey)) selectedUserKey = null; + if(selectedUserKey && selectedUserKey.startsWith("req:") + && !LINK_REQUESTS.some(r=>"req:"+r.id===selectedUserKey)) selectedUserKey = null; + masterHost.querySelectorAll(".am-tr").forEach(r=>r.classList.toggle("sel", r.dataset.selkey===selectedUserKey)); + const split = masterHost.closest(".am-split"); + if(split) split.classList.toggle("has-sel", !!selectedUserKey); + renderDetail(); + } + function detailHead(title, subtitle){ + const head = h("div","am-detail-head"); + head.appendChild(avatar(title, true)); + const txt = h("div","am-detail-title"); + txt.appendChild(h("h4", null, title)); + if(subtitle) txt.appendChild(h("span","am-role", subtitle)); + head.appendChild(txt); + const close = h("button","am-btn am-detail-close", T("close")); + close.type = "button"; + close.addEventListener("click", ()=>{ selectedUserKey = null; syncSelection(); }); + head.appendChild(close); + return head; + } + function renderDetail(){ + if(!detailHost) return; + detailHost.innerHTML = ""; + if(selectedUserKey === "new"){ + detailHost.appendChild(detailHead(T("amNewUser"), "")); + detailHost.appendChild(newUserForm()); + return; + } + if(selectedUserKey && selectedUserKey.startsWith("u:")){ + const u = USERS.find(x=>"u:"+x.id===selectedUserKey); + detailHost.appendChild(detailHead(u.displayName, T(roleKey(u.role)) + (u.displayName!==u.username ? ` Β· ${u.username}` : ""))); + const badge = resultBadge(u); + if(badge) detailHost.appendChild(badge); + detailHost.appendChild(userTools(u)); + return; + } + if(selectedUserKey && selectedUserKey.startsWith("req:")){ + const r = LINK_REQUESTS.find(x=>"req:"+x.id===selectedUserKey); + detailHost.appendChild(detailHead(r.pupil.displayName, `${T("amRequestOne")} Β· ${T("amRequestVia")} ${r.parent.displayName}`)); + detailHost.appendChild(requestTools(r)); + return; + } + detailHost.appendChild(h("div","am-detail-empty", T("amDetailEmpty"))); + } + /* 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). */ @@ -487,6 +582,84 @@ return wrap; } + /* nieuw-gebruiker-formulier in het detailpaneel: eerst de rol kiezen + (super aanmaken blijft op het Systeem-tabblad - die rol hoort niet bij + één school), daarna de velden die bij die rol horen */ + function newUserForm(){ + const wrap = h("div","am-tools"); + const roles = creatableRoles().filter(r=>r!=="super"); + const roleSel = h("select","am-sel"); + roles.forEach(r=>roleSel.appendChild(new Option(T(roleKey(r)), r))); + roleSel.value = roles.includes("pupil") ? "pupil" : roles[0]; + wrap.appendChild(field("amFieldRole", roleSel)); + const body = h("div","am-tools"); + wrap.appendChild(body); + const build = ()=>{ + body.innerHTML = ""; + const role = roleSel.value; + const name = h("input","am-inp"); + name.placeholder = T("amUserPh"); + name.addEventListener("keydown", ev=>ev.stopPropagation()); + body.appendChild(field("amFieldName", name)); + let cs = null; + if(role==="pupil"){ cs = classSel(""); body.appendChild(field("amFieldClass", cs)); } + let pwInp = null; + if(role!=="pupil"){ + 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); + body.appendChild(field("amFieldPw", pwWrap)); + } + const go = h("button","am-btn am-primary", T("amNewUser")); + go.type = "button"; + go.disabled = true; + name.addEventListener("input", ()=>{ go.disabled = name.value.trim().length < 2; }); + const errEl = h("div","am-form-err"); + go.addEventListener("click", async ()=>{ + errEl.textContent = ""; + const reqBody = { role, username: name.value.trim(), displayName: name.value.trim() }; + if(role==="pupil" && cs && cs.value) reqBody.classId = +cs.value; + if(pwInp && pwInp.value.trim()) reqBody.password = pwInp.value.trim(); + if(currentUser.role==="super") reqBody.school = +selSchool; + try{ + const r = await api("/admin/users", { body: reqBody }); + rowResults.set(r.user.id, { + label: r.user.password ? T("amResultPw") : T("amResultCode"), + value: r.user.password || r.user.linkCode, + }); + if(role==="pupil"){ + openGroups.add(r.user.classId ? "class:"+r.user.classId : "class:none"); + openGroups.add("pupils"); + } + /* de nieuwe gebruiker wordt direct geselecteerd: het eenmalige + wachtwoord of de koppelcode staat dan in het detailpaneel */ + selectedUserKey = "u:"+r.user.id; + reload(); + }catch(e){ errEl.textContent = e.message; } + }); + body.appendChild(go); + body.appendChild(errEl); + setTimeout(()=>name.focus(), 0); + }; + roleSel.addEventListener("change", build); + build(); + 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. */ @@ -548,7 +721,7 @@ }); tools.appendChild(field("amFieldName", nameInp)); const acts = h("div","am-actions"); - acts.appendChild(dangerBtn("πŸ—‘ " + T("amDelete"), "amDeleteSchoolWarn", async ()=>{ + acts.appendChild(dangerBtn(T("amDelete"), "amDeleteSchoolWarn", async ()=>{ try{ await api("/admin/schools/"+s.id, {method:"DELETE"}); if(String(selSchool)===String(s.id)) selSchool = ""; @@ -575,7 +748,7 @@ 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 }))); + admins.forEach(a=>list.appendChild(userRow(a, { sub:true }))); }); panel.appendChild(list); @@ -645,7 +818,7 @@ 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 ()=>{ + acts.appendChild(dangerBtn(T("amResetClass"), "amConfirmReset", async ()=>{ try{ const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"}); await reload(); @@ -653,7 +826,7 @@ render(); }catch(e){ msg(e.message); } })); - acts.appendChild(dangerBtn("πŸ—‘ " + T("amDelete"), "amDeleteClassWarn", async ()=>{ + acts.appendChild(dangerBtn(T("amDelete"), "amDeleteClassWarn", async ()=>{ try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); } catch(e){ msg(e.message); } })); @@ -684,7 +857,7 @@ panel.appendChild(f); /* multireset: alle leerlingwachtwoorden van de hele school in één keer */ - panel.appendChild(dangerBtn("πŸ”„ " + T("amResetSchool"), "amConfirmResetSchool", async ()=>{ + panel.appendChild(dangerBtn(T("amResetSchool"), "amConfirmResetSchool", async ()=>{ const schoolId = currentUser.role==="super" ? selSchool : currentUser.schoolId; if(!schoolId) return; try{ @@ -907,12 +1080,21 @@ adopteren (thuisaccount wordt de schoolleerling, evt. met klas en - bij naamconflict - een andere gebruikersnaam), koppelen aan een bestaande leerling, of afwijzen */ - function linkRequestRow(r){ - const row = h("div","am-row"); - row.appendChild(h("span","am-name","πŸ§’ " + r.pupil.displayName - + (r.pupil.displayName!==r.pupil.username ? ` (${r.pupil.username})` : ""))); - row.appendChild(h("span","am-role", `${T("amRequestVia")} πŸ‘ͺ ${r.parent.displayName}`)); - const tools = h("div","am-row-tools"); + /* aanmeldverzoek als tabelrij; afhandelen gebeurt in het detailpaneel */ + function requestCells(r){ + const row = h("div","am-row am-tr"); + row.dataset.selkey = "req:"+r.id; + row.appendChild(nameCellFor(r.pupil.displayName, r.pupil.username)); + row.appendChild(h("span","am-cell", T("amRequestOne"))); + row.appendChild(h("span","am-cell", `${T("amRequestVia")} ${r.parent.displayName}`)); + const stCell = h("span","am-cell am-status"); + stCell.appendChild(h("span","am-chip on", T("amReqNew"))); + row.appendChild(stCell); + row.addEventListener("click", ()=>{ selectedUserKey = "req:"+r.id; syncSelection(); }); + return row; + } + function requestTools(r){ + const tools = h("div","am-tools"); const adoptWrap = h("div","am-inline"); const cs = classSel(null); @@ -925,7 +1107,7 @@ adopt.addEventListener("click", async ()=>{ const body = { action:"adopt", classId: cs.value ? +cs.value : null }; if(un.style.display!=="none" && un.value.trim()) body.username = un.value.trim(); - try{ await api("/admin/link-requests/"+r.id+"/resolve", { body }); reload(); } + try{ await api("/admin/link-requests/"+r.id+"/resolve", { body }); selectedUserKey = null; reload(); } catch(e){ /* naamconflict binnen de school: veld tonen om direct een andere gebruikersnaam mee te geven */ @@ -945,7 +1127,7 @@ mb.type = "button"; mb.addEventListener("click", async ()=>{ if(!ms.value) return; - try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"match", pupilId: +ms.value } }); reload(); } + try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"match", pupilId: +ms.value } }); selectedUserKey = null; reload(); } catch(e){ msg(e.message); } }); matchWrap.appendChild(ms); @@ -953,32 +1135,50 @@ tools.appendChild(field("amMatchBtn", matchWrap)); const actions = h("div","am-actions"); - actions.appendChild(dangerBtn("πŸ—‘ " + T("amReject"), "amRejectWarn", async ()=>{ - try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"reject" } }); reload(); } + actions.appendChild(dangerBtn(T("amReject"), "amRejectWarn", async ()=>{ + try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"reject" } }); selectedUserKey = null; reload(); } catch(e){ msg(e.message); } })); tools.appendChild(field("amFieldActions", actions)); - row.appendChild(tools); - return row; + return tools; } function renderGebruikersPanel(panel){ - /* openstaande aanmeldingen bovenaan: dit vraagt actie van de school */ - if(LINK_REQUESTS.length){ - panel.appendChild(h("div","am-group", `${T("amLinkRequests")} (${LINK_REQUESTS.length})`)); - const rl = h("div","am-reqlist am-list"); - LINK_REQUESTS.forEach(r=>rl.appendChild(linkRequestRow(r))); - panel.appendChild(rl); - } - const filterBar = h("div","am-add"); + const schoolChosenTop = currentUser.role!=="super" || !!selSchool; + /* werkbalk: zoeken links, nieuwe gebruiker rechts */ + const bar = h("div","am-toolbar"); 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); + bar.appendChild(filterInp); + if(schoolChosenTop && creatableRoles().some(r=>r!=="super")){ + const add = h("button","am-btn am-primary", T("amNewUser")); + add.type = "button"; + add.addEventListener("click", ()=>{ selectedUserKey = "new"; syncSelection(); }); + bar.appendChild(add); + } + panel.appendChild(bar); + + /* master (tabel) + detail (bewerkpaneel) */ + const split = h("div","am-split"); + const master = h("div","am-master"); + const detail = h("div","am-detail"); + masterHost = master; detailHost = detail; + + /* openstaande aanmeldingen bovenaan: dit vraagt actie van de school */ + if(LINK_REQUESTS.length){ + master.appendChild(h("div","am-group", `${T("amLinkRequests")} (${LINK_REQUESTS.length})`)); + const rl = h("div","am-reqlist am-list"); + LINK_REQUESTS.forEach(r=>rl.appendChild(requestCells(r))); + master.appendChild(rl); + } + /* kolomkoppen boven de tabel */ + const thead = h("div","am-thead"); + ["amFieldName","amFieldRole","amFieldClass","amColStatus"].forEach(k=>thead.appendChild(h("span",null,T(k)))); + master.appendChild(thead); const list = h("div","am-list am-userlist"); const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")], ["parent", T("amParents")]]; @@ -986,18 +1186,16 @@ const us = USERS.filter(u=>u.role===role); if(!us.length) return; list.appendChild(h("div","am-group", `${label} (${us.length})`)); - us.forEach(u=>list.appendChild(userRow(u))); + us.forEach(u=>list.appendChild(userCells(u))); }); const pupils = USERS.filter(u=>u.role==="pupil"); - const schoolChosen = currentUser.role!=="super" || !!selSchool; - const creatable = creatableRoles(); - const canAddPupil = schoolChosen && creatable.includes("pupil"); + const schoolChosen = schoolChosenTop; /* leerlingen staan onder inklapbare groepskoppen (standaard dicht): zo blijft de lijst overzichtelijk bij grote scholen; zoeken kijkt altijd door alle groepen heen (zie applyUserFilter) */ - const pupilGroup = (key, label, us, formEl)=>{ + const pupilGroup = (key, label, us)=>{ const g = h("div","am-group am-group-toggle"); g.dataset.collapsible = "1"; g.dataset.gkey = key; @@ -1009,8 +1207,7 @@ applyUserFilter(panel); }); list.appendChild(g); - us.forEach(u=>list.appendChild(userRow(u))); - if(formEl) list.appendChild(formEl); + us.forEach(u=>list.appendChild(userCells(u))); }; /* per-klas indeling heeft alleen zin met één gekozen school (CLASSES is dan @@ -1018,29 +1215,22 @@ één groep - 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 || canAddPupil){ - pupilGroup("pupils", T("amPupils"), pupils, canAddPupil ? addForm("pupil") : null); - } + if(pupils.length) pupilGroup("pupils", T("amPupils"), pupils); }else{ CLASSES.forEach(c=>{ const us = pupils.filter(u=>u.classId===c.id); - if(!us.length && !canAddPupil) return; - pupilGroup("class:"+c.id, c.name, us, canAddPupil ? addForm("pupil", c.id) : null); + if(us.length) pupilGroup("class:"+c.id, c.name, us); }); const noClass = pupils.filter(u=>!u.classId); - if(noClass.length || canAddPupil){ - pupilGroup("class:none", T("amNoClass"), noClass, canAddPupil ? addForm("pupil") : null); - } + if(noClass.length) pupilGroup("class:none", T("amNoClass"), noClass); } - panel.appendChild(list); + master.appendChild(list); + split.appendChild(master); + split.appendChild(detail); + panel.appendChild(split); - /* staf-aanmaakformulieren - welke rollen deze gebruiker mag aanmaken (CREATABLE_ROLES). - "super" aanmaken staat in het Systeem-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); + syncSelection(); /* selectie valideren + detailpaneel vullen */ } function switchAdminTab(tab){ @@ -1063,7 +1253,7 @@ SCHOOLS.forEach(s=>sel.appendChild(new Option(s.name, s.id))); sel.value = selSchool ?? ""; sel.setAttribute("aria-label", T("amPickSchool")); - sel.addEventListener("change", ()=>{ selSchool = sel.value; reload(); }); + sel.addEventListener("change", ()=>{ selSchool = sel.value; selectedUserKey = null; reload(); }); bar.appendChild(sel); return bar; } diff --git a/public/js/cms.js b/public/js/cms.js index 47bea0d..555f052 100644 --- a/public/js/cms.js +++ b/public/js/cms.js @@ -206,7 +206,7 @@ let CMS = {}; } registerSettingsSection({ - id:"inhoud", icon:"πŸ“’", group:"system", order:32, + id:"inhoud", icon:"πŸ“’", group:"system", order:31, label:()=>T("stInhoud"), sub:()=>T("stInhoudSub"), visible:()=>!!currentUser && can("content.manage"), activate(){ diff --git a/public/js/core.js b/public/js/core.js index f92ec3f..d097612 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version β€” shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.3.85-beta"; +const VERSION = "0.3.86-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -276,6 +276,9 @@ const I18N = { amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.", amDeleteWarn:"Verwijdert dit account definitief, inclusief borden en woorden.", amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode", + amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", + amDetailEmpty:"Selecteer links een gebruiker om te bewerken, of maak een nieuwe aan.", + amNewUser:"Nieuwe gebruiker", amRequestOne:"Aanmelding", amReqNew:"nieuw", amParents:"Ouders", amParentCode:"Ouder-code", amResultParentCode:"Ouder-code", amParentCodeWarn:"Maakt een eenmalige ouder-code aan; een eerdere code vervalt.", @@ -565,6 +568,9 @@ const I18N = { amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.", amDeleteWarn:"Deletes this account permanently, including boards and words.", amResultPw:"New password", amResultCode:"New link code", + amColStatus:"Status", amNeverLoggedIn:"not signed in yet", + amDetailEmpty:"Select a user on the left to edit, or create a new one.", + amNewUser:"New user", amRequestOne:"Enrolment", amReqNew:"new", amParents:"Parents", amParentCode:"Parent code", amResultParentCode:"Parent code", amParentCodeWarn:"Generates a one-time parent code; a previous code stops working.",