Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1

Open
bes-r wants to merge 192 commits from bes-r/avatar-realism into main AGit
4 changed files with 146 additions and 116 deletions
Showing only changes of commit 5b4c3163a5 - Show all commits

View file

@ -1 +1 @@
0.4.10-beta
0.4.11-beta

View file

@ -1320,6 +1320,10 @@
/* Systeem-tab: schoolrij duidelijk zwaarder dan de beheerder-subrijen */
.am-row-school{background:var(--accent-soft);}
.am-row-school > .am-name{font-size:15px;}
/* klasrij (naam + klasbeheer) onderscheidt zich van de Groepsleiding/
Leerlingen-rijen eronder - zelfde "kop, geen lid"-taal als .am-row-school */
.am-classhead{background:var(--accent-soft); border-left:3px solid var(--accent);}
.am-classhead > .am-name{font-size:15px;}
.am-row-sub{
margin-left:22px; background:var(--surface);
border-left:3px solid var(--line); border-radius:var(--radius-s);

View file

@ -26,6 +26,10 @@
const openGroups = new Set();
const closedGroups = new Set(); /* door de gebruiker bewust dichtgeklapt */
const openRows = new Set();
/* apart van openRows: klas-id's en gebruiker-id's zijn losse reeksen in de
database en kunnen dus samenvallen (klas 3 én gebruiker 3) - een gedeelde
Set zou dan per ongeluk de verkeerde bewerktools open/dicht klappen */
const openClassTools = new Set();
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
const creatableRoles = () =>
@ -555,6 +559,60 @@
return row;
}
/* klasrij (naam + "Bewerken") binnen de gebruikerslijst: klasbeheer
(verplaatsen naar een andere school, wachtwoorden resetten, verwijderen)
staat zo letterlijk bij de klasleden eronder, i.p.v. in een los
klassenpaneel. Zelfde opzet als userRow() hierboven, met een eigen
openClassTools-Set (zie daar voor waarom). */
function classHeadRow(c, otherSchools, canManage){
const row = h("div","am-row am-classhead");
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(!canManage) return row;
const tools = h("div","am-row-tools");
if(currentUser.role==="super" && otherSchools.length){
const ss = h("select","am-sel");
ss.appendChild(new Option(T("amMoveToSchool"), ""));
otherSchools.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id)));
ss.addEventListener("change", async ()=>{
if(!ss.value) return;
try{ await api("/admin/classes/"+c.id, {method:"PATCH", body:{ schoolId: +ss.value }}); reload(); }
catch(e){ msg(e.message); }
});
tools.appendChild(field("amFieldMoveSchool", ss));
}
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); }
}));
tools.appendChild(field("amFieldActions", acts));
const edit = h("button","am-edit", T("amEdit"));
edit.type = "button";
const sync = ()=>{
const open = openClassTools.has(c.id);
tools.style.display = open ? "" : "none";
edit.classList.toggle("on", open);
};
edit.addEventListener("click", ()=>{
if(openClassTools.has(c.id)) openClassTools.delete(c.id); else openClassTools.add(c.id);
sync();
});
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(){
@ -686,6 +744,13 @@
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 = "";
@ -700,6 +765,7 @@
openGroups.add(r.user.classId ? "class:"+r.user.classId : "class:none");
openGroups.add("pupils");
}
if(role==="teacher" && presetClassId) openGroups.add("classteachers:"+presetClassId);
reload();
}catch(e){ errEl.textContent = e.message; }
});
@ -912,96 +978,9 @@
panel.appendChild(wrap);
}
/* klassen (admin/super binnen de gekozen school). Een klas hoort altijd bij
een school; de systeemmanager kan een klas naar een andere school
verplaatsen. Zonder gekozen school ("alle scholen") is er geen concrete
school om klassen bij te tonen of aan te maken. */
function renderKlassenPanel(panel){
if(currentUser.role==="super" && !selSchool){
panel.appendChild(h("div","guestnote", T("amPickSchoolFirst")));
return;
}
const canResetPw = can("schools.resetPasswords");
const otherSchools = SCHOOLS.filter(sc=>String(sc.id)!==String(selSchool));
CLASSES.forEach(c=>{
const pupilCount = USERS.filter(u=>u.role==="pupil" && Number(u.classId)===Number(c.id)).length;
let edit = null;
if(canResetPw){
const tools = h("div","am-tools");
/* systeemmanager: klas naar een andere school verplaatsen */
if(currentUser.role==="super" && otherSchools.length){
const ss = h("select","am-sel");
ss.appendChild(new Option(T("amMoveToSchool"), ""));
otherSchools.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id)));
ss.addEventListener("change", async ()=>{
if(!ss.value) return;
try{ await api("/admin/classes/"+c.id, {method:"PATCH", body:{ schoolId: +ss.value }}); reload(); }
catch(e){ msg(e.message); }
});
tools.appendChild(field("amFieldMoveSchool", ss));
}
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); }
}));
tools.appendChild(field("amFieldActions", acts));
edit = { key:"klas:"+c.id, tools };
}
panel.appendChild(groupCard({
title:c.name,
summary:`${pupilCount} ${(pupilCount===1 ? T("amPupilOne") : T("amPupils")).toLowerCase()}`,
edit,
}));
});
/* lege school: maak duidelijk dat er (nog) geen klassen zijn en dat
bestaande klassen bij een andere school kunnen horen */
if(!CLASSES.length){
panel.appendChild(h("div","guestnote", T("amNoClassesHere")));
}
if(canResetPw){
const f = h("div","am-add");
f.appendChild(h("div","am-add-title", T("amNewClass")));
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); }
}));
}
}
/* Klassenbeheer zit niet meer in een los paneel: klasrij (classHeadRow) +
klasleden staan nu samen in renderGebruikersPanel's schoolboom (zie
daar), zodat klasbeheer en klasleden letterlijk bij elkaar staan. */
/* 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
@ -1212,6 +1191,13 @@
}
const collapsed = curGroup && curGroup.dataset.collapsible
&& !openGroups.has(curGroup.dataset.gkey) && !q;
if(el.classList.contains("am-classhead")){
/* een klasrij is een kop voor de groepen erna (Groepsleiding/
Leerlingen), geen filterbare gebruikersrij en geen toevoegrij -
mag dus nooit verdwijnen door de invouwstatus van iets ervóór */
el.style.display = "";
return;
}
if(el.classList.contains("am-row")){
const name = el.querySelector(".am-name");
const match = !q || (name && name.textContent.toLowerCase().includes(q));
@ -1373,29 +1359,76 @@
const loose = USERS.filter(u=>["teacher","admin","pupil"].includes(u.role) && u.schoolId==null);
if(loose.length) group("gnoschool", T("amNoSchoolGroup"), loose);
}else{
/* één school gekozen: groepeer op rol, leerlingen per klas - elke groep
krijgt direct een "snel toevoegen"-formulier eronder (net als bij
systeemmanagers), zodat een nieuwe schoolbeheerder/leerkracht/
leerling niet via een ander tabblad hoeft. Een groep zonder leden
blijft zichtbaar zolang er wél iets aan toegevoegd kan worden -
anders is een lege klas nergens te vinden om iemand aan toe te voegen. */
[["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]].forEach(([role, label])=>{
const us = USERS.filter(u=>u.role===role);
if(us.length || creatableRoles().includes(role)) group("role:"+role, label, us, true);
if(creatableRoles().includes(role)) list.appendChild(addForm(role));
});
const pupils = USERS.filter(u=>u.role==="pupil");
if(currentUser.role==="teacher"){
/* groepsleiding zelf: platte lijst - server-side toch al beperkt tot
de eigen klas(sen), en geen klassenbeheer, dus geen boom nodig */
if(pupils.length || creatableRoles().includes("pupil")) group("pupils", T("amPupils"), pupils, true);
if(creatableRoles().includes("pupil")) list.appendChild(addForm("pupil"));
}else{
/* schoolbeheerder/systeemmanager: klassen, gebruikers en klasbeheer
in één boom, in plaats van een los klassenpaneel - klas, dan
daarbinnen Groepsleiding en Leerlingen als twee eigen sublijstjes,
elk met een eigen "snel toevoegen". Een groep zonder leden blijft
zichtbaar zolang er wél iets aan toegevoegd kan worden - anders is
een lege klas nergens te vinden om iemand aan toe te voegen. */
const canManageClasses = can("classes.manage");
const otherSchools = SCHOOLS.filter(sc=>String(sc.id)!==String(selSchool));
CLASSES.forEach(c=>{
list.appendChild(classHeadRow(c, otherSchools, canManageClasses));
const staff = USERS.filter(u=>u.role==="teacher" && (u.classIds||[]).some(id=>Number(id)===Number(c.id)));
if(staff.length || creatableRoles().includes("teacher")) group("classteachers:"+c.id, T("roleTeacher"), staff, true);
if(creatableRoles().includes("teacher")) list.appendChild(addForm("teacher", c.id));
const us = pupils.filter(u=>u.classId===c.id);
if(us.length || creatableRoles().includes("pupil")) group("class:"+c.id, c.name, us, true);
if(us.length || creatableRoles().includes("pupil")) group("class:"+c.id, T("amPupils"), us, true);
if(creatableRoles().includes("pupil")) list.appendChild(addForm("pupil", c.id));
});
const noClass = pupils.filter(u=>!u.classId);
if(noClass.length || creatableRoles().includes("pupil")) group("class:none", T("amNoClass"), noClass, true);
if(canManageClasses){
const f = h("div","am-add");
f.appendChild(h("div","am-add-title", T("amNewClass")));
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);
list.appendChild(f);
/* multireset: alle leerlingwachtwoorden van de hele school in één keer */
list.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); }
}));
}
const admins = USERS.filter(u=>u.role==="admin");
if(admins.length || creatableRoles().includes("admin")) group("role:admin", T("roleAdmin"), admins, true);
if(creatableRoles().includes("admin")) list.appendChild(addForm("admin"));
/* niet ingedeeld: leerkrachten zonder klaskoppeling + leerlingen zonder
klas - een schoolbeheerder staat hier bewust nooit in, ook niet als
die toevallig aan geen enkele klas gekoppeld is: dat is geen "niet
ingedeeld"-toestand voor een schoolbeheerder, die hoort al bij
"Schoolbeheerder" hierboven */
const unassigned = [
...USERS.filter(u=>u.role==="teacher" && !(u.classIds||[]).length),
...pupils.filter(u=>!u.classId),
];
if(unassigned.length || creatableRoles().includes("pupil")) group("class:none", T("amNoClass"), unassigned, true);
if(creatableRoles().includes("pupil")) list.appendChild(addForm("pupil"));
}
const parents = USERS.filter(u=>u.role==="parent");
@ -1422,22 +1455,15 @@
function renderSchoolPanel(panel){
/* de schoolkiezer erboven moet ook echt iets doen: "alle scholen" toont
het scholenoverzicht (aanmaken/hernoemen/verwijderen, hoort niet bij
één school), een gekozen school toont juist die school se klassen -
nooit allebei tegelijk, anders lijkt Scholen de kiezer te negeren.
Gebruikers hangt hier bewust niet van af: dat paneel schakelt zelf al
tussen een schooloverzicht en een school-specifieke weergave. */
één school); een gekozen school toont Gebruikers, dat zelf de volledige
klassen+gebruikers-boom voor die school opbouwt (zie renderGebruikersPanel) -
klassen hebben dus geen eigen kop/paneel meer hier. */
if(currentUser.role==="super" && !selSchool){
panel.appendChild(h("h3","am-h3", T("amSchools")));
const sub = h("div","am-section");
renderScholenPanel(sub);
panel.appendChild(sub);
}
if(can("classes.manage") && (currentUser.role!=="super" || selSchool)){
panel.appendChild(h("h3","am-h3", T("amClasses")));
const sub = h("div","am-section");
renderKlassenPanel(sub);
panel.appendChild(sub);
}
panel.appendChild(h("h3","am-h3", T("amUsers")));
const sub = h("div","am-section");
renderGebruikersPanel(sub);

View file

@ -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.4.10-beta";
const VERSION = "0.4.11-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;