diff --git a/VERSION b/VERSION
index 2a6aad5..1760d49 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.4.14-beta
+0.4.15-beta
diff --git a/public/css/teach.css b/public/css/teach.css
index daa269b..826813f 100644
--- a/public/css/teach.css
+++ b/public/css/teach.css
@@ -135,7 +135,8 @@
#pupilView{display:none;}
body.pupil-mode #board,body.pupil-mode #fab,body.pupil-mode #fabBoardsBtn,
body.pupil-mode #boardStrip,body.pupil-mode #fabmenu,
- body.pupil-mode #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave{display:none !important;}
+ body.pupil-mode #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave,
+ body.pupil-mode #btnCast{display:none !important;}
body.pupil-mode #pupilView{
display:flex; flex-wrap:wrap; gap:18px; align-content:flex-start;
position:absolute; inset:var(--tbar) 0 0 0; overflow:auto; padding:18px; box-sizing:border-box;
@@ -160,7 +161,8 @@
#parentView{display:none;}
body.parent-mode #board,body.parent-mode #fab,body.parent-mode #fabBoardsBtn,
body.parent-mode #boardStrip,body.parent-mode #fabmenu,
- body.parent-mode #hud,body.parent-mode #btnFolders,body.parent-mode #btnSave{display:none !important;}
+ body.parent-mode #hud,body.parent-mode #btnFolders,body.parent-mode #btnSave,
+ body.parent-mode #btnCast{display:none !important;}
body.parent-mode #parentView{
display:block; position:absolute; inset:var(--tbar) 0 0 0;
overflow:auto; padding:24px 18px; box-sizing:border-box;
@@ -1316,12 +1318,16 @@
font-size:11.5px; font-weight:700; text-transform:uppercase; letter-spacing:.4px;
color:var(--muted); margin:14px 0 6px;
}
-.am-person{cursor:pointer;}
+.am-person,.am-target{cursor:pointer;}
.am-person-info{flex:1; display:flex; flex-direction:column; min-width:0; gap:1px;}
.am-person-info .am-name{font-size:14px;}
.am-person-info .am-role{font-size:11.5px; color:var(--muted); font-weight:600;}
.am-sheet-backdrop{position:fixed; inset:0; background:rgba(15,22,38,.4); z-index:60; display:none;}
.am-sheet-backdrop.open{display:block;}
+/* de Cast-sheet opent vanaf het bord zelf (buiten Instellingen om), dus moet
+ boven de zwevende pen-HUD (#hud, z-index 5500) uitkomen - zelfde laag als
+ .modalwrap (6000), niet de 60 die volstaat zodra je al in Instellingen zit */
+#castBackdrop{z-index:6000;}
.am-sheet{
position:absolute; left:0; right:0; bottom:0; max-width:520px; margin:0 auto;
background:var(--surface); border-radius:18px 18px 0 0; padding:16px 18px 24px;
@@ -1456,6 +1462,7 @@
}
button.am-chip{cursor:pointer; color:var(--muted);}
button.am-chip.on{border-color:var(--accent); background:var(--accent-soft); color:var(--accent-ink);}
+.am-chip.active{border-color:var(--green); background:color-mix(in srgb, var(--green) 15%, var(--surface)); color:var(--green);}
.am-chipx{border:none; background:transparent; color:var(--red); font-weight:900; cursor:pointer; padding:0; font-size:11px;}
.am-reset{
background:var(--surface-2); border:1px solid var(--line); border-radius:12px;
diff --git a/public/index.html b/public/index.html
index 4494662..be8443c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -43,6 +43,7 @@
+
@@ -61,6 +62,7 @@
+
diff --git a/public/js/admin.js b/public/js/admin.js
index 1c98eb9..965dbe6 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -621,19 +621,9 @@
return box;
}
- /* toewijzingsrij: kies een van je eigen borden (uit BS, board.js) voor een klas of
- een individuele leerling. Geen momentopname - de leerling-pagina zoekt het bord
- live op, dus nieuwe woorden die je toevoegt komen vanzelf door zonder opnieuw
- toe te wijzen. Een expliciete "Toewijzen"-knop i.p.v. direct bij het kiezen van
- een bord opslaan, zodat een klas-brede wijziging niet per ongeluk gebeurt. */
- /* widgets van een eigen bord (voor de één-widget-keuze in assignmentRow);
- widgets zonder wid komen uit borden van vóór v0.3.13 en verschijnen pas
- na de eerstvolgende save van dat bord */
- function boardWidgets(boardId){
- let found = null;
- BS.folders.forEach(fo=>fo.boards.forEach(bo=>{ if(bo.id===boardId) found = bo; }));
- return ((found && found.data && found.data.widgets) || []).filter(w=>w.wid);
- }
+ /* toewijzingsrij: overzicht van de huidige toewijzing (aangemaakt via de
+ Cast-knop op het whiteboard, cast.js) + loskoppelen. Aanmaken/wijzigen
+ gebeurt niet meer hier - zie cast.js. */
function describeCurrent(current){
if(!current) return "";
let s = `${T("amAssignedAs")} â${current.boardName || current.boardId}â`;
@@ -643,56 +633,23 @@
}
return s;
}
- function assignmentRow(target, boardOptions, current){
- const sel = h("select","am-sel");
- sel.appendChild(new Option(T("amNoAssignment"), ""));
- boardOptions.forEach(b=>sel.appendChild(new Option(b.label, b.id)));
- sel.value = current ? current.boardId : "";
- /* keuze: heel bord (default, bestaand gedrag) of één widget + modus */
- const widgetSel = h("select","am-sel am-widget-sel");
- const modeSel = h("select","am-sel am-mode-sel");
- modeSel.appendChild(new Option(T("amModeWork"), "werken"));
- modeSel.appendChild(new Option(T("amModeWatch"), "kijken"));
- const syncWidgetSel = ()=>{
- widgetSel.innerHTML = "";
- widgetSel.appendChild(new Option(T("amWholeBoard"), ""));
- boardWidgets(sel.value).forEach(w=>widgetSel.appendChild(new Option(T("wg_"+w.id), w.wid)));
- widgetSel.value = (current && current.boardId===sel.value && current.widgetId) ? current.widgetId : "";
- widgetSel.style.display = sel.value ? "" : "none";
- syncModeSel();
- };
- const syncModeSel = ()=>{ modeSel.style.display = (sel.value && widgetSel.value) ? "" : "none"; };
- modeSel.value = current && current.mode ? current.mode : "werken";
- sel.addEventListener("change", syncWidgetSel);
- widgetSel.addEventListener("change", syncModeSel);
- const info = h("span","am-code", describeCurrent(current));
- syncWidgetSel();
- const apply = h("button","am-btn", T("amAssign"));
- apply.type = "button";
- apply.addEventListener("click", async ()=>{
- try{
- if(sel.value){
- const body = { boardId: sel.value };
- if(widgetSel.value){ body.widgetId = widgetSel.value; body.mode = modeSel.value; }
- await api(`/assignments/${target.type}/${target.id}`, {method:"PUT", body});
- }
- else await api(`/assignments/${target.type}/${target.id}`, {method:"DELETE"});
- if(target.type==="class"){ await reload(); }
- else{
- const a = (await api("/assignments/pupil/"+target.id)).assignment;
- info.textContent = describeCurrent(a);
- msg(T("amAssignSaved"));
- }
- }catch(e){ msg(e.message); }
- });
- /* kop toont compact de huidige stand; de keuzes klappen uit via Bewerken */
+ function assignmentRow(target, current){
+ const info = h("span","am-code", describeCurrent(current) || T("amNoAssignment"));
const tools = h("div","am-tools");
- const inline = h("div","am-inline");
- inline.appendChild(sel);
- inline.appendChild(widgetSel);
- inline.appendChild(modeSel);
- inline.appendChild(apply);
- tools.appendChild(inline);
+ if(current){
+ const del = dangerBtn(T("amUnlink"), null, async ()=>{
+ try{
+ await api(`/assignments/${target.type}/${target.id}`, {method:"DELETE"});
+ if(target.type==="class"){ await reload(); }
+ else{
+ info.textContent = T("amNoAssignment");
+ del.remove();
+ msg(T("amAssignSaved"));
+ }
+ }catch(e){ msg(e.message); }
+ });
+ tools.appendChild(del);
+ }
tools.appendChild(info);
return groupCard({
title: target.label,
@@ -730,13 +687,10 @@
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
return;
}
- const boardOptions = [];
- BS.folders.forEach(fo=>fo.boards.forEach(bo=>boardOptions.push({ id: bo.id, label: `${fo.name} / ${bo.name}` })));
-
panel.appendChild(h("div","am-group", T("amAssignClasses")));
const classList = h("div","am-list");
if(!CLASSES.length) classList.appendChild(h("div","guestnote", T("amNoClass")));
- CLASSES.forEach(c=>classList.appendChild(assignmentRow({type:"class", id:c.id, label:c.name}, boardOptions, ASSIGNMENTS[c.id])));
+ CLASSES.forEach(c=>classList.appendChild(assignmentRow({type:"class", id:c.id, label:c.name}, ASSIGNMENTS[c.id])));
panel.appendChild(classList);
panel.appendChild(h("div","am-group", T("amAssignPupils")));
@@ -752,7 +706,7 @@
const u = pupils.find(x=>String(x.id)===pupilSel.value);
let current = null;
try{ current = (await api("/assignments/pupil/"+u.id)).assignment; }catch(e){}
- pupilRowHolder.appendChild(assignmentRow({type:"pupil", id:u.id, label:u.displayName}, boardOptions, current));
+ pupilRowHolder.appendChild(assignmentRow({type:"pupil", id:u.id, label:u.displayName}, current));
});
}
diff --git a/public/js/cast.js b/public/js/cast.js
new file mode 100644
index 0000000..6346b1a
--- /dev/null
+++ b/public/js/cast.js
@@ -0,0 +1,146 @@
+/* teach - cast (đĄ): het geopende bord (of één widget erop) rechtstreeks
+ vanaf de toolbar uitzenden naar een klas of leerling, i.p.v. de omweg via
+ Instellingen > Toewijzingen. Hergebruikt dezelfde PUT/DELETE-routes als
+ admin.js's assignmentRow - geen serverwijziging, alleen een korter pad. */
+"use strict";
+const btnCast = document.getElementById("btnCast");
+btnCast.title = T("cast");
+document.addEventListener("langchange", ()=>{
+ btnCast.title = T("cast");
+ if(castOpen) renderCast();
+});
+
+function h(tag, cls, text){
+ const el = document.createElement(tag);
+ if(cls) el.className = cls;
+ if(text != null) el.textContent = text;
+ return el;
+}
+
+const castBackdrop = h("div","am-sheet-backdrop");
+castBackdrop.id = "castBackdrop";
+document.body.appendChild(castBackdrop);
+castBackdrop.addEventListener("click", e=>{ if(e.target===castBackdrop) closeCast(); });
+
+let castOpen = false;
+let castLoading = false;
+let castClasses = [], castPupils = [];
+let castSelWidget = "", castSelMode = "werken";
+/* toewijzingen die deze sessie al gelukt zijn ("type:id"), zodat je hetzelfde
+ bord naar meerdere klassen/leerlingen achter elkaar kunt casten zonder dat
+ de sheet na elke keer sluit */
+let castActiveKeys = new Set();
+let castError = "";
+
+/* widgets van het live-open bord, zelfde bron als serializeBoard() (board.js) */
+function currentBoardWidgets(){
+ return [...instances].map(inst=>({ wid: inst.wid, id: inst.def.id })).filter(w=>w.wid);
+}
+
+btnCast.addEventListener("click", async ()=>{
+ stashCurrent(); /* verse widget-state, zelfde als btnFolders doet */
+ castOpen = true;
+ castSelWidget = ""; castSelMode = "werken";
+ castActiveKeys = new Set(); castError = "";
+ castLoading = true;
+ renderCast();
+ try{
+ const [cRes, uRes] = await Promise.all([ api("/admin/classes"), api("/admin/users") ]);
+ castClasses = cRes.classes || [];
+ castPupils = (uRes.users || []).filter(u=>u.role==="pupil");
+ }catch(e){
+ castClasses = []; castPupils = []; castError = e.message;
+ }
+ castLoading = false;
+ renderCast();
+});
+
+function closeCast(){ castOpen = false; renderCast(); }
+
+function castTargetRow(target){
+ const row = h("div","am-row am-target");
+ 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(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;
+}
+
+function renderCast(){
+ castBackdrop.classList.toggle("open", castOpen);
+ castBackdrop.innerHTML = "";
+ if(!castOpen) return;
+
+ const sheet = h("div","am-sheet");
+ castBackdrop.appendChild(sheet);
+
+ const head = h("div","am-kebab-row");
+ const titleWrap = h("div");
+ titleWrap.appendChild(h("h2","am-nav-title", T("castTitle")));
+ titleWrap.appendChild(h("div","am-role", `${BS.folders[BS.f].name} / ${curSlot().name}`));
+ head.appendChild(titleWrap);
+ const closeBtn = h("button","am-btn", T("close"));
+ closeBtn.type = "button";
+ closeBtn.addEventListener("click", closeCast);
+ head.appendChild(closeBtn);
+ sheet.appendChild(head);
+
+ sheet.appendChild(h("div","am-msg", castError));
+
+ 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";
+ });
+ modeSel.addEventListener("change", ()=>{ castSelMode = modeSel.value; });
+ const whatRow = h("div","am-inline");
+ whatRow.appendChild(widgetSel);
+ whatRow.appendChild(modeSel);
+ sheet.appendChild(whatRow);
+
+ 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);
+ });
+ 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);
+}
diff --git a/public/js/core.js b/public/js/core.js
index 9a02296..5993d12 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.14-beta";
+const VERSION = "0.4.15-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@@ -315,7 +315,12 @@ const I18N = {
amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)",
amPickPupil:"â kies een leerling â", amNoAssignment:"â geen toewijzing â",
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?",
+ castClasses:"Klassen", castPupils:"Leerlingen",
+ castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
+ castActive:"â Actief",
amProgress:"Voortgang", amProgressEmpty:"Nog geen voortgang voor deze leerling.",
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
amCopy:"kopieer", amCopyAll:"kopieer alles",
@@ -617,7 +622,12 @@ const I18N = {
amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)",
amPickPupil:"â pick a pupil â", amNoAssignment:"â no assignment â",
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?",
+ castClasses:"Classes", castPupils:"Pupils",
+ castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
+ castActive:"â Active",
amProgress:"Progress", amProgressEmpty:"No progress for this pupil yet.",
amConfirmDel:"Are you sure you want to delete this user?",
amCopy:"copy", amCopyAll:"copy all",