All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m5s
181 lines
18 KiB
JavaScript
181 lines
18 KiB
JavaScript
/* 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;
|
|
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"]})
|
|
})();
|