/* teach - cast (📡): het geopende bord (of één widget erop) rechtstreeks vanaf de toolbar uitzenden naar een klas of leerling, i.p.v. de omweg via Instellingen > Toewijzingen. Hergebruikt dezelfde PUT/DELETE-routes als admin.js's assignmentRow - geen serverwijziging, alleen een korter pad. */ "use strict"; const btnCast = document.getElementById("btnCast"); btnCast.title = T("cast"); document.addEventListener("langchange", ()=>{ btnCast.title = T("cast"); if(castOpen) renderCast(); }); function h(tag, cls, text){ const el = document.createElement(tag); if(cls) el.className = cls; if(text != null) el.textContent = text; return el; } const castBackdrop = h("div","am-sheet-backdrop"); castBackdrop.id = "castBackdrop"; document.body.appendChild(castBackdrop); castBackdrop.addEventListener("click", e=>{ if(e.target===castBackdrop&&!castSaving) closeCast(); }); let castOpen = false; let castLoading = false; let castClasses = [], castPupils = []; let castSelWidget = "", castSelMode = "werken"; let castPathTitle = "", castPathSteps = []; let castShareKind = "path"; let castSelectedTargets = new Set(); let castPupilQuery = ""; let castSaving = false; let castNotice = ""; /* toewijzingen die deze sessie al gelukt zijn ("type:id"), zodat je hetzelfde bord naar meerdere klassen/leerlingen achter elkaar kunt casten zonder dat de sheet na elke keer sluit */ let castActiveKeys = new Set(); let castError = ""; /* widgets van het live-open bord, zelfde bron als serializeBoard() (board.js) */ function currentBoardWidgets(){ return [...instances].map(inst=>({ wid: inst.wid, id: inst.def.id, cat: inst.def.cat, icon: inst.def.icon || "🧩", })).filter(w=>w.wid); } function castPathWidgetAllowed(widgetId){ const widget = currentBoardWidgets().find(item=>item.wid===widgetId); return !!widget && ["taal","rekenen"].includes(widget.cat); } function castWidgetLabel(widget){ const widgets = currentBoardWidgets().filter(item=>item.id===widget.id); const index = widgets.findIndex(item=>item.wid===widget.wid); return T("wg_"+widget.id) + (widgets.length>1 ? ` ${index+1}` : ""); } function castTargetKey(target){ return target.type+":"+target.id; } function castClassSelected(classId){ return castSelectedTargets.has("class:"+classId); } function castPupilCoveredByClass(target){ return target.type==="pupil" && target.classId && castClassSelected(target.classId); } function castToggleTarget(target, checked){ const key=castTargetKey(target); if(checked){ castSelectedTargets.add(key); if(target.type==="class"){ castPupils.filter(p=>String(p.classId)===String(target.id)) .forEach(p=>castSelectedTargets.delete("pupil:"+p.id)); } }else castSelectedTargets.delete(key); castNotice="";castError=""; } function castSelectedTargetObjects(){ const targets=[]; castClasses.forEach(c=>{ const target={type:"class",id:c.id,label:c.name}; if(castSelectedTargets.has(castTargetKey(target)))targets.push(target); }); castPupils.forEach(p=>{ const target={type:"pupil",id:p.id,label:p.displayName,classId:p.classId}; if(castSelectedTargets.has(castTargetKey(target)))targets.push(target); }); return targets; } function castAffectedPupilCount(){ const pupilIds=new Set(); castSelectedTargetObjects().forEach(target=>{ if(target.type==="pupil")pupilIds.add(String(target.id)); else castPupils.filter(p=>String(p.classId)===String(target.id)).forEach(p=>pupilIds.add(String(p.id))); }); return pupilIds.size; } function castPayload(){ const body={boardId:curSlot().id}; if(castShareKind==="path"){ body.title=castPathTitle.trim(); body.sequence=castPathSteps.map(step=>({boardId:step.boardId,widgetId:step.widgetId,mode:step.mode})); }else if(castShareKind==="widget"){ body.widgetId=castSelWidget; body.mode=castSelMode; } return body; } function castValidationKey(){ if(castShareKind==="path"&&!castPathSteps.length)return "castNeedPath"; if(castShareKind==="path"&&!castPathTitle.trim())return "castNeedTitle"; if(castShareKind==="widget"&&!castSelWidget)return "castNeedWidget"; if(!castSelectedTargetObjects().length)return "castNeedTarget"; return ""; } function castRefreshSendValidation(){ const invalid=castValidationKey(); const validation=castBackdrop.querySelector(".cast-validation"); const send=castBackdrop.querySelector(".cast-send-button"); if(validation){validation.textContent=invalid?T(invalid):"";validation.hidden=!invalid;} if(send)send.disabled=castSaving||!!invalid; } async function castSendSelected(){ const invalid=castValidationKey(); if(invalid){castError=T(invalid);renderCast(true);return;} const targets=castSelectedTargetObjects(); const payload=castPayload(); castSaving=true;castError="";castNotice="";renderCast(true); const results=await Promise.allSettled(targets.map(target=> api(`/assignments/${target.type}/${target.id}`,{method:"PUT",body:payload}) )); let saved=0,failed=0; results.forEach((result,index)=>{ const key=castTargetKey(targets[index]); if(result.status==="fulfilled"){ saved+=1;castActiveKeys.add(key);castSelectedTargets.delete(key); }else failed+=1; }); castSaving=false; if(saved)castNotice=T("castSent").replace("{count}",saved); if(failed)castError=T("castPartial").replace("{count}",failed); renderCast(true); } btnCast.addEventListener("click", async ()=>{ stashCurrent(); /* verse widget-state, zelfde als btnFolders doet */ castOpen = true; castSelWidget = ""; castSelMode = "werken"; castPathTitle = ""; castPathSteps = []; castShareKind = currentBoardWidgets().some(w=>castPathWidgetAllowed(w.wid)) ? "path" : "board"; castSelectedTargets = new Set(); castPupilQuery = ""; castSaving = false; castNotice = ""; castActiveKeys = new Set(); castError = ""; castLoading = true; renderCast(); try{ const [cRes, uRes] = await Promise.all([ api("/admin/classes"), api("/admin/users") ]); castClasses = cRes.classes || []; castPupils = (uRes.users || []).filter(u=>u.role==="pupil"); }catch(e){ castClasses = []; castPupils = []; castError = e.message; } castLoading = false; renderCast(); }); function closeCast(){ castOpen = false; renderCast(); } function castTargetPicker(target){ const row = h("label","cast-target-card"); const covered=castPupilCoveredByClass(target); const checkbox=h("input","cast-target-check"); checkbox.type="checkbox"; checkbox.checked=covered||castSelectedTargets.has(castTargetKey(target)); checkbox.disabled=covered||castSaving; checkbox.addEventListener("change",()=>{ castToggleTarget(target,checkbox.checked); renderCast(true); }); row.appendChild(checkbox); const info = h("div","am-person-info"); info.appendChild(h("span","am-name", target.label)); if(target.sub) info.appendChild(h("span","am-role", target.sub)); row.appendChild(info); if(covered)row.appendChild(h("span","cast-target-via",T("castViaClass"))); else if(castActiveKeys.has(castTargetKey(target)))row.appendChild(h("span","am-chip active",T("castActive"))); return row; } function renderCast(preserveScroll=false){ const previousScroll=preserveScroll ? castBackdrop.querySelector(".am-sheet")?.scrollTop || 0 : 0; castBackdrop.classList.toggle("open", castOpen); castBackdrop.innerHTML = ""; if(!castOpen) return; const sheet = h("div","am-sheet cast-composer-sheet"); castBackdrop.appendChild(sheet); if(preserveScroll)requestAnimationFrame(()=>{sheet.scrollTop=previousScroll;}); const head = h("div","am-kebab-row"); const titleWrap = h("div"); titleWrap.appendChild(h("h2","am-nav-title", T("castTitle"))); titleWrap.appendChild(h("div","am-role", `${BS.folders[BS.f].name} / ${curSlot().name}`)); head.appendChild(titleWrap); const closeBtn = h("button","am-btn", T("close")); closeBtn.type = "button"; closeBtn.disabled = castSaving; closeBtn.addEventListener("click", closeCast); head.appendChild(closeBtn); sheet.appendChild(head); sheet.appendChild(h("p","cast-composer-intro",T("castIntro"))); const status=h("div","cast-status"); status.setAttribute("aria-live","polite"); if(castError){status.classList.add("error");status.textContent=castError;} else if(castNotice){status.classList.add("success");status.textContent=castNotice;} sheet.appendChild(status); const guide=h("ol","cast-guide"); [T("castStepChoose"),T("castStepBuild"),T("castStepAudience")].forEach((label,index)=>{ const item=h("li");item.append(h("span",null,String(index+1)),h("strong",null,label));guide.appendChild(item); }); sheet.appendChild(guide); const composer=h("section","cast-composer-card"); composer.appendChild(h("h3",null,T("castWhat"))); const kindTabs=h("div","cast-kind-tabs"); [["path","castKindPath","castKindPathHelp"],["widget","castKindWidget","castKindWidgetHelp"],["board","castKindBoard","castKindBoardHelp"]] .forEach(([kind,labelKey,helpKey])=>{ const button=h("button","cast-kind-button"+(castShareKind===kind?" active":"")); button.type="button";button.disabled=castSaving; button.append(h("strong",null,T(labelKey)),h("span",null,T(helpKey))); button.addEventListener("click",()=>{ castShareKind=kind;castActiveKeys.clear();castError="";castNotice="";renderCast(true); }); kindTabs.appendChild(button); }); composer.appendChild(kindTabs); composer.appendChild(h("p","cast-live-hint",T("castLiveStateHint"))); const learningWidgets=currentBoardWidgets().filter(widget=>castPathWidgetAllowed(widget.wid)); const widgetGrid=(selectOnly=false)=>{ const grid=h("div","cast-widget-grid"); learningWidgets.forEach(widget=>{ const selected=selectOnly&&castSelWidget===widget.wid; const added=castPathSteps.filter(step=>step.widgetId===widget.wid).length; const card=h("button","cast-widget-card"+(selected?" selected":"")); card.type="button";card.disabled=castSaving||(!selectOnly&&castPathSteps.length>=12); card.setAttribute("aria-pressed",String(selected)); card.appendChild(h("span","cast-widget-icon",widget.icon)); const copy=h("span","cast-widget-copy"); copy.append(h("strong",null,castWidgetLabel(widget)),h("small",null,T(widget.cat==="taal"?"catTaal":"catRekenen"))); card.appendChild(copy); if(added&&!selectOnly)card.appendChild(h("span","cast-widget-count",String(added))); else card.appendChild(h("span","cast-widget-plus",selected?"✓":"+")); card.addEventListener("click",()=>{ if(selectOnly)castSelWidget=widget.wid; else{ castPathSteps.push({boardId:curSlot().id,widgetId:widget.wid,widgetType:widget.id,widgetLabel:castWidgetLabel(widget),icon:widget.icon,mode:"werken"}); if(!castPathTitle)castPathTitle=T("castPathDefault"); } castActiveKeys.clear();castError="";castNotice="";renderCast(true); }); grid.appendChild(card); }); return grid; }; if(castShareKind==="path"){ const titleField=h("label","cast-field"); titleField.appendChild(h("span",null,T("castPathName"))); const pathTitle=h("input","am-inp"); pathTitle.type="text";pathTitle.maxLength=80;pathTitle.placeholder=T("castPathPlaceholder");pathTitle.value=castPathTitle; pathTitle.addEventListener("input",()=>{ castPathTitle=pathTitle.value;castActiveKeys.clear();castNotice="";castRefreshSendValidation(); }); titleField.appendChild(pathTitle);composer.appendChild(titleField); const libraryHead=h("div","cast-section-head"); const libraryCopy=h("div");libraryCopy.append(h("strong",null,T("castWidgetLibrary")),h("small",null,T("castWidgetLibraryHint"))); libraryHead.appendChild(libraryCopy); const libraryActions=h("div","cast-mini-actions"); const addAll=h("button","am-btn",T("castAddAll"));addAll.type="button"; addAll.disabled=castSaving||!learningWidgets.some(widget=>!castPathSteps.some(step=>step.widgetId===widget.wid))||castPathSteps.length>=12; addAll.addEventListener("click",()=>{ learningWidgets.filter(widget=>!castPathSteps.some(step=>step.widgetId===widget.wid)) .slice(0,12-castPathSteps.length).forEach(widget=>castPathSteps.push({ boardId:curSlot().id,widgetId:widget.wid,widgetType:widget.id, widgetLabel:castWidgetLabel(widget),icon:widget.icon,mode:"werken", })); if(castPathSteps.length&&!castPathTitle)castPathTitle=T("castPathDefault"); castActiveKeys.clear();castError="";castNotice="";renderCast(true); }); const clearPath=h("button","am-btn",T("castClearPath"));clearPath.type="button";clearPath.disabled=castSaving||!castPathSteps.length; clearPath.addEventListener("click",()=>{castPathSteps=[];castActiveKeys.clear();castError="";castNotice="";renderCast(true);}); libraryActions.append(addAll,clearPath);libraryHead.appendChild(libraryActions);composer.appendChild(libraryHead); if(learningWidgets.length)composer.appendChild(widgetGrid()); else composer.appendChild(h("div","cast-empty",T("castNoLearningWidgets"))); const sequenceHead=h("div","cast-section-head"); sequenceHead.appendChild(h("strong",null,T("castSequenceOrder"))); const count=h("span","cast-sequence-count",T("castPathHint").replace("{count}",castPathSteps.length)); sequenceHead.appendChild(count);composer.appendChild(sequenceHead); if(!castPathSteps.length)composer.appendChild(h("div","cast-path-empty",T("castPathEmpty"))); else{ const pathList=h("div","cast-path-list"); castPathSteps.forEach((step,index)=>{ const row=h("div","cast-path-step"); row.append(h("span","cast-path-number",String(index+1)),h("span","cast-step-icon",step.icon||"🧩")); const label=h("span","cast-step-copy");label.append(h("strong",null,step.widgetLabel||T("wg_"+step.widgetType)),h("small",null,T("castStepMode"))); row.appendChild(label); const mode=h("select","am-sel cast-step-mode"); mode.append(new Option(T("amModeWork"),"werken"),new Option(T("amModeWatch"),"kijken"));mode.value=step.mode; mode.setAttribute("aria-label",T("castStepMode"));mode.disabled=castSaving; mode.addEventListener("change",()=>{step.mode=mode.value;castActiveKeys.clear();castNotice="";}); const controls=h("div","cast-step-controls"); const up=h("button","am-btn","↑");up.type="button";up.disabled=castSaving||index===0;up.title=T("castMoveUp"); up.addEventListener("click",()=>{[castPathSteps[index-1],castPathSteps[index]]=[castPathSteps[index],castPathSteps[index-1]];castActiveKeys.clear();renderCast(true);}); const down=h("button","am-btn","↓");down.type="button";down.disabled=castSaving||index===castPathSteps.length-1;down.title=T("castMoveDown"); down.addEventListener("click",()=>{[castPathSteps[index+1],castPathSteps[index]]=[castPathSteps[index],castPathSteps[index+1]];castActiveKeys.clear();renderCast(true);}); const remove=h("button","am-btn cast-step-remove","×");remove.type="button";remove.disabled=castSaving;remove.title=T("castRemoveStep"); remove.setAttribute("aria-label",T("castRemoveStep")); remove.addEventListener("click",()=>{castPathSteps.splice(index,1);castActiveKeys.clear();castNotice="";renderCast(true);}); controls.append(up,down,remove);row.append(mode,controls);pathList.appendChild(row); }); composer.appendChild(pathList); } const progress=h("progress","cast-path-progress");progress.max=12;progress.value=castPathSteps.length;composer.appendChild(progress); }else if(castShareKind==="widget"){ composer.appendChild(h("p","cast-kind-note",T("castSingleHint"))); if(learningWidgets.length)composer.appendChild(widgetGrid(true)); else composer.appendChild(h("div","cast-empty",T("castNoLearningWidgets"))); const modeField=h("label","cast-field cast-mode-field");modeField.appendChild(h("span",null,T("castStepMode"))); const mode=h("select","am-sel");mode.append(new Option(T("amModeWork"),"werken"),new Option(T("amModeWatch"),"kijken"));mode.value=castSelMode;mode.disabled=castSaving; mode.addEventListener("change",()=>{castSelMode=mode.value;castActiveKeys.clear();castNotice="";}); modeField.appendChild(mode);composer.appendChild(modeField); }else composer.appendChild(h("div","cast-board-note",T("castBoardHint"))); sheet.appendChild(composer); const audience=h("section","cast-composer-card cast-audience"); const audienceHead=h("div","cast-section-head"); const audienceCopy=h("div");audienceCopy.append(h("h3",null,T("castAudienceTitle")),h("small",null,T("castAudienceHint"))); const clearTargets=h("button","am-btn",T("castClearTargets"));clearTargets.type="button";clearTargets.disabled=castSaving||!castSelectedTargets.size; clearTargets.addEventListener("click",()=>{castSelectedTargets.clear();castError="";castNotice="";renderCast(true);}); audienceHead.append(audienceCopy,clearTargets);audience.appendChild(audienceHead); audience.appendChild(h("div","am-group",T("castClasses"))); const classList=h("div","cast-target-grid cast-class-grid"); if(castLoading)classList.appendChild(h("div","cast-empty","…")); else if(!castClasses.length)classList.appendChild(h("div","cast-empty",T("castNoClasses"))); else castClasses.forEach(c=>{ const count=castPupils.filter(p=>String(p.classId)===String(c.id)).length; classList.appendChild(castTargetPicker({type:"class",id:c.id,label:c.name,sub:T("castPupilCount").replace("{count}",count)})); }); audience.appendChild(classList); const pupilHead=h("div","cast-pupil-head");pupilHead.appendChild(h("div","am-group",T("castPupils"))); const selectVisible=h("button","am-btn",T("castSelectVisible"));selectVisible.type="button";selectVisible.disabled=castSaving||!castPupils.length; pupilHead.appendChild(selectVisible);audience.appendChild(pupilHead); const search=h("input","am-inp cast-pupil-search");search.type="search";search.placeholder=T("castAudienceSearch");search.value=castPupilQuery; audience.appendChild(search); const pupilList=h("div","cast-target-grid cast-pupil-grid"); const pupilRows=[]; if(castLoading)pupilList.appendChild(h("div","cast-empty","…")); else if(!castPupils.length)pupilList.appendChild(h("div","cast-empty",T("castNoPupils"))); else castPupils.forEach(pupil=>{ const className=castClasses.find(c=>String(c.id)===String(pupil.classId))?.name||T("amUnassigned"); const target={type:"pupil",id:pupil.id,label:pupil.displayName,classId:pupil.classId,sub:className}; const row=castTargetPicker(target);row.dataset.search=(pupil.displayName+" "+className).toLocaleLowerCase(); pupilRows.push({row,target});pupilList.appendChild(row); }); const applyPupilFilter=()=>{ castPupilQuery=search.value; const query=castPupilQuery.trim().toLocaleLowerCase(); pupilRows.forEach(({row})=>{row.hidden=!!query&&!row.dataset.search.includes(query);}); }; search.addEventListener("input",applyPupilFilter);applyPupilFilter(); selectVisible.addEventListener("click",()=>{ pupilRows.filter(({row})=>!row.hidden).forEach(({target})=>{ if(!castPupilCoveredByClass(target))castSelectedTargets.add(castTargetKey(target)); }); castError="";castNotice="";renderCast(true); }); audience.appendChild(pupilList);sheet.appendChild(audience); const sendbar=h("footer","cast-sendbar"); const summary=h("div","cast-send-summary"); const targetCount=castSelectedTargetObjects().length,affected=castAffectedPupilCount(); summary.append(h("strong",null,T("castTargetCount").replace("{count}",targetCount)), h("span",null,T("castAffectedPupils").replace("{count}",affected))); const invalid=castValidationKey(); const validation=h("small","cast-validation",invalid?T(invalid):"");validation.hidden=!invalid;summary.appendChild(validation); const send=h("button","cast-send-button",castSaving?T("castSending"):T("castSend").replace("{count}",targetCount)); send.type="button";send.disabled=castSaving||!!invalid;send.addEventListener("click",castSendSelected); sendbar.append(summary,send);sheet.appendChild(sendbar); }