All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
/* 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 = "";
|
||
}
|