feat: Scholen, Klassen en Toewijzingen als groepskaarten (v0.3.89-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s

- Scholen: één gezamenlijk kader per school - getinte kop met grijs schoolicoon, naam en de beheerder-teller als uitklaptoggle; beheerders standaard verborgen en als compacte subrijen (36px) met status-glyph (● actief / ○ nog niet geactiveerd, uitleg via title); hernoemen/verwijderen klapt uit binnen hetzelfde kader - geen verwarring meer tussen scholen en beheerders
- Klassen: kaart per klas met leerlingtal in de kop; de reset-/verwijderacties verborgen tot Bewerken
- Toewijzingen: kaart per klas met de huidige stand compact in de kop; bord-/widget-/moduskeuze klapt uit via Bewerken
- Het status-glyph en de kaartkop-wrap zijn onderdeel van het gedeelde groepskaart-component; bewust klein grijs schoolicoon terug (functioneel onderscheid, emoji-scan-uitzondering)
- Geverifieerd in headless Chromium (14 checks) + screenshots 390px/desktop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
This commit is contained in:
Ramon 2026-07-17 14:27:19 +02:00
parent c3492ab2ca
commit 08618ba0b3
4 changed files with 51 additions and 50 deletions

View file

@ -1 +1 @@
0.3.88-beta
0.3.89-beta

View file

@ -2017,7 +2017,7 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000
#settingsModal .am-card:has(.am-card-subs[style*="none"]):not(:has(.am-card-tools:not([style*="none"]))) .am-card-head{border-bottom:none;}
#settingsModal .am-ico{filter:grayscale(1); opacity:.55; font-size:13px; flex:none;}
#settingsModal .am-card-title{font-weight:650; font-size:13.5px; flex:1; min-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
#settingsModal .am-card-sum{margin-left:auto; flex:none; font-size:12px; color:var(--muted);}
#settingsModal .am-card-sum{margin-left:auto; flex:0 1 auto; min-width:0; font-size:12px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
#settingsModal .am-card-toggle{cursor:pointer; user-select:none; padding:4px 6px; border-radius:5px;}
#settingsModal .am-card-toggle:hover{background:var(--surface-3); color:var(--ink);}
#settingsModal .am-card-tools{padding:10px 12px; border-bottom:1px solid var(--line); display:flex; flex-wrap:wrap; gap:10px;}

View file

@ -478,9 +478,18 @@
/* inline-variant voor het Systeem-tabblad (schoolbeheerder-subrijen):
naam + rol met een "Bewerken"-tekstknop die het formulier uitklapt */
function userRow(u, opts){
const row = h("div","am-row" + (opts && opts.sub ? " am-row-sub" : ""));
const compact = !!(opts && opts.compact);
const row = h("div","am-row" + (opts && opts.sub ? " am-row-sub" : "") + (compact ? " am-card-sub" : ""));
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
if(compact){
/* compacte kaart-subrij: status als glyph met uitleg-title i.p.v. tekst */
row.appendChild(h("span","am-role", T(roleKey(u.role))));
const glyph = h("span","am-glyph" + (u.pending ? "" : " ok"), u.pending ? "○" : "●");
glyph.title = u.pending ? T("amPending") : T("amStatusActive");
row.appendChild(glyph);
}else{
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` · ${T("amPending")}` : "")));
}
const tools = h("div","am-row-tools");
const badge = resultBadge(u);
if(badge) tools.appendChild(badge);
@ -754,16 +763,10 @@
achter een bewuste extra stap (-knop klapt het formulier uit, zoals
bij gebruikers) en bevestigt daarna nog met de naam erin. */
function renderScholenPanel(panel){
const list = h("div","am-list");
SCHOOLS.forEach(s=>{
const admins = SYS_ADMINS.filter(a=>Number(a.schoolId)===Number(s.id));
const row = h("div","am-row am-row-school");
row.appendChild(h("span","am-name", s.name));
row.appendChild(h("span","am-role",
`${admins.length} ${(admins.length===1 ? T("amAdminOne") : T("amAdmins")).toLowerCase()}`));
/* zelfde -systeem als bij gebruikers: hernoemen en verwijderen
zitten achter het gereedschapsblok, de rij zelf blijft rustig */
const tools = h("div","am-row-tools");
/* bewerk-gereedschap van de school zelf (hernoemen/verwijderen) */
const tools = h("div","am-tools");
const nameInp = h("input","am-inp");
nameInp.value = s.name;
nameInp.maxLength = 80;
@ -784,27 +787,17 @@
}catch(e){ msg(e.message); }
}));
tools.appendChild(field("amFieldActions", acts));
const edit = h("button","am-edit", T("amEdit"));
edit.type = "button";
const key = "school:"+s.id;
const sync = ()=>{
const open = openRows.has(key);
tools.style.display = open ? "" : "none";
edit.classList.toggle("on", open);
};
edit.addEventListener("click", ()=>{
if(openRows.has(key)) openRows.delete(key); else openRows.add(key);
sync();
/* één kader om de school en haar beheerders; beheerders standaard
ingeklapt achter de teller, als compacte subrijen */
panel.appendChild(groupCard({
icon:"🏫",
title:s.name,
summary:`${admins.length} ${(admins.length===1 ? T("amAdminOne") : T("amAdmins")).toLowerCase()}`,
key:"schooladmins:"+s.id,
edit:{ key:"school:"+s.id, tools },
subs: admins.map(a=>userRow(a, { sub:true, compact:true })),
}));
});
sync();
row.appendChild(edit);
row.appendChild(tools);
list.appendChild(row);
/* de schoolbeheerders van deze school, duidelijk onderscheiden van de
schoolrij (ingesprongen) maar met hetzelfde bewerk-gereedschap */
admins.forEach(a=>list.appendChild(userRow(a, { sub:true })));
});
panel.appendChild(list);
const wrap = h("div","am-add-wrap");
const toggle = h("button","tbtn ghost", T("amNewSchool"));
@ -864,12 +857,9 @@
const canResetPw = can("schools.resetPasswords");
/* klassen als volwaardige rijen (zelfde maat als de gebruikersvakken),
met de gevaarlijke acties als aparte, bevestigde knoppen */
const list = h("div","am-list");
CLASSES.forEach(c=>{
const row = h("div","am-row");
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()}`));
let edit = null;
if(canResetPw){
const acts = h("div","am-actions");
acts.appendChild(dangerBtn(T("amResetClass"), "amConfirmReset", async ()=>{
@ -884,11 +874,16 @@
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
catch(e){ msg(e.message); }
}));
row.appendChild(acts);
const tools = h("div","am-tools");
tools.appendChild(field("amFieldActions", acts));
edit = { key:"klas:"+c.id, tools };
}
list.appendChild(row);
panel.appendChild(groupCard({
title:c.name,
summary:`${pupilCount} ${(pupilCount===1 ? T("amPupilOne") : T("amPupils")).toLowerCase()}`,
edit,
}));
});
panel.appendChild(list);
if(canResetPw){
const f = h("div","am-add");
const inp = h("input","am-inp");
@ -947,8 +942,6 @@
return s;
}
function assignmentRow(target, boardOptions, current){
const row = h("div","am-row");
row.appendChild(h("span","am-name", target.label));
const sel = h("select","am-sel");
sel.appendChild(new Option(T("amNoAssignment"), ""));
boardOptions.forEach(b=>sel.appendChild(new Option(b.label, b.id)));
@ -971,10 +964,6 @@
sel.addEventListener("change", syncWidgetSel);
widgetSel.addEventListener("change", syncModeSel);
const info = h("span","am-code", describeCurrent(current));
row.appendChild(sel);
row.appendChild(widgetSel);
row.appendChild(modeSel);
row.appendChild(info);
syncWidgetSel();
const apply = h("button","am-btn", T("amAssign"));
apply.type = "button";
@ -994,8 +983,20 @@
}
}catch(e){ msg(e.message); }
});
row.appendChild(apply);
return row;
/* kop toont compact de huidige stand; de keuzes klappen uit via Bewerken */
const tools = h("div","am-tools");
const inline = h("div","am-inline");
inline.appendChild(sel);
inline.appendChild(widgetSel);
inline.appendChild(modeSel);
inline.appendChild(apply);
tools.appendChild(inline);
tools.appendChild(info);
return groupCard({
title: target.label,
summary: describeCurrent(current) || T("amNoAssignment"),
edit: { key: "assign:"+target.type+":"+target.id, tools },
});
}
/* vult een <select> met leerlingen gegroepeerd per klas (optgroup), gedeeld

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.88-beta";
const VERSION = "0.3.89-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -278,7 +278,7 @@ const I18N = {
amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.",
amDeleteWarn:"Verwijdert dit account definitief, inclusief borden en woorden.",
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", amFieldSchool:"School",
amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", amFieldSchool:"School", amStatusActive:"actief",
amDetailEmpty:"Selecteer links een gebruiker om te bewerken, of maak een nieuwe aan.",
amNewUser:"Nieuwe gebruiker", amRequestOne:"Aanmelding", amReqNew:"nieuw",
amParents:"Ouders",
@ -572,7 +572,7 @@ const I18N = {
amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.",
amDeleteWarn:"Deletes this account permanently, including boards and words.",
amResultPw:"New password", amResultCode:"New link code",
amColStatus:"Status", amNeverLoggedIn:"not signed in yet", amFieldSchool:"School",
amColStatus:"Status", amNeverLoggedIn:"not signed in yet", amFieldSchool:"School", amStatusActive:"active",
amDetailEmpty:"Select a user on the left to edit, or create a new one.",
amNewUser:"New user", amRequestOne:"Enrolment", amReqNew:"new",
amParents:"Parents",