diff --git a/VERSION b/VERSION index 1063753..b1c0caf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.58-beta +0.4.59-beta diff --git a/public/css/teach.css b/public/css/teach.css index afc1ef1..b590114 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -2475,6 +2475,28 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000 #settingsModal .live-session-widget,#settingsModal .live-session-time,#settingsModal .live-session-score{max-width:100%;color:var(--muted);font-size:11px;line-height:1.35} #settingsModal .live-session-score{font-weight:850;color:var(--accent-ink)} +#settingsModal .am-combo-shell{display:flex;flex-direction:column;gap:14px} +#settingsModal .am-combo-hero{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr);gap:14px;align-items:stretch;padding:16px;border:1px solid color-mix(in srgb,var(--accent) 18%,var(--line));border-radius:18px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 10%,var(--surface)),var(--surface))} +#settingsModal .am-combo-hero-copy{display:flex;flex-direction:column;gap:8px} +#settingsModal .am-combo-kicker{color:var(--accent-ink);font-size:11px;font-weight:900;text-transform:uppercase;letter-spacing:.48px} +#settingsModal .am-combo-title{margin:0;font-size:clamp(18px,3vw,26px);line-height:1.05} +#settingsModal .am-combo-lead{margin:0;max-width:62ch;color:var(--muted);font-size:13px;line-height:1.45} +#settingsModal .am-combo-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:4px} +#settingsModal .am-combo-stat{padding:10px 11px;border-radius:12px;background:var(--surface-2);display:flex;flex-direction:column;gap:2px} +#settingsModal .am-combo-stat strong{font-size:19px;line-height:1;color:var(--ink)} +#settingsModal .am-combo-stat span{font-size:10.5px;font-weight:850;color:var(--muted);text-transform:uppercase;letter-spacing:.35px} +#settingsModal .am-combo-chart{width:100%;min-height:140px;align-self:center} +#settingsModal .am-combo-chart rect{filter:drop-shadow(0 3px 8px rgba(20,35,60,.12))} +#settingsModal .am-combo-chart-value{fill:#fff;font-size:18px;font-weight:900} +#settingsModal .am-combo-chart-label{fill:var(--muted);font-size:10px;font-weight:800} +#settingsModal .am-combo-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;align-items:start} +#settingsModal .am-combo-pane{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:0 3px 14px rgba(20,35,60,.05)} +#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} +@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}} + #settingsModal .progress-level-badge{ display:inline-flex; align-self:flex-start; padding:4px 10px; margin-bottom:10px; border-radius:999px; background:var(--accent-soft); color:var(--accent-ink); diff --git a/public/js/admin.js b/public/js/admin.js index d2ca84f..ca291e9 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -213,6 +213,7 @@ T("amNewPw"), T("amParentCode"), "leerling","pupil","ouder","parent","wachtwoord","password", "koppelcode","aanmelding","enrolment", T("amNewClass"), T("amResetClass"), "klas","class","groep","group", + T("amLevelYearGroup"), T("amLevelLevel"), "leerjaar","year group","niveau","level","differentiatie", T("amNewSchool"), T("amNewSuper"), "school","systeemmanager","system manager"], search:q=>[ ...USERS @@ -229,15 +230,17 @@ .map(s=>({ icon:"🏫", label:s.name, sub:T("amSchools"), go:()=>{ selSchool=String(s.id); navLevel={type:"overview"}; goSettingsSection("school"); } })), ] }, - { tab:"toewijzingen", icon:"🎯", group:"school", order:12, labelKey:"amAssignments", subKey:"stToewijzSub", + { tab:"toewijzingen", icon:"🎯", group:"school", order:12, labelKey:"amAssignmentsLevels", subKey:"amAssignmentsLevelsSub", show:()=>can("assignments.manage"), keywords:()=>[T("amModeWatch"), T("amModeWork"), T("amDashboardTitle"), T("castPathTitle"), - "bord","board","delen","share","toewijzen","assign","widget","dashboard","opdrachtenreeks","learning path"] }, + T("amLevelYearGroup"), T("amLevelLevel"), T("amAssignmentsLevels"), + "bord","board","delen","share","toewijzen","assign","widget","dashboard","opdrachtenreeks","learning path", + "niveau","level","groep","niveaugroep","leerjaar","year group","differentiatie"] }, { tab:"voortgang", icon:"📈", group:"school", order:13, labelKey:"amProgress", subKey:"stVoortgangSub", show:()=>can("progress.view"), keywords:()=>["voortgang","progress","score","resultaten","results","tijdlijn","timeline"] }, { tab:"klassenmanagement", icon:"🎓", group:"school", order:14, labelKey:"amLevels", subKey:"amLevelsSub", - show:()=>can("assignments.manage"), + show:()=>false, keywords:()=>[T("amLevelYearGroup"), T("amLevelLevel"), "niveau","level","groep","niveaugroep", "leerjaar","year group","differentiatie"] }, { tab:"opslag", icon:"🖼", group:"system", order:32, labelKey:"stOpslag", subKey:"stOpslagSub", @@ -712,6 +715,73 @@ addGroup(T("amNoClass"), byClass.get("none")); } + function svgEl(tag, attrs){ + const el = document.createElementNS("http://www.w3.org/2000/svg", tag); + Object.entries(attrs || {}).forEach(([k,v])=>el.setAttribute(k, String(v))); + return el; + } + + function combinedOverviewChart(){ + const items = [ + { label: T("amAssignClasses"), value: CLASSES.filter(c=>ASSIGNMENTS[c.id]).length, color: "var(--blue)" }, + { label: T("amAssignPupils"), value: USERS.filter(u=>u.role==="pupil" && !!ASSIGNMENTS[u.id]).length, color: "var(--green)" }, + { label: T("amUnassigned"), value: USERS.filter(u=>u.role==="pupil" && !u.classId).length, color: "var(--orange)" }, + { label: T("amLevels"), value: CLASSES.length, color: "var(--purple)" }, + ]; + const svg = svgEl("svg", { viewBox: "0 0 520 140", class: "am-combo-chart", role: "img", "aria-label": T("amAssignmentsLevels") }); + const max = Math.max(1, ...items.map(item => item.value)); + items.forEach((item, index) => { + const x = 34 + index * 122; + const height = Math.max(8, Math.round((item.value / max) * 72)); + svg.appendChild(svgEl("rect", { x: x, y: 88 - height, width: 72, height, rx: 12, fill: item.color })); + const value = svgEl("text", { x: x + 36, y: 102, class: "am-combo-chart-value", "text-anchor": "middle" }); + value.textContent = String(item.value); + svg.appendChild(value); + const label = svgEl("text", { x: x + 36, y: 124, class: "am-combo-chart-label", "text-anchor": "middle" }); + label.textContent = item.label; + svg.appendChild(label); + }); + return svg; + } + + function renderAssignmentsLevelsPanel(panel){ + const schoolChosen = currentUser.role!=="super" || !!selSchool; + if(!schoolChosen){ + panel.appendChild(h("div","guestnote", T("amPickSchoolHint"))); + return; + } + const shell = h("div","am-combo-shell"); + const hero = h("section","am-combo-hero"); + const heroCopy = h("div","am-combo-hero-copy"); + heroCopy.appendChild(h("div","am-combo-kicker", T("amAssignmentsLevels"))); + heroCopy.appendChild(h("h2","am-combo-title", T("amAssignmentsLevels"))); + heroCopy.appendChild(h("p","am-combo-lead", T("amAssignmentsLevelsSub"))); + const statStrip = h("div","am-combo-stats"); + const classAssignments = CLASSES.filter(c=>ASSIGNMENTS[c.id]).length; + const pupilOverrides = USERS.filter(u=>u.role==="pupil" && !!ASSIGNMENTS[u.id]).length; + const pupils = USERS.filter(u=>u.role==="pupil"); + [[classAssignments, T("amAssignClasses")], [pupilOverrides, T("amAssignPupils")], [pupils.length, T("amPupils")], [CLASSES.length, T("amClasses")]].forEach(([n, label])=>{ + const stat = h("div","am-combo-stat"); + stat.appendChild(h("strong", null, String(n))); + stat.appendChild(h("span", null, label)); + statStrip.appendChild(stat); + }); + heroCopy.appendChild(statStrip); + hero.append(heroCopy, combinedOverviewChart()); + + const layout = h("div","am-combo-layout"); + const assignPane = h("section","am-combo-pane am-combo-pane-assign"); + assignPane.appendChild(h("div","am-combo-pane-title", T("amAssignments"))); + renderToewijzingenPanel(assignPane); + const levelPane = h("section","am-combo-pane am-combo-pane-levels"); + levelPane.appendChild(h("div","am-combo-pane-title", T("amLevels"))); + renderKlassenmanagementPanel(levelPane); + layout.append(assignPane, levelPane); + + shell.append(hero, layout); + panel.appendChild(shell); + } + /* leerling-omgeving: welk eigen bord ziet een klas (standaard), of een individuele leerling (uitzondering voor wie op een ander niveau werkt) - zie roadmap-memo. */ function renderToewijzingenPanel(panel){ @@ -1759,7 +1829,7 @@ const tabDefs = adminTabDefs(); if(!tabDefs.some(([t])=>t===adminTab)) adminTab = tabDefs[0][0]; - const panels = { school: renderSchoolNavigator, toewijzingen: renderToewijzingenPanel, voortgang: renderVoortgangPanel, klassenmanagement: renderKlassenmanagementPanel, opslag: renderImageStoragePanel }; + const panels = { school: renderSchoolNavigator, toewijzingen: renderAssignmentsLevelsPanel, voortgang: renderVoortgangPanel, opslag: renderImageStoragePanel }; tabDefs.forEach(([t])=>{ const panel = h("div","am-tabpanel"); panel.dataset.panel = t; diff --git a/public/js/core.js b/public/js/core.js index 31b8e1a..848e5a6 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.58-beta"; +const VERSION = "0.4.59-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 @@ -399,6 +399,7 @@ const I18N = { amPwOptional:"wachtwoord (leeg = koppelcode)", amOwnPassword:"Eigen wachtwoord ter bevestiging", amOwnPasswordPh:"jouw huidige wachtwoord…", amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)", + amAssignmentsLevels:"Toewijzingen & niveaus", amAssignmentsLevelsSub:"Een gecombineerd overzicht van bords-toewijzingen, klasniveaus en live status.", amPickPupil:"— kies een leerling —", amNoAssignment:"— geen toewijzing —", amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.", amUnlink:"Loskoppelen", @@ -801,6 +802,7 @@ const I18N = { amPwOptional:"password (empty = link code)", amOwnPassword:"Your own password, to confirm", amOwnPasswordPh:"your current password…", amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)", + amAssignmentsLevels:"Assignments & levels", amAssignmentsLevelsSub:"A combined view of board assignments, class levels, and live status.", amPickPupil:"— pick a pupil —", amNoAssignment:"— no assignment —", amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.", amUnlink:"Unlink",