feat: voeg leerjaarprofielen toe aan taal en rekenen (v0.4.41-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
This commit is contained in:
parent
629329894d
commit
fabe721991
22 changed files with 554 additions and 67 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.40-beta
|
||||
0.4.41-beta
|
||||
|
|
|
|||
|
|
@ -661,6 +661,10 @@
|
|||
.wiki-status strong{white-space:nowrap}.wiki-status span{color:var(--muted)}
|
||||
.wiki-link{width:max-content;max-width:100%;color:var(--accent);font-size:12px;font-weight:700;text-decoration:none}.wiki-link:hover,.wiki-link:focus-visible{text-decoration:underline}
|
||||
.wiki-tule{border:1px solid var(--line);border-radius:9px;background:var(--surface)}.wiki-tule summary{cursor:pointer;padding:10px 12px;font-weight:700}.wiki-tule p{margin:0;padding:0 12px 12px;color:var(--muted);font-size:12.5px;line-height:1.55}
|
||||
.wiki-profile-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:0 12px 12px}
|
||||
.wiki-profile-column{display:flex;flex-direction:column;gap:6px}.wiki-profile-column h3{margin:0 0 3px}
|
||||
.wiki-profile-row{display:grid;grid-template-columns:74px 1fr;gap:3px 8px;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--surface-2)}
|
||||
.wiki-profile-row small{grid-column:1/-1;color:var(--muted);line-height:1.4}
|
||||
.wiki-source-links{display:flex;flex-wrap:wrap;gap:8px;padding:0 12px 12px}
|
||||
.wiki-language-note{margin:0;padding:8px 11px;border-radius:7px;background:var(--surface-2);color:var(--muted);font-size:12px}
|
||||
.wiki-search{display:flex;flex-direction:column;gap:5px}.wiki-search label{font-size:12px;font-weight:700}
|
||||
|
|
@ -709,6 +713,7 @@
|
|||
.wiki-hero{flex-direction:column}.wiki-facts{grid-template-columns:auto auto;border-left:0;border-top:3px solid var(--accent)}
|
||||
.wiki-status{grid-template-columns:1fr}.wiki-status strong{white-space:normal}
|
||||
.wiki-grid{grid-template-columns:1fr}
|
||||
.wiki-profile-grid{grid-template-columns:1fr}
|
||||
}
|
||||
/* =========================================================
|
||||
Letter game widget
|
||||
|
|
@ -1498,6 +1503,27 @@ body.tb-left #rolePreviewBanner,body.tb-right #rolePreviewBanner{top:8px}
|
|||
.am-row-tools .am-field{flex:0 1 auto; min-width:150px;}
|
||||
.am-inline{display:flex; gap:6px; align-items:center;}
|
||||
.am-inline > .am-inp, .am-inline > .am-pw{flex:1 1 auto; min-width:0;}
|
||||
|
||||
.learning-profile-badge{
|
||||
margin:8px 10px 0;padding:6px 10px;border-radius:10px;
|
||||
background:color-mix(in srgb,var(--accent) 12%,var(--surface));
|
||||
color:var(--muted);font-size:12px;font-weight:700;line-height:1.3;
|
||||
}
|
||||
.learning-level-card{
|
||||
margin:10px 0;padding:12px;border:1px solid var(--line);border-radius:12px;
|
||||
background:var(--surface-2);
|
||||
}
|
||||
.learning-level-card h3{margin:0 0 6px;}
|
||||
.learning-level-card .guestnote{margin:4px 0 10px;}
|
||||
.learning-source-note{margin-top:14px;}
|
||||
.parent-levels{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;}
|
||||
.parent-levels .learning-level-card{margin:0;}
|
||||
.learning-level-summary{margin:8px 0 0;}
|
||||
.managed-learning-level .lg-levels,
|
||||
.managed-learning-level .clock-level-control,
|
||||
.managed-learning-level .place-level-control,
|
||||
.managed-learning-level .money-level-control{display:none;}
|
||||
@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;}
|
||||
.am-btn.am-danger.armed{background:var(--red); color:#fff;}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<script src="js/wiki.js?v=__V__" defer></script>
|
||||
<script src="js/cms.js?v=__V__" defer></script>
|
||||
<script src="js/data.js?v=__V__" defer></script>
|
||||
<script src="js/learning-progressions.js?v=__V__" defer></script>
|
||||
<script src="js/avatar.js?v=__V__" defer></script>
|
||||
<script src="js/explorer.js?v=__V__" defer></script>
|
||||
<script src="js/picker.js?v=__V__" defer></script>
|
||||
|
|
|
|||
|
|
@ -1239,7 +1239,8 @@
|
|||
}
|
||||
function levelSel(current){
|
||||
const sel = h("select","am-sel");
|
||||
[1,2,3].forEach(n=>sel.appendChild(new Option(T("lvl"+n), n)));
|
||||
[1,2,3].forEach(n=>sel.appendChild(new Option(
|
||||
typeof learningLevelLabel==="function" ? learningLevelLabel(n) : T("lvl"+n), n)));
|
||||
sel.value = current ?? 1;
|
||||
return sel;
|
||||
}
|
||||
|
|
@ -1258,6 +1259,12 @@
|
|||
const ygSel = yearGroupSel(current && current.yearGroup);
|
||||
const lvlSel = levelSel(current && current.level);
|
||||
row.append(field("amLevelYearGroup", ygSel), field("amLevelLevel", lvlSel));
|
||||
const summary = h("div","guestnote learning-level-summary");
|
||||
const updateSummary=()=>{
|
||||
const p=typeof learningProfile==="function" ? learningProfile(subject,+ygSel.value,+lvlSel.value) : null;
|
||||
summary.textContent=p ? p.title+" — "+p.goals.join(" · ") : "";
|
||||
};
|
||||
ygSel.addEventListener("change",updateSummary);lvlSel.addEventListener("change",updateSummary);updateSummary();
|
||||
const saveBtn = h("button","am-btn", T("save"));
|
||||
saveBtn.type = "button";
|
||||
saveBtn.addEventListener("click", async ()=>{
|
||||
|
|
@ -1269,6 +1276,7 @@
|
|||
});
|
||||
row.appendChild(saveBtn);
|
||||
box.appendChild(row);
|
||||
box.appendChild(summary);
|
||||
return box;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -354,7 +354,16 @@ function makeWidget(def, saved){
|
|||
});
|
||||
|
||||
board.appendChild(win);
|
||||
const api = def.mount(body, saved ? saved.state : null) || {};
|
||||
/* 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"
|
||||
? 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);
|
||||
}
|
||||
if(saved && saved.min) setMin(true);
|
||||
if(saved && saved.max) setMax(true);
|
||||
const inst = { def, win, api, wid: (saved && saved.wid) || genWidgetId() };
|
||||
|
|
|
|||
|
|
@ -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.40-beta";
|
||||
const VERSION = "0.4.41-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -245,6 +245,9 @@ const I18N = {
|
|||
wikiOfficialSource:"Open officiële SLO-publicatie",
|
||||
wikiOfficialArea:"Bekijk {area} in de officiële publicatie",
|
||||
wikiTuleTitle:"TULE en de nieuwe leerlijnen",
|
||||
wikiYearProfiles:"Leerjaarprofielen taal en rekenen",
|
||||
wikiYearProfilesText:"Praktische Teach-uitwerking voor widgetopdrachten per groep. Gebaseerd op de kerndoelen 2026 en TULE; geen officiële SLO-jaarnormen.",
|
||||
wikiYear:"Groep {n}", wikiProfileGoals:"Leerdoelen",
|
||||
wikiTuleText:"TULE werkt de kerndoelen uit 2006 voorbeeldmatig uit. Nieuwe SLO-leerlijnen bij de doelen van 2026 zijn nog in ontwikkeling. Een niveau-indeling in Teach is daarom geen officiële SLO-leerlijn.",
|
||||
wikiTuleSource:"Open TULE bij SLO", wikiProgressionsSource:"Volg de ontwikkeling van nieuwe leerlijnen",
|
||||
wikiSearchLabel:"Doorzoek de curriculumwiki",
|
||||
|
|
@ -320,6 +323,9 @@ const I18N = {
|
|||
parentUnlink:"Ontkoppel",
|
||||
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
|
||||
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
|
||||
parentLevel:"🎯 Leerniveau", parentLevelTaal:"Taal", parentLevelRekenen:"Rekenen",
|
||||
parentLevelYear:"Groep", parentLevelPath:"Uitwerking", parentLevelSave:"Opslaan",
|
||||
parentLevelSaved:"Leerniveau opgeslagen.",
|
||||
dtLen:"Lengte", dtContrast:"Hoog contrast",
|
||||
dtLock:"Vastzetten: kan niet meer verschuiven of draaien, alleen nog langs tekenen",
|
||||
dtMode:"Basis: grote hoofdhoeken en hele centimeters. Volledig: graden per 2° met dubbele schaal, millimeters en evenwijdige hulplijnen.",
|
||||
|
|
@ -625,6 +631,9 @@ const I18N = {
|
|||
wikiOfficialSource:"Open the official SLO publication",
|
||||
wikiOfficialArea:"View {area} in the official publication",
|
||||
wikiTuleTitle:"TULE and the new learning progressions",
|
||||
wikiYearProfiles:"Language and maths year profiles",
|
||||
wikiYearProfilesText:"Practical Teach interpretation for widget tasks per year. Based on the 2026 targets and TULE; not official SLO year standards.",
|
||||
wikiYear:"Year {n}", wikiProfileGoals:"Learning goals",
|
||||
wikiTuleText:"TULE is an illustrative elaboration of the 2006 targets. New SLO learning progressions for the 2026 targets are still being developed. Teach levels are therefore not official SLO progressions.",
|
||||
wikiTuleSource:"Open TULE at SLO", wikiProgressionsSource:"Follow the development of new progressions",
|
||||
wikiSearchLabel:"Search the curriculum wiki",
|
||||
|
|
@ -700,6 +709,9 @@ const I18N = {
|
|||
parentUnlink:"Unlink",
|
||||
parentUnlinkWarn:"Removes only the link; your child's account remains.",
|
||||
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
|
||||
parentLevel:"🎯 Learning level", parentLevelTaal:"Language", parentLevelRekenen:"Maths",
|
||||
parentLevelYear:"Year", parentLevelPath:"Path", parentLevelSave:"Save",
|
||||
parentLevelSaved:"Learning level saved.",
|
||||
dtLen:"Length", dtContrast:"High contrast",
|
||||
dtLock:"Lock in place: can no longer move or rotate, only draw along it",
|
||||
dtMode:"Basic: big main angles and whole centimetres. Full: degrees every 2° with a double scale, millimetres and parallel guide lines.",
|
||||
|
|
@ -1001,6 +1013,10 @@ async function hydrateFromServer(prefetchedData){
|
|||
}
|
||||
stopPupilView();
|
||||
stopParentView();
|
||||
/* Een thuiskind beheert het eigen leerjaar. Haal dit op voordat de
|
||||
bordwidgets mounten, zodat de eerste opdracht meteen bij de keuze past. */
|
||||
if(currentUser && currentUser.role === "pupil" && !currentUser.schoolId &&
|
||||
typeof refreshSelfLearningLevels === "function") await refreshSelfLearningLevels();
|
||||
const res = prefetchedData || await api("/me/data");
|
||||
const d = res.data || {};
|
||||
DATA_REV = typeof res.rev === "number" ? res.rev : 0;
|
||||
|
|
@ -1014,6 +1030,7 @@ async function hydrateFromServer(prefetchedData){
|
|||
}
|
||||
function resetToGuest(){
|
||||
currentUser = null;
|
||||
if(typeof refreshSelfLearningLevels === "function") refreshSelfLearningLevels();
|
||||
stopPupilView();
|
||||
stopParentView();
|
||||
THEMES = {nl:[],en:[]};
|
||||
|
|
|
|||
133
public/js/learning-progressions.js
Normal file
133
public/js/learning-progressions.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/* 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:["","korte bekende woorden","korte klankzuivere woorden","woorden vlot herkennen","langere woorden automatiseren","complexere woorden vlot lezen","schooltaalwoorden automatiseren","laagfrequente schooltaalwoorden","complexe woorden nauwkeurig lezen"],
|
||||
hak:["","woorden in klanken horen","klanken hakken en plakken","klankgroepen samenvoegen","spellingpatronen herkennen","lettergrepen onderzoeken","complexe klankgroepen","woordopbouw en morfemen","woordstructuur verklaren"],
|
||||
ball:["","korte beeldwoorden","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 caps=[0,5,7,9,12,15,18,22,99],cap=caps[year]+(learningLevel(level)-2)*2;
|
||||
const filtered=words.filter(item=>languageWordComplexity(Array.isArray(item)?item[0]:item)<=cap);
|
||||
if(filtered.length>=Math.min(4,words.length))return filtered;
|
||||
return [...words].sort((a,b)=>languageWordComplexity(a[0])-languageWordComplexity(b[0])).slice(0,Math.min(4,words.length))
|
||||
}
|
||||
function languageTaskRules(yearGroup,level){
|
||||
const year=learningYearGroup(yearGroup);if(!year)return null;const l=learningLevel(level);
|
||||
return{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)
|
||||
}
|
||||
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"]})
|
||||
})();
|
||||
|
|
@ -98,6 +98,51 @@ function parentChildRow(c){
|
|||
const actions = prH("div","am-actions");
|
||||
actions.appendChild(progBtn);
|
||||
|
||||
/* Ouder en thuiskind delen exact dezelfde instelling in pupil_levels.
|
||||
Schoolkinderen staan hier bewust niet tussen: hun leerkracht is leidend. */
|
||||
const levelHolder = prH("div","parent-levels");
|
||||
levelHolder.style.display = "none";
|
||||
async function loadLevels(){
|
||||
levelHolder.innerHTML = "";
|
||||
try{
|
||||
const data = await api(`/parent/children/${c.id}/levels`);
|
||||
["taal","rekenen"].forEach(subject=>{
|
||||
const current = data.levels?.[subject] || {yearGroup:4,level:2};
|
||||
const card = prH("section","learning-level-card");
|
||||
card.appendChild(prH("h3",null,T(subject==="taal"?"parentLevelTaal":"parentLevelRekenen")));
|
||||
const summary = prH("p","guestnote");
|
||||
const controls = prH("div","am-inline");
|
||||
const year = prH("select","am-sel"), level = prH("select","am-sel");
|
||||
for(let n=1;n<=8;n++) year.add(new Option(T("parentLevelYear")+" "+n,n));
|
||||
for(let n=1;n<=3;n++) level.add(new Option(learningLevelLabel(n),n));
|
||||
year.value=current.yearGroup; level.value=current.level;
|
||||
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=prH("button","am-btn",T("parentLevelSave"));save.type="button";
|
||||
save.onclick=async()=>{
|
||||
try{
|
||||
await api(`/parent/children/${c.id}/level`,{method:"PUT",body:{subject,yearGroup:+year.value,level:+level.value}});
|
||||
prMsg(T("parentLevelSaved")); await loadLevels();
|
||||
}catch(e){prMsg(e.message);}
|
||||
};
|
||||
controls.append(year,level,save);card.append(summary,controls);levelHolder.appendChild(card);
|
||||
});
|
||||
}catch(e){levelHolder.appendChild(prH("div","guestnote",e.message));}
|
||||
}
|
||||
if(!c.schoolId){
|
||||
const levelBtn=prH("button","am-btn",T("parentLevel"));levelBtn.type="button";
|
||||
levelBtn.addEventListener("click",()=>{
|
||||
const key="level:"+c.id;
|
||||
if(parentOpen.has(key)){parentOpen.delete(key);levelHolder.style.display="none";}
|
||||
else{parentOpen.add(key);levelHolder.style.display="";loadLevels();}
|
||||
});
|
||||
actions.appendChild(levelBtn);
|
||||
if(parentOpen.has("level:"+c.id)){levelHolder.style.display="";loadLevels();}
|
||||
}
|
||||
|
||||
/* thuiskind zonder lopende aanmelding: aanmelden bij een school via een
|
||||
doorzoekbare lijst (input + datalist gevuld uit de publieke /schools) */
|
||||
const applyHolder = prH("div","am-add");
|
||||
|
|
@ -133,6 +178,7 @@ function parentChildRow(c){
|
|||
catch(e){ prMsg(e.message); }
|
||||
}));
|
||||
tools.appendChild(actions);
|
||||
tools.appendChild(levelHolder);
|
||||
tools.appendChild(applyHolder);
|
||||
tools.appendChild(progHolder);
|
||||
row.appendChild(tools);
|
||||
|
|
|
|||
|
|
@ -81,15 +81,16 @@ async function renderPupilWidgets(widgets, mode, preview=false){
|
|||
api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } })
|
||||
.catch(e=>console.warn("voortgang loggen mislukt:", e.message));
|
||||
}};
|
||||
/* alleen aanvullen wat het bord zelf nog niet expliciet heeft ingesteld -
|
||||
een leerkracht die zelf al een niveau op dit bord koos, blijft leidend. */
|
||||
/* De vakinstelling van de leerkracht is per leerling en dus leidend boven
|
||||
de algemene widgetstate van het bord. Zo krijgt dezelfde opdracht voor
|
||||
twee leerlingen toch passende woorden en getalgrenzen. */
|
||||
const lvl = levels[def.cat];
|
||||
const state = w.state ? { ...w.state } : {};
|
||||
if(lvl){
|
||||
if(state.level == null) state.level = lvl.level;
|
||||
if(def.cat === "rekenen" && lvl.yearGroup != null) state.yearGroup = lvl.yearGroup;
|
||||
}
|
||||
const state = typeof applyLearningLevelToState === "function"
|
||||
? applyLearningLevelToState(def, w.state, lvl, true)
|
||||
: (w.state ? { ...w.state } : {});
|
||||
def.mount(body, Object.keys(state).length ? state : null, opts);
|
||||
if(typeof appendLearningProfileBadge === "function")
|
||||
appendLearningProfileBadge(body, def.cat, state, def.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
==========================================================*/
|
||||
function mountBall(root, initState, opts){
|
||||
const onProgress = opts && opts.onProgress;
|
||||
const S = { card:0, snd:true, lives:6, guessed:new Set(), over:false, order:[], idx:0, stars:0 };
|
||||
if(initState){ S.card = initState.card ?? 0; S.snd = initState.snd !== false; }
|
||||
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, snd:true, lives:6, guessed:new Set(), over:false, order:[], idx:0, stars:0 };
|
||||
if(initState){S.level=initState.level||1;S.yearGroup=initState.yearGroup||null;S.managedLearningLevel=!!initState.managedLearningLevel;S.card = initState.card ?? 0; S.snd = initState.snd !== false;}
|
||||
const assignedRules=languageTaskRules(S.yearGroup,S.level),maxLives=assignedRules?.lives||6;
|
||||
const play = f=>{ if(S.snd) f(); };
|
||||
|
||||
root.innerHTML = `
|
||||
|
|
@ -29,7 +30,7 @@ function mountBall(root, initState, opts){
|
|||
nextBtn = el(".bl-next"), kbdEl = el(".bl-kbd"),
|
||||
scoreEl = el(".bl-score"), sndBtn = el(".lg-snd");
|
||||
|
||||
function list(){ S.card = normWordSource(S.card); return wordSourceList(S.card); }
|
||||
function list(){ S.card = normWordSource(S.card); return languageWordsForYear(wordSourceList(S.card),S.yearGroup,S.level); }
|
||||
function buildSrc(){ fillWordSourceSel(srcSel, S.card); }
|
||||
"abcdefghijklmnopqrstuvwxyz".split("").forEach(l=>{
|
||||
const b = document.createElement("button");
|
||||
|
|
@ -43,14 +44,14 @@ function mountBall(root, initState, opts){
|
|||
wordEl.textContent = word().split("").map(l=>(reveal || S.guessed.has(l)) ? l : "_").join(" ");
|
||||
}
|
||||
function drawBalloons(){
|
||||
balloonsEl.textContent = "🎈".repeat(S.lives) + "💥".repeat(6-S.lives);
|
||||
balloonsEl.textContent = "🎈".repeat(S.lives) + "💥".repeat(maxLives-S.lives);
|
||||
}
|
||||
function newWord(){
|
||||
if(!S.order.length || S.idx >= S.order.length){
|
||||
S.order = shuffle(list().map((_,i)=>i));
|
||||
S.idx = 0;
|
||||
}
|
||||
S.lives = 6; S.guessed = new Set(); S.over = false;
|
||||
S.lives = maxLives; S.guessed = new Set(); S.over = false;
|
||||
msgEl.textContent = ""; msgEl.className = "bl-msg";
|
||||
nextBtn.classList.remove("show");
|
||||
kbdEl.querySelectorAll("button").forEach(b=>{ b.disabled = false; b.style.opacity = 1; });
|
||||
|
|
@ -106,6 +107,6 @@ function mountBall(root, initState, opts){
|
|||
document.addEventListener("langchange", ()=>{ S.card=0; S.order=[]; labels(); newWord(); });
|
||||
document.addEventListener("wordschange", ()=>{ buildSrc(); S.order=[]; newWord(); });
|
||||
labels(); newWord();
|
||||
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
|
||||
return { getState: ()=>({card:S.card, snd:S.snd}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
Flash words widget
|
||||
==========================================================*/
|
||||
function mountFlits(root, initState){
|
||||
const S = { card:0, dur:2, idx:0, order:[], score:0, tries:0 };
|
||||
if(initState){ S.card = initState.card ?? 0; S.dur = initState.dur || 2; S.score=+initState.score||0; S.tries=+initState.tries||0; }
|
||||
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, dur:2, idx:0, order:[], score:0, tries:0 };
|
||||
if(initState){ S.level=initState.level||1;S.yearGroup=initState.yearGroup||null;S.managedLearningLevel=!!initState.managedLearningLevel;S.card = initState.card ?? 0; S.dur = initState.dur || 2; S.score=+initState.score||0; S.tries=+initState.tries||0; }
|
||||
const assignedRules=languageTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.dur=assignedRules.flashSeconds;
|
||||
let flashTimer = null, assessed=false;
|
||||
|
||||
root.innerHTML = `
|
||||
|
|
@ -33,7 +34,7 @@ function mountFlits(root, initState){
|
|||
const srcSel = el(".fl-src"), durSel = el(".fl-dur"),
|
||||
wordEl = el(".fl-word"), picEl = el(".fl-pic"), cntEl = el(".fl-cnt"), reflect=el(".fl-reflect");
|
||||
|
||||
function list(){ S.card = normWordSource(S.card); return wordSourceList(S.card); }
|
||||
function list(){ S.card = normWordSource(S.card); return languageWordsForYear(wordSourceList(S.card),S.yearGroup,S.level); }
|
||||
function buildSrc(){
|
||||
fillWordSourceSel(srcSel, S.card);
|
||||
durSel.innerHTML = "";
|
||||
|
|
@ -106,5 +107,6 @@ function mountFlits(root, initState){
|
|||
document.addEventListener("langchange", ()=>{ S.card=0; labels(); newRound(); hide(); });
|
||||
document.addEventListener("wordschange", ()=>{ buildSrc(); newRound(); hide(); });
|
||||
labels(); newRound();
|
||||
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
|
||||
return { getState: ()=>({card:S.card, dur:S.dur, score:S.score, tries:S.tries}) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ function soundChunks(word, lang=LANG){
|
|||
Chop & blend widget — words in sounds, with read-aloud
|
||||
==========================================================*/
|
||||
function mountHak(root, initState){
|
||||
const S = { card:0, idx:0, order:[], chopped:false };
|
||||
if(initState) S.card = initState.card ?? 0;
|
||||
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, idx:0, order:[], chopped:false };
|
||||
if(initState){S.level=initState.level||1;S.yearGroup=initState.yearGroup||null;S.managedLearningLevel=!!initState.managedLearningLevel;S.card = initState.card ?? 0;}
|
||||
const canSpeak = "speechSynthesis" in window;
|
||||
|
||||
root.innerHTML = `
|
||||
|
|
@ -45,7 +45,7 @@ function mountHak(root, initState){
|
|||
const srcSel = el(".hk-src"), wordEl = el(".hk-word"),
|
||||
picEl = el(".hk-pic"), cntEl = el(".hk-cnt");
|
||||
|
||||
function list(){ S.card = normWordSource(S.card); return wordSourceList(S.card); }
|
||||
function list(){ S.card = normWordSource(S.card); return languageWordsForYear(wordSourceList(S.card),S.yearGroup,S.level); }
|
||||
function buildSrc(){ fillWordSourceSel(srcSel, S.card); }
|
||||
function cur(){ return list()[S.order[S.idx]]; }
|
||||
function newRound(){ S.order = shuffle(list().map((_,i)=>i)); S.idx = 0; }
|
||||
|
|
@ -107,5 +107,6 @@ function mountHak(root, initState){
|
|||
document.addEventListener("langchange", ()=>{ S.card=0; labels(); newRound(); S.chopped=false; display(); });
|
||||
document.addEventListener("wordschange", ()=>{ buildSrc(); newRound(); S.chopped=false; display(); });
|
||||
labels(); newRound(); display();
|
||||
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
|
||||
return { getState: ()=>({card:S.card}) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -647,12 +647,15 @@ function learningShell(root, settings, stage){
|
|||
function field(label, el){const w=document.createElement('label');const s=document.createElement('span');s.textContent=label;w.append(s,el);return w}
|
||||
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={}){
|
||||
const copy={...state,...extra};delete copy.yearGroup;delete copy.managedLearningLevel;return copy
|
||||
}
|
||||
|
||||
function mountFractions(root,init){
|
||||
const S=Object.assign({den:4,show:true,activeD:0,activeN:0},init||{}), den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show;
|
||||
const S=Object.assign({level:1,yearGroup:null,managedLearningLevel:false,den:4,show:true,activeD:0,activeN:0},init||{}),rules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&rules)S.den=rules.fractionDen;const den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show;den.disabled=!!S.managedLearningLevel;
|
||||
const gcd=(a,b)=>b?gcd(b,a%b):a,summary=()=>{if(!S.activeD)return'';const divisor=gcd(S.activeN,S.activeD),fraction=`${S.activeN}/${S.activeD}`,simple=`${S.activeN/divisor}/${S.activeD/divisor}`;return T('fractionSelected').replace('{fraction}',divisor>1?`${fraction} = ${simple}`:fraction)};
|
||||
const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;if(S.activeD>S.den){S.activeD=0;S.activeN=0}stage.className='fraction-game';stage.innerHTML='';const hint=document.createElement('p');hint.className='play-mission';hint.textContent=T('fractionHint');const result=document.createElement('output');result.className='fraction-summary';result.textContent=summary();const wall=document.createElement('div');wall.className='fraction-wall';for(let d=1;d<=S.den;d++){const row=document.createElement('div');for(let n=0;n<d;n++){const c=document.createElement('button');c.type='button';c.style.setProperty('--fraction-hue',d*31);c.textContent=S.show?`1/${d}`:'';c.className='fraction-piece';c.classList.toggle('selected',S.activeD===d&&n<S.activeN);c.setAttribute('aria-label',`${n+1}/${d}`);c.onclick=()=>{S.activeD=d;S.activeN=n+1;draw()};row.append(c)}wall.append(row)}const clear=btn(T('fractionClear'),()=>{S.activeD=0;S.activeN=0;draw()});clear.classList.add('fraction-clear');stage.append(hint,result,wall,clear)};
|
||||
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})};
|
||||
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);draw();return{getState:()=>persistedLearningState(S)};
|
||||
}
|
||||
const clockTurnDelta=(next,previous)=>((next-previous+540)%360)-180;
|
||||
const clockHourCrossing=(next,previous)=>{const delta=clockTurnDelta(next,previous);return delta>0&&next<previous?1:delta<0&&next>previous?-1:0};
|
||||
|
|
@ -660,11 +663,11 @@ const clockStepHour=(hour,step,use24)=>use24?(hour+step+24)%24:((hour-1+step+12)
|
|||
const clock24Angle=(angle,delta)=>(angle+delta+720)%720;
|
||||
const clock24Hour=(angle,minute)=>((Math.round((angle-minute/2)/30)%24)+24)%24;
|
||||
function mountClock(root,init){
|
||||
const S=Object.assign({h:12,m:0,level:1,style:'both',game:'set',amode:'blocks',input:'',linkHands:true,use24:true,task:true,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[],answerDigits:[null,null,null,null]},init||{});
|
||||
if(typeof S.level==='string')S.level={hour:1,quarter:2,five:2,minute:3}[S.level]||1;S.level=Math.max(1,Math.min(3,+S.level||1));S.amode=S.amode==='type'?'type':'blocks';S.input=String(S.input||'').replace(/\D/g,'').slice(0,4);
|
||||
const S=Object.assign({h:12,m:0,level:1,yearGroup:null,managedLearningLevel:false,style:'both',game:'set',amode:'blocks',input:'',linkHands:true,use24:true,task:true,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[],answerDigits:[null,null,null,null]},init||{});
|
||||
if(typeof S.level==='string')S.level={hour:1,quarter:2,five:2,minute:3}[S.level]||1;S.level=Math.max(1,Math.min(3,+S.level||1));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.clockLevel;S.amode=S.amode==='type'?'type':'blocks';S.input=String(S.input||'').replace(/\D/g,'').slice(0,4);
|
||||
const style=document.createElement('select'),game=document.createElement('select'),linkHands=document.createElement('input'),use24=document.createElement('input'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||||
add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);style.value=S.style;game.value=S.game;linkHands.type='checkbox';linkHands.checked=S.linkHands;linkHands.onchange=()=>{S.linkHands=linkHands.checked};use24.type='checkbox';use24.checked=S.use24;use24.onchange=()=>{S.use24=use24.checked;newTask()};const linkSetting=field(T('clockLinkHands'),linkHands),hour24Setting=field(T('clock24Hour'),use24),syncSettings=()=>{linkSetting.hidden=game.value!=='set';hour24Setting.hidden=S.level!==3};
|
||||
const minuteValues=()=>S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
|
||||
const minuteValues=()=>assignedRules?.clockMinutes||(S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i)),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
|
||||
const uses24=()=>S.level===3&&S.use24;
|
||||
const daypartKey=hour=>hour<6?'clockNight':hour<12?'clockMorning':hour<18?'clockAfternoon':'clockEvening',clockHour=hour=>hour%12||12;
|
||||
const words=(hour,minute)=>{const shown=clockHour(hour),next=shown===12?1:shown+1;let result;if(minute===0)result=T('clockWordsHour').replace('{hour}',shown);else if(minute===15)result=T('clockWordsQuarterPast').replace('{hour}',shown);else if(minute===30)result=T('clockWordsHalf').replace('{hour}',next);else if(minute===45)result=T('clockWordsQuarterTo').replace('{hour}',next);else result=T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?shown:next);return uses24()?result+' '+T(daypartKey(hour)):result};
|
||||
|
|
@ -711,13 +714,13 @@ function mountClock(root,init){
|
|||
if(S.task&&S.game==='read'){const layout=document.createElement('div');layout.className='clock-read-layout';layout.append(assignment(S.targetH,S.targetM),S.amode==='blocks'?blockAnswer():typeAnswer());card.append(layout)}
|
||||
if(S.task&&S.game==='choose'){card.append(assignment(S.targetH,S.targetM));if(!S.solved){const choices=document.createElement('div');choices.className='clock-choices '+(S.amode==='blocks'?'wood':'plain');const asWords=['digital','both'].includes(S.style);S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(asWords){const button=btn(words(option.h,option.m),choose);button.classList.add('clock-word-choice');choices.append(button)}else if(S.amode==='blocks')choices.append(timeBlocks(option.h,option.m,choose));else{const button=btn(time(option.h,option.m),choose);button.classList.add('clock-digital-choice');choices.append(button)}});card.append(choices)}}
|
||||
const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'ghost'));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)};
|
||||
style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLearningStyle'),style),field(T('clockGame'),game),linkSetting,hour24Setting],stage);syncSettings();newTask();return{getState:()=>({...S,choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})};
|
||||
style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLearningStyle'),style),field(T('clockGame'),game),linkSetting,hour24Setting],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(S,{choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})};
|
||||
}
|
||||
const PLACE_COLUMNS=['HD','TD','D','H','T','E'];
|
||||
const placeLevelDigits=level=>level===1?2:level===2?3:6;
|
||||
function mountPlacevalue(root,init){
|
||||
const S=Object.assign({number:1342,game:'explore',level:1,target:null,answerDigits:null,score:0,streak:0,solved:false,feedback:''},init||{});
|
||||
S.level=Math.max(1,Math.min(3,+S.level||1));S.game=S.game==='build'?'build':'explore';if(!Array.isArray(S.answerDigits))S.answerDigits=null;
|
||||
const S=Object.assign({number:1342,game:'explore',level:1,yearGroup:null,managedLearningLevel:false,target:null,answerDigits:null,score:0,streak:0,solved:false,feedback:''},init||{});
|
||||
S.level=Math.max(1,Math.min(3,+S.level||1));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.placeLevel;S.game=S.game==='build'?'build':'explore';if(!Array.isArray(S.answerDigits))S.answerDigits=null;
|
||||
const input=num(S.number,0,999999),game=document.createElement('select'),stage=document.createElement('div');
|
||||
game.add(new Option(T('placeGameExplore'),'explore'));game.add(new Option(T('placeGameBuild'),'build'));game.value=S.game;
|
||||
const gameSetting=field(T('placeGame'),game),numberSetting=field(T('placeNumber'),input),diceBtn=btn('🎲',()=>{input.value=Math.floor(Math.random()*999999);drawExplore()});
|
||||
|
|
@ -763,14 +766,14 @@ function mountPlacevalue(root,init){
|
|||
const actions=document.createElement('div');actions.className='place-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('placeNextTask'),newTask,'ghost'));card.append(actions);stage.append(card);
|
||||
};
|
||||
input.oninput=drawExplore;game.onchange=()=>{S.game=game.value;syncSettings();if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild()};
|
||||
learningShell(root,[gameSetting,numberSetting,diceBtn],stage);syncSettings();
|
||||
learningShell(root,[gameSetting,numberSetting,diceBtn],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();
|
||||
if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild();
|
||||
return{getState:()=>({...S,answerDigits:S.answerDigits?[...S.answerDigits]:null})};
|
||||
return{getState:()=>persistedLearningState(S,{answerDigits:S.answerDigits?[...S.answerDigits]:null})};
|
||||
}
|
||||
function mountMoney(root,init){
|
||||
const legacy=init||{},S=Object.assign({level:1,style:'both',game:'make',category:'all',productId:'juice',amode:'blocks',targetCents:375,paidCents:500,selected:[],given:[],input:'',answerDigits:[0,null,0,0],score:0,streak:0,solved:false,feedback:''},legacy);
|
||||
const legacy=init||{},S=Object.assign({level:1,yearGroup:null,managedLearningLevel:false,style:'both',game:'make',category:'all',productId:'juice',amode:'blocks',targetCents:375,paidCents:500,selected:[],given:[],input:'',answerDigits:[0,null,0,0],score:0,streak:0,solved:false,feedback:''},legacy);
|
||||
if(legacy.targetCents==null&&Number.isFinite(legacy.target))S.targetCents=Math.round(legacy.target*100);
|
||||
S.level=Math.max(1,Math.min(3,+S.level||1));S.style=['amount','words','both'].includes(S.style)?S.style:'both';S.game=['make','count','change'].includes(S.game)?S.game:'make';S.category=['all','food','school','play'].includes(S.category)?S.category:'all';S.amode=S.amode==='type'?'type':'blocks';S.selected=Array.isArray(S.selected)?S.selected.map(Number).filter(Number.isFinite):[];S.given=Array.isArray(S.given)?S.given.map(Number).filter(Number.isFinite):[];
|
||||
S.level=Math.max(1,Math.min(3,+S.level||1));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.moneyLevel;S.style=['amount','words','both'].includes(S.style)?S.style:'both';S.game=['make','count','change'].includes(S.game)?S.game:'make';S.category=['all','food','school','play'].includes(S.category)?S.category:'all';S.amode=S.amode==='type'?'type':'blocks';S.selected=Array.isArray(S.selected)?S.selected.map(Number).filter(Number.isFinite):[];S.given=Array.isArray(S.given)?S.given.map(Number).filter(Number.isFinite):[];
|
||||
const style=document.createElement('select'),game=document.createElement('select'),category=document.createElement('select'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||||
add(style,[['amount','moneyStyleAmount'],['words','moneyStyleWords'],['both','moneyStyleBoth']]);add(game,[['make','moneyGameMake'],['count','moneyGameCount'],['change','moneyGameChange']]);add(category,[['all','moneyCategoryAll'],['food','moneyCategoryFood'],['school','moneyCategorySchool'],['play','moneyCategoryPlay']]);style.value=S.style;game.value=S.game;category.value=S.category;
|
||||
const styleSetting=field(T('moneyLearningStyle'),style),gameSetting=field(T('moneyGame'),game),categorySetting=field(T('moneyCategory'),category),syncSettings=()=>{styleSetting.hidden=S.game==='count';categorySetting.hidden=S.game!=='change'};
|
||||
|
|
@ -810,12 +813,12 @@ function mountMoney(root,init){
|
|||
if(S.game==='count'){const target=document.createElement('div');target.className='money-count-task';target.append(pile(S.given));card.append(target,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||||
if(S.game==='change'){const item=currentProduct(),shop=document.createElement('div');shop.className='money-shop-task';const product=document.createElement('div');product.className='money-product';const emoji=document.createElement('span'),productName=document.createElement('strong');emoji.className='money-product-icon';emoji.textContent=item.icon;productName.className='money-product-name';productName.textContent=LANG==='nl'?item.nl:item.en;const priceLabel=document.createElement('b');priceLabel.textContent=T('moneyPrice');product.append(emoji,productName,priceLabel,amountDisplay(S.targetCents,true));const paid=document.createElement('div');paid.className='money-paid';const paidLabel=document.createElement('b');paidLabel.textContent=T('moneyPaid');const paidAmount=document.createElement('output');paidAmount.textContent=format(S.paidCents);paid.append(paidLabel,paidAmount,pile([S.paidCents]));shop.append(product,paid);card.append(shop,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||||
const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)};
|
||||
style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[styleSetting,gameSetting,categorySetting],stage);syncSettings();newTask();return{getState:()=>({...S,selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})};
|
||||
style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[styleSetting,gameSetting,categorySetting],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(S,{selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})};
|
||||
}
|
||||
|
||||
function mountWordtypes(root,init){
|
||||
const S=Object.assign({sentence:LANG==='nl'?'De vrolijke hond rent snel':'The cheerful dog runs quickly',answers:{}},init||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence;
|
||||
const types=['noun','verb','adjective','other'],draw=()=>{S.sentence=input.value.slice(0,120);stage.className='wordtypes';stage.innerHTML='';S.sentence.split(/\s+/).filter(Boolean).forEach((word,i)=>{const w=document.createElement('button');w.type='button';w.textContent=word;const type=S.answers[i]||'other';w.dataset.type=type;w.onclick=()=>{S.answers[i]=types[(types.indexOf(w.dataset.type)+1)%types.length];draw()};stage.append(w)});const legend=document.createElement('p');legend.textContent=T('wordtypeHint');stage.append(legend)};input.oninput=draw;learningShell(root,[field(T('wordtypeSentence'),input)],stage);draw();return{getState:()=>({...S})};
|
||||
const year=learningYearGroup(init?.yearGroup),defaultSentence=year&&year<=3?(LANG==='nl'?'De hond rent':'The dog runs'):year&&year>=7?(LANG==='nl'?'De zorgvuldige onderzoeker beoordeelt de bron kritisch':'The careful researcher assesses the source critically'):(LANG==='nl'?'De vrolijke hond rent snel':'The cheerful dog runs quickly'),savedSentence=init?.sentence,isLegacyDefault=['De vrolijke hond rent snel','The cheerful dog runs quickly'].includes(savedSentence),base=init?.managedLearningLevel&&isLegacyDefault?{...init,sentence:defaultSentence}:init,S=Object.assign({level:1,yearGroup:null,managedLearningLevel:false,sentence:defaultSentence,answers:{}},base||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence;
|
||||
const rules=languageTaskRules(S.yearGroup,S.level),allTypes=['noun','verb','adjective','other'],types=rules?allTypes.slice(0,rules.wordtypeCount):allTypes,draw=()=>{S.sentence=input.value.slice(0,120);stage.className='wordtypes';stage.innerHTML='';S.sentence.split(/\s+/).filter(Boolean).forEach((word,i)=>{const w=document.createElement('button');w.type='button';w.textContent=word;const type=S.answers[i]||'other';w.dataset.type=type;w.onclick=()=>{S.answers[i]=types[(types.indexOf(w.dataset.type)+1)%types.length];draw()};stage.append(w)});const legend=document.createElement('p');legend.textContent=T('wordtypeHint');stage.append(legend)};input.oninput=draw;learningShell(root,[field(T('wordtypeSentence'),input)],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);draw();return{getState:()=>persistedLearningState(S)};
|
||||
}
|
||||
function mountSchedule(root,init){
|
||||
const defaults=LANG==='nl'?[{time:'08:30',text:'Start'},{time:'10:15',text:'Pauze'}]:[{time:'08:30',text:'Start'},{time:'10:15',text:'Break'}],S=Object.assign({items:defaults},init||{}),time=document.createElement('input'),text=document.createElement('input'),stage=document.createElement('div');time.type='time';time.value='09:00';text.placeholder=T('scheduleActivity');
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
function mountLetterGame(root, initState, opts){
|
||||
const readonly = !!(opts && opts.readonly);
|
||||
const onProgress = opts && opts.onProgress;
|
||||
const S = { level:1, card:0, wordIdx:0, stars:0, lang:LANG, count:6, order:[], snd:true, amode:"blocks" };
|
||||
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, wordIdx:0, stars:0, lang:LANG, count:6, order:[], snd:true, amode:"blocks" };
|
||||
if(initState){
|
||||
S.level = initState.level||1;
|
||||
S.yearGroup = initState.yearGroup||null;
|
||||
S.managedLearningLevel = !!initState.managedLearningLevel;
|
||||
S.card = initState.card ?? "g";
|
||||
S.count = initState.count||6;
|
||||
S.snd = initState.snd !== false;
|
||||
|
|
@ -167,6 +169,7 @@ function mountLetterGame(root, initState, opts){
|
|||
|
||||
root.querySelectorAll(".lg-levels button").forEach(b=>{
|
||||
b.addEventListener("click", ()=>{ S.level = +b.dataset.l; newRound(); render(); });
|
||||
b.disabled = S.managedLearningLevel;
|
||||
});
|
||||
nextBtn.addEventListener("click", ()=>{
|
||||
S.wordIdx++;
|
||||
|
|
@ -182,7 +185,7 @@ function mountLetterGame(root, initState, opts){
|
|||
|
||||
function fullList(){
|
||||
S.card = normWordSource(S.card);
|
||||
return wordSourceList(S.card);
|
||||
return languageWordsForYear(wordSourceList(S.card),S.yearGroup,S.level);
|
||||
}
|
||||
function newRound(){
|
||||
const list = fullList();
|
||||
|
|
@ -751,13 +754,16 @@ function mountLetterGame(root, initState, opts){
|
|||
}
|
||||
|
||||
buildNumSel();
|
||||
const assignedRules=languageTaskRules(S.yearGroup,S.level);
|
||||
if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.letterLevel;
|
||||
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
|
||||
updateSndBtn();
|
||||
newRound();
|
||||
render();
|
||||
document.addEventListener("langchange", updateSndBtn);
|
||||
return {
|
||||
getState: ()=>({ level:S.level, card:S.card, count:S.count, snd:S.snd, amode:S.amode }),
|
||||
setState: st=>{ if(st){ S.level=st.level||1; S.card=st.card??0; S.count=st.count||6; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; buildNumSel(); updateSndBtn(); newRound(); render(); } }
|
||||
setState: st=>{ if(st){ S.level=st.level||1; S.yearGroup=st.yearGroup||null; S.managedLearningLevel=!!st.managedLearningLevel; const rules=languageTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&rules)S.level=rules.letterLevel; S.card=st.card??0; S.count=st.count||6; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; buildNumSel(); updateSndBtn(); newRound(); render(); } }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -775,4 +781,3 @@ function confetti(container){
|
|||
setTimeout(()=>c.remove(), 3200);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@
|
|||
function mountMath(root, initState, mode, opts){
|
||||
const ROUND = 10;
|
||||
const onProgress = opts && opts.onProgress;
|
||||
const S = { level:1, yearGroup:null, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false };
|
||||
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. */
|
||||
S.yearGroup = initState.yearGroup || null;
|
||||
S.managedLearningLevel = !!initState.managedLearningLevel;
|
||||
S.snd = initState.snd!==false;
|
||||
S.amode = initState.amode||"blocks";
|
||||
}
|
||||
|
|
@ -171,28 +172,29 @@ function mountMath(root, initState, mode, opts){
|
|||
klassenmanagement ingesteld voor deze leerling/dit bord) blijft het
|
||||
oude gedrag exact hetzelfde. */
|
||||
const slo = mathRangeFor(S.yearGroup, L);
|
||||
const rules = mathTaskRules(S.yearGroup, L);
|
||||
let a, b, ans, txt;
|
||||
if(mode==="add"){
|
||||
const max = slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100);
|
||||
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
|
||||
a = rnd(max+1); b = rnd(max-a+1); ans = a+b;
|
||||
txt = `${a} + ${b} =`;
|
||||
}else if(mode==="sub"){
|
||||
const max = slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100);
|
||||
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
|
||||
a = rnd(max+1); b = rnd(a+1); ans = a-b;
|
||||
txt = `${a} − ${b} =`;
|
||||
}else if(mode==="mul"){
|
||||
const tmax = slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12);
|
||||
a = 1+rnd(tmax); b = 1+rnd(L===3 ? 12 : 10); ans = a*b;
|
||||
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
|
||||
a = 1+rnd(tmax); b = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10)); ans = a*b;
|
||||
txt = `${a} × ${b} =`;
|
||||
}else if(mode==="div"){
|
||||
const tmax = slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12);
|
||||
const t = 1+rnd(tmax), q = 1+rnd(L===3 ? 12 : 10);
|
||||
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
|
||||
const t = 1+rnd(tmax), q = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10));
|
||||
a = t*q; ans = q;
|
||||
txt = `${a} ${S.lang==="nl" || LANG==="nl" ? ":" : "÷"} ${t} =`;
|
||||
}else{ /* pct */
|
||||
const sets = L===1 ? [10,50,100] : L===2 ? [10,20,25,50,75,100] : [5,10,20,25,30,40,50,60,70,75,80,90];
|
||||
const sets = rules ? rules.percentSets : (L===1 ? [10,50,100] : L===2 ? [10,20,25,50,75,100] : [5,10,20,25,30,40,50,60,70,75,80,90]);
|
||||
const p = sets[rnd(sets.length)];
|
||||
const step = {5:20,10:10,20:5,25:4,30:10,40:5,50:2,60:5,70:10,75:4,80:5,90:10,100:1}[p];
|
||||
const step = {5:20,10:10,15:20,20:5,25:4,30:10,40:5,50:2,60:5,70:10,75:4,80:5,90:10,100:1}[p];
|
||||
const base = step*(1+rnd(12));
|
||||
ans = p*base/100;
|
||||
txt = `${p}% ${T("mOf")} ${base} =`;
|
||||
|
|
@ -270,7 +272,7 @@ function mountMath(root, initState, mode, opts){
|
|||
if(doneEl.classList.contains("show")) return;
|
||||
if(k==="⌫"){ if(!S.busy){ S.input = S.input.slice(0,-1); aEl.textContent = S.input; } }
|
||||
else if(k==="✓") check();
|
||||
else if(!S.busy && S.input.length<4){ S.input += k; aEl.textContent = S.input; }
|
||||
else if(!S.busy && S.input.length<8){ S.input += k; aEl.textContent = S.input; }
|
||||
});
|
||||
padEl.appendChild(b);
|
||||
});
|
||||
|
|
@ -281,6 +283,7 @@ function mountMath(root, initState, mode, opts){
|
|||
newQuestion();
|
||||
});
|
||||
root.querySelectorAll(".lg-levels button").forEach(b=>{
|
||||
b.disabled = S.managedLearningLevel;
|
||||
b.addEventListener("click", ()=>{
|
||||
S.level = +b.dataset.l;
|
||||
S.done = 0; S.good = 0;
|
||||
|
|
@ -316,9 +319,9 @@ function mountMath(root, initState, mode, opts){
|
|||
|
||||
labels();
|
||||
newQuestion();
|
||||
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
|
||||
return {
|
||||
getState: ()=>({ level:S.level, snd:S.snd, amode:S.amode }),
|
||||
setState: st=>{ if(st){ S.level=st.level||1; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
|
||||
setState: st=>{ if(st){ S.level=st.level||1;S.yearGroup=st.yearGroup||null;S.managedLearningLevel=!!st.managedLearningLevel; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/* widget: interactieve getallenlijn */
|
||||
function mountNumberline(root, initState){
|
||||
const S=Object.assign({min:0,max:20,step:1,value:0,target:10,score:0,streak:0,solved:false},initState||{});
|
||||
const S=Object.assign({level:1,yearGroup:null,managedLearningLevel:false,min:0,max:20,step:1,value:0,target:10,score:0,streak:0,solved:false},initState||{});
|
||||
const rules=mathTaskRules(S.yearGroup,S.level);
|
||||
if(S.managedLearningLevel&&rules){S.min=rules.numberlineMin;S.max=rules.numberlineMax;S.step=rules.numberlineStep;S.value=S.min;S.target=S.min+S.step;}
|
||||
root.innerHTML=`
|
||||
<div class="nlw">
|
||||
<div class="nlw-settings">
|
||||
<label><span class="nlw-from-label"></span><input class="nlw-min" type="number" min="-100" max="99"></label>
|
||||
<label><span class="nlw-to-label"></span><input class="nlw-max" type="number" min="-99" max="200"></label>
|
||||
<label><span class="nlw-from-label"></span><input class="nlw-min" type="number" min="-1000000" max="999999"></label>
|
||||
<label><span class="nlw-to-label"></span><input class="nlw-max" type="number" min="-999999" max="1000000"></label>
|
||||
<label><span class="nlw-step-label"></span><select class="nlw-step"><option>1</option><option>2</option><option>5</option><option>10</option></select></label>
|
||||
<button class="tbtn ghost nlw-new" type="button"></button>
|
||||
</div>
|
||||
|
|
@ -21,9 +23,11 @@ function mountNumberline(root, initState){
|
|||
</div>`;
|
||||
const q=s=>root.querySelector(s), minEl=q('.nlw-min'), maxEl=q('.nlw-max'), stepEl=q('.nlw-step'),
|
||||
line=q('.nlw-line'), valueEl=q('.nlw-value'), task=q('.nlw-task'), feedback=q('.nlw-feedback'),score=q('.nlw-score');
|
||||
if(![...stepEl.options].some(o=>+o.value===S.step))stepEl.add(new Option(S.step,S.step));
|
||||
minEl.value=S.min; maxEl.value=S.max; stepEl.value=S.step;
|
||||
[minEl,maxEl,stepEl].forEach(el=>el.disabled=!!S.managedLearningLevel);
|
||||
function normalize(){
|
||||
let min=Math.max(-100,Math.min(99,+minEl.value||0)); let max=Math.max(-99,Math.min(200,+maxEl.value||20));
|
||||
let min=Math.max(-1000000,Math.min(999999,+minEl.value||0)); let max=Math.max(-999999,Math.min(1000000,+maxEl.value||20));
|
||||
if(max<=min) max=min+1; const step=+stepEl.value||1;
|
||||
if((max-min)/step>60) max=min+60*step;
|
||||
S.min=min; S.max=max; S.step=step;
|
||||
|
|
@ -62,6 +66,7 @@ function mountNumberline(root, initState){
|
|||
q('.nlw-step-label').textContent=T('numberlineStep'); q('.nlw-new').textContent=T('numberlineNew');
|
||||
q('.nlw-check').textContent=T('numberlineCheck'); render();
|
||||
}
|
||||
root.classList.toggle("managed-learning-level",!!S.managedLearningLevel);
|
||||
document.addEventListener('langchange',labels); labels(); normalize();
|
||||
return {getState:()=>({...S})};
|
||||
return {getState:()=>({min:S.min,max:S.max,step:S.step,value:S.value,target:S.target,score:S.score,streak:S.streak,solved:S.solved})};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,27 @@
|
|||
/* widget: zinsbouwer met aanpasbare woordbanken */
|
||||
function mountSentence(root, initState){
|
||||
const defaults=LANG==='nl' ? {
|
||||
const year=learningYearGroup(initState?.yearGroup);
|
||||
const early=year&&year<=3,advanced=year&&year>=7;
|
||||
const defaults=LANG==='nl' ? (early ? {
|
||||
who:['De hond','Ik','Sam','De juf'],does:['leest','speelt','maakt','ziet'],what:['een boek','buiten','een huis','de bal'],
|
||||
} : advanced ? {
|
||||
who:['Hoewel de bron','De schrijver','Onze klas','Volgens de onderzoeker'],does:['vergelijkt','beargumenteert','verklaart','beoordeelt'],what:['de informatie kritisch','twee standpunten','de uitkomst zorgvuldig','het bewijs in de tekst'],
|
||||
} : {
|
||||
who:['De hond','Mijn vriend','De juf','Sara'], does:['lees','speelt','maakt','ziet'],
|
||||
what:['een boek','op het plein','een mooie tekening','de rode bal'],
|
||||
}) : (early ? {
|
||||
who:['The dog','I','Sam','The teacher'],does:['reads','plays','makes','sees'],what:['a book','outside','a house','the ball'],
|
||||
} : advanced ? {
|
||||
who:['Although the source','The writer','Our class','According to the researcher'],does:['compares','argues','explains','assesses'],what:['the information critically','two viewpoints','the result carefully','the evidence in the text'],
|
||||
} : {
|
||||
who:['The dog','My friend','The teacher','Sarah'], does:['reads','plays','makes','sees'],
|
||||
what:['a book','in the playground','a beautiful drawing','the red ball'],
|
||||
};
|
||||
const S={banks:JSON.parse(JSON.stringify(initState?.banks||defaults)),sentence:[...(initState?.sentence||[])]};
|
||||
});
|
||||
const legacyNl={who:['De hond','Mijn vriend','De juf','Sara'],does:['lees','speelt','maakt','ziet'],what:['een boek','op het plein','een mooie tekening','de rode bal']};
|
||||
const legacyEn={who:['The dog','My friend','The teacher','Sarah'],does:['reads','plays','makes','sees'],what:['a book','in the playground','a beautiful drawing','the red ball']};
|
||||
const savedBanks=initState?.banks,isLegacyDefault=savedBanks&&[legacyNl,legacyEn].some(bank=>JSON.stringify(bank)===JSON.stringify(savedBanks));
|
||||
const chosenBanks=initState?.managedLearningLevel&&isLegacyDefault?defaults:(savedBanks||defaults);
|
||||
const S={banks:JSON.parse(JSON.stringify(chosenBanks)),sentence:[...(initState?.sentence||[])]};
|
||||
root.innerHTML=`
|
||||
<div class="sb">
|
||||
<div class="sb-sentence" aria-live="polite"></div>
|
||||
|
|
@ -53,6 +67,7 @@ function mountSentence(root, initState){
|
|||
[...kind.options].forEach(o=>o.textContent=T('sentence_'+o.value)); word.placeholder=T('sentenceAdd');
|
||||
q('.sb-surprise').textContent=T('sentenceSurprise');q('.sb-clear').textContent=T('sentenceClear'); q('.sb-speak').textContent=T('sentenceSpeak'); renderSentence();renderBanks();
|
||||
}
|
||||
root.classList.toggle("managed-learning-level",!!initState?.managedLearningLevel);
|
||||
document.addEventListener('langchange',labels);labels();
|
||||
return {getState:()=>({banks:S.banks,sentence:S.sentence})};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,25 @@
|
|||
tule.appendChild(sourceLinks);
|
||||
root.appendChild(tule);
|
||||
|
||||
const profiles=node("details","wiki-tule wiki-year-profiles");
|
||||
profiles.appendChild(node("summary",null,T("wikiYearProfiles")));
|
||||
profiles.appendChild(node("p",null,T("wikiYearProfilesText")));
|
||||
const profileGrid=node("div","wiki-profile-grid");
|
||||
[["taal","📖"],["rekenen","🔢"]].forEach(([subject,icon])=>{
|
||||
const column=node("section","wiki-profile-column");
|
||||
column.appendChild(node("h3",null,icon+" "+(subject==="taal"?T("parentLevelTaal"):T("parentLevelRekenen"))));
|
||||
LEARNING_YEAR_PROFILES[subject].slice(1).forEach((profile,index)=>{
|
||||
const row=node("article","wiki-profile-row");
|
||||
row.appendChild(node("strong",null,T("wikiYear").replace("{n}",index+1)));
|
||||
row.appendChild(node("span","wiki-profile-title",profile.title));
|
||||
row.appendChild(node("small",null,T("wikiProfileGoals")+": "+profile.goals.join(" · ")));
|
||||
column.appendChild(row);
|
||||
});
|
||||
profileGrid.appendChild(column);
|
||||
});
|
||||
profiles.appendChild(profileGrid);
|
||||
root.appendChild(profiles);
|
||||
|
||||
if(LANG==="en")root.appendChild(node("p","wiki-language-note",T("wikiLanguageNote")));
|
||||
|
||||
const searchWrap=node("div","wiki-search");
|
||||
|
|
@ -187,7 +206,7 @@
|
|||
visible:()=>!!currentUser&&can("curriculum.view"),
|
||||
activate(payload){showSettingsDom("curriculum");renderCurriculumWiki(payload);},
|
||||
keywords:()=>[
|
||||
T("wikiTitle"),T("wikiSub"),"SLO","kerndoelen","curriculum","leerlijnen","TULE",
|
||||
T("wikiTitle"),T("wikiSub"),T("wikiYearProfiles"),"SLO","kerndoelen","curriculum","leerlijnen","TULE",
|
||||
...CURRICULUM_PO_2026.areas.flatMap(area=>[area.title,area.description])
|
||||
],
|
||||
searchItems:needle=>{
|
||||
|
|
|
|||
55
src/api.js
55
src/api.js
|
|
@ -932,7 +932,7 @@ export default async function api(app) {
|
|||
const yg = Number(yearGroup);
|
||||
if (!Number.isInteger(yg) || yg < 1 || yg > 8) return fail(reply, 400, 'ongeldige groep');
|
||||
const lvl = Number(level);
|
||||
if (!Number.isInteger(lvl) || lvl < 1) return fail(reply, 400, 'ongeldig niveau');
|
||||
if (!Number.isInteger(lvl) || lvl < 1 || lvl > 3) return fail(reply, 400, 'ongeldig niveau');
|
||||
return { subject, yearGroup: yg, level: lvl };
|
||||
};
|
||||
|
||||
|
|
@ -1034,6 +1034,24 @@ export default async function api(app) {
|
|||
return row ? describePupilLevel(row) : { yearGroup: null, level: 1 };
|
||||
});
|
||||
|
||||
// Alleen een leerling zonder school beheert het eigen niveau. Zodra een
|
||||
// school het account adopteert, neemt de klas/leerlinginstelling van de
|
||||
// leerkracht het automatisch over en wordt zelf wijzigen geweigerd.
|
||||
app.put('/my/level', async (req, reply) => {
|
||||
need(req, reply);
|
||||
if (req.user.role !== 'pupil') return fail(reply, 403, 'alleen voor leerlingen');
|
||||
if (req.user.school_id != null) return fail(reply, 403, 'het leerniveau wordt door de school beheerd');
|
||||
const body = parseLevelBody(req, reply);
|
||||
if (body.error) return body;
|
||||
if (!['taal', 'rekenen'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
await pool.query(
|
||||
`INSERT INTO pupil_levels (pupil_id, subject, year_group, level) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (pupil_id, subject) WHERE pupil_id IS NOT NULL
|
||||
DO UPDATE SET year_group = $3, level = $4, updated_at = now()`,
|
||||
[req.user.id, body.subject, body.yearGroup, body.level]);
|
||||
return { ok: true, level: { yearGroup: body.yearGroup, level: body.level } };
|
||||
});
|
||||
|
||||
// ---- voortgang (leerling-omgeving) ---------------------------------------------
|
||||
// Leerlingen loggen elk afgerond "moment" in een widget (ronde sommen, voltooid
|
||||
// woord, geknapte ballon). teacher_id/board_id komen server-side uit de effectieve
|
||||
|
|
@ -1185,6 +1203,38 @@ export default async function api(app) {
|
|||
})) };
|
||||
});
|
||||
|
||||
app.get('/parent/children/:id/levels', async (req, reply) => {
|
||||
needParent(req, reply);
|
||||
if (!(await parentLinked(req.user.id, req.params.id))) return fail(reply, 403, 'geen rechten');
|
||||
const child = (await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id])).rows[0];
|
||||
if (!child || child.role !== 'pupil') return fail(reply, 404, 'kind onbekend');
|
||||
if (child.school_id != null) return { schoolManaged: true, levels: {} };
|
||||
const rows = (await pool.query(
|
||||
"SELECT * FROM pupil_levels WHERE pupil_id = $1 AND subject IN ('taal','rekenen')",
|
||||
[child.id])).rows;
|
||||
return {
|
||||
schoolManaged: false,
|
||||
levels: Object.fromEntries(rows.map((row) => [row.subject, describePupilLevel(row)])),
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/parent/children/:id/level', async (req, reply) => {
|
||||
needParent(req, reply);
|
||||
if (!(await parentLinked(req.user.id, req.params.id))) return fail(reply, 403, 'geen rechten');
|
||||
const child = (await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id])).rows[0];
|
||||
if (!child || child.role !== 'pupil') return fail(reply, 404, 'kind onbekend');
|
||||
if (child.school_id != null) return fail(reply, 403, 'het leerniveau wordt door de school beheerd');
|
||||
const body = parseLevelBody(req, reply);
|
||||
if (body.error) return body;
|
||||
if (!['taal', 'rekenen'].includes(body.subject)) return fail(reply, 400, 'ongeldig vakgebied');
|
||||
await pool.query(
|
||||
`INSERT INTO pupil_levels (pupil_id, subject, year_group, level) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (pupil_id, subject) WHERE pupil_id IS NOT NULL
|
||||
DO UPDATE SET year_group = $3, level = $4, updated_at = now()`,
|
||||
[child.id, body.subject, body.yearGroup, body.level]);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// Kind aanmaken: pupil zonder school (thuiskind), meteen gekoppeld.
|
||||
// Het gegenereerde wachtwoord wordt eenmalig teruggegeven.
|
||||
app.post('/parent/children', async (req, reply) => {
|
||||
|
|
@ -1329,6 +1379,9 @@ export default async function api(app) {
|
|||
if (e.code === '23505') return fail(reply, 409, 'gebruikersnaam bestaat al binnen deze school');
|
||||
throw e;
|
||||
}
|
||||
// De thuiskeuze mag na adoptie niet als leerling-override boven de
|
||||
// klasinstelling blijven hangen; vanaf nu bepaalt de leerkracht dit.
|
||||
await pool.query('DELETE FROM pupil_levels WHERE pupil_id = $1', [r.pupil_id]);
|
||||
// het kind hoort nu bij deze school: ook aanmeldingen bij andere
|
||||
// scholen zijn daarmee vervallen
|
||||
await pool.query('DELETE FROM school_link_requests WHERE pupil_id = $1', [r.pupil_id]);
|
||||
|
|
|
|||
74
test/learning-progressions.test.js
Normal file
74
test/learning-progressions.test.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {runInNewContext} from 'node:vm';
|
||||
|
||||
async function progressionContext(){
|
||||
const source=await readFile('public/js/learning-progressions.js','utf8');
|
||||
const pure=source.slice(0,source.indexOf('(function registerOwnLearningLevelSettings'));
|
||||
const context={LANG:'nl'};
|
||||
runInNewContext(pure+`
|
||||
;globalThis.__profiles=LEARNING_YEAR_PROFILES;
|
||||
globalThis.__languageWordsForYear=languageWordsForYear;
|
||||
globalThis.__languageTaskRules=languageTaskRules;
|
||||
globalThis.__mathTaskRules=mathTaskRules;
|
||||
`,context);
|
||||
return context;
|
||||
}
|
||||
|
||||
test('taal en rekenen hebben een compleet inhoudelijk profiel voor groep 1 tot en met 8',async()=>{
|
||||
const {__profiles:profiles}=await progressionContext();
|
||||
for(const subject of ['taal','rekenen']){
|
||||
assert.equal(profiles[subject].length,9);
|
||||
for(let year=1;year<=8;year++){
|
||||
const profile=profiles[subject][year];
|
||||
assert.ok(profile.phase&&profile.title,subject+' groep '+year);
|
||||
assert.equal(profile.goals.length,3,subject+' groep '+year+' heeft drie concrete accenten');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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'],
|
||||
['brandweerauto','x'],['onderzoeker','x'],['verantwoordelijkheid','x']];
|
||||
const early=JSON.parse(JSON.stringify(c.__languageWordsForYear(words,2,2)));
|
||||
const late=JSON.parse(JSON.stringify(c.__languageWordsForYear(words,8,2)));
|
||||
assert.ok(early.length<late.length);
|
||||
assert.ok(early.every(([word])=>!['brandweerauto','verantwoordelijkheid'].includes(word)));
|
||||
assert.equal(c.__languageTaskRules(2,2).flashSeconds,3);
|
||||
assert.equal(c.__languageTaskRules(7,2).flashSeconds,.5);
|
||||
assert.ok(c.__languageTaskRules(2,2).lives>c.__languageTaskRules(7,2).lives);
|
||||
});
|
||||
|
||||
test('rekenprofiel vergroot getalgebied, tafels en breuken per leerjaar',async()=>{
|
||||
const c=await progressionContext();
|
||||
assert.equal(c.__mathTaskRules(2,3).addSub,20);
|
||||
assert.equal(c.__mathTaskRules(3,1).timesTable,2);
|
||||
assert.equal(c.__mathTaskRules(3,3).timesTable,10);
|
||||
assert.equal(c.__mathTaskRules(4,3).addSub,100);
|
||||
assert.equal(c.__mathTaskRules(6,3).addSub,10000);
|
||||
assert.equal(c.__mathTaskRules(1,2).fractionDen,2);
|
||||
assert.equal(c.__mathTaskRules(8,3).fractionDen,12);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(c.__mathTaskRules(3,2).clockMinutes)),[0,15,30,45]);
|
||||
});
|
||||
|
||||
test('alle taal- en rekenwidgets ontvangen de centrale leerjaarstate en tonen het profiel',async()=>{
|
||||
const [html,board,pupil,progressions,letters,flits,hak,ball,math,numberline,learning]=await Promise.all([
|
||||
readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8'),
|
||||
readFile('public/js/pupil.js','utf8'),readFile('public/js/learning-progressions.js','utf8'),
|
||||
...['letters','flits','hak','ball','math','numberline','learning'].map(name=>readFile('public/js/widgets/'+name+'.js','utf8')),
|
||||
]);
|
||||
assert.ok(html.indexOf('js/data.js')<html.indexOf('js/learning-progressions.js'));
|
||||
assert.ok(html.indexOf('js/learning-progressions.js')<html.indexOf('js/widgets/letters.js'));
|
||||
assert.match(board,/learningStateForWidget/);
|
||||
assert.match(pupil,/applyLearningLevelToState/);
|
||||
assert.match(pupil,/appendLearningProfileBadge/);
|
||||
assert.match(progressions,/managedLearningLevel/);
|
||||
for(const source of [letters,flits,hak,ball])assert.match(source,/languageWordsForYear/);
|
||||
assert.match(math,/mathTaskRules/);
|
||||
assert.match(numberline,/mathTaskRules/);
|
||||
for(const mount of ['mountFractions','mountClock','mountPlacevalue','mountMoney','mountWordtypes']){
|
||||
assert.ok(learning.includes('function '+mount),mount);
|
||||
}
|
||||
});
|
||||
|
|
@ -85,6 +85,43 @@ test('ouder ziet gekoppelde kinderen met school en aanmeldstatus', async () => {
|
|||
await app.close();
|
||||
});
|
||||
|
||||
test('ouder leest en wijzigt het leerniveau van een gekoppeld thuiskind', async () => {
|
||||
const respond = (sql) => {
|
||||
if (sql.includes('FROM parent_children WHERE parent_id')) return { rows: [{ 1: 1 }] };
|
||||
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [homeChild] };
|
||||
if (sql.includes("subject IN ('taal','rekenen')")) return { rows: [
|
||||
{ subject:'taal',year_group:3,level:2 },{ subject:'rekenen',year_group:4,level:1 },
|
||||
] };
|
||||
return undefined;
|
||||
};
|
||||
const {app,calls}=await makeApp({user:parent,respond});
|
||||
const get=await app.inject({method:'GET',url:'/api/parent/children/30/levels',cookies});
|
||||
assert.equal(get.statusCode,200,get.body);
|
||||
assert.deepEqual(get.json().levels,{taal:{yearGroup:3,level:2},rekenen:{yearGroup:4,level:1}});
|
||||
const put=await app.inject({method:'PUT',url:'/api/parent/children/30/level',cookies,
|
||||
payload:{subject:'rekenen',yearGroup:5,level:3}});
|
||||
assert.equal(put.statusCode,200,put.body);
|
||||
const insert=calls.find(call=>call.sql.startsWith('INSERT INTO pupil_levels'));
|
||||
assert.deepEqual(insert.params,[30,'rekenen',5,3]);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('ouder kan schoolniveau of niveau van een vreemd kind niet overschrijven', async () => {
|
||||
const {app}=await makeApp({user:parent,respond:(sql)=>{
|
||||
if(sql.includes('FROM parent_children WHERE parent_id'))return{rows:[{1:1}]};
|
||||
if(sql.startsWith('SELECT * FROM users WHERE id'))return{rows:[{...homeChild,school_id:2}]};
|
||||
return undefined;
|
||||
}});
|
||||
const school=await app.inject({method:'PUT',url:'/api/parent/children/30/level',cookies,
|
||||
payload:{subject:'taal',yearGroup:4,level:2}});
|
||||
assert.equal(school.statusCode,403,school.body);
|
||||
const {app:other}=await makeApp({user:parent});
|
||||
const strange=await other.inject({method:'PUT',url:'/api/parent/children/99/level',cookies,
|
||||
payload:{subject:'taal',yearGroup:4,level:2}});
|
||||
assert.equal(strange.statusCode,403,strange.body);
|
||||
await app.close();await other.close();
|
||||
});
|
||||
|
||||
test('parent-routes zijn niet voor andere rollen', async () => {
|
||||
const { app } = await makeApp({ user: admin });
|
||||
const res = await app.inject({ method: 'GET', url: '/api/parent/children', cookies });
|
||||
|
|
@ -213,6 +250,8 @@ test('adopteren zet school en klas op het thuisaccount en ruimt verzoeken op', a
|
|||
assert.equal(res.statusCode, 200, res.body);
|
||||
const upd = calls.find((c) => c.sql.includes('UPDATE users SET school_id'));
|
||||
assert.deepEqual(upd.params, [2, 5, null, 30]);
|
||||
assert.ok(calls.some((c) => c.sql.startsWith('DELETE FROM pupil_levels')),
|
||||
'de oude thuiskeuze vervalt wanneer de leerkracht de regie krijgt');
|
||||
assert.ok(calls.some((c) => c.sql.startsWith('DELETE FROM school_link_requests WHERE pupil_id')));
|
||||
await app.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const cookies = { teach_session: 'x'.repeat(64) };
|
|||
const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
||||
const admin = { id: 4, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
||||
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 100, data: {}, data_rev: 0 };
|
||||
const homePupil = { id: 10, username: 'thuis', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 };
|
||||
|
||||
test('klassenmanagement: klas-standaard opslaan doet de juiste upsert-query', async () => {
|
||||
const { app, calls } = await makeApp(admin);
|
||||
|
|
@ -74,6 +75,15 @@ test('klassenmanagement: groep buiten 1-8 geeft 400', async () => {
|
|||
assert.equal(res.statusCode, 400, res.body);
|
||||
});
|
||||
|
||||
test('klassenmanagement: alleen de drie beschreven uitwerkingen zijn geldig', async () => {
|
||||
const { app } = await makeApp(teacher);
|
||||
const res = await app.inject({
|
||||
method: 'PUT', url: '/api/levels/class/100', cookies,
|
||||
payload: { subject: 'rekenen', yearGroup: 4, level: 4 },
|
||||
});
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
});
|
||||
|
||||
test('klassenmanagement: GET class levert klas-standaard én leerling-overrides in één keer', async () => {
|
||||
const { app } = await makeApp(admin, {
|
||||
classDefault: { year_group: 4, level: 2 },
|
||||
|
|
@ -113,6 +123,21 @@ test('klassenmanagement: leerkracht-rol wordt geweigerd op /my/level (alleen lee
|
|||
assert.equal(res.statusCode, 403, res.body);
|
||||
});
|
||||
|
||||
test('thuiskind kan eigen taalniveau opslaan, schoolleerling niet', async () => {
|
||||
const { app, calls } = await makeApp(homePupil);
|
||||
const res = await app.inject({ method: 'PUT', url: '/api/my/level', cookies,
|
||||
payload: { subject: 'taal', yearGroup: 5, level: 2 } });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const insert = calls.find(call=>call.sql.startsWith('INSERT INTO pupil_levels'));
|
||||
assert.deepEqual(insert.params,[10,'taal',5,2]);
|
||||
|
||||
const { app: schoolApp } = await makeApp(pupil);
|
||||
const denied = await schoolApp.inject({ method: 'PUT', url: '/api/my/level', cookies,
|
||||
payload: { subject: 'taal', yearGroup: 5, level: 2 } });
|
||||
assert.equal(denied.statusCode,403,denied.body);
|
||||
await app.close();await schoolApp.close();
|
||||
});
|
||||
|
||||
/* Echte (niet regex-)test van de SLO-getalgrenzen: data.js's SLO_MATH_RANGES/
|
||||
mathRangeFor zijn pure functies zonder DOM-afhankelijkheid, dus rechtstreeks
|
||||
uitvoerbaar in een geïsoleerde VM-context - zelfde techniek als elders in
|
||||
|
|
@ -137,10 +162,11 @@ test('SLO-rekengrenzen: groep bepaalt de daadwerkelijke getalgrens, niet meer de
|
|||
assert.equal(mathRangeFor(undefined, 2), null);
|
||||
});
|
||||
|
||||
test('math.js: gen() gebruikt mathRangeFor als er een groep is, anders de oude vaste klem', async () => {
|
||||
test('math.js: gen() gebruikt leerjaarregels en behoudt zonder groep de oude vaste klem', async () => {
|
||||
const source = await readFile('public/js/widgets/math.js', 'utf8');
|
||||
assert.match(source, /const slo = mathRangeFor\(S\.yearGroup, L\)/);
|
||||
assert.match(source, /const max = slo \? slo\.addSub : \(L===1 \? 10 : L===2 \? 20 : 100\)/);
|
||||
assert.match(source, /const tmax = slo \? slo\.timesTable : \(L===1 \? 5 : L===2 \? 10 : 12\)/);
|
||||
assert.match(source, /const rules = mathTaskRules\(S\.yearGroup, L\)/);
|
||||
assert.match(source, /const max = rules \? rules\.addSub/);
|
||||
assert.match(source, /const tmax = rules \? rules\.timesTable/);
|
||||
assert.ok(source.includes('S.yearGroup = initState.yearGroup || null'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue