teach/public/js/parent.js
Ramon fabe721991
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
feat: voeg leerjaarprofielen toe aan taal en rekenen (v0.4.41-beta)
2026-07-20 22:45:01 +02:00

279 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* teach - ouderportaal: ouders (role parent) zien geen whiteboard maar een
portaal met hun gekoppelde kinderen. Per kind: school/klas (of "thuis"),
voortgangstijdlijn, en voor thuiskinderen een aanmelding bij een school
(doorzoekbare lijst; de school handelt het verzoek af in het beheer).
Kinderen komen erbij door ze zelf aan te maken (thuiskind, wachtwoord
eenmalig zichtbaar) of via een eenmalige ouder-code van de school. */
"use strict";
const parentView = document.getElementById("parentView");
/* opengeklapte voortgang/aanmeldformulieren en eenmalige resultaten
(kind-wachtwoord) blijven behouden over her-renders van het portaal */
const parentOpen = new Set();
const parentResults = new Map();
let PARENT_SCHOOLS = [];
function prH(tag, cls, text){
const el = document.createElement(tag);
if(cls) el.className = cls;
if(text != null) el.textContent = text;
return el;
}
/* arm-then-confirm zoals dangerBtn in het beheer: eerste klik wapent ("zeker?"),
tweede klik voert uit, na 4s ontwapent hij vanzelf */
function prDangerBtn(label, warnKey, fn){
const b = prH("button","am-btn am-danger", label);
b.type = "button";
if(warnKey) b.title = T(warnKey);
let timer = null;
b.addEventListener("click", async ()=>{
if(!timer){
b.classList.add("armed");
b.textContent = "⚠ " + T("amSure");
timer = setTimeout(()=>{ timer = null; b.classList.remove("armed"); b.textContent = label; }, 4000);
return;
}
clearTimeout(timer); timer = null;
b.classList.remove("armed"); b.textContent = label;
await fn(b);
});
return b;
}
function prMsg(t){
const m = parentView.querySelector(".am-msg");
if(m) m.textContent = t || "";
}
async function parentReload(){
let children = [];
try{ children = (await api("/parent/children")).children || []; }
catch(e){ /* render toont dan een lege lijst; melding via prMsg hieronder */ }
renderParentView(children);
}
function parentChildRow(c){
const row = prH("div","am-row");
row.appendChild(prH("span","am-name","🧒 " + c.displayName + (c.displayName!==c.username ? ` (${c.username})` : "")));
const where = c.schoolName
? c.schoolName + (c.className ? ` · ${c.className}` : "")
: T("parentHome") + (c.appliedTo ? ` · ${T("parentApplied")} ${c.appliedTo}` : "");
row.appendChild(prH("span","am-role", where));
const tools = prH("div","am-row-tools");
const r = parentResults.get(c.id);
if(r){
const res = prH("div","am-result");
res.appendChild(prH("span","am-result-lbl", r.label + ": "));
const code = prH("code", null, r.value);
res.appendChild(code);
res.appendChild(prH("div","guestnote", T("parentPwNote")));
tools.appendChild(res);
}
/* voortgangstijdlijn (zelfde regelopbouw als het beheer: widget, score, tijd) */
const progHolder = prH("div","am-list");
async function loadProgress(){
progHolder.innerHTML = "";
let events = [];
try{ events = (await api("/progress/pupil/"+c.id)).events || []; }
catch(e){ progHolder.appendChild(prH("div","guestnote", e.message)); return; }
if(!events.length){ progHolder.appendChild(prH("div","guestnote", T("parentProgressEmpty"))); return; }
events.forEach(ev=>{
const line = prH("div","am-row");
line.appendChild(prH("span","am-name", T("wg_"+ev.widgetType) || ev.widgetType));
const scoreParts = [];
if(ev.attempts) scoreParts.push(`${ev.correct}/${ev.attempts} ${T("mGoed")}`);
if(ev.stars) scoreParts.push("★".repeat(ev.stars));
line.appendChild(prH("span","am-code", scoreParts.join(" · ")));
line.appendChild(prH("span","am-code", new Date(ev.createdAt).toLocaleString(LANG==="nl" ? "nl-NL" : "en-GB")));
progHolder.appendChild(line);
});
}
const progBtn = prH("button","am-btn", T("parentProgress"));
progBtn.type = "button";
progBtn.addEventListener("click", ()=>{
const key = "prog:"+c.id;
if(parentOpen.has(key)){ parentOpen.delete(key); progHolder.innerHTML = ""; }
else{ parentOpen.add(key); loadProgress(); }
});
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");
applyHolder.style.display = "none";
if(!c.schoolId && !c.appliedTo){
const applyBtn = prH("button","am-btn", T("parentApply"));
applyBtn.type = "button";
const inp = prH("input","am-inp");
inp.placeholder = T("parentSchoolPh");
inp.setAttribute("list","parentSchoolList");
const go = prH("button","am-btn", T("parentApplyBtn"));
go.type = "button";
go.addEventListener("click", async ()=>{
const s = PARENT_SCHOOLS.find(x=>x.name.toLowerCase()===inp.value.trim().toLowerCase());
if(!s){ prMsg(T("parentSchoolPh")); return; }
try{ await api(`/parent/children/${c.id}/apply`, { body:{ schoolId: s.id } }); parentOpen.delete("apply:"+c.id); parentReload(); }
catch(e){ prMsg(e.message); }
});
applyHolder.appendChild(inp);
applyHolder.appendChild(go);
applyBtn.addEventListener("click", ()=>{
const key = "apply:"+c.id;
if(parentOpen.has(key)) parentOpen.delete(key); else parentOpen.add(key);
applyHolder.style.display = parentOpen.has(key) ? "" : "none";
if(parentOpen.has(key)) inp.focus();
});
applyHolder.style.display = parentOpen.has("apply:"+c.id) ? "" : "none";
actions.appendChild(applyBtn);
}
actions.appendChild(prDangerBtn(T("parentUnlink"), "parentUnlinkWarn", async ()=>{
try{ await api("/parent/children/"+c.id, { method:"DELETE" }); parentResults.delete(c.id); parentReload(); }
catch(e){ prMsg(e.message); }
}));
tools.appendChild(actions);
tools.appendChild(levelHolder);
tools.appendChild(applyHolder);
tools.appendChild(progHolder);
row.appendChild(tools);
if(parentOpen.has("prog:"+c.id)) loadProgress();
return row;
}
/* ingeklapt formulier (" …"-knop) zoals addForm in het beheer */
function parentCollapsedForm(label, build){
const wrap = prH("div","am-add-wrap");
const toggle = prH("button","tbtn ghost", label);
toggle.type = "button";
const f = prH("div","am-add");
f.style.display = "none";
toggle.addEventListener("click", ()=>{
f.style.display = f.style.display==="none" ? "" : "none";
const first = f.querySelector("input");
if(f.style.display!=="none" && first) first.focus();
});
build(f);
wrap.appendChild(toggle);
wrap.appendChild(f);
return wrap;
}
function renderParentView(children){
parentView.innerHTML = "";
const inner = prH("div","parent-inner");
inner.appendChild(prH("h2","parent-title", T("parentTitle")));
inner.appendChild(prH("div","am-msg"));
const list = prH("div","am-list");
if(!children.length) list.appendChild(prH("div","guestnote", T("parentNoChildren")));
children.forEach(c=>list.appendChild(parentChildRow(c)));
inner.appendChild(list);
/* datalist voor de doorzoekbare schoolkeuze (gedeeld door alle rijen) */
const dl = document.createElement("datalist");
dl.id = "parentSchoolList";
PARENT_SCHOOLS.forEach(s=>dl.appendChild(new Option(s.name)));
inner.appendChild(dl);
/* kind aanmaken (thuiskind): naam -> account zonder school + wachtwoord */
inner.appendChild(parentCollapsedForm(T("parentAddChild"), f=>{
const inp = prH("input","am-inp");
inp.placeholder = T("parentChildNamePh");
inp.maxLength = 30;
const go = prH("button","am-btn", T("parentAddBtn"));
go.type = "button";
go.addEventListener("click", async ()=>{
try{
const r = await api("/parent/children", { body:{ name: inp.value.trim() } });
parentResults.set(r.child.id, { label: T("amFieldPw"), value: r.child.password });
parentReload();
}catch(e){ prMsg(e.message); }
});
inp.addEventListener("keydown", ev=>{ if(ev.key==="Enter") go.click(); });
f.appendChild(inp);
f.appendChild(go);
}));
/* schoolkind koppelen met de eenmalige ouder-code van de school */
inner.appendChild(parentCollapsedForm(T("parentLinkTitle"), f=>{
f.appendChild(prH("div","guestnote", T("parentLinkNote")));
const inp = prH("input","am-inp");
inp.placeholder = T("parentLinkPh");
inp.maxLength = 9;
inp.style.textTransform = "uppercase";
const go = prH("button","am-btn", T("parentLinkBtn"));
go.type = "button";
go.addEventListener("click", async ()=>{
try{
await api("/parent/children/link", { body:{ code: inp.value.trim().toUpperCase() } });
inp.value = "";
parentReload();
}catch(e){ prMsg(e.message); }
});
inp.addEventListener("keydown", ev=>{ if(ev.key==="Enter") go.click(); });
f.appendChild(inp);
f.appendChild(go);
}));
parentView.appendChild(inner);
}
function startParentView(){
document.body.classList.add("parent-mode");
parentOpen.clear();
parentResults.clear();
(async ()=>{
try{ PARENT_SCHOOLS = await api("/schools"); }catch(e){ PARENT_SCHOOLS = []; }
parentReload();
})();
}
function stopParentView(){
document.body.classList.remove("parent-mode");
parentView.innerHTML = "";
}