feat: simplify assignment sequence sharing
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
This commit is contained in:
parent
b8e9e46819
commit
05f3dbe56c
6 changed files with 421 additions and 118 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.60-beta
|
0.4.61-beta
|
||||||
|
|
|
||||||
|
|
@ -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{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;}
|
#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 */
|
/* Visuele opdracht- en reeksbouwer voor groepsleiding */
|
||||||
.cast-path-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
#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-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-composer-intro{max-width:72ch;margin:0 0 10px;color:var(--muted);font-size:13px;line-height:1.45}
|
||||||
.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-status{min-height:0;margin:0 0 10px;padding:0;font-size:13px;font-weight:850}
|
||||||
.cast-path-step .am-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.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-path-step .am-btn{min-width:30px;padding:4px 7px}
|
.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 */
|
/* Leerkrachtdashboard: scanbare status per leerling */
|
||||||
#settingsModal .teacher-dashboard{margin:10px 0 20px}
|
#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{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-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)}
|
#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.) */
|
/* Voortgang-tab: klassenmanagement-koppeling + grafiek (v0.4.35-beta e.v.) */
|
||||||
/* Realtime leersessies bovenin het klasdashboard */
|
/* 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-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 .teacher-dashboard{margin-top:0}
|
||||||
#settingsModal .am-combo-pane .am-group:first-child{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: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{
|
#settingsModal .progress-level-badge{
|
||||||
display:inline-flex; align-self:flex-start; padding:4px 10px; margin-bottom:10px;
|
display:inline-flex; align-self:flex-start; padding:4px 10px; margin-bottom:10px;
|
||||||
|
|
|
||||||
|
|
@ -792,6 +792,17 @@
|
||||||
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
|
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
|
||||||
return;
|
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")));
|
panel.appendChild(h("div","am-group", T("amAssignClasses")));
|
||||||
const classList = h("div","am-list");
|
const classList = h("div","am-list");
|
||||||
if(!CLASSES.length) classList.appendChild(h("div","guestnote", T("amNoClass")));
|
if(!CLASSES.length) classList.appendChild(h("div","guestnote", T("amNoClass")));
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,18 @@ function h(tag, cls, text){
|
||||||
const castBackdrop = h("div","am-sheet-backdrop");
|
const castBackdrop = h("div","am-sheet-backdrop");
|
||||||
castBackdrop.id = "castBackdrop";
|
castBackdrop.id = "castBackdrop";
|
||||||
document.body.appendChild(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 castOpen = false;
|
||||||
let castLoading = false;
|
let castLoading = false;
|
||||||
let castClasses = [], castPupils = [];
|
let castClasses = [], castPupils = [];
|
||||||
let castSelWidget = "", castSelMode = "werken";
|
let castSelWidget = "", castSelMode = "werken";
|
||||||
let castPathTitle = "", castPathSteps = [];
|
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
|
/* toewijzingen die deze sessie al gelukt zijn ("type:id"), zodat je hetzelfde
|
||||||
bord naar meerdere klassen/leerlingen achter elkaar kunt casten zonder dat
|
bord naar meerdere klassen/leerlingen achter elkaar kunt casten zonder dat
|
||||||
de sheet na elke keer sluit */
|
de sheet na elke keer sluit */
|
||||||
|
|
@ -35,18 +40,109 @@ let castError = "";
|
||||||
|
|
||||||
/* widgets van het live-open bord, zelfde bron als serializeBoard() (board.js) */
|
/* widgets van het live-open bord, zelfde bron als serializeBoard() (board.js) */
|
||||||
function currentBoardWidgets(){
|
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){
|
function castPathWidgetAllowed(widgetId){
|
||||||
const widget = currentBoardWidgets().find(item=>item.wid===widgetId);
|
const widget = currentBoardWidgets().find(item=>item.wid===widgetId);
|
||||||
return !!widget && ["taal","rekenen"].includes(widget.cat);
|
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 ()=>{
|
btnCast.addEventListener("click", async ()=>{
|
||||||
stashCurrent(); /* verse widget-state, zelfde als btnFolders doet */
|
stashCurrent(); /* verse widget-state, zelfde als btnFolders doet */
|
||||||
castOpen = true;
|
castOpen = true;
|
||||||
castSelWidget = ""; castSelMode = "werken";
|
castSelWidget = ""; castSelMode = "werken";
|
||||||
castPathTitle = ""; castPathSteps = [];
|
castPathTitle = ""; castPathSteps = [];
|
||||||
|
castShareKind = currentBoardWidgets().some(w=>castPathWidgetAllowed(w.wid)) ? "path" : "board";
|
||||||
|
castSelectedTargets = new Set(); castPupilQuery = ""; castSaving = false; castNotice = "";
|
||||||
castActiveKeys = new Set(); castError = "";
|
castActiveKeys = new Set(); castError = "";
|
||||||
castLoading = true;
|
castLoading = true;
|
||||||
renderCast();
|
renderCast();
|
||||||
|
|
@ -63,37 +159,36 @@ btnCast.addEventListener("click", async ()=>{
|
||||||
|
|
||||||
function closeCast(){ castOpen = false; renderCast(); }
|
function closeCast(){ castOpen = false; renderCast(); }
|
||||||
|
|
||||||
function castTargetRow(target){
|
function castTargetPicker(target){
|
||||||
const row = h("div","am-row am-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");
|
const info = h("div","am-person-info");
|
||||||
info.appendChild(h("span","am-name", target.label));
|
info.appendChild(h("span","am-name", target.label));
|
||||||
if(target.sub) info.appendChild(h("span","am-role", target.sub));
|
if(target.sub) info.appendChild(h("span","am-role", target.sub));
|
||||||
row.appendChild(info);
|
row.appendChild(info);
|
||||||
const key = target.type+":"+target.id;
|
if(covered)row.appendChild(h("span","cast-target-via",T("castViaClass")));
|
||||||
if(castActiveKeys.has(key)) row.appendChild(h("span","am-chip active", T("castActive")));
|
else if(castActiveKeys.has(castTargetKey(target)))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();
|
|
||||||
});
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCast(){
|
function renderCast(preserveScroll=false){
|
||||||
|
const previousScroll=preserveScroll ? castBackdrop.querySelector(".am-sheet")?.scrollTop || 0 : 0;
|
||||||
castBackdrop.classList.toggle("open", castOpen);
|
castBackdrop.classList.toggle("open", castOpen);
|
||||||
castBackdrop.innerHTML = "";
|
castBackdrop.innerHTML = "";
|
||||||
if(!castOpen) return;
|
if(!castOpen) return;
|
||||||
|
|
||||||
const sheet = h("div","am-sheet");
|
const sheet = h("div","am-sheet cast-composer-sheet");
|
||||||
castBackdrop.appendChild(sheet);
|
castBackdrop.appendChild(sheet);
|
||||||
|
if(preserveScroll)requestAnimationFrame(()=>{sheet.scrollTop=previousScroll;});
|
||||||
|
|
||||||
const head = h("div","am-kebab-row");
|
const head = h("div","am-kebab-row");
|
||||||
const titleWrap = h("div");
|
const titleWrap = h("div");
|
||||||
|
|
@ -102,98 +197,193 @@ function renderCast(){
|
||||||
head.appendChild(titleWrap);
|
head.appendChild(titleWrap);
|
||||||
const closeBtn = h("button","am-btn", T("close"));
|
const closeBtn = h("button","am-btn", T("close"));
|
||||||
closeBtn.type = "button";
|
closeBtn.type = "button";
|
||||||
|
closeBtn.disabled = castSaving;
|
||||||
closeBtn.addEventListener("click", closeCast);
|
closeBtn.addEventListener("click", closeCast);
|
||||||
head.appendChild(closeBtn);
|
head.appendChild(closeBtn);
|
||||||
sheet.appendChild(head);
|
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 guide=h("ol","cast-guide");
|
||||||
const widgetSel = h("select","am-sel");
|
[T("castStepChoose"),T("castStepBuild"),T("castStepAudience")].forEach((label,index)=>{
|
||||||
widgetSel.appendChild(new Option(T("amWholeBoard"), ""));
|
const item=h("li");item.append(h("span",null,String(index+1)),h("strong",null,label));guide.appendChild(item);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
modeSel.addEventListener("change", ()=>{ castSelMode = modeSel.value; });
|
sheet.appendChild(guide);
|
||||||
const whatRow = h("div","am-inline");
|
|
||||||
whatRow.appendChild(widgetSel);
|
|
||||||
whatRow.appendChild(modeSel);
|
|
||||||
sheet.appendChild(whatRow);
|
|
||||||
|
|
||||||
sheet.appendChild(h("div","am-group", T("castPathTitle")));
|
const composer=h("section","cast-composer-card");
|
||||||
const pathTitle = h("input","am-inp");
|
composer.appendChild(h("h3",null,T("castWhat")));
|
||||||
pathTitle.type = "text";
|
const kindTabs=h("div","cast-kind-tabs");
|
||||||
pathTitle.maxLength = 80;
|
[["path","castKindPath","castKindPathHelp"],["widget","castKindWidget","castKindWidgetHelp"],["board","castKindBoard","castKindBoardHelp"]]
|
||||||
pathTitle.placeholder = T("castPathPlaceholder");
|
.forEach(([kind,labelKey,helpKey])=>{
|
||||||
pathTitle.value = castPathTitle;
|
const button=h("button","cast-kind-button"+(castShareKind===kind?" active":""));
|
||||||
pathTitle.addEventListener("input", ()=>{ castPathTitle = pathTitle.value; castActiveKeys.clear(); });
|
button.type="button";button.disabled=castSaving;
|
||||||
sheet.appendChild(pathTitle);
|
button.append(h("strong",null,T(labelKey)),h("span",null,T(helpKey)));
|
||||||
const addStep = h("button","am-btn", T("castPathAdd"));
|
button.addEventListener("click",()=>{
|
||||||
addStep.type = "button";
|
castShareKind=kind;castActiveKeys.clear();castError="";castNotice="";renderCast(true);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
sheet.appendChild(addStep);
|
kindTabs.appendChild(button);
|
||||||
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);
|
composer.appendChild(kindTabs);
|
||||||
sheet.appendChild(h("div","guestnote", T("castPathHint").replace("{count}",castPathSteps.length)));
|
composer.appendChild(h("p","cast-live-hint",T("castLiveStateHint")));
|
||||||
}
|
|
||||||
|
|
||||||
sheet.appendChild(h("div","am-group", T("castClasses")));
|
const learningWidgets=currentBoardWidgets().filter(widget=>castPathWidgetAllowed(widget.wid));
|
||||||
const classList = h("div","am-list");
|
const widgetGrid=(selectOnly=false)=>{
|
||||||
if(castLoading) classList.appendChild(h("div","guestnote", "…"));
|
const grid=h("div","cast-widget-grid");
|
||||||
else if(!castClasses.length) classList.appendChild(h("div","guestnote", T("castNoClasses")));
|
learningWidgets.forEach(widget=>{
|
||||||
else castClasses.forEach(c=>classList.appendChild(castTargetRow({type:"class", id:c.id, label:c.name})));
|
const selected=selectOnly&&castSelWidget===widget.wid;
|
||||||
sheet.appendChild(classList);
|
const added=castPathSteps.filter(step=>step.widgetId===widget.wid).length;
|
||||||
|
const card=h("button","cast-widget-card"+(selected?" selected":""));
|
||||||
sheet.appendChild(h("div","am-group", T("castPupils")));
|
card.type="button";card.disabled=castSaving||(!selectOnly&&castPathSteps.length>=12);
|
||||||
const pupilList = h("div","am-list");
|
card.setAttribute("aria-pressed",String(selected));
|
||||||
if(castLoading) pupilList.appendChild(h("div","guestnote", "…"));
|
card.appendChild(h("span","cast-widget-icon",widget.icon));
|
||||||
else if(!castPupils.length) pupilList.appendChild(h("div","guestnote", T("castNoPupils")));
|
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{
|
else{
|
||||||
const byClass = new Map();
|
castPathSteps.push({boardId:curSlot().id,widgetId:widget.wid,widgetType:widget.id,widgetLabel:castWidgetLabel(widget),icon:widget.icon,mode:"werken"});
|
||||||
castPupils.forEach(u=>{
|
if(!castPathTitle)castPathTitle=T("castPathDefault");
|
||||||
const key = u.classId || "none";
|
|
||||||
if(!byClass.has(key)) byClass.set(key, []);
|
|
||||||
byClass.get(key).push(u);
|
|
||||||
});
|
|
||||||
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
/* 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. */
|
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
|
/* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte
|
||||||
achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk
|
achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk
|
||||||
zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en
|
zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en
|
||||||
|
|
@ -404,11 +404,28 @@ const I18N = {
|
||||||
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
|
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
|
||||||
amUnlink:"Loskoppelen",
|
amUnlink:"Loskoppelen",
|
||||||
amWholeBoard:"— heel bord —", amModeWatch:"kijken", amModeWork:"werken",
|
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",
|
castClasses:"Klassen", castPupils:"Leerlingen",
|
||||||
castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
|
castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
|
||||||
castActive:"✓ Actief", castPathTitle:"Opdrachtenreeks (optioneel)", castPathPlaceholder:"Naam van de reeks…",
|
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",
|
amDashboardTitle:"Klasdashboard", amProgressDetail:"Detail per leerling",
|
||||||
amLiveSessions:"Live leersessies", amLiveConnected:"● Live verbonden", amLiveReconnecting:"○ Verbinden…",
|
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.",
|
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.",
|
amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.",
|
||||||
amUnlink:"Unlink",
|
amUnlink:"Unlink",
|
||||||
amWholeBoard:"— whole board —", amModeWatch:"watch", amModeWork:"work",
|
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",
|
castClasses:"Classes", castPupils:"Pupils",
|
||||||
castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
|
castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
|
||||||
castActive:"✓ Active", castPathTitle:"Learning path (optional)", castPathPlaceholder:"Name of the path…",
|
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.",
|
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",
|
amDashboardTitle:"Class dashboard", amProgressDetail:"Pupil details",
|
||||||
amLiveSessions:"Live learning sessions", amLiveConnected:"● Live connected", amLiveReconnecting:"○ Connecting…",
|
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.",
|
amLiveNow:"Active now", amLiveRecent:"Most recent sessions", amLiveNone:"No pupil is active yet.", amLiveRecentNone:"No recent sessions yet.",
|
||||||
|
|
|
||||||
34
test/assignment-composer.test.js
Normal file
34
test/assignment-composer.test.js
Normal file
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue