From 629329894da6a6c6dbc2fcf217b995e6f5b55a94 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 20 Jul 2026 16:06:33 +0200 Subject: [PATCH] feat: laat systeemmanager leerlingwidgets toevoegen (v0.4.40-beta) --- VERSION | 2 +- public/css/teach.css | 9 +++++ public/js/core.js | 6 ++- public/js/role-preview.js | 82 ++++++++++++++++++++++++++++++++++++++- test/role-preview.test.js | 13 +++++-- 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 8307d81..9eb3e8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.39-beta +0.4.40-beta diff --git a/public/css/teach.css b/public/css/teach.css index 5b99773..81c5219 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1310,6 +1310,15 @@ .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} + +.role-preview-note .role-preview-add-widget{display:flex;margin-top:9px} +.role-preview-widget-picker{width:min(760px,94vw);max-height:88vh;overflow:auto} +.role-preview-widget-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin-top:12px} +.role-preview-widget-card{width:100%;display:grid;grid-template-columns:50px minmax(0,1fr);align-items:center;gap:10px;padding:11px;border:1px solid var(--line);border-radius:14px;background:var(--surface);color:var(--ink);text-align:left;cursor:pointer} +.role-preview-widget-card:hover,.role-preview-widget-card:focus-visible{border-color:var(--accent);outline:2px solid var(--accent-soft)} +.role-preview-widget-card .gi{font-size:30px;text-align:center}.role-preview-widget-card>span:last-child{min-width:0}.role-preview-widget-card .gn,.role-preview-widget-card .gd{display:block}.role-preview-widget-card .gd{margin-top:3px;color:var(--muted);font-size:11px;line-height:1.35} +body.role-preview-updating #board{visibility:hidden} +@media(max-width:560px){.role-preview-widget-grid{grid-template-columns:1fr}} 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}} diff --git a/public/js/core.js b/public/js/core.js index fc77f79..cfebc7f 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -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.39-beta"; +const VERSION = "0.4.40-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -289,6 +289,8 @@ const I18N = { 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.", + vwPupilAddWidget:"Widget toevoegen", vwPupilChooseWidget:"Kies een leerlingwidget", + vwPupilAddWidgetNote:"Alleen taal- en rekenwidgets die in de leerlingomgeving werken worden getoond.", vwNote:"Deze voorkeuren gelden voor dit apparaat.", pwChanged:"Wachtwoord gewijzigd ✓", pwCurrentPh:"huidig wachtwoord", namePh:"naam", passPh:"wachtwoord", @@ -667,6 +669,8 @@ const I18N = { 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.", + vwPupilAddWidget:"Add widget", vwPupilChooseWidget:"Choose a pupil widget", + vwPupilAddWidgetNote:"Only language and maths widgets supported in the pupil environment are shown.", vwNote:"These preferences apply to this device.", pwChanged:"Password changed ✓", pwCurrentPh:"current password", namePh:"name", passPh:"password", diff --git a/public/js/role-preview.js b/public/js/role-preview.js index 4e0609a..07e710a 100644 --- a/public/js/role-preview.js +++ b/public/js/role-preview.js @@ -14,6 +14,78 @@ parent:"vwRoleParent",pupil:"vwRolePupil" }; let renderSequence=0; + let pupilWidgetPicker=null; + + function closePupilWidgetPicker(){ + if(pupilWidgetPicker)pupilWidgetPicker.classList.remove("open"); + } + function openPupilWidgetPicker(){ + if(!isSystemManager()||activeRole()!=="pupil")return; + if(!pupilWidgetPicker){ + pupilWidgetPicker=document.createElement("div"); + pupilWidgetPicker.className="modalwrap"; + pupilWidgetPicker.id="pupilPreviewWidgetPicker"; + pupilWidgetPicker.setAttribute("role","dialog"); + pupilWidgetPicker.setAttribute("aria-modal","true"); + pupilWidgetPicker.addEventListener("click",event=>{ + if(event.target===pupilWidgetPicker)closePupilWidgetPicker(); + }); + document.body.appendChild(pupilWidgetPicker); + } + const modal=document.createElement("div"); + modal.className="modal role-preview-widget-picker"; + const head=document.createElement("div"); + head.className="gallery-head"; + const title=document.createElement("h2"); + title.id="pupilPreviewWidgetTitle"; + title.textContent=T("vwPupilChooseWidget"); + const close=document.createElement("button"); + close.type="button";close.className="wclose";close.textContent="✕"; + close.title=T("close");close.setAttribute("aria-label",T("close")); + close.addEventListener("click",closePupilWidgetPicker); + head.append(title,close);modal.appendChild(head); + const note=document.createElement("p"); + note.className="guestnote";note.textContent=T("vwPupilAddWidgetNote"); + modal.appendChild(note); + const host=document.createElement("div"); + pupilAllowedDefs().forEach(def=>{ + const card=document.createElement("button"); + card.type="button";card.className="gcard role-preview-widget-card"; + const icon=document.createElement("span"); + icon.className="gi";icon.textContent=def.icon; + const text=document.createElement("span"); + const name=document.createElement("strong"); + name.className="gn";name.textContent=T("wg_"+def.id); + const desc=document.createElement("small"); + desc.className="gd";desc.textContent=T("wg_"+def.id+"_d"); + text.append(name,desc);card.append(icon,text); + card.addEventListener("click",()=>addPupilPreviewWidget(def)); + host.appendChild(card); + }); + host.className="role-preview-widget-grid"; + modal.appendChild(host); + pupilWidgetPicker.setAttribute("aria-labelledby",title.id); + pupilWidgetPicker.replaceChildren(modal); + pupilWidgetPicker.classList.add("open"); + close.focus(); + } + async function addPupilPreviewWidget(def){ + if(!isSystemManager()||activeRole()!=="pupil"||!pupilAllowedDefs().includes(def))return; + closePupilWidgetPicker(); + const previewRole=activeRole(); + document.body.classList.add("role-preview-updating"); + stopPupilView(); + setActiveViewRole("super"); + try{ + makeWidget(def); + await doSave(); + }finally{ + setActiveViewRole(previewRole); + document.body.classList.remove("role-preview-updating"); + renderPreviewSurface(); + } + } + const isSystemManager=()=>!!currentUser&¤tUser.role==="super"; const roleLabel=role=>T(ROLE_KEYS[role]||ROLE_KEYS.super); @@ -39,6 +111,7 @@ renderSequence++; document.body.classList.remove("role-preview-active","role-preview-parent","role-preview-pupil"); banner.hidden=true; + closePupilWidgetPicker(); if(isSystemManager()){ stopPupilView(); stopParentView(); @@ -60,6 +133,7 @@ const sequence=++renderSequence; if(!isSystemManager()){clearPreviewSurface();syncControls();return;} const role=activeRole(); + closePupilWidgetPicker(); document.body.classList.remove("role-preview-active","role-preview-parent","role-preview-pupil"); stopPupilView(); stopParentView(); @@ -74,7 +148,13 @@ const snapshot=serializeBoard(); await renderPupilWidgets(snapshot.widgets||[],"kijken",true); if(sequence!==renderSequence)return; - pupilView.prepend(previewNotice(T("vwPupilPreview"))); + const note=previewNotice(T("vwPupilPreview")); + const add=document.createElement("button"); + add.type="button";add.className="tbtn role-preview-add-widget"; + add.textContent="+ "+T("vwPupilAddWidget"); + add.addEventListener("click",openPupilWidgetPicker); + note.appendChild(add); + pupilView.prepend(note); } syncControls(); } diff --git a/test/role-preview.test.js b/test/role-preview.test.js index 6767712..aca8c0e 100644 --- a/test/role-preview.test.js +++ b/test/role-preview.test.js @@ -38,22 +38,27 @@ test('Weergave bevat de vijf rollen en laadt de veilige voorbeeldmodule vóór d assert.match(preview,/renderParentView\(\[\]\)/); assert.match(preview,/renderPupilWidgets\(snapshot\.widgets\|\|\[\],"kijken",true\)/); assert.match(preview,/querySelectorAll\("button,input,select,textarea"\)/); + assert.match(preview,/pupilAllowedDefs\(\)\.forEach/); + assert.match(preview,/pupilAllowedDefs\(\)\.includes\(def\)/); + assert.match(preview,/setActiveViewRole\("super"\)[\s\S]*?makeWidget\(def\)[\s\S]*?await doSave\(\)/); + assert.match(preview,/role-preview-add-widget/); 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']){ + for(const key of ['vwRole','vwRoleSystem','vwRoleAdmin','vwRoleTeacher','vwRoleParent','vwRolePupil','vwRoleNote','vwPreviewReset','vwPupilAddWidget','vwPupilChooseWidget','vwPupilAddWidgetNote']){ 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([ + const [client,server,settings,admin,explorer,pupil]=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') + readFile('public/js/explorer.js','utf8'), + readFile('public/js/pupil.js','utf8') ]); assert.match(client,/function activeRoles\(\)/); assert.doesNotMatch(server,/VIEW_AS_ROLE|activeRole/); @@ -61,4 +66,6 @@ test('voorbeeldrol begrenst clientmenu’s maar verandert serverpermissies niet' assert.match(admin,/activeRoles\(\)\.flatMap/); assert.match(admin,/show:\(\)=>activeRole\(\)==="super"/); assert.match(explorer,/activeRole\(\) === "pupil"/); + assert.match(pupil,/async function renderPupilWidgets\(widgets, mode, preview=false\)/); + assert.match(pupil,/if\(!preview\)await Promise\.all/); });