/* teach - gedeelde leerjaarprofielen voor taal en rekenen Praktische Teach-uitwerking van de nieuwe kerndoelen po (SLO, juli 2026) en TULE. SLO ontwikkelt de nieuwe leerlijnen nog en kent ze geen wettelijke status toe; deze profielen zijn daarom geen officiele SLO-jaarnormen. */ "use strict"; const LEARNING_LEVEL_LABELS=Object.freeze({ nl:{1:"Basis",2:"Op niveau",3:"Extra uitdaging"}, en:{1:"Foundation",2:"At level",3:"Extra challenge"} }); const lp=(phase,title,goals)=>Object.freeze({phase,title,goals:Object.freeze(goals)}); const LEARNING_YEAR_PROFILES=Object.freeze({ taal:Object.freeze([ null, lp("1-2","Luisteren, spreken en taal ontdekken",["luisteren en vertellen","rijm en klanken herkennen","woordenschat rond de leefwereld"]), lp("1-2","Klanken, letters en beginnende geletterdheid",["klanken aan letters koppelen","begin- en eindklank herkennen","eenvoudige woorden en zinnen begrijpen"]), lp("3-4","Aanvankelijk lezen en spellen",["klankzuivere woorden lezen en spellen","woorden vlot herkennen","eenvoudige zinnen maken en begrijpen"]), lp("3-4","Vlot lezen, spellingpatronen en zinnen",["clusters en tweetekenklanken lezen","veelvoorkomende spellingpatronen toepassen","zinnen uitbreiden en teksten begrijpen"]), lp("5-6","Lezen met begrip en doelgericht schrijven",["leesstrategieen inzetten","meerlettergrepige woorden spellen","teksten en zinnen doelgericht opbouwen"]), lp("5-6","Taalverzorging, woordenschat en tekstverband",["spellingregels beredeneren","onbekende woorden uit context begrijpen","zinsbouw en tekstverband verzorgen"]), lp("7-8","Bronnen, argumentatie en verzorgde taal",["informatie vergelijken en beoordelen","meningen onderbouwen","werkwoordspelling en zinsbouw verzorgen"]), lp("7-8","Kritisch en bewust taal gebruiken",["bronnen kritisch verwerken","teksten plannen, formuleren en reviseren","taalkeuzes en strategieen verantwoorden"]) ]), rekenen:Object.freeze([ null, lp("1-2","Tellen, vergelijken en ordenen",["hoeveelheden tot 20 herkennen","tellen, vergelijken en splitsen","vormen, patronen en dagelijkse tijd verkennen"]), lp("1-2","Getalbegrip en informeel rekenen",["getallen tot 100 verkennen","optellen en aftrekken tot 20","meten, geld en tijd in situaties gebruiken"]), lp("3-4","Basisbewerkingen tot 20 en getallen tot 100",["optellen en aftrekken tot 20 automatiseren","getalstructuur tot 100 gebruiken","tafels van 2, 5 en 10 opbouwen"]), lp("3-4","Rekenen tot 100 en tafels automatiseren",["handig optellen en aftrekken tot 100","tafels en deeltafels automatiseren","tijd, geld en meten met passende eenheden"]), lp("5-6","Getallen tot 1.000 en breuken",["bewerkingen tot 1.000 uitvoeren","tafels flexibel gebruiken","breuken als deel, verhouding en getal herkennen"]), lp("5-6","Grote getallen, kommagetallen en procenten",["standaardprocedures gebruiken","kommagetallen en breuken verbinden","eenvoudige procenten en verhoudingen berekenen"]), lp("7-8","Verhoudingen, procenten en meten",["breuken, kommagetallen en procenten verbinden","bewerkingen doelmatig kiezen","meetkundige problemen oplossen"]), lp("7-8","Redeneren en rekenen in samenhang",["meer-stapsproblemen modelleren","schatten en uitkomsten beoordelen","rekenwijzen helder uitleggen"]) ]) }); const LANGUAGE_WIDGET_TASKS=Object.freeze({ letters:["","letters en beginklanken","letters en ontbrekende klanken","klankzuivere woorden opbouwen","clusters en tweetekenklanken","meerlettergrepige woorden","regelwoorden en langere woorden","complexe spelling en werkwoorden","spellingkennis zelfstandig toepassen"], flits:["","letters herkennen en benoemen","korte klankzuivere woorden","woorden vlot herkennen","langere woorden automatiseren","complexere woorden vlot lezen","schooltaalwoorden automatiseren","laagfrequente schooltaalwoorden","complexe woorden nauwkeurig lezen"], hak:["","beginklanken horen en koppelen","klanken hakken en plakken","klankgroepen samenvoegen","spellingpatronen herkennen","lettergrepen onderzoeken","complexe klankgroepen","woordopbouw en morfemen","woordstructuur verklaren"], ball:["","letters bij beeldankers kiezen","korte woorden raden","woorden actief herkennen","langere woorden spellen","complexere spelling","woordenschat consolideren","complexe woordbeelden","gevorderde spelling"], sentence:["","eenvoudige zinnen vertellen","wie-doet-wat-zinnen","enkelvoudige zinnen","zinnen uitbreiden","informatieve zinnen","zinsverband en leestekens","argumenterende zinnen","gevarieerde teksten formuleren"], wordtypes:["","woorden benoemen","naamwoorden en doe-woorden","naamwoord en werkwoord","drie woordsoorten","woordsoorten in context","woordsoorten functioneel benoemen","woordsoort en zinsfunctie","taalstructuur analyseren"] }); const MATH_WIDGET_TASKS=Object.freeze({ madd:["","optellen tot 10","optellen tot 20","optellen tot 20","optellen tot 100","optellen tot 1.000","optellen tot 10.000","grote getallen optellen","optellen en schatten"], msub:["","aftrekken tot 10","aftrekken tot 20","aftrekken tot 20","aftrekken tot 100","aftrekken tot 1.000","aftrekken tot 10.000","grote getallen aftrekken","verschillen en schattingen"], mmul:["","groepjes maken","herhaald optellen","tafels 2, 5 en 10","tafels tot en met 10","tafels flexibel gebruiken","grotere vermenigvuldigingen","vermenigvuldigen in context","samengestelde vermenigvuldigingen"], mdiv:["","eerlijk verdelen","verdelen in groepjes","delen vanuit bekende tafels","deeltafels tot en met 10","delen met tafelfeiten","delen en rest interpreteren","delen in context","verhoudingsgewijs delen"], mpct:["","deel van een geheel","helft en geheel","helft en kwart","breuk als deel","breuk en percentage","10%, 25%, 50%, 75%, 100%","veelvoorkomende percentages","percentages en verhoudingen"], numberline:["","tellen tot 20","sprongen tot 50","getallen tot 100","sprongen tot 100","grotere sprongen","getallen positioneren","negatieve en grote getallen","schaal en bereik"], fractions:["","helft en geheel","helft en kwart","eenvoudige breuken","breuken vergelijken","gelijkwaardige breuken","vereenvoudigen en vergelijken","breuken en verhoudingen","breuk, verhouding en percentage"], clock:["","dagelijkse tijd","hele en halve uren","hele, halve en kwartieren","tijd per vijf minuten","tijd en tijdsduur","24-uurs tijd","24-uurs tijdsproblemen","tijdzones en tijdsduur"], placevalue:["","hoeveelheid en cijfer","tientallen en eenheden","getallen tot 100","getallen tot 1.000","getallen tot 10.000","getallen tot 100.000","getallen tot 1.000.000","grote getalstructuur"], money:["","munten herkennen","hele eurobedragen","eenvoudige bedragen","euros en centen","bedragen en teruggeven","precies rekenen met centen","korting en wisselgeld","budget en procentuele verandering"] }); function learningYearGroup(value){const year=Number(value);return Number.isInteger(year)&&year>=1&&year<=8?year:null} function learningLevel(value){return Math.max(1,Math.min(3,Number(value)||1))} function learningProfile(subject,yearGroup,level){ const year=learningYearGroup(yearGroup),profile=year&&LEARNING_YEAR_PROFILES[subject]&&LEARNING_YEAR_PROFILES[subject][year]; return profile?Object.assign({subject:subject,yearGroup:year,level:learningLevel(level)},profile):null } function learningLevelLabel(level){return (LEARNING_LEVEL_LABELS[LANG]||LEARNING_LEVEL_LABELS.nl)[learningLevel(level)]} function learningProfileSummary(subject,yearGroup,level,widgetId){ const p=learningProfile(subject,yearGroup,level);if(!p)return""; const tasks=subject==="taal"?LANGUAGE_WIDGET_TASKS:MATH_WIDGET_TASKS,task=tasks[widgetId]&&tasks[widgetId][p.yearGroup]; return (LANG==="nl"?"Groep ":"Year ")+p.yearGroup+" | "+learningLevelLabel(p.level)+" | "+(task||p.title) } function languageWordComplexity(word){ const w=String(word||"").toLocaleLowerCase("nl-NL").replace(/[^a-z]/g,""); const clusters=(w.match(/[bcdfghjklmnpqrstvwxz]{2,}/g)||[]).reduce((n,x)=>n+x.length-1,0); const patterns=(w.match(/aai|ooi|oei|eeuw|ieuw|sch|ng|nk|ch|aa|ee|oo|uu|oe|ie|ei|ij|ou|au|ui|eu/g)||[]).length; const syllables=(w.match(/[aeiouy]+/g)||[]).length; return w.length+clusters*2+patterns+Math.max(0,syllables-1)*2 } function languageWordsForYear(words,yearGroup,level){ if(!Array.isArray(words)||!words.length)return[]; const year=learningYearGroup(yearGroup);if(!year)return words; const introduced=words.filter(item=>!Array.isArray(item)||item[2]==null||Number(item[2])<=year); const hasExplicitYears=words.some(item=>Array.isArray(item)&&item[2]!=null); if(!introduced.length&&hasExplicitYears)return[]; const available=introduced.length?introduced:words; const caps=[0,5,7,9,12,15,18,22,99],cap=caps[year]+(learningLevel(level)-2)*2; if(year===1){ const anchors=available.filter(item=>Array.isArray(item)&&Number(item[2])===1); const custom=available.filter(item=>!Array.isArray(item)||item[2]==null).filter(item=>languageWordComplexity(Array.isArray(item)?item[0]:item)<=cap); return anchors.concat(custom) } const filtered=available.filter(item=>languageWordComplexity(Array.isArray(item)?item[0]:item)<=cap); if(filtered.length>=Math.min(4,available.length))return filtered; return [...available].sort((a,b)=>languageWordComplexity(a[0])-languageWordComplexity(b[0])).slice(0,Math.min(4,available.length)) } function languageWidgetWords(words,yearGroup,level,widgetId){ const selected=languageWordsForYear(words,yearGroup,level),year=learningYearGroup(yearGroup); if(year!==1||!["flits","hak","ball"].includes(widgetId))return selected; const initials=new Map(); selected.forEach(item=>{ const word=String(Array.isArray(item)?item[0]:item||"").toLocaleLowerCase("nl-NL"); const letter=(word.match(/[a-z]/)||[])[0]; if(letter&&!initials.has(letter))initials.set(letter,[letter,Array.isArray(item)?item[1]:"",1,word]); }); return [...initials.values()] } function languageTaskRules(yearGroup,level){ const year=learningYearGroup(yearGroup);if(!year)return null;const l=learningLevel(level); return{literacyMode:year===1?"letters":year===2?"sounds":"words",letterLevel:year<=2?Math.min(2,l):year===3?Math.max(2,l):3,flashSeconds:year<=2?3:year<=4?2:year<=6?1:.5,lives:year<=3?7:year<=5?6:5,wordtypeCount:year<=3?2:year===4?3:4} } function mathTaskRules(yearGroup,level){ const year=learningYearGroup(yearGroup);if(!year)return null;const l=learningLevel(level),factor=l===1?.25:l===2?.6:1; const addCap=[0,10,20,20,100,1000,10000,100000,1000000][year],numberMax=[0,20,50,100,100,1000,10000,100000,1000000][year]; const lineStep=[0,1,1,1,2,20,200,2000,20000][year]; return{addSub:Math.max(10,Math.round(addCap*factor/5)*5),timesTable:year<3?2:year===3?(l===1?2:l===2?5:10):year===4?(l===1?5:10):12,factorMax:year<=3?5:year<=4?10:year<=6?12:20,numberlineMax:Math.max(20,Math.round(numberMax*(l===1?.4:l===2?.7:1))),numberlineMin:year>=7&&l>=2?-100:0,numberlineStep:lineStep,fractionDen:year===1?2:year<=3?4:year===4?8:12,clockLevel:year<=2?1:year<=4?2:3,clockMinutes:year===1?[0]:year===2?[0,30]:year===3?[0,15,30,45]:null,placeLevel:year<=2?1:year<=4?2:3,moneyLevel:year<=2?1:year<=4?2:3,percentSets:year<=5?[50,100]:year===6?[10,25,50,75,100]:year===7?[5,10,20,25,50,75,100]:[5,10,15,20,25,30,40,50,60,70,75,80,90,100]} } let SELF_LEARNING_LEVELS={taal:null,rekenen:null}; async function refreshSelfLearningLevels(){ if(!currentUser||currentUser.role!=="pupil"||currentUser.schoolId!=null){SELF_LEARNING_LEVELS={taal:null,rekenen:null};return SELF_LEARNING_LEVELS} const pairs=await Promise.all(["taal","rekenen"].map(async subject=>{try{return[subject,await api("/my/level?subject="+subject)]}catch(_){return[subject,null]}})); SELF_LEARNING_LEVELS=Object.fromEntries(pairs);return SELF_LEARNING_LEVELS } function applyLearningLevelToState(def,state,level,managed){ const next=state?Object.assign({},state):{};if(!def||!["taal","rekenen"].includes(def.cat)||!level||level.yearGroup==null)return next; next.yearGroup=learningYearGroup(level.yearGroup);next.level=learningLevel(level.level);next.managedLearningLevel=managed!==false;return next } 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(); const badge=document.createElement("div");badge.className="learning-profile-badge";badge.textContent=summary; const p=learningProfile(subject,state.yearGroup,state.level);badge.title=p?p.goals.join(" | "):"";root.prepend(badge) } (function registerOwnLearningLevelSettings(){ const host=document.createElement("div");host.className="st-section";host.dataset.section="learning-level";host.style.display="none";host.id="learningLevelSettings";document.getElementById("stContent").appendChild(host); const text=()=>LANG==="nl"?{title:"Leerniveau",sub:"Kies zelf het passende leerjaar voor taal en rekenen.",note:"Deze keuze bepaalt woorden, getalgrenzen en opdrachten. De profielen zijn een Teach-uitwerking op basis van SLO/TULE; geen officiele SLO-jaarnormen.",save:"Opslaan",saved:"Leerniveau opgeslagen.",year:"Groep",taal:"Taal",rekenen:"Rekenen"}:{title:"Learning level",sub:"Choose the suitable year for language and maths.",note:"This choice determines words, number ranges and tasks. The profiles are a Teach interpretation based on SLO/TULE; not official SLO year standards.",save:"Save",saved:"Learning level saved.",year:"Year",taal:"Language",rekenen:"Maths"}; const render=async()=>{ host.innerHTML="";const tx=text(),msg=document.createElement("div");msg.className="formMsg";host.appendChild(msg);await refreshSelfLearningLevels(); for(const subject of ["taal","rekenen"]){ const current=SELF_LEARNING_LEVELS[subject]||{yearGroup:4,level:2},card=document.createElement("section");card.className="learning-level-card"; const h=document.createElement("h3");h.textContent=tx[subject];const summary=document.createElement("p");summary.className="guestnote";const controls=document.createElement("div");controls.className="am-inline"; const year=document.createElement("select"),level=document.createElement("select");year.className=level.className="am-sel"; for(let n=1;n<=8;n++)year.add(new Option(tx.year+" "+n,n));for(let n=1;n<=3;n++)level.add(new Option(learningLevelLabel(n),n));year.value=current.yearGroup||4;level.value=current.level||2; const update=()=>{const p=learningProfile(subject,+year.value,+level.value);summary.textContent=p?p.title+" - "+p.goals.join(" | "):""};year.onchange=level.onchange=update;update(); const save=document.createElement("button");save.type="button";save.className="am-btn";save.textContent=tx.save;save.onclick=async()=>{try{await api("/my/level",{method:"PUT",body:{subject:subject,yearGroup:+year.value,level:+level.value}});await refreshSelfLearningLevels();msg.textContent=tx.saved;msg.classList.add("ok");loadCurrentSlot()}catch(e){msg.textContent=e.message;msg.classList.remove("ok")}}; controls.append(year,level,save);card.append(h,summary,controls);host.appendChild(card) } const note=document.createElement("p");note.className="guestnote learning-source-note";note.textContent=tx.note;host.appendChild(note) }; registerSettingsSection({id:"learning-level",icon:"🎯",group:"personal",order:1.5,label:()=>text().title,sub:()=>text().sub,visible:()=>!!currentUser&¤tUser.role==="pupil"&¤tUser.schoolId==null,activate(){showSettingsDom("learning-level");return render()},keywords:()=>[text().title,text().year,"taal","rekenen","groep","niveau"]}) })();