diff --git a/VERSION b/VERSION index dc8c842..bcd548c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.60-beta +0.4.61-beta diff --git a/public/css/teach.css b/public/css/teach.css index b590114..60c3cd3 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -2425,12 +2425,33 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000 #settingsModal .am-card-sub{font-size:12.5px; padding:5px 12px 5px 30px; flex-wrap:wrap;} #settingsModal .am-card-sub .am-name{font-size:12.5px; font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;} -/* Opdrachtenreeks in het cast-venster */ -.cast-path-list{display:flex;flex-direction:column;gap:6px;margin-top:8px} -.cast-path-step{display:grid;grid-template-columns:28px minmax(0,1fr) auto auto auto;align-items:center;gap:6px;padding:7px;border:1px solid var(--line);border-radius:10px;background:var(--surface-2)} -.cast-path-number{width:24px;height:24px;display:grid;place-items:center;border-radius:50%;background:var(--accent);color:#fff;font-size:12px;font-weight:900} -.cast-path-step .am-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.cast-path-step .am-btn{min-width:30px;padding:4px 7px} +/* Visuele opdracht- en reeksbouwer voor groepsleiding */ +#castBackdrop .cast-composer-sheet{width:min(960px,calc(100vw - 32px));max-width:960px;max-height:min(90dvh,900px);padding:18px 20px 0;overscroll-behavior:contain} +.cast-composer-intro{max-width:72ch;margin:0 0 10px;color:var(--muted);font-size:13px;line-height:1.45} +.cast-status{min-height:0;margin:0 0 10px;padding:0;font-size:13px;font-weight:850} +.cast-status:empty{display:none}.cast-status.error{padding:9px 11px;border-radius:10px;background:var(--red-soft);color:var(--red)}.cast-status.success{padding:9px 11px;border-radius:10px;background:color-mix(in srgb,var(--green) 14%,var(--surface));color:var(--green)} +.cast-guide{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:0 0 14px;padding:0;list-style:none} +.cast-guide li{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:12px;background:var(--surface-2);color:var(--muted);font-size:11px} +.cast-guide li span{width:24px;height:24px;display:grid;place-items:center;flex:none;border-radius:50%;background:var(--accent);color:#fff;font-weight:950}.cast-guide li strong{color:var(--ink)} +.cast-composer-card{display:flex;flex-direction:column;gap:10px;margin-bottom:14px;padding:15px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:0 3px 14px rgba(20,35,60,.05)} +.cast-composer-card>h3,.cast-section-head h3{margin:0;font-size:16px}.cast-composer-card>.am-group,.cast-pupil-head>.am-group{margin:4px 0 0} +.cast-kind-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px} +.cast-kind-button{display:flex;min-width:0;min-height:66px;flex-direction:column;align-items:flex-start;justify-content:center;gap:3px;padding:10px 12px;border:1.5px solid var(--line);border-radius:13px;background:var(--surface-2);color:var(--ink);font:inherit;text-align:left;cursor:pointer} +.cast-kind-button:hover{border-color:var(--accent)}.cast-kind-button.active{border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}.cast-kind-button span{color:var(--muted);font-size:11px;line-height:1.25}.cast-kind-button:disabled{opacity:.55;cursor:default} +.cast-live-hint{margin:0;padding:8px 10px;border-left:3px solid var(--blue);border-radius:0 9px 9px 0;background:color-mix(in srgb,var(--blue) 7%,var(--surface));color:var(--muted);font-size:11px;line-height:1.4} +.cast-field{display:flex;flex-direction:column;gap:5px;color:var(--muted);font-size:11.5px;font-weight:850}.cast-field .am-inp,.cast-field .am-sel{width:100%}.cast-mode-field{max-width:360px} +.cast-section-head,.cast-pupil-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.cast-section-head>div:first-child{display:flex;min-width:0;flex-direction:column;gap:2px}.cast-section-head small{color:var(--muted);font-size:11px;font-weight:650;line-height:1.35}.cast-mini-actions{display:flex!important;flex-direction:row!important;flex:none;gap:6px} +.cast-widget-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(178px,1fr));gap:8px} +.cast-widget-card{display:grid;grid-template-columns:38px minmax(0,1fr) 28px;align-items:center;gap:8px;min-height:62px;padding:9px 10px;border:1.5px solid var(--line);border-radius:13px;background:var(--surface-2);color:var(--ink);font:inherit;text-align:left;cursor:pointer} +.cast-widget-card:hover{border-color:var(--accent);transform:translateY(-1px)}.cast-widget-card.selected{border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}.cast-widget-card:disabled{opacity:.5;cursor:default;transform:none} +.cast-widget-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:11px;background:var(--surface);font-size:22px}.cast-widget-copy{display:flex;min-width:0;flex-direction:column;gap:2px}.cast-widget-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.cast-widget-copy small{color:var(--muted);font-size:10.5px;font-weight:750}.cast-widget-plus,.cast-widget-count{width:25px;height:25px;display:grid;place-items:center;border-radius:50%;background:var(--accent);color:#fff;font-size:15px;font-weight:950}.cast-widget-count{background:var(--green)} +.cast-path-list{display:flex;flex-direction:column;gap:7px}.cast-path-step{display:grid;grid-template-columns:28px 34px minmax(120px,1fr) minmax(135px,180px) auto;align-items:center;gap:8px;padding:8px;border:1px solid var(--line);border-radius:12px;background:var(--surface-2)} +.cast-path-number{width:26px;height:26px;display:grid;place-items:center;border-radius:50%;background:var(--accent);color:#fff;font-size:12px;font-weight:900}.cast-step-icon{font-size:22px;text-align:center}.cast-step-copy{display:flex;min-width:0;flex-direction:column;gap:2px}.cast-step-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.cast-step-copy small{color:var(--muted);font-size:10px}.cast-step-mode{min-width:0;width:100%}.cast-step-controls{display:flex;gap:4px}.cast-path-step .am-btn{min-width:32px;min-height:34px;padding:4px 7px}.cast-step-remove{color:var(--red)!important}.cast-sequence-count{flex:none;padding:4px 8px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:10.5px;font-weight:850}.cast-path-progress{width:100%;height:7px;accent-color:var(--green)} +.cast-path-empty,.cast-empty,.cast-board-note,.cast-kind-note{margin:0;padding:12px;border:1px dashed var(--line);border-radius:12px;background:var(--surface-2);color:var(--muted);font-size:12px;line-height:1.45}.cast-board-note{border-color:color-mix(in srgb,var(--accent) 35%,var(--line));background:var(--accent-soft);color:var(--accent-ink)} +.cast-audience{gap:8px}.cast-target-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(205px,1fr));gap:7px}.cast-target-card{display:flex;min-width:0;align-items:center;gap:9px;padding:9px 10px;border:1px solid var(--line);border-radius:12px;background:var(--surface-2);cursor:pointer}.cast-target-card:hover{border-color:var(--accent)}.cast-target-card:has(.cast-target-check:checked){border-color:var(--accent);background:var(--accent-soft)}.cast-target-check{width:19px;height:19px;flex:none;accent-color:var(--accent)}.cast-target-via{flex:none;padding:3px 7px;border-radius:999px;background:var(--accent);color:#fff;font-size:9.5px;font-weight:900}.cast-pupil-search{width:100%}.cast-pupil-grid{max-height:250px;overflow-y:auto;padding:2px}.cast-target-card[hidden]{display:none} +.cast-sendbar{position:sticky;z-index:2;bottom:0;display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px -20px 0;padding:13px 20px calc(13px + env(safe-area-inset-bottom));border-top:1px solid var(--line);background:color-mix(in srgb,var(--surface) 94%,transparent);backdrop-filter:blur(14px)} +.cast-send-summary{display:flex;min-width:0;flex-direction:column;gap:2px}.cast-send-summary strong{font-size:13px}.cast-send-summary span{color:var(--muted);font-size:11px}.cast-validation{color:var(--orange);font-size:10.5px;font-weight:800}.cast-send-button{min-height:44px;flex:none;padding:0 18px;border:0;border-radius:12px;background:var(--accent);color:#fff;font:inherit;font-weight:900;cursor:pointer;box-shadow:0 4px 12px color-mix(in srgb,var(--accent) 30%,transparent)}.cast-send-button:hover{filter:brightness(.94)}.cast-send-button:disabled{opacity:.5;cursor:default;box-shadow:none} +body.hc .cast-kind-button,body.hc .cast-widget-card,body.hc .cast-target-card,body.hc .cast-composer-card{border-width:2px} /* Leerkrachtdashboard: scanbare status per leerling */ #settingsModal .teacher-dashboard{margin:10px 0 20px} @@ -2452,7 +2473,18 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000 #settingsModal .dashboard-path-bar{display:block;height:5px;border-radius:99px;background:var(--surface-3);overflow:hidden} #settingsModal .dashboard-path-bar i{display:block;height:100%;border-radius:inherit;background:var(--green)} #settingsModal .dashboard-score{font-size:11.5px;font-weight:800;color:var(--muted)} -@media (max-width:600px){#settingsModal .dashboard-summary{grid-template-columns:1fr 1fr}.cast-path-step{grid-template-columns:26px minmax(0,1fr) auto auto auto}} +@media (max-width:700px){ + #castBackdrop .cast-composer-sheet{width:100%;max-width:none;max-height:94dvh;padding:14px 14px 0;border-radius:18px 18px 0 0} + .cast-guide li{justify-content:center;padding:7px}.cast-guide li strong{display:none} + .cast-kind-tabs{grid-template-columns:1fr}.cast-kind-button{min-height:54px}.cast-kind-button span{display:none} + .cast-section-head{align-items:flex-start;flex-direction:column}.cast-section-head .cast-mini-actions{align-self:stretch}.cast-mini-actions .am-btn{flex:1} + .cast-widget-grid{grid-template-columns:1fr 1fr}.cast-widget-card{grid-template-columns:34px minmax(0,1fr) 24px;min-height:58px;padding:7px}.cast-widget-icon{width:34px;height:34px;font-size:19px} + .cast-path-step{grid-template-columns:26px 30px minmax(0,1fr) auto}.cast-step-mode{grid-column:2 / -1;grid-row:2}.cast-step-controls{grid-column:4;grid-row:1}.cast-step-copy small{display:none} + .cast-target-grid{grid-template-columns:1fr}.cast-pupil-head{align-items:flex-end}.cast-pupil-head .am-btn{font-size:11px} + .cast-sendbar{margin-left:-14px;margin-right:-14px;padding-left:14px;padding-right:14px}.cast-send-summary span{display:none}.cast-send-button{padding:0 13px} +} +@media (max-width:420px){.cast-widget-grid{grid-template-columns:1fr}.cast-send-summary small{max-width:170px}.cast-sequence-count{white-space:normal}} +@media (max-width:600px){#settingsModal .dashboard-summary{grid-template-columns:1fr 1fr}} /* Voortgang-tab: klassenmanagement-koppeling + grafiek (v0.4.35-beta e.v.) */ /* Realtime leersessies bovenin het klasdashboard */ @@ -2494,8 +2526,10 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000 #settingsModal .am-combo-pane-title{font-size:13px;font-weight:900;text-transform:uppercase;letter-spacing:.35px;color:var(--accent-ink)} #settingsModal .am-combo-pane .teacher-dashboard{margin-top:0} #settingsModal .am-combo-pane .am-group:first-child{margin-top:0} +#settingsModal .assignment-composer-cta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--line));border-radius:14px;background:var(--accent-soft)} +#settingsModal .assignment-composer-cta>div{display:flex;min-width:0;flex-direction:column;gap:3px}#settingsModal .assignment-composer-cta strong{font-size:13px;color:var(--accent-ink)}#settingsModal .assignment-composer-cta span{color:var(--muted);font-size:11px;line-height:1.35}#settingsModal .assignment-composer-cta .tbtn{flex:none;background:var(--accent);color:#fff} @media (max-width:920px){#settingsModal .am-combo-hero{grid-template-columns:1fr}#settingsModal .am-combo-layout{grid-template-columns:1fr}} -@media (max-width:600px){#settingsModal .am-combo-stats{grid-template-columns:1fr 1fr}} +@media (max-width:600px){#settingsModal .am-combo-stats{grid-template-columns:1fr 1fr}#settingsModal .assignment-composer-cta{align-items:stretch;flex-direction:column}#settingsModal .assignment-composer-cta .tbtn{width:100%}} #settingsModal .progress-level-badge{ display:inline-flex; align-self:flex-start; padding:4px 10px; margin-bottom:10px; diff --git a/public/js/admin.js b/public/js/admin.js index ca291e9..48190d3 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -792,6 +792,17 @@ panel.appendChild(h("div","guestnote", T("amPickSchoolHint"))); return; } + const composerCta=h("section","assignment-composer-cta"); + const composerCopy=h("div"); + composerCopy.append(h("strong",null,T("castOpenComposer")),h("span",null,T("castOpenComposerHint"))); + const composerButton=h("button","tbtn",T("castOpenComposer")); + composerButton.type="button"; + composerButton.addEventListener("click",()=>{ + document.getElementById("settingsWrap").classList.remove("open"); + document.getElementById("btnCast").click(); + }); + composerCta.append(composerCopy,composerButton); + panel.appendChild(composerCta); panel.appendChild(h("div","am-group", T("amAssignClasses"))); const classList = h("div","am-list"); if(!CLASSES.length) classList.appendChild(h("div","guestnote", T("amNoClass"))); diff --git a/public/js/cast.js b/public/js/cast.js index 4861cc2..cc92c05 100644 --- a/public/js/cast.js +++ b/public/js/cast.js @@ -20,13 +20,18 @@ function h(tag, cls, text){ const castBackdrop = h("div","am-sheet-backdrop"); castBackdrop.id = "castBackdrop"; document.body.appendChild(castBackdrop); -castBackdrop.addEventListener("click", e=>{ if(e.target===castBackdrop) closeCast(); }); +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 */ @@ -35,18 +40,109 @@ 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 })).filter(w=>w.wid); + 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(); @@ -63,37 +159,36 @@ btnCast.addEventListener("click", async ()=>{ function closeCast(){ castOpen = false; renderCast(); } -function castTargetRow(target){ - const row = h("div","am-row am-target"); +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); - const key = target.type+":"+target.id; - if(castActiveKeys.has(key)) row.appendChild(h("span","am-chip active", T("castActive"))); - row.addEventListener("click", async ()=>{ - try{ - const body = { boardId: curSlot().id }; - if(castPathSteps.length){ - body.title = castPathTitle.trim(); - body.sequence = castPathSteps.map(step=>({boardId:step.boardId, widgetId:step.widgetId, mode:step.mode})); - }else if(castSelWidget){ body.widgetId = castSelWidget; body.mode = castSelMode; } - await api(`/assignments/${target.type}/${target.id}`, { method:"PUT", body }); - castActiveKeys.add(key); - castError = ""; - }catch(e){ castError = e.message; } - renderCast(); - }); + 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(){ +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"); + 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"); @@ -102,98 +197,193 @@ function renderCast(){ 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("div","am-msg", castError)); + 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); - sheet.appendChild(h("div","am-group", T("castWhat"))); - const widgetSel = h("select","am-sel"); - widgetSel.appendChild(new Option(T("amWholeBoard"), "")); - currentBoardWidgets().forEach(w=>widgetSel.appendChild(new Option(T("wg_"+w.id), w.wid))); - widgetSel.value = castSelWidget; - const modeSel = h("select","am-sel"); - modeSel.appendChild(new Option(T("amModeWork"), "werken")); - modeSel.appendChild(new Option(T("amModeWatch"), "kijken")); - modeSel.value = castSelMode; - modeSel.style.display = castSelWidget ? "" : "none"; - widgetSel.addEventListener("change", ()=>{ - castSelWidget = widgetSel.value; - modeSel.style.display = castSelWidget ? "" : "none"; - addStep.disabled = !castPathWidgetAllowed(castSelWidget) || castPathSteps.length >= 12; + 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); }); - modeSel.addEventListener("change", ()=>{ castSelMode = modeSel.value; }); - const whatRow = h("div","am-inline"); - whatRow.appendChild(widgetSel); - whatRow.appendChild(modeSel); - sheet.appendChild(whatRow); + sheet.appendChild(guide); - sheet.appendChild(h("div","am-group", T("castPathTitle"))); - 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(); }); - sheet.appendChild(pathTitle); - const addStep = h("button","am-btn", T("castPathAdd")); - addStep.type = "button"; - addStep.disabled = !castPathWidgetAllowed(castSelWidget) || castPathSteps.length >= 12; - addStep.title = T("castPathOnlyLearning"); - addStep.addEventListener("click", ()=>{ - if(!castPathWidgetAllowed(castSelWidget) || castPathSteps.length >= 12) return; - const widget = currentBoardWidgets().find(item=>item.wid===castSelWidget); - castPathSteps.push({boardId:curSlot().id, widgetId:castSelWidget, widgetType:widget?.id || "", mode:castSelMode}); - if(!castPathTitle) castPathTitle = T("castPathDefault"); - castActiveKeys.clear(); - renderCast(); + 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)})); }); - sheet.appendChild(addStep); - if(castPathSteps.length){ - const pathList = h("div","cast-path-list"); - castPathSteps.forEach((step,index)=>{ - const row = h("div","cast-path-step"); - row.appendChild(h("span","cast-path-number", String(index+1))); - const label = h("span","am-name", T("wg_"+step.widgetType)); - label.appendChild(h("small","am-role", " · "+T(step.mode==="kijken"?"amModeWatch":"amModeWork"))); - row.appendChild(label); - const up = h("button","am-btn","↑"); up.type="button"; up.disabled=index===0; - up.addEventListener("click",()=>{ [castPathSteps[index-1],castPathSteps[index]]=[castPathSteps[index],castPathSteps[index-1]]; castActiveKeys.clear(); renderCast(); }); - const down = h("button","am-btn","↓"); down.type="button"; down.disabled=index===castPathSteps.length-1; - down.addEventListener("click",()=>{ [castPathSteps[index+1],castPathSteps[index]]=[castPathSteps[index],castPathSteps[index+1]]; castActiveKeys.clear(); renderCast(); }); - const remove = h("button","am-btn","×"); remove.type="button"; - remove.setAttribute("aria-label",T("amDelete")); - remove.addEventListener("click",()=>{ castPathSteps.splice(index,1); castActiveKeys.clear(); renderCast(); }); - row.append(up,down,remove); - pathList.appendChild(row); - }); - sheet.appendChild(pathList); - sheet.appendChild(h("div","guestnote", T("castPathHint").replace("{count}",castPathSteps.length))); - } + audience.appendChild(classList); - sheet.appendChild(h("div","am-group", T("castClasses"))); - const classList = h("div","am-list"); - if(castLoading) classList.appendChild(h("div","guestnote", "…")); - else if(!castClasses.length) classList.appendChild(h("div","guestnote", T("castNoClasses"))); - else castClasses.forEach(c=>classList.appendChild(castTargetRow({type:"class", id:c.id, label:c.name}))); - sheet.appendChild(classList); - - sheet.appendChild(h("div","am-group", T("castPupils"))); - const pupilList = h("div","am-list"); - if(castLoading) pupilList.appendChild(h("div","guestnote", "…")); - else if(!castPupils.length) pupilList.appendChild(h("div","guestnote", T("castNoPupils"))); - else{ - const byClass = new Map(); - castPupils.forEach(u=>{ - const key = u.classId || "none"; - if(!byClass.has(key)) byClass.set(key, []); - byClass.get(key).push(u); + 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)); }); - castClasses.forEach(c=>(byClass.get(c.id) || []).forEach(u=> - pupilList.appendChild(castTargetRow({type:"pupil", id:u.id, label:u.displayName, sub:c.name})))); - (byClass.get("none") || []).forEach(u=> - pupilList.appendChild(castTargetRow({type:"pupil", id:u.id, label:u.displayName, sub:T("amUnassigned")}))); - } - sheet.appendChild(pupilList); + 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); } diff --git a/public/js/core.js b/public/js/core.js index 613dac5..0d3dc54 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.60-beta"; +const VERSION = "0.4.61-beta"; /* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en @@ -404,11 +404,28 @@ const I18N = { amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.", amUnlink:"Loskoppelen", amWholeBoard:"— heel bord —", amModeWatch:"kijken", amModeWork:"werken", - cast:"Cast", castTitle:"Cast bord", castWhat:"Wat cast je?", + cast:"Cast", castTitle:"Opdrachten klaarzetten", castWhat:"Wat wil je doorgeven?", castClasses:"Klassen", castPupils:"Leerlingen", castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.", castActive:"✓ Actief", castPathTitle:"Opdrachtenreeks (optioneel)", castPathPlaceholder:"Naam van de reeks…", - castPathAdd:"+ Geselecteerde widget als stap", castPathOnlyLearning:"Reeksen ondersteunen taal- en rekenwidgets.", castPathDefault:"Mijn opdrachtenreeks", castPathHint:"{count} van maximaal 12 stappen. Sleepvolgorde wordt met de pijlen bepaald.", + castPathAdd:"+ Geselecteerde widget als stap", castPathOnlyLearning:"Reeksen ondersteunen taal- en rekenwidgets.", castPathDefault:"Mijn opdrachtenreeks", castPathHint:"{count} van maximaal 12 stappen. Gebruik de pijlen om de volgorde te bepalen.", + castIntro:"Stel een opdracht of reeks samen vanuit de widgets op dit bord en geef deze daarna in één keer door.", + castStepChoose:"Kies vorm", castStepBuild:"Stel samen", castStepAudience:"Geef door", + castKindPath:"Opdrachtenreeks", castKindPathHelp:"Meerdere widgets na elkaar", + castKindWidget:"Eén opdracht", castKindWidgetHelp:"Eén gekozen widget", + castKindBoard:"Heel bord", castKindBoardHelp:"Alle geschikte widgets", + castLiveStateHint:"De huidige inhoud en instellingen van iedere widget worden gebruikt. Latere aanpassingen op het bord komen live bij leerlingen door.", + castPathName:"Naam voor leerlingen", castWidgetLibrary:"Widgets op dit bord", castWidgetLibraryHint:"Tik op een widget om die aan de reeks toe te voegen.", + castAddAll:"Alles toevoegen", castClearPath:"Reeks leegmaken", castNoLearningWidgets:"Zet eerst een taal- of rekenwidget op dit bord.", + castSequenceOrder:"Volgorde van de opdrachten", castPathEmpty:"Kies hierboven minimaal één widget. Je kunt een widget ook vaker toevoegen.", + castStepMode:"Wat doet de leerling?", castMoveUp:"Eerder in de reeks", castMoveDown:"Later in de reeks", castRemoveStep:"Verwijder deze stap", + castSingleHint:"Kies de widget die iedere leerling direct opent.", castBoardHint:"Leerlingen openen alle taal- en rekenwidgets van dit bord. Andere bordhulpmiddelen blijven verborgen.", + castAudienceTitle:"Aan wie geef je dit?", castAudienceHint:"Kies één of meer klassen en eventueel losse leerlingen. Een klaskeuze omvat automatisch alle leerlingen uit die klas.", + castAudienceSearch:"Zoek leerling of klas…", castSelectVisible:"Selecteer zichtbare leerlingen", castClearTargets:"Selectie wissen", castViaClass:"via klas", + castPupilCount:"{count} leerlingen", castTargetCount:"{count} doelgroep(en) gekozen", castAffectedPupils:"Bereikt ongeveer {count} leerling(en)", + castSend:"Doorgeven aan {count}", castSending:"Bezig met doorgeven…", castSent:"Opdracht succesvol doorgegeven aan {count} doelgroep(en).", castPartial:"Doorgeven mislukte voor {count} doelgroep(en); deze blijven geselecteerd.", + castNeedPath:"Voeg minimaal één widget aan de reeks toe.", castNeedTitle:"Geef de reeks een herkenbare naam.", castNeedWidget:"Kies één widget.", castNeedTarget:"Kies minimaal één klas of leerling.", + castOpenComposer:"+ Nieuwe opdracht of reeks", castOpenComposerHint:"Gebruik de widgets van het geopende bord en geef ze in één keer door aan meerdere doelgroepen.", amDashboardTitle:"Klasdashboard", amProgressDetail:"Detail per leerling", amLiveSessions:"Live leersessies", amLiveConnected:"● Live verbonden", amLiveReconnecting:"○ Verbinden…", amLiveNow:"Nu actief", amLiveRecent:"Meest recente sessies", amLiveNone:"Nog geen leerling actief.", amLiveRecentNone:"Nog geen recente sessies.", @@ -807,11 +824,28 @@ const I18N = { amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.", amUnlink:"Unlink", amWholeBoard:"— whole board —", amModeWatch:"watch", amModeWork:"work", - cast:"Cast", castTitle:"Cast board", castWhat:"What are you casting?", + cast:"Cast", castTitle:"Prepare assignments", castWhat:"What would you like to assign?", castClasses:"Classes", castPupils:"Pupils", castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.", castActive:"✓ Active", castPathTitle:"Learning path (optional)", castPathPlaceholder:"Name of the path…", castPathAdd:"+ Add selected widget as step", castPathOnlyLearning:"Paths support language and maths widgets.", castPathDefault:"My learning path", castPathHint:"{count} of up to 12 steps. Use the arrows to set the order.", + castIntro:"Build an assignment or path from the widgets on this board, then share it in one go.", + castStepChoose:"Choose format", castStepBuild:"Build it", castStepAudience:"Assign it", + castKindPath:"Learning path", castKindPathHelp:"Several widgets in sequence", + castKindWidget:"One assignment", castKindWidgetHelp:"One selected widget", + castKindBoard:"Whole board", castKindBoardHelp:"All suitable widgets", + castLiveStateHint:"Each widget uses its current content and settings. Later board changes are reflected live for pupils.", + castPathName:"Name shown to pupils", castWidgetLibrary:"Widgets on this board", castWidgetLibraryHint:"Tap a widget to add it to the path.", + castAddAll:"Add all", castClearPath:"Clear path", castNoLearningWidgets:"First add a language or maths widget to this board.", + castSequenceOrder:"Assignment order", castPathEmpty:"Choose at least one widget above. You can also add a widget more than once.", + castStepMode:"What does the pupil do?", castMoveUp:"Move earlier", castMoveDown:"Move later", castRemoveStep:"Remove this step", + castSingleHint:"Choose the widget every pupil should open directly.", castBoardHint:"Pupils open all language and maths widgets on this board. Other board tools remain hidden.", + castAudienceTitle:"Who receives this?", castAudienceHint:"Choose one or more classes and, if needed, individual pupils. Selecting a class automatically covers every pupil in it.", + castAudienceSearch:"Search pupil or class…", castSelectVisible:"Select visible pupils", castClearTargets:"Clear selection", castViaClass:"via class", + castPupilCount:"{count} pupils", castTargetCount:"{count} target(s) selected", castAffectedPupils:"Reaches about {count} pupil(s)", + castSend:"Assign to {count}", castSending:"Assigning…", castSent:"Assignment successfully shared with {count} target(s).", castPartial:"Sharing failed for {count} target(s); they remain selected.", + castNeedPath:"Add at least one widget to the path.", castNeedTitle:"Give the path a recognisable name.", castNeedWidget:"Choose one widget.", castNeedTarget:"Choose at least one class or pupil.", + castOpenComposer:"+ New assignment or path", castOpenComposerHint:"Use widgets from the open board and share them with several targets in one go.", amDashboardTitle:"Class dashboard", amProgressDetail:"Pupil details", amLiveSessions:"Live learning sessions", amLiveConnected:"● Live connected", amLiveReconnecting:"○ Connecting…", amLiveNow:"Active now", amLiveRecent:"Most recent sessions", amLiveNone:"No pupil is active yet.", amLiveRecentNone:"No recent sessions yet.", diff --git a/test/assignment-composer.test.js b/test/assignment-composer.test.js new file mode 100644 index 0000000..4e44cad --- /dev/null +++ b/test/assignment-composer.test.js @@ -0,0 +1,34 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +test('leerkracht bouwt visueel een reeks en geeft die in één actie aan meerdere doelgroepen', async () => { + const [cast, admin, css, core] = await Promise.all([ + readFile('public/js/cast.js', 'utf8'), + readFile('public/js/admin.js', 'utf8'), + readFile('public/css/teach.css', 'utf8'), + readFile('public/js/core.js', 'utf8'), + ]); + + assert.match(cast, /castShareKind\s*=\s*"path"/); + assert.match(cast, /castSelectedTargets\s*=\s*new Set/); + assert.match(cast, /cast-widget-card/); + assert.match(cast, /castPathSteps\.push/); + assert.match(cast, /slice\(0,12-castPathSteps\.length\)/); + assert.match(cast, /castPupilCoveredByClass/); + assert.match(cast, /Promise\.allSettled\(targets\.map/); + assert.match(cast, /castSendSelected/); + + assert.match(admin, /assignment-composer-cta[\s\S]*btnCast/); + assert.match(css, /cast-widget-grid[\s\S]*cast-path-step[\s\S]*cast-target-grid/); + assert.match(css, /cast-sendbar\{position:sticky/); + assert.match(css, /@media \(max-width:700px\)[\s\S]*cast-composer-sheet/); + + for (const key of [ + 'castIntro', 'castKindPath', 'castLiveStateHint', 'castWidgetLibrary', 'castSequenceOrder', + 'castAudienceTitle', 'castSelectVisible', 'castSend', 'castSent', + 'castNeedPath', 'castNeedTarget', 'castOpenComposer', + ]) { + assert.equal(core.split(`${key}:`).length - 1, 2, `${key} moet tweetalig zijn`); + } +});