/* teach - opdrachten samenstellen en delen. Nieuwe opdrachten komen uit de volledige leerlingwidgetbibliotheek en staan los van het geopende bord. Oude bordtoewijzingen blijven via dezelfde API achterwaarts compatibel. */ "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", castSingleTheme = null; 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 dezelfde opdracht naar meerdere klassen/leerlingen achter elkaar kunt sturen zonder dat de sheet na elke keer sluit */ let castActiveKeys = new Set(); let castError = ""; const CAST_WIDGET_CATEGORIES=new Set(["taal","rekenen"]); const CAST_THEME_WIDGETS=new Set(["letters","flits","hak","ball"]); function assignmentLibraryWidgets(){ return REGISTRY.filter(widget=>CAST_WIDGET_CATEGORIES.has(widget.cat)); } function castWidgetLabel(widget){ return T("wg_"+widget.id); } function castThemeCatalog(){ const builtIn=typeof languageThemeList==="function"?languageThemeList(LANG).map(theme=>({ id:"builtin:"+theme.id,name:theme.name,icon:theme.icon||"📚",words:theme.words||[],source:"builtin", })):[]; const personal=(THEMES[LANG]||[]).filter(theme=>Array.isArray(theme.words)&&theme.words.length).map((theme,index)=>({ id:"personal:"+index,name:theme.name,icon:"★",words:theme.words,source:"personal", })); return builtIn.concat(personal); } function castThemePayload(theme){ if(!theme)return null; return { id:theme.id,name:theme.name, words:(theme.words||[]).slice(0,120).map(word=>Array.isArray(word)?word.slice(0,4):word), }; } function castThemeSelect(theme,onChange){ const select=h("select","am-sel cast-step-theme"); select.setAttribute("aria-label",T("castTheme")); select.appendChild(new Option(T("castThemeNone"),"")); const catalog=castThemeCatalog(); [["builtin","castBuiltInThemes"],["personal","castOwnThemes"]].forEach(([source,labelKey])=>{ const items=catalog.filter(item=>item.source===source); if(!items.length)return; const group=document.createElement("optgroup");group.label=T(labelKey); items.forEach(item=>group.appendChild(new Option(`${item.icon} ${item.name} (${item.words.length})`,item.id))); select.appendChild(group); }); select.value=theme?.id||""; select.disabled=castSaving; select.addEventListener("change",()=>{ const selected=catalog.find(item=>item.id===select.value)||null; onChange(selected);castActiveKeys.clear();castNotice=""; }); return select; } 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={}; if(castShareKind==="path"){ body.title=castPathTitle.trim(); body.sequence=castPathSteps.map(step=>({ widgetType:step.widgetType,mode:step.mode,theme:castThemePayload(step.theme), })); }else if(castShareKind==="widget"){ const widget=assignmentLibraryWidgets().find(item=>item.id===castSelWidget); body.title=widget?castWidgetLabel(widget):T("castSingleAssignment"); body.sequence=[{widgetType:castSelWidget,mode:castSelMode,theme:castThemePayload(castSingleTheme)}]; } 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 ()=>{ castOpen = true; castSelWidget = ""; castSelMode = "werken"; castSingleTheme = null; castPathTitle = ""; castPathSteps = []; castShareKind = "path"; 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",T("castLibrarySubtitle"))); 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"]] .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=assignmentLibraryWidgets(); const widgetGrid=(selectOnly=false)=>{ const grid=h("div","cast-widget-grid"); learningWidgets.forEach(widget=>{ const selected=selectOnly&&castSelWidget===widget.id; const added=castPathSteps.filter(step=>step.widgetType===widget.id).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){ if(castSelWidget!==widget.id)castSingleTheme=null; castSelWidget=widget.id; } else{ castPathSteps.push({widgetType:widget.id,widgetLabel:castWidgetLabel(widget),icon:widget.icon,mode:"werken",theme:null}); 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.widgetType===widget.id))||castPathSteps.length>=12; addAll.addEventListener("click",()=>{ learningWidgets.filter(widget=>!castPathSteps.some(step=>step.widgetType===widget.id)) .slice(0,12-castPathSteps.length).forEach(widget=>castPathSteps.push({ widgetType:widget.id,widgetLabel:castWidgetLabel(widget),icon:widget.icon,mode:"werken",theme:null, })); 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("castIndependentStep"))); row.appendChild(label); const themeWrap=h("label","cast-step-option cast-step-theme-option"); themeWrap.appendChild(h("span",null,T("castTheme"))); if(CAST_THEME_WIDGETS.has(step.widgetType))themeWrap.appendChild(castThemeSelect(step.theme,value=>{step.theme=value;})); else{ const unavailable=h("span","cast-theme-na",T("castThemeNotApplicable"));themeWrap.appendChild(unavailable); } const modeWrap=h("label","cast-step-option"); modeWrap.appendChild(h("span",null,T("castStepMode"))); 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="";}); modeWrap.appendChild(mode); 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(themeWrap,modeWrap,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"))); composer.appendChild(widgetGrid(true)); if(castSelWidget){ const themeField=h("label","cast-field cast-theme-field"); themeField.appendChild(h("span",null,T("castTheme"))); if(CAST_THEME_WIDGETS.has(castSelWidget))themeField.appendChild(castThemeSelect(castSingleTheme,value=>{castSingleTheme=value;})); else themeField.appendChild(h("span","cast-theme-na",T("castThemeNotApplicable"))); composer.appendChild(themeField); } 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); } 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); }