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 130 additions and 47 deletions
Showing only changes of commit 75244cde14 - Show all commits

View file

@ -1 +1 @@
0.3.32-beta
0.3.33-beta

View file

@ -1129,6 +1129,13 @@
min-height:20px; font-size:13.5px; font-weight:700; color:var(--green); margin:4px 0;
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
}
/* vaste schoolkiezer voor de systeemmanager, boven alle beheer-tabs */
.am-schoolbar{
display:flex; align-items:center; gap:8px; margin:0 0 10px;
padding:8px 10px; background:var(--surface-2); border-radius:12px;
}
.am-schoolbar-lbl{font-size:16px;}
.am-schoolbar .am-sel{flex:1; min-width:0;}
.am-add{display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0;}
.am-inp,.am-sel,.am-pw{
border:1px solid var(--line); border-radius:var(--radius); height:var(--control-h);
@ -1143,6 +1150,20 @@
.am-pw{width:120px;}
.am-list{display:flex; flex-direction:column; gap:5px;}
.am-group{font-weight:700; font-size:13px; margin-top:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px;}
/* 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);}
/* uitklapbaar gereedschapsblok per gebruikersrij (achter het ✏-knopje) */
.am-row-tools{
display:flex; gap:8px; align-items:center; flex-wrap:wrap; width:100%;
margin-top:6px; padding-top:8px; border-top:1px dashed var(--line);
}
.am-edit{
border:none; background:transparent; border-radius:8px; cursor:pointer;
font-size:14px; padding:4px 8px; opacity:.6;
}
.am-edit:hover{opacity:1; background:var(--surface-3);}
.am-edit.on{opacity:1; background:var(--accent-soft);}
.am-row{
display:flex; gap:8px; align-items:center; background:var(--surface-2);
border-radius:var(--radius); padding:7px 10px; flex-wrap:wrap;

View file

@ -17,6 +17,10 @@
let adminTab = "gebruikers";
/* live zoekfilter voor de gebruikerslijst, blijft ook behouden over her-renders */
let userFilter = "";
/* uitgeklapte leerling-groepen en gebruikersrijen (standaard alles dicht
voor een rustig overzicht), blijven behouden over her-renders */
const openGroups = new Set();
const openRows = new Set();
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
const creatableRoles = () =>
@ -127,11 +131,17 @@
function userRow(u){
const row = h("div","am-row");
/* actieknoppen (kopieer/genereer/verwijder) samen in één groep, zodat
ze op smalle schermen als blok rechts blijven staan i.p.v. los
tussen de andere rij-elementen te wrappen */
/* 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");
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
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{
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 */
const rs = h("select","am-sel");
@ -141,7 +151,7 @@
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{role: rs.value}}); reload(); }
catch(e){ msg(e.message); }
});
row.appendChild(rs);
tools.appendChild(rs);
/* extra functies naast de hoofdrol (bv. een teacher die ook beheerdersrechten
krijgt) - los van de hoofdrol hierboven, direct toggelbaar per rol */
@ -158,9 +168,7 @@
});
extraWrap.appendChild(chip);
});
row.appendChild(extraWrap);
}else{
row.appendChild(h("span","am-role", T(roleKey(u.role))));
tools.appendChild(extraWrap);
}
if(u.role==="pupil"){
/* klas (uitwisselen met andere klassen) */
@ -169,7 +177,7 @@
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{classId: cs.value ? +cs.value : null}}); }
catch(e){ msg(e.message); }
});
row.appendChild(cs);
tools.appendChild(cs);
/* wachtwoord inzien en aanpassen - alleen zichtbaar zolang de leerling
nog niet is ingelogd; daarna kan alleen een nieuw wachtwoord gezet worden */
const pw = h("input","am-pw");
@ -182,7 +190,7 @@
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{password: pw.value}}); reload(); }
catch(e){ msg(e.message); }
});
row.appendChild(pw);
tools.appendChild(pw);
const pwCopy = h("button","am-copy","📋");
pwCopy.type = "button";
pwCopy.title = T("amCopy");
@ -203,9 +211,7 @@
}
}else{
if(u.pending && u.linkCode){
row.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode));
}else if(u.pending){
row.appendChild(h("span","am-code", T("amPending")));
tools.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode));
}
if(can("users.staffCredentials") && u.role!=="pupil" && u.id!==currentUser.id){
const nc = h("button","am-btn", T("amNewCode"));
@ -239,7 +245,24 @@
});
actions.appendChild(del);
}
if(actions.childNodes.length) row.appendChild(actions);
if(actions.childNodes.length) tools.appendChild(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();
});
sync();
row.appendChild(edit);
row.appendChild(tools);
}
return row;
}
@ -346,16 +369,12 @@
return box;
}
/* systeem: schoolkiezer + nieuwe school + systeemmanagers beheren.
Dit tabblad is waar een super zaken regelt die niet aan één school vastzitten. */
/* systeem: nieuwe school + systeemmanagers beheren. Dit tabblad is waar een
super zaken regelt die niet aan één school vastzitten; de schoolkiezer
zelf staat als vaste contextbalk boven álle beheer-tabs (schoolContextBar),
zodat geen enkel tabblad meer afhankelijk is van een ander tabblad. */
function renderScholenPanel(panel){
const bar = h("div","am-add");
const sel = h("select","am-sel");
sel.appendChild(new Option(T("amAllSchools"), ""));
SCHOOLS.forEach(s=>sel.appendChild(new Option(s.name, s.id)));
sel.value = selSchool ?? "";
sel.addEventListener("change", ()=>{ selSchool = sel.value; reload(); });
bar.appendChild(sel);
const inp = h("input","am-inp");
inp.placeholder = T("amNamePh");
inp.addEventListener("keydown", ev=>ev.stopPropagation());
@ -614,27 +633,37 @@
});
}
/* zoekfilter voor de gebruikerslijst: verbergt niet-matchende rijen en lege groepen
via style.display i.p.v. een her-render, zodat het zoekveld focus houdt tijdens
het typen (een volledige render() zou het veld elke toets opnieuw aanmaken) */
/* zoekfilter + inklapbare groepen voor de gebruikerslijst: één plek die de
zichtbaarheid bepaalt (style.display i.p.v. her-render, zodat het zoekveld
focus houdt). Ingeklapte groepen verbergen hun rijen; zodra er gezocht
wordt tellen alle groepen mee zodat een treffer nooit verstopt zit. */
function applyUserFilter(panel){
const q = userFilter.trim().toLowerCase();
const list = panel.querySelector(".am-list");
if(!list) return;
let pendingGroup = null, groupVisible = false;
let curGroup = null, groupVisible = false;
const closeGroup = ()=>{
if(curGroup) curGroup.style.display = (groupVisible || curGroup.dataset.collapsible) ? "" : "none";
};
[...list.children].forEach(el=>{
if(el.classList.contains("am-group")){
if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none";
pendingGroup = el; groupVisible = false;
}else if(el.classList.contains("am-row")){
closeGroup();
curGroup = el; groupVisible = false;
return;
}
const collapsed = curGroup && curGroup.dataset.collapsible
&& !openGroups.has(curGroup.dataset.gkey) && !q;
if(el.classList.contains("am-row")){
const name = el.querySelector(".am-name");
const match = !q || (name && name.textContent.toLowerCase().includes(q));
el.style.display = match ? "" : "none";
el.style.display = (match && !collapsed) ? "" : "none";
if(match) groupVisible = true;
}else{
/* toevoegrij (am-add-wrap): volgt de ingeklapte staat van zijn groep */
el.style.display = collapsed ? "none" : "";
}
/* .am-add-wrap (toevoegrij) blijft altijd zichtbaar, ongeacht het filter */
});
if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none";
closeGroup();
}
/* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren.
@ -657,7 +686,7 @@
staffGroups.forEach(([role, label])=>{
const us = USERS.filter(u=>u.role===role);
if(!us.length) return;
list.appendChild(h("div","am-group", label));
list.appendChild(h("div","am-group", `${label} (${us.length})`));
us.forEach(u=>list.appendChild(userRow(u)));
});
@ -666,29 +695,42 @@
const creatable = creatableRoles();
const canAddPupil = schoolChosen && creatable.includes("pupil");
/* 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 g = h("div","am-group am-group-toggle");
g.dataset.collapsible = "1";
g.dataset.gkey = key;
const sync = ()=>{ g.textContent = `${openGroups.has(key) ? "▾" : "▸"} ${label} (${us.length})`; };
sync();
g.addEventListener("click", ()=>{
if(openGroups.has(key)) openGroups.delete(key); else openGroups.add(key);
sync();
applyUserFilter(panel);
});
list.appendChild(g);
us.forEach(u=>list.appendChild(userRow(u)));
if(formEl) list.appendChild(formEl);
};
/* per-klas indeling heeft alleen zin met één gekozen school (CLASSES is dan
betrouwbaar); zonder gekozen school (super, alle scholen) of voor groepsleiding
gewoon een platte lijst - anders vallen leerlingen met een classId die niet in
éé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){
list.appendChild(h("div","am-group", T("rolePupil")));
pupils.forEach(u=>list.appendChild(userRow(u)));
if(pupils.length || canAddPupil){
pupilGroup("pupils", T("amPupils"), pupils, canAddPupil ? addForm("pupil") : null);
}
if(canAddPupil) list.appendChild(addForm("pupil"));
}else{
CLASSES.forEach(c=>{
const us = pupils.filter(u=>u.classId===c.id);
if(!us.length && !canAddPupil) return;
list.appendChild(h("div","am-group", c.name));
us.forEach(u=>list.appendChild(userRow(u)));
if(canAddPupil) list.appendChild(addForm("pupil", c.id));
pupilGroup("class:"+c.id, c.name, us, canAddPupil ? addForm("pupil", c.id) : null);
});
const noClass = pupils.filter(u=>!u.classId);
if(noClass.length || canAddPupil){
list.appendChild(h("div","am-group", T("amNoClass")));
noClass.forEach(u=>list.appendChild(userRow(u)));
if(canAddPupil) list.appendChild(addForm("pupil"));
pupilGroup("class:none", T("amNoClass"), noClass, canAddPupil ? addForm("pupil") : null);
}
}
panel.appendChild(list);
@ -708,8 +750,26 @@
modal.querySelectorAll(".am-tabpanel").forEach(p=>{ p.style.display = p.dataset.panel===tab ? "block" : "none"; });
}
/* vaste contextbalk voor de systeemmanager: op elk beheer-tabblad direct
de school kiezen, zonder eerst naar het Systeem-tabblad te moeten */
function schoolContextBar(){
if(currentUser.role !== "super") return null;
const bar = h("div","am-schoolbar");
bar.appendChild(h("span","am-schoolbar-lbl","🏫"));
const sel = h("select","am-sel");
sel.appendChild(new Option(T("amAllSchools"), ""));
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(); });
bar.appendChild(sel);
return bar;
}
function render(){
modal.innerHTML = "";
const sb = schoolContextBar();
if(sb) modal.appendChild(sb);
modal.appendChild(h("div","am-msg"));
const rp = resetPanel();
if(rp) modal.appendChild(rp);

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.3.32-beta";
const VERSION = "0.3.33-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -181,7 +181,8 @@ const I18N = {
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
pupilWatching:"meekijken",
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
amPickSchoolHint:"Kies eerst een school bij het tabblad “Systeem”.",
amPickSchoolHint:"Kies hierboven eerst een school (🏫).", amPickSchool:"Kies een school",
amPupils:"Leerlingen", amEdit:"Bewerken",
amNewSchool:" School", amNewClass:" Klas", amNewTeacher:" Groepsleiding",
amNewAdmin:" Schoolbeheerder", amNewSuper:" Systeemmanager",
amNewPupil:" Leerling", amUserPh:"gebruikersnaam…", amNamePh:"naam…",
@ -365,7 +366,8 @@ const I18N = {
pupilEmpty:"Your teacher hasn't set up anything for you yet.",
pupilWatching:"watching",
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",
amPickSchoolHint:"First pick a school in the “System” tab.",
amPickSchoolHint:"First pick a school above (🏫).", amPickSchool:"Pick a school",
amPupils:"Pupils", amEdit:"Edit",
amNewSchool:" School", amNewClass:" Class", amNewTeacher:" Group leader",
amNewAdmin:" School admin", amNewSuper:" System manager",
amNewPupil:" Pupil", amUserPh:"username…", amNamePh:"name…",