feat: voeg rolvoorbeeld voor systeemmanager toe (v0.4.39-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
This commit is contained in:
parent
9bab7f39c0
commit
b81f5ac5dd
11 changed files with 252 additions and 24 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.38-beta
|
0.4.39-beta
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
108
public/js/role-preview.js
Normal 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&¤tUser.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();
|
||||||
|
})();
|
||||||
|
|
@ -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
64
test/role-preview.test.js
Normal 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 clientmenu’s 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"/);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue