/* 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); /* 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(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 = ""; }