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
11 changed files with 252 additions and 24 deletions
Showing only changes of commit b81f5ac5dd - Show all commits

View file

@ -1 +1 @@
0.4.38-beta 0.4.39-beta

View file

@ -640,7 +640,8 @@
.wiki-goal-focus{outline:3px solid var(--accent);outline-offset:3px;animation:wikiFocus 1.4s ease-out} .wiki-goal-focus{outline:3px solid var(--accent);outline-offset:3px;animation:wikiFocus 1.4s ease-out}
@keyframes wikiFocus{from{background:var(--accent-soft)}to{background:var(--surface)}} @keyframes wikiFocus{from{background:var(--accent-soft)}to{background:var(--surface)}}
.winfo[hidden]{display:none}\n .widget-curriculum-modal{width:min(620px,94vw);max-height:min(760px,88vh);overflow:auto} .winfo[hidden]{display:none}
.widget-curriculum-modal{width:min(620px,94vw);max-height:min(760px,88vh);overflow:auto}
.widget-curriculum-head{display:flex;align-items:center;gap:12px;margin-bottom:8px} .widget-curriculum-head{display:flex;align-items:center;gap:12px;margin-bottom:8px}
.widget-curriculum-head h2{flex:1;margin:0;font-size:20px} .widget-curriculum-head h2{flex:1;margin:0;font-size:20px}
.widget-curriculum-goals{display:flex;flex-direction:column;gap:8px;margin:14px 0} .widget-curriculum-goals{display:flex;flex-direction:column;gap:8px;margin:14px 0}
@ -1298,6 +1299,20 @@
.lisub{font-size:13px; font-weight:700; color:var(--muted); margin-bottom:8px;} .lisub{font-size:13px; font-weight:700; color:var(--muted); margin-bottom:8px;}
#ownPwBlock .lrow{margin-bottom:10px;} #ownPwBlock .lrow{margin-bottom:10px;}
#ownPwBlock input{flex:1; min-width:0;} #ownPwBlock input{flex:1; min-width:0;}
/* systeemmanager: veilige voorbeeldweergave van vaste rollen */
.vw-role-block[hidden],#rolePreviewBanner[hidden]{display:none}
.vw-role-block{margin-bottom:8px;padding:10px 12px;border:1px solid var(--accent-soft);border-radius:12px;background:var(--surface-2)}
.vw-role-block .vw-row{align-items:flex-start;padding-top:0}.vw-role-seg{flex-wrap:wrap;justify-content:flex-end;max-width:min(620px,72%)}
.vw-role-note{margin-top:7px}
#rolePreviewBanner{position:fixed;top:calc(var(--tbar) + 8px);left:50%;z-index:5700;display:flex;align-items:center;gap:8px;transform:translateX(-50%);padding:7px 9px 7px 12px;border:1px solid var(--orange);border-radius:999px;background:var(--surface);box-shadow:var(--shadow-2);font-size:12px}
#rolePreviewBanner span{font-weight:800;color:var(--orange)}#rolePreviewBanner button{border:0;border-radius:999px;padding:5px 9px;background:color-mix(in srgb,var(--orange) 16%,var(--surface));color:var(--ink);font:inherit;font-weight:800;cursor:pointer}
.role-preview-note{max-width:760px;margin:0 auto 14px;padding:11px 14px;border:1px solid var(--orange);border-radius:12px;background:color-mix(in srgb,var(--orange) 16%,var(--surface));color:var(--ink);font-size:13px;line-height:1.45}
body.role-preview-active #parentView button:disabled,body.role-preview-active #parentView input:disabled,body.role-preview-active #parentView select:disabled{opacity:.55;cursor:not-allowed}
body.role-preview-pupil #pupilView .widget-body{pointer-events:none;user-select:none}
body.tb-left #rolePreviewBanner,body.tb-right #rolePreviewBanner{top:8px}
@media(max-width:720px){.vw-role-block .vw-row{align-items:stretch;flex-direction:column}.vw-role-seg{max-width:none;justify-content:flex-start}#rolePreviewBanner{max-width:calc(100vw - 24px);white-space:nowrap}#rolePreviewBanner strong{display:none}}
/* ---------- weergave-instellingen (tabblad "Weergave") ---------- */ /* ---------- weergave-instellingen (tabblad "Weergave") ---------- */
.vw-row{ .vw-row{
display:flex; align-items:center; justify-content:space-between; gap:12px; display:flex; align-items:center; justify-content:space-between; gap:12px;

View file

@ -52,6 +52,7 @@
<script src="js/admin.js?v=__V__" defer></script> <script src="js/admin.js?v=__V__" defer></script>
<script src="js/cast.js?v=__V__" defer></script> <script src="js/cast.js?v=__V__" defer></script>
<script src="js/image-catalog.js?v=__V__" defer></script> <script src="js/image-catalog.js?v=__V__" defer></script>
<script src="js/role-preview.js?v=__V__" defer></script>
<script src="js/app.js?v=__V__" defer></script> <script src="js/app.js?v=__V__" defer></script>
</head> </head>
<body> <body>
@ -74,6 +75,7 @@
<button class="tbtn ghost" id="btnFs" title="Fullscreen"></button> <button class="tbtn ghost" id="btnFs" title="Fullscreen"></button>
</div> </div>
<div id="rolePreviewBanner" hidden><strong data-i18n="vwPreview"></strong><span id="rolePreviewLabel"></span><button type="button" data-i18n="vwPreviewReset"></button></div>
<div id="board"></div> <div id="board"></div>
<div id="pupilView"></div> <div id="pupilView"></div>
<div id="parentView"></div> <div id="parentView"></div>
@ -248,6 +250,19 @@
</div> </div>
<div class="st-section" data-section="view" style="display:none;"> <div class="st-section" data-section="view" style="display:none;">
<div class="vw-role-block" id="superViewRole" hidden>
<div class="vw-row">
<span class="vw-lbl" data-i18n="vwRole"></span>
<div class="seg vw-role-seg" id="segViewRole">
<button type="button" data-role="super" data-i18n="vwRoleSystem"></button>
<button type="button" data-role="admin" data-i18n="vwRoleAdmin"></button>
<button type="button" data-role="teacher" data-i18n="vwRoleTeacher"></button>
<button type="button" data-role="parent" data-i18n="vwRoleParent"></button>
<button type="button" data-role="pupil" data-i18n="vwRolePupil"></button>
</div>
</div>
<div class="guestnote vw-role-note" data-i18n="vwRoleNote"></div>
</div>
<div class="vw-row"> <div class="vw-row">
<span class="vw-lbl" data-i18n="vwTheme"></span> <span class="vw-lbl" data-i18n="vwTheme"></span>
<div class="seg" id="segTheme"> <div class="seg" id="segTheme">

View file

@ -50,7 +50,7 @@
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */ /* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
const creatableRoles = () => const creatableRoles = () =>
[...new Set([currentUser.role, ...(currentUser.extraRoles||[])].flatMap(r => CREATABLE_ROLES[r] || []))]; [...new Set(activeRoles().flatMap(r => CREATABLE_ROLES[r] || []))];
/* wie er als groepsleiding aan een klas gekoppeld kan staan - zelfde rollenset /* wie er als groepsleiding aan een klas gekoppeld kan staan - zelfde rollenset
als de server accepteert in POST /admin/classes/:id/teachers (niet alleen als de server accepteert in POST /admin/classes/:id/teachers (niet alleen
@ -238,7 +238,7 @@
keywords:()=>[T("amLevelYearGroup"), T("amLevelLevel"), "niveau","level","groep","niveaugroep", keywords:()=>[T("amLevelYearGroup"), T("amLevelLevel"), "niveau","level","groep","niveaugroep",
"leerjaar","year group","differentiatie"] }, "leerjaar","year group","differentiatie"] },
{ tab:"opslag", icon:"🖼", group:"system", order:32, labelKey:"stOpslag", subKey:"stOpslagSub", { tab:"opslag", icon:"🖼", group:"system", order:32, labelKey:"stOpslag", subKey:"stOpslagSub",
show:()=>currentUser.role==="super", show:()=>activeRole()==="super",
keywords:()=>["opslag","storage","afbeeldingen","images","quota"] }, keywords:()=>["opslag","storage","afbeeldingen","images","quota"] },
]; ];
const adminTabDefs = ()=>ADMIN_SECTIONS.filter(d=>d.show()).map(d=>[d.tab, T(d.labelKey)]); const adminTabDefs = ()=>ADMIN_SECTIONS.filter(d=>d.show()).map(d=>[d.tab, T(d.labelKey)]);
@ -360,7 +360,7 @@
/* school koppelen/losmaken (systeemmanager): losse accounts terugzetten /* school koppelen/losmaken (systeemmanager): losse accounts terugzetten
naar hun school, of bewust losmaken - super/ouder zijn per definitie naar hun school, of bewust losmaken - super/ouder zijn per definitie
schoolloos en vallen hierbuiten */ schoolloos en vallen hierbuiten */
if(currentUser.role==="super" && ["teacher","admin","pupil"].includes(u.role)){ if(activeRole()==="super" && ["teacher","admin","pupil"].includes(u.role)){
const ss = h("select","am-sel"); const ss = h("select","am-sel");
ss.appendChild(new Option(T("amNoSchool"), "")); ss.appendChild(new Option(T("amNoSchool"), ""));
SCHOOLS.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id))); SCHOOLS.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id)));
@ -1202,7 +1202,7 @@
(alleen systeemmanager), wachtwoorden resetten, verwijderen. */ (alleen systeemmanager), wachtwoorden resetten, verwijderen. */
function classActionsPanel(c, otherSchools){ function classActionsPanel(c, otherSchools){
const box = h("div","am-kebab-panel"); const box = h("div","am-kebab-panel");
if(currentUser.role==="super" && otherSchools.length){ if(activeRole()==="super" && otherSchools.length){
const ss = h("select","am-sel"); const ss = h("select","am-sel");
ss.appendChild(new Option(T("amMoveToSchool"), "")); ss.appendChild(new Option(T("amMoveToSchool"), ""));
otherSchools.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id))); otherSchools.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id)));
@ -1425,7 +1425,7 @@
const school = SCHOOLS.find(s=>String(s.id)===String(selSchool)); const school = SCHOOLS.find(s=>String(s.id)===String(selSchool));
const canManageClasses = can("classes.manage"); const canManageClasses = can("classes.manage");
if(currentUser.role==="super" && school){ if(activeRole()==="super" && school){
const titleRow = h("div","am-kebab-row"); const titleRow = h("div","am-kebab-row");
titleRow.appendChild(h("h2","am-nav-title", school.name)); titleRow.appendChild(h("h2","am-nav-title", school.name));
const kebab = h("button","am-kebab","⋯"); const kebab = h("button","am-kebab","⋯");
@ -1602,7 +1602,7 @@
validateSheet(); validateSheet();
panel.classList.add("am-nav-panel"); panel.classList.add("am-nav-panel");
if(currentUser.role==="teacher"){ if(activeRole()==="teacher"){
const header = h("div","am-nav-header"); const header = h("div","am-nav-header");
const searchInp = h("input","am-inp am-filter"); const searchInp = h("input","am-inp am-filter");
searchInp.type = "search"; searchInp.type = "search";

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* 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. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.38-beta"; const VERSION = "0.4.39-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -233,7 +233,7 @@ const I18N = {
stOpenSection:"open dit onderdeel", stOpenSection:"open dit onderdeel",
stGroupPersonal:"Persoonlijk", stGroupSchool:"School", stGroupSystem:"Systeem", stGroupPersonal:"Persoonlijk", stGroupSchool:"School", stGroupSystem:"Systeem",
stAccountSub:"Je gegevens, wachtwoord en reservekopieën.", stAccountSub:"Je gegevens, wachtwoord en reservekopieën.",
stViewSub:"Weergave op dit apparaat: thema, taal en menubalk.", stViewSub:"Weergave op dit apparaat: rolvoorbeeld, thema, taal en menubalk.",
wikiTitle:"Curriculumwiki", wikiTitle:"Curriculumwiki",
wikiSub:"Nieuwe SLO-kerndoelen voor het primair onderwijs, per leergebied.", wikiSub:"Nieuwe SLO-kerndoelen voor het primair onderwijs, per leergebied.",
wikiKicker:"Primair onderwijs · juli 2026", wikiKicker:"Primair onderwijs · juli 2026",
@ -284,6 +284,11 @@ const I18N = {
vwContrast:"Hoog contrast", vwOn:"Aan", vwOff:"Uit", vwLang:"Taal", vwContrast:"Hoog contrast", vwOn:"Aan", vwOff:"Uit", vwLang:"Taal",
vwTbar:"Menubalk", vwTop:"Boven", vwLeft:"Links", vwRight:"Rechts", vwTbar:"Menubalk", vwTop:"Boven", vwLeft:"Links", vwRight:"Rechts",
vwMotion:"Animaties", vwGlass:"Glas-effect", vwMotion:"Animaties", vwGlass:"Glas-effect",
vwRole:"Weergave als", vwRoleSystem:"Systeem", vwRoleAdmin:"Schoolbeheerder", vwRoleTeacher:"Leerkracht", vwRoleParent:"Ouder", vwRolePupil:"Leerling",
vwRoleNote:"Alleen de interface verandert. Je blijft ingelogd als systeemmanager; serverrechten en gegevens wijzigen niet.",
vwPreview:"Voorbeeldweergave", vwPreviewReset:"Terug naar systeem",
vwParentPreview:"Voorbeeld van het ouderportaal. Echte kindgegevens en acties zijn in deze weergave uitgeschakeld.",
vwPupilPreview:"Voorbeeld van de leerlingomgeving met de taal- en rekenwidgets van het huidige bord. Bediening en voortgang zijn uitgeschakeld.",
vwNote:"Deze voorkeuren gelden voor dit apparaat.", vwNote:"Deze voorkeuren gelden voor dit apparaat.",
pwChanged:"Wachtwoord gewijzigd ✓", pwCurrentPh:"huidig wachtwoord", pwChanged:"Wachtwoord gewijzigd ✓", pwCurrentPh:"huidig wachtwoord",
namePh:"naam", passPh:"wachtwoord", namePh:"naam", passPh:"wachtwoord",
@ -606,7 +611,7 @@ const I18N = {
stOpenSection:"open this section", stOpenSection:"open this section",
stGroupPersonal:"Personal", stGroupSchool:"School", stGroupSystem:"System", stGroupPersonal:"Personal", stGroupSchool:"School", stGroupSystem:"System",
stAccountSub:"Your details, password and backups.", stAccountSub:"Your details, password and backups.",
stViewSub:"Display on this device: theme, language and toolbar.", stViewSub:"Display on this device: role preview, theme, language and toolbar.",
wikiTitle:"Curriculum wiki", wikiTitle:"Curriculum wiki",
wikiSub:"New SLO attainment targets for Dutch primary education, grouped by learning area.", wikiSub:"New SLO attainment targets for Dutch primary education, grouped by learning area.",
wikiKicker:"Primary education · July 2026", wikiKicker:"Primary education · July 2026",
@ -657,6 +662,11 @@ const I18N = {
vwContrast:"High contrast", vwOn:"On", vwOff:"Off", vwLang:"Language", vwContrast:"High contrast", vwOn:"On", vwOff:"Off", vwLang:"Language",
vwTbar:"Menu bar", vwTop:"Top", vwLeft:"Left", vwRight:"Right", vwTbar:"Menu bar", vwTop:"Top", vwLeft:"Left", vwRight:"Right",
vwMotion:"Animations", vwGlass:"Glass effect", vwMotion:"Animations", vwGlass:"Glass effect",
vwRole:"View as", vwRoleSystem:"System", vwRoleAdmin:"School administrator", vwRoleTeacher:"Teacher", vwRoleParent:"Parent", vwRolePupil:"Pupil",
vwRoleNote:"Only the interface changes. You remain signed in as system manager; server permissions and data do not change.",
vwPreview:"Preview mode", vwPreviewReset:"Return to system",
vwParentPreview:"Preview of the parent portal. Real child data and actions are disabled in this view.",
vwPupilPreview:"Preview of the pupil environment using language and maths widgets from the current board. Controls and progress are disabled.",
vwNote:"These preferences apply to this device.", vwNote:"These preferences apply to this device.",
pwChanged:"Password changed ✓", pwCurrentPh:"current password", pwChanged:"Password changed ✓", pwCurrentPh:"current password",
namePh:"name", passPh:"password", namePh:"name", passPh:"password",
@ -863,8 +873,10 @@ function canPersist(){
kind-accounts); schoolleerlingen ook niet (zij zien de toewijzing van de kind-accounts); schoolleerlingen ook niet (zij zien de toewijzing van de
leerkracht). Thuiskinderen (leerling zónder school, ouderportaal) hebben leerkracht). Thuiskinderen (leerling zónder school, ouderportaal) hebben
wél een eigen whiteboard met eigen opslag. */ wél een eigen whiteboard met eigen opslag. */
if(!currentUser || currentUser.role === "parent") return false; if(!currentUser) return false;
if(currentUser.role === "pupil") return !currentUser.schoolId; const role=typeof activeRole==="function" ? activeRole() : currentUser.role;
if(role==="parent") return false;
if(role==="pupil") return currentUser.role==="pupil" && !currentUser.schoolId;
return true; return true;
} }
/* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save /* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save

View file

@ -425,7 +425,7 @@ function withBranch(rootLabel, main, sub, opts){
==========================================================*/ ==========================================================*/
function withSharedBranches(kind, own, hooks){ function withSharedBranches(kind, own, hooks){
/* gasten en leerlingen zien alleen de eigen boom */ /* gasten en leerlingen zien alleen de eigen boom */
if(!currentUser || currentUser.role === "pupil") return own; if(!currentUser || activeRole() === "pupil") return own;
const GLOBAL = "🌐 " + T("shGlobal"); const GLOBAL = "🌐 " + T("shGlobal");
const SCHOOL = "🏫 " + T("shSchool"); const SCHOOL = "🏫 " + T("shSchool");
const branches = [GLOBAL]; const branches = [GLOBAL];
@ -629,7 +629,7 @@ function withSharedBranches(kind, own, hooks){
zonder selectie op de map waar je nu in staat (alle items eronder) */ zonder selectie op de map waar je nu in staat (alle items eronder) */
lockLabel(path){ lockLabel(path){
if(!isShared(path) || scopeOf(path[0]) !== "school") return null; if(!isShared(path) || scopeOf(path[0]) !== "school") return null;
const roles = [currentUser.role, ...(currentUser.extraRoles||[])]; const roles = activeRoles();
if(!roles.includes("admin") && !roles.includes("super")) return null; if(!roles.includes("admin") && !roles.includes("super")) return null;
return "🔒 " + T("exLock"); return "🔒 " + T("exLock");
}, },

View file

@ -26,10 +26,24 @@ const CREATABLE_ROLES = {
teacher: ['pupil'], teacher: ['pupil'],
}; };
/* can(action) gebruikt de ingelogde gebruiker (currentUser, uit core.js) - hoofdrol /* Een systeemmanager kan uitsluitend de clientweergave van een andere rol
plus eventuele extra stafrollen (currentUser.extraRoles) */ simuleren. Het HttpOnly-account en alle serverautorisatie blijven ongewijzigd. */
function can(action){ const VIEW_ROLES = ["super","admin","teacher","parent","pupil"];
if(!currentUser) return false; let VIEW_AS_ROLE = "super";
const roles = [currentUser.role, ...(currentUser.extraRoles||[])]; function setActiveViewRole(role){
return (PERMISSIONS[action] || []).some(r => roles.includes(r)); VIEW_AS_ROLE = VIEW_ROLES.includes(role) ? role : "super";
}
function activeRole(){
if(!currentUser) return null;
return currentUser.role==="super" ? VIEW_AS_ROLE : currentUser.role;
}
function activeRoles(){
if(!currentUser) return [];
const role=activeRole();
return role===currentUser.role ? [currentUser.role,...(currentUser.extraRoles||[])] : [role];
}
/* can(action) gebruikt het actieve weergaveprofiel. De server blijft iedere
aanvraag vanzelf tegen het echte account autoriseren. */
function can(action){
return (PERMISSIONS[action] || []).some(r => activeRoles().includes(r));
} }

View file

@ -18,7 +18,7 @@ function pupilAllowedDefs(){
return REGISTRY.filter(d => PUPIL_WIDGET_CATS.includes(d.cat)); return REGISTRY.filter(d => PUPIL_WIDGET_CATS.includes(d.cat));
} }
async function renderPupilWidgets(widgets, mode){ async function renderPupilWidgets(widgets, mode, preview=false){
pupilView.innerHTML = ""; pupilView.innerHTML = "";
const defs = pupilAllowedDefs(); const defs = pupilAllowedDefs();
const watch = mode === "kijken"; const watch = mode === "kijken";
@ -38,7 +38,7 @@ async function renderPupilWidgets(widgets, mode){
- één keer per vakgebied dat hier daadwerkelijk voorkomt, niet per widget. */ - één keer per vakgebied dat hier daadwerkelijk voorkomt, niet per widget. */
const subjects = [...new Set(usable.map(x => x.def.cat))]; const subjects = [...new Set(usable.map(x => x.def.cat))];
const levels = {}; const levels = {};
await Promise.all(subjects.map(async subject=>{ if(!preview)await Promise.all(subjects.map(async subject=>{
try{ levels[subject] = await api("/my/level?subject="+subject); } try{ levels[subject] = await api("/my/level?subject="+subject); }
catch(e){ levels[subject] = null; } catch(e){ levels[subject] = null; }
})); }));

108
public/js/role-preview.js Normal file
View file

@ -0,0 +1,108 @@
/* teach - veilige rolvoorbeeldweergave voor systeemmanagers.
Alleen de clientinterface en zichtbare menu's veranderen. De sessiecookie,
currentUser en serverautorisatie blijven altijd van de systeemmanager. */
"use strict";
(function(){
const roleRow=document.getElementById("superViewRole");
const roleSeg=document.getElementById("segViewRole");
const banner=document.getElementById("rolePreviewBanner");
const bannerLabel=document.getElementById("rolePreviewLabel");
const resetButton=banner.querySelector("button");
const ROLE_KEYS={
super:"vwRoleSystem",admin:"vwRoleAdmin",teacher:"vwRoleTeacher",
parent:"vwRoleParent",pupil:"vwRolePupil"
};
let renderSequence=0;
const isSystemManager=()=>!!currentUser&&currentUser.role==="super";
const roleLabel=role=>T(ROLE_KEYS[role]||ROLE_KEYS.super);
function storedRole(){
try{
const value=localStorage.getItem("teach.superViewRole");
return VIEW_ROLES.includes(value)?value:"super";
}catch(e){return"super";}
}
function storeRole(role){
try{localStorage.setItem("teach.superViewRole",role);}catch(e){}
}
function previewNotice(text){
const note=document.createElement("aside");
note.className="role-preview-note";
const strong=document.createElement("strong");
strong.textContent=T("vwPreview")+": ";
note.append(strong,document.createTextNode(text));
return note;
}
function clearPreviewSurface(){
renderSequence++;
document.body.classList.remove("role-preview-active","role-preview-parent","role-preview-pupil");
banner.hidden=true;
if(isSystemManager()){
stopPupilView();
stopParentView();
}
}
function syncControls(){
const enabled=isSystemManager();
roleRow.hidden=!enabled;
const role=enabled?activeRole():"super";
roleSeg.querySelectorAll("button").forEach(button=>{
const selected=button.dataset.role===role;
button.classList.toggle("active",selected);
button.setAttribute("aria-pressed",String(selected));
});
banner.hidden=!enabled||role==="super";
bannerLabel.textContent=enabled&&role!=="super"?roleLabel(role):"";
}
async function renderPreviewSurface(){
const sequence=++renderSequence;
if(!isSystemManager()){clearPreviewSurface();syncControls();return;}
const role=activeRole();
document.body.classList.remove("role-preview-active","role-preview-parent","role-preview-pupil");
stopPupilView();
stopParentView();
if(role==="parent"){
document.body.classList.add("role-preview-active","role-preview-parent","parent-mode");
renderParentView([]);
const inner=parentView.querySelector(".parent-inner");
if(inner)inner.prepend(previewNotice(T("vwParentPreview")));
parentView.querySelectorAll("button,input,select,textarea").forEach(control=>{control.disabled=true;});
}else if(role==="pupil"){
document.body.classList.add("role-preview-active","role-preview-pupil","pupil-mode");
const snapshot=serializeBoard();
await renderPupilWidgets(snapshot.widgets||[],"kijken",true);
if(sequence!==renderSequence)return;
pupilView.prepend(previewNotice(T("vwPupilPreview")));
}
syncControls();
}
function chooseRole(role){
if(!isSystemManager()||!VIEW_ROLES.includes(role))return;
setActiveViewRole(role);
storeRole(role);
updateUserUI();
document.dispatchEvent(new CustomEvent("userchange"));
}
roleSeg.querySelectorAll("button").forEach(button=>{
button.addEventListener("click",()=>chooseRole(button.dataset.role));
});
resetButton.addEventListener("click",()=>chooseRole("super"));
document.addEventListener("userchange",()=>{
if(isSystemManager()){
const saved=storedRole();
if(activeRole()!==saved)setActiveViewRole(saved);
}else{
setActiveViewRole("super");
}
syncControls();
renderPreviewSurface();
});
document.addEventListener("langchange",()=>{
syncControls();
if(isSystemManager()&&["parent","pupil"].includes(activeRole()))renderPreviewSurface();
});
syncControls();
})();

View file

@ -44,7 +44,7 @@ function registerSettingsSection(sec){ ST_SECTIONS.push(sec); }
label:()=>T("stView"), sub:()=>T("stViewSub"), label:()=>T("stView"), sub:()=>T("stViewSub"),
visible:()=>true, visible:()=>true,
activate(){ showDomSection("view"); }, activate(){ showDomSection("view"); },
keywords:()=>[T("vwTheme"), T("vwContrast"), T("vwLang"), T("vwTbar"), T("vwMotion"), T("vwGlass"), keywords:()=>[T("vwRole"), T("vwRoleSystem"), T("vwRoleAdmin"), T("vwRoleTeacher"), T("vwRoleParent"), T("vwRolePupil"), T("vwTheme"), T("vwContrast"), T("vwLang"), T("vwTbar"), T("vwMotion"), T("vwGlass"),
"thema","theme","donker","dark","licht","light","contrast","taal","language", "thema","theme","donker","dark","licht","light","contrast","taal","language",
"menubalk","toolbar","animaties","animations","glas","glass"], "menubalk","toolbar","animaties","animations","glas","glass"],
}); });

64
test/role-preview.test.js Normal file
View file

@ -0,0 +1,64 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import {runInNewContext} from 'node:vm';
test('alleen een systeemmanager kan een geldig actief weergaveprofiel projecteren',async()=>{
const source=await readFile('public/js/permissions.js','utf8');
const context={currentUser:{role:'super',extraRoles:[]}};
runInNewContext(source+';globalThis.api={setActiveViewRole,activeRole,activeRoles,can,VIEW_ROLES}',context);
const api=context.api;
assert.deepEqual(Array.from(api.VIEW_ROLES),['super','admin','teacher','parent','pupil']);
api.setActiveViewRole('parent');
assert.equal(api.activeRole(),'parent');
assert.equal(api.can('curriculum.view'),true);
assert.equal(api.can('admin.access'),false);
api.setActiveViewRole('pupil');
assert.equal(api.activeRole(),'pupil');
assert.equal(api.can('curriculum.view'),false);
api.setActiveViewRole('onbekend');
assert.equal(api.activeRole(),'super');
context.currentUser={role:'teacher',extraRoles:[]};
api.setActiveViewRole('parent');
assert.equal(api.activeRole(),'teacher');
});
test('Weergave bevat de vijf rollen en laadt de veilige voorbeeldmodule vóór de appstart',async()=>{
const [html,preview,core,css,version]=await Promise.all([
readFile('public/index.html','utf8'),
readFile('public/js/role-preview.js','utf8'),
readFile('public/js/core.js','utf8'),
readFile('public/css/teach.css','utf8'),
readFile('VERSION','utf8')
]);
for(const role of ['super','admin','teacher','parent','pupil'])assert.match(html,new RegExp('data-role="'+role+'"'));
assert.match(html,/id="superViewRole" hidden/);
assert.ok(html.indexOf('js/role-preview.js')<html.indexOf('js/app.js'));
assert.match(preview,/currentUser\.role==="super"/);
assert.match(preview,/renderParentView\(\[\]\)/);
assert.match(preview,/renderPupilWidgets\(snapshot\.widgets\|\|\[\],"kijken",true\)/);
assert.match(preview,/querySelectorAll\("button,input,select,textarea"\)/);
assert.doesNotMatch(preview,/currentUser\.role\s*=(?!=)/);
assert.doesNotMatch(preview,/api\(/);
assert.match(css,/body\.role-preview-pupil #pupilView \.widget-body\{pointer-events:none/);
for(const key of ['vwRole','vwRoleSystem','vwRoleAdmin','vwRoleTeacher','vwRoleParent','vwRolePupil','vwRoleNote','vwPreviewReset']){
assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
}
assert.ok(core.includes('const VERSION = "'+version.trim()+'"'));
});
test('voorbeeldrol begrenst clientmenus maar verandert serverpermissies niet',async()=>{
const [client,server,settings,admin,explorer]=await Promise.all([
readFile('public/js/permissions.js','utf8'),
readFile('src/permissions.js','utf8'),
readFile('public/js/settings.js','utf8'),
readFile('public/js/admin.js','utf8'),
readFile('public/js/explorer.js','utf8')
]);
assert.match(client,/function activeRoles\(\)/);
assert.doesNotMatch(server,/VIEW_AS_ROLE|activeRole/);
assert.match(settings,/T\("vwRole"\)/);
assert.match(admin,/activeRoles\(\)\.flatMap/);
assert.match(admin,/show:\(\)=>activeRole\(\)==="super"/);
assert.match(explorer,/activeRole\(\) === "pupil"/);
});