diff --git a/VERSION b/VERSION index 955cbea..efed92e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.36-beta +0.3.37-beta diff --git a/public/css/teach.css b/public/css/teach.css index ddc0661..0b51315 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1153,6 +1153,8 @@ /* inklapbare groepskop (leerlingen per klas): klikbaar, pijltje in de tekst */ .am-group-toggle{cursor:pointer; user-select:none;} .am-group-toggle:hover{color:var(--ink);} +/* formulier-fout direct bij het formulier zelf */ +.am-form-err{width:100%; min-height:0; color:var(--red); font-size:13px; font-weight:700;} /* Systeem-tab: schoolrij duidelijk zwaarder dan de beheerder-subrijen */ .am-row-school{background:var(--accent-soft);} .am-row-school > .am-name{font-size:15px;} diff --git a/public/js/admin.js b/public/js/admin.js index 222f4bf..220fdc5 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -198,11 +198,19 @@ 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){ - /* super mag de rol van andere stafaccounts wijzigen - leerlingen blijven altijd leerling */ + /* éé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; @@ -211,23 +219,38 @@ catch(e){ msg(e.message); } }); tools.appendChild(field("amFieldRole", rs)); - - /* extra functies naast de hoofdrol (bv. een teacher die ook beheerdersrechten - krijgt) - los van de hoofdrol hierboven, direct toggelbaar per rol */ - const extraWrap = h("div","am-classes"); - ["teacher","admin","super"].filter(r=>r!==u.role).forEach(r=>{ - const has = (u.extraRoles||[]).includes(r); - const chip = h("button","am-chip", (has?"✓ ":"+ ")+T(roleKey(r))); - chip.type = "button"; - chip.classList.toggle("on", has); - chip.addEventListener("click", async ()=>{ - const next = has ? (u.extraRoles||[]).filter(x=>x!==r) : [...(u.extraRoles||[]), r]; - try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{extraRoles: next}}); reload(); } + } + /* 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); } }); - extraWrap.appendChild(chip); + chip.appendChild(x); + wrap.appendChild(chip); }); - tools.appendChild(field("amFieldExtraRoles", extraWrap)); + 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) */ @@ -367,7 +390,10 @@ 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(); @@ -376,16 +402,22 @@ 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); } + /* 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", ()=>{ diff --git a/public/js/core.js b/public/js/core.js index 55bb2da..e2bfa35 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.36-beta"; +const VERSION = "0.3.37-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -184,8 +184,9 @@ const I18N = { amPickSchoolHint:"Kies hierboven eerst een school (🏫).", amPickSchool:"Kies een school", amPupils:"Leerlingen", amPupilOne:"Leerling", amEdit:"Bewerken", amAdmins:"Schoolbeheerders", amAdminOne:"Schoolbeheerder", + amClassAdd:"— voeg toe aan klas… —", amClassRemove:"Uit deze klas halen", amSure:"weet je het zeker?", amDelete:"verwijder", - amFieldName:"Naam", amFieldRole:"Functie", amFieldExtraRoles:"Extra functies", amFieldClass:"Klas", + amFieldName:"Naam", amFieldRole:"Functie", amFieldClass:"Klas", amFieldClasses:"Klassen", amFieldPw:"Wachtwoord", amFieldActions:"Acties", amPwTypePh:"typ een wachtwoord…", amNewPwWarn:"Maakt een nieuw wachtwoord aan. Het oude wachtwoord werkt daarna niet meer en de gebruiker wordt overal uitgelogd.", amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.", @@ -381,8 +382,9 @@ const I18N = { amPickSchoolHint:"First pick a school above (🏫).", amPickSchool:"Pick a school", amPupils:"Pupils", amPupilOne:"Pupil", amEdit:"Edit", amAdmins:"School admins", amAdminOne:"School admin", + amClassAdd:"— add to class… —", amClassRemove:"Remove from this class", amSure:"are you sure?", amDelete:"delete", - amFieldName:"Name", amFieldRole:"Role", amFieldExtraRoles:"Extra roles", amFieldClass:"Class", + amFieldName:"Name", amFieldRole:"Role", amFieldClass:"Class", amFieldClasses:"Classes", amFieldPw:"Password", amFieldActions:"Actions", amPwTypePh:"type a password…", amNewPwWarn:"Generates a new password. The old one stops working and the user is signed out everywhere.", amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.", diff --git a/src/api.js b/src/api.js index 7fc491e..3f47840 100644 --- a/src/api.js +++ b/src/api.js @@ -408,10 +408,20 @@ export default async function api(app) { const key = String(r.user_id); extraByUser.set(key, [...(extraByUser.get(key) || []), r.role]); }); + // klas-koppelingen van groepsleiding (class_teachers), ook in één keer + const ctRows = rows.length + ? (await pool.query('SELECT user_id, class_id FROM class_teachers WHERE user_id = ANY($1)', [rows.map((u) => u.id)])).rows + : []; + const classesByUser = new Map(); + ctRows.forEach((r) => { + const key = String(r.user_id); + classesByUser.set(key, [...(classesByUser.get(key) || []), Number(r.class_id)]); + }); return { users: rows.map((u) => ({ ...publicUser({ ...u, extra_roles: extraByUser.get(String(u.id)) || [] }), linkCode: u.link_code || null, everLoggedIn: !!u.last_login_at, + classIds: classesByUser.get(String(u.id)) || [], })) }; });