Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
5 changed files with 73 additions and 27 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.36-beta
|
||||
0.3.37-beta
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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", ()=>{
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
10
src/api.js
10
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)) || [],
|
||||
})) };
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue