feat: Klassen en Gebruikers samengevoegd tot één boom per school (v0.4.11-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
- klas, Groepsleiding en Leerlingen staan nu letterlijk bij elkaar: elke klas krijgt een eigen rij (naam + "Bewerken" voor verplaatsen/resetten/ verwijderen, overgenomen uit het oude losse Klassen-paneel) met daaronder twee eigen sublijstjes - Groepsleiding (leerkrachten gekoppeld aan die klas) en Leerlingen - elk met een eigen "+"-knop - "+ Groepsleiding" bij een klas maakt de nieuwe leerkracht niet alleen aan, maar koppelt die ook meteen aan die klas (hergebruikt de bestaande, al geteste POST /admin/classes/:id/teachers-route - geen serverwijziging) - Schoolbeheerder blijft een eigen, vlakke groep los van de klassenboom (ook als een schoolbeheerder toevallig aan een klas gekoppeld is, om dubbele vermeldingen te voorkomen); Niet ingedeeld bundelt leerkrachten zonder klas en leerlingen zonder klas - renderKlassenPanel is verwijderd (zijn inhoud leeft nu in de klas-tak van renderGebruikersPanel); de losse "Klassen"-kop in het School-tabblad is weg - nieuwe openClassTools-Set voor het in-/uitklappen van klasbeheer, apart van openRows (klas- en gebruiker-id's kunnen samenvallen) - geverifieerd in een echte headless Chromium: klasrij + twee sublijsten per klas, Schoolbeheerder/Niet ingedeeld correct gevuld, en "+ Groepsleiding" koppelt de nieuwe leerkracht daadwerkelijk aan de klas (classIds bevestigd)
This commit is contained in:
parent
e088f83d1f
commit
5b4c3163a5
4 changed files with 146 additions and 116 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.10-beta
|
||||
0.4.11-beta
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue