From 17ee47dd095b44e3b0ec84116e67fed422dc37a2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 21 Jul 2026 07:58:56 +0200 Subject: [PATCH] fix: maak bordniveaus instelbaar (v0.4.42-beta) --- VERSION | 2 +- public/css/teach.css | 5 ++++ public/js/board.js | 42 +++++++++++++++++++++++++----- public/js/core.js | 2 +- public/js/learning-progressions.js | 33 +++++++++++++++++++++++ public/js/widgets/learning.js | 2 ++ public/js/widgets/math.js | 5 ++-- test/learning-progressions.test.js | 25 ++++++++++++++++++ 8 files changed, 104 insertions(+), 12 deletions(-) diff --git a/VERSION b/VERSION index 02c6787..177aff4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.41-beta +0.4.42-beta diff --git a/public/css/teach.css b/public/css/teach.css index 2c2381c..ab19372 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1523,6 +1523,11 @@ body.tb-left #rolePreviewBanner,body.tb-right #rolePreviewBanner{top:8px} .managed-learning-level .clock-level-control, .managed-learning-level .place-level-control, .managed-learning-level .money-level-control{display:none;} +.board-learning-level{display:grid;grid-template-columns:auto auto minmax(130px,1fr);align-items:end;gap:8px;padding:8px 10px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--accent) 7%,var(--surface));font-size:11px} +.board-learning-level label{display:grid;gap:3px;color:var(--muted);font-weight:800} +.board-learning-select{min-width:104px;border:1px solid var(--line);border-radius:8px;padding:6px 8px;background:var(--surface);color:var(--ink);font:inherit;font-weight:750} +.board-learning-summary{align-self:center;overflow:hidden;color:var(--muted);font-weight:700;text-overflow:ellipsis;white-space:nowrap} +@media(max-width:620px){.board-learning-level{grid-template-columns:1fr 1fr}.board-learning-summary{grid-column:1/-1}.board-learning-select{width:100%;min-width:0}} @media(max-width:620px){.learning-level-card .am-inline{align-items:stretch;flex-direction:column;}} /* gevaarlijke acties: afwijkende kleur + tweede klik als bevestiging */ .am-btn.am-danger{background:var(--red-soft); color:var(--red); font-weight:800;} diff --git a/public/js/board.js b/public/js/board.js index 72aca91..63ee169 100644 --- a/public/js/board.js +++ b/public/js/board.js @@ -355,15 +355,43 @@ function makeWidget(def, saved){ board.appendChild(win); /* Op het eigen bord van een thuiskind is de centrale leerjaarinstelling - leidend. Staf- en gastborden behouden exact hun opgeslagen widgetstate. */ - const mountedState = typeof learningStateForWidget === "function" + leidend. Leerkracht en systeemmanager krijgen hieronder een opgeslagen + keuze per widget; overige borden behouden hun bestaande widgetstate. */ + const initialState = typeof learningStateForWidget === "function" ? learningStateForWidget(def, saved ? saved.state : null) : (saved ? saved.state : null); - const api = def.mount(body, mountedState) || {}; - if(typeof appendLearningProfileBadge === "function" && mountedState && - ["taal","rekenen"].includes(def.cat)){ - appendLearningProfileBadge(body, def.cat, mountedState, def.id); - } + let boardLearning = typeof boardLearningSelection === "function" + ? boardLearningSelection(def, initialState) : null; + let mountedApi = {}; + const mountContent = state=>{ + body.replaceChildren(); + const mountedState = typeof boardLearningMountState === "function" + ? boardLearningMountState(def, state, boardLearning) : state; + mountedApi = def.mount(body, mountedState) || {}; + if(typeof appendLearningProfileBadge === "function" && mountedState && + ["taal","rekenen"].includes(def.cat)){ + appendLearningProfileBadge(body, def.cat, mountedState, def.id); + } + if(boardLearning && typeof appendBoardLearningLevelControls === "function"){ + appendBoardLearningLevelControls(body,def,boardLearning,next=>{ + const live=mountedApi.getState?mountedApi.getState():{}; + boardLearning=next; + mountContent(Object.assign({},live,next,{managedLearningLevel:false})); + markBoardChange(); + }); + } + }; + mountContent(initialState); + /* Het widget-API blijft stabiel terwijl een niveauwissel de inhoud opnieuw + mount. De auteurskeuze wordt hier terug aan de widgetstate toegevoegd; + widgetmodules laten leerlinggebonden velden zelf bewust buiten getState. */ + const api = { + getState:()=>{ + const state=mountedApi.getState?mountedApi.getState():null; + return boardLearning?Object.assign({},state||{},boardLearning,{managedLearningLevel:false}):state + }, + setState:state=>{mountContent(state)} + }; if(saved && saved.min) setMin(true); if(saved && saved.max) setMax(true); const inst = { def, win, api, wid: (saved && saved.wid) || genWidgetId() }; diff --git a/public/js/core.js b/public/js/core.js index 6fbc792..06af6e6 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.41-beta"; +const VERSION = "0.4.42-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/public/js/learning-progressions.js b/public/js/learning-progressions.js index a2b4500..277e030 100644 --- a/public/js/learning-progressions.js +++ b/public/js/learning-progressions.js @@ -106,6 +106,39 @@ function learningStateForWidget(def,state){ if(!currentUser||currentUser.role!=="pupil"||currentUser.schoolId!=null)return state; return applyLearningLevelToState(def,state,SELF_LEARNING_LEVELS[def.cat],true) } +/* Op het eigen auteursbord kiest de leerkracht/systeemmanager een leerjaar en + differentiatie per widget. Een systeemmanager blijft eigenaar van dit bord + in de systeem-, schoolbeheerder- en leerkrachtweergave. Ouder- en + leerlingvoorbeelden gebruiken een aparte, alleen-lezen voorbeeldweergave. */ +function canConfigureBoardLearningLevel(def){ + if(!def||!["taal","rekenen"].includes(def.cat)||!currentUser)return false; + const role=typeof activeRole==="function"?activeRole():currentUser.role; + return currentUser.role==="teacher"|| + (currentUser.role==="super"&&["super","admin","teacher"].includes(role)) +} +function boardLearningSelection(def,state){ + if(!canConfigureBoardLearningLevel(def))return null; + const savedLevel=state&&state.level!=null?Number(state.level):null; + return{yearGroup:learningYearGroup(state&&state.yearGroup)||4,level:Number.isInteger(savedLevel)?learningLevel(savedLevel):2} +} +function boardLearningMountState(def,state,selection){ + return selection?applyLearningLevelToState(def,state,selection,true):state +} +function appendBoardLearningLevelControls(root,def,selection,onChange){ + if(!root||!selection||typeof onChange!=="function")return null; + const bar=document.createElement("div");bar.className="board-learning-level"; + const yearLabel=document.createElement("label"),yearText=document.createElement("span"),year=document.createElement("select"); + const levelLabel=document.createElement("label"),levelText=document.createElement("span"),level=document.createElement("select"); + const summary=document.createElement("span");summary.className="board-learning-summary"; + year.className=level.className="board-learning-select"; + for(let n=1;n<=8;n++)year.add(new Option((LANG==="nl"?"Groep ":"Year ")+n,n)); + for(let n=1;n<=3;n++)level.add(new Option(learningLevelLabel(n),n)); + year.value=selection.yearGroup;level.value=selection.level; + const labels=()=>{yearText.textContent=LANG==="nl"?"Leerjaar":"Year";levelText.textContent=LANG==="nl"?"Niveau":"Level";summary.textContent=learningProfileSummary(def.cat,+year.value,+level.value,def.id)}; + const changed=()=>{labels();onChange({yearGroup:+year.value,level:+level.value})}; + year.addEventListener("change",changed);level.addEventListener("change",changed); + yearLabel.append(yearText,year);levelLabel.append(levelText,level);bar.append(yearLabel,levelLabel,summary);labels();root.prepend(bar);return bar +} function appendLearningProfileBadge(root,subject,state,widgetId){ const summary=learningProfileSummary(subject,state&&state.yearGroup,state&&state.level,widgetId);if(!summary)return; const old=root.querySelector(":scope > .learning-profile-badge");if(old)old.remove(); diff --git a/public/js/widgets/learning.js b/public/js/widgets/learning.js index f26a3ce..d9f9843 100644 --- a/public/js/widgets/learning.js +++ b/public/js/widgets/learning.js @@ -648,6 +648,8 @@ function field(label, el){const w=document.createElement('label');const s=docume function btn(text,fn,kind='ghost'){const b=document.createElement('button');b.type='button';b.className='tbtn '+kind;b.textContent=text;b.onclick=fn;return b} function num(value,min,max){const i=document.createElement('input');i.type='number';i.value=value;i.min=min;i.max=max;return i} function persistedLearningState(state,extra={}){ + /* Leerlinggebonden velden worden niet door de widget zelf bewaard. board.js + voegt op het auteursbord de expliciete leerjaarkeuze na getState weer toe. */ const copy={...state,...extra};delete copy.yearGroup;delete copy.managedLearningLevel;return copy } diff --git a/public/js/widgets/math.js b/public/js/widgets/math.js index c3c0f2a..61010fd 100644 --- a/public/js/widgets/math.js +++ b/public/js/widgets/math.js @@ -8,9 +8,8 @@ function mountMath(root, initState, mode, opts){ const S = { level:1, yearGroup:null, managedLearningLevel:false, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false }; if(initState){ S.level = initState.level||1; - /* yearGroup komt van klassenmanagement (pupil.js), niet van het bord zelf - - bewust niet in getState() opgenomen, dit is een per-leerling waarde, - geen door de leerkracht op het bord ingestelde inhoud. */ + /* yearGroup komt bij leerlingen van klassenmanagement (pupil.js). Op een + auteursbord bewaart board.js de aparte leerjaarkeuze rondom getState(). */ S.yearGroup = initState.yearGroup || null; S.managedLearningLevel = !!initState.managedLearningLevel; S.snd = initState.snd!==false; diff --git a/test/learning-progressions.test.js b/test/learning-progressions.test.js index 9c95046..88faadf 100644 --- a/test/learning-progressions.test.js +++ b/test/learning-progressions.test.js @@ -28,6 +28,28 @@ test('taal en rekenen hebben een compleet inhoudelijk profiel voor groep 1 tot e } }); +test('alleen leerkracht en systeemmanager kunnen het leerjaar op hun eigen bord kiezen',async()=>{ + const source=await readFile('public/js/learning-progressions.js','utf8'); + const pure=source.slice(0,source.indexOf('(function registerOwnLearningLevelSettings')); + const context={LANG:'nl',currentUser:{role:'teacher'}}; + context.activeRole=()=>context.currentUser.role; + runInNewContext(pure+';globalThis.__api={canConfigureBoardLearningLevel,boardLearningSelection}',context); + const def={cat:'taal'}; + assert.equal(context.__api.canConfigureBoardLearningLevel(def),true); + assert.deepEqual(JSON.parse(JSON.stringify(context.__api.boardLearningSelection(def,null))),{yearGroup:4,level:2}); + assert.deepEqual(JSON.parse(JSON.stringify(context.__api.boardLearningSelection(def,{}))),{yearGroup:4,level:2}); + assert.deepEqual(JSON.parse(JSON.stringify(context.__api.boardLearningSelection(def,{yearGroup:7,level:3}))),{yearGroup:7,level:3}); + context.currentUser={role:'super'}; + assert.equal(context.__api.canConfigureBoardLearningLevel({cat:'rekenen'}),true); + context.activeRole=()=>'teacher'; + assert.equal(context.__api.canConfigureBoardLearningLevel(def),true); + context.activeRole=()=>'pupil'; + assert.equal(context.__api.canConfigureBoardLearningLevel(def),false); + context.currentUser={role:'admin'}; + context.activeRole=()=>context.currentUser.role; + assert.equal(context.__api.canConfigureBoardLearningLevel(def),false); +}); + test('taalprofiel selecteert eenvoudiger woorden in de onderbouw en bouwt taaksteun af',async()=>{ const c=await progressionContext(); const words=[['kat','x'],['vis','x'],['pen','x'],['maan','x'],['boek','x'],['fiets','x'], @@ -62,6 +84,9 @@ test('alle taal- en rekenwidgets ontvangen de centrale leerjaarstate en tonen he assert.ok(html.indexOf('js/data.js')