diff --git a/VERSION b/VERSION index 0b28cd5..e510274 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.87-beta +0.3.88-beta diff --git a/public/css/teach.css b/public/css/teach.css index b5adbbe..530daea 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -2004,3 +2004,42 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000 padding:14px; max-height:none; box-shadow:var(--shadow-2); } } + +/* ---- groepskaart: één kader om items die bij elkaar horen ---- */ +#settingsModal .am-card{ + border:1px solid var(--line); border-radius:var(--radius-s); background:var(--surface); + overflow:hidden; margin-bottom:10px; +} +#settingsModal .am-card-head{ + display:flex; gap:9px; align-items:center; padding:9px 12px; + background:var(--surface-2); border-bottom:1px solid var(--line); +} +#settingsModal .am-card:has(.am-card-subs[style*="none"]):not(:has(.am-card-tools:not([style*="none"]))) .am-card-head{border-bottom:none;} +#settingsModal .am-ico{filter:grayscale(1); opacity:.55; font-size:13px; flex:none;} +#settingsModal .am-card-title{font-weight:650; font-size:13.5px; flex:1; min-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;} +#settingsModal .am-card-sum{margin-left:auto; flex:none; font-size:12px; color:var(--muted);} +#settingsModal .am-card-toggle{cursor:pointer; user-select:none; padding:4px 6px; border-radius:5px;} +#settingsModal .am-card-toggle:hover{background:var(--surface-3); color:var(--ink);} +#settingsModal .am-card-tools{padding:10px 12px; border-bottom:1px solid var(--line); display:flex; flex-wrap:wrap; gap:10px;} +#settingsModal .am-card-subs > .am-row{border-bottom:1px solid var(--line);} +#settingsModal .am-card-subs > .am-row:last-child{border-bottom:none;} +/* compacte subrij binnen een kaart */ +#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-role{font-size:11.5px; margin-left:auto;} +#settingsModal .am-card-sub .am-edit{flex:none;} +#settingsModal .am-card-sub .am-row-tools{width:100%;} +#settingsModal .img-quota-pick{margin-bottom:8px; max-width:420px;} +/* status-glyph (● actief / ○ wacht) met uitleg via title */ +#settingsModal .am-glyph{flex:none; font-size:10px; color:var(--muted); cursor:default;} +#settingsModal .am-glyph.ok{color:var(--green);} +/* mobiel: rijen binnen het beheer niet meer verticaal stapelen - de generieke + stapelregel maakte school en subrij ononderscheidbaar; wrap vangt overloop */ +@media (max-width:600px){ + #settingsModal .am-row{flex-direction:row; align-items:center; flex-wrap:wrap;} + /* kaartkop mag wrappen: titel houdt regel 1, samenvatting+Bewerken eronder */ + #settingsModal .am-card-head{flex-wrap:wrap;} + /* compacte subrij op telefoon: naam + Bewerken op regel 1, verbruik eronder */ + #settingsModal .am-card-sub .am-role{order:3; flex-basis:100%; margin-left:0;} + #settingsModal .am-card-sub .am-edit{margin-left:auto;} +} diff --git a/public/js/admin.js b/public/js/admin.js index 7bee177..c859c1a 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -116,6 +116,63 @@ return badge; } + /* ---- groepskaart: één kader om wat bij elkaar hoort ------------------------ + Kop (grijs icoon + titel + samenvatting-als-toggle + Bewerken) met daaronder + optioneel uitklapbare compacte subrijen en/of een uitklapbaar bewerkblok. + De open-status leeft in een Set die re-renders overleeft; andere modules + (bv. de Opslag-tab in image-catalog.js) geven hun eigen Set mee. */ + function groupCard(opts){ + const openSet = opts.openSet || openGroups; + const editSet = opts.editSet || openRows; + const card = h("div","am-card"); + const head = h("div","am-card-head"); + card.appendChild(head); + if(opts.icon) head.appendChild(h("span","am-ico", opts.icon)); + head.appendChild(h("span","am-card-title", opts.title)); + const sum = h("span","am-card-sum"); + head.appendChild(sum); + let tools = null; + if(opts.edit){ + tools = h("div","am-card-tools"); + tools.appendChild(opts.edit.tools); + const btn = h("button","am-edit", T("amEdit")); + btn.type = "button"; + const ekey = opts.edit.key; + const esync = ()=>{ + const open = editSet.has(ekey); + tools.style.display = open ? "" : "none"; + btn.classList.toggle("on", open); + }; + btn.addEventListener("click", ()=>{ + if(editSet.has(ekey)) editSet.delete(ekey); else editSet.add(ekey); + esync(); + }); + esync(); + head.appendChild(btn); + card.appendChild(tools); + } + if(opts.subs && opts.subs.length){ + const subHost = h("div","am-card-subs"); + opts.subs.forEach(el=>subHost.appendChild(el)); + card.appendChild(subHost); + sum.classList.add("am-card-toggle"); + const sync = ()=>{ + const open = openSet.has(opts.key); + sum.textContent = `${opts.summary} ${open ? "▾" : "▸"}`; + subHost.style.display = open ? "" : "none"; + }; + sum.addEventListener("click", ()=>{ + if(openSet.has(opts.key)) openSet.delete(opts.key); else openSet.add(opts.key); + sync(); + }); + sync(); + }else if(opts.summary){ + sum.textContent = opts.summary; + } + return card; + } + window.amGroupCard = groupCard; + /* ---- beheer-secties in het instellingen-register (settings.js) ------------ Elke sectie meldt zichzelf aan met rol-zichtbaarheid, een activate() en zoekgegevens; de schil bouwt daar navigatie + zoeken uit. Per rol alleen diff --git a/public/js/core.js b/public/js/core.js index adf04fc..df43290 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.3.87-beta"; +const VERSION = "0.3.88-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -190,6 +190,8 @@ const I18N = { imgLoading:"Afbeeldingen laden…", imgSaved:"Afbeelding opgeslagen ✓", imgDeleted:"Afbeelding verwijderd ✓", imgThemeAdded:"Thema toegevoegd ✓", imgStorageAdmin:"Opslaglimieten afbeeldingen", imgQuotaUsage:"{used} gebruikt · {limit}", imgQuotaDefault:"Standaard", imgQuotaCustom:"Aangepast", + imgPickSchool:"— kies een school —", imgSystemScope:"Systeem (zonder school)", + imgQuotaDefaultOf:"Standaard ({limit})", imgQuotaMb:"limiet in MB", imgQuotaInvalid:"Vul een geldige limiet in.", imgSchools:"Scholen", imgUsers:"Gebruikers", imgSearchUsers:"Zoek gebruiker…", cats:["Dieren","Eten","Natuur","Vervoer","Spullen","Lichaam"], @@ -482,6 +484,8 @@ const I18N = { imgLoading:"Loading images…", imgSaved:"Image saved ✓", imgDeleted:"Image deleted ✓", imgThemeAdded:"Theme added ✓", imgStorageAdmin:"Image storage limits", imgQuotaUsage:"{used} used · {limit}", imgQuotaDefault:"Default", imgQuotaCustom:"Custom", + imgPickSchool:"— pick a school —", imgSystemScope:"System (no school)", + imgQuotaDefaultOf:"Default ({limit})", imgQuotaMb:"limit in MB", imgQuotaInvalid:"Enter a valid limit.", imgSchools:"Schools", imgUsers:"Users", imgSearchUsers:"Search user…", cats:["Animals","Food","Nature","Transport","Things","Body"], diff --git a/public/js/image-catalog.js b/public/js/image-catalog.js index 099311a..f5ca6d6 100644 --- a/public/js/image-catalog.js +++ b/public/js/image-catalog.js @@ -344,19 +344,28 @@ async function openImageCatalog(onSelect){ /* Systeembeheer: limieten per gebruiker en school. Wordt door admin.js als extra Instellingen-tab aangeroepen. */ +/* Opslag-tab: eerst een school kiezen (of "Systeem" voor school-loze accounts), + daarna één groepskaart met de schoolquota in de kop en de gebruikers van die + school als compacte subrijen. Bewerkvelden zijn verborgen tot "Bewerken". + Keuze en open-status leven op moduleniveau: de save-handler en admin-render() + herbouwen het paneel en mogen niets vergeten. */ +let imageQuotaSchool = ""; +const imageQuotaOpen = new Set(); +let imageQuotaFilter = ""; function renderImageStoragePanel(panel){ panel.innerHTML=""; - const heading=document.createElement("div");heading.className="am-h3";heading.textContent=T("imgStorageAdmin"); - const status=document.createElement("div");status.className="am-msg";panel.append(heading,status); + const status=document.createElement("div");status.className="am-msg";panel.append(status); const host=document.createElement("div");host.className="img-quota-admin";host.textContent=T("imgLoading");panel.append(host); const fmt=value=>value==null?T("imgUnlimited"):imageBytes(value); - const editor=(item,type)=>{ - const row=document.createElement("div");row.className="am-row img-quota-row"; - const info=document.createElement("div");info.className="am-name";info.textContent=item.name||item.displayName; - const use=document.createElement("span");use.className="am-role";use.textContent=T("imgQuotaUsage").replace("{used}",imageBytes(item.usedBytes)).replace("{limit}",fmt(item.limitBytes)); - info.append(document.createElement("br"),use); + /* het bewerkblok (limiet-keuze + opslaan) achter de Bewerken-knop per kaart/rij */ + const editorTools=(item,type)=>{ + const wrap=document.createElement("div");wrap.className="am-inline"; const mode=document.createElement("select");mode.className="am-sel"; - [["default","imgQuotaDefault"],["custom","imgQuotaCustom"],["unlimited","imgUnlimited"]].forEach(([v,k])=>mode.add(new Option(T(k),v))); + const defaultLabel=type==="users"&&item.defaultBytes!=null + ? T("imgQuotaDefaultOf").replace("{limit}",imageBytes(item.defaultBytes)) + : T("imgQuotaDefault"); + [[defaultLabel,"default"],[T("imgQuotaCustom"),"custom"],[T("imgUnlimited"),"unlimited"]] + .forEach(([label,v])=>mode.add(new Option(label,v))); mode.value=item.overrideBytes==null?"default":item.overrideBytes===-1?"unlimited":"custom"; const amount=document.createElement("input");amount.className="am-inp";amount.type="number";amount.min="0";amount.max=String(10*1024);amount.step="10"; amount.value=item.overrideBytes!=null&&item.overrideBytes>=0?Math.round(item.overrideBytes/(1024*1024)):""; @@ -368,18 +377,69 @@ function renderImageStoragePanel(panel){ try{await api("/images/admin/"+type+"/"+item.id+"/quota",{method:"PATCH",body:{limitBytes}});renderImageStoragePanel(panel)} catch(err){status.textContent=err.message} }; - row.append(info,mode,amount,save);return row; + wrap.append(mode,amount,save); + return wrap; + }; + const usage=item=>T("imgQuotaUsage").replace("{used}",imageBytes(item.usedBytes)).replace("{limit}",fmt(item.limitBytes)); + /* compacte gebruikersrij binnen de kaart: naam + verbruik, Bewerken klapt uit */ + const userSub=u=>{ + const row=document.createElement("div");row.className="am-row am-card-sub"; + const nm=document.createElement("span");nm.className="am-name";nm.textContent=u.displayName; + const use=document.createElement("span");use.className="am-role";use.textContent=usage(u); + row.append(nm,use); + const tools=document.createElement("div");tools.className="am-row-tools"; + tools.appendChild(editorTools(u,"users")); + const btn=document.createElement("button");btn.className="am-edit";btn.type="button";btn.textContent=T("amEdit"); + const key="u:"+u.id; + const sync=()=>{const open=imageQuotaOpen.has(key);tools.style.display=open?"":"none";btn.classList.toggle("on",open)}; + btn.addEventListener("click",()=>{imageQuotaOpen.has(key)?imageQuotaOpen.delete(key):imageQuotaOpen.add(key);sync()}); + sync(); + row.append(btn,tools); + return row; }; (async()=>{ try{ const data=await api("/images/admin/quotas");host.innerHTML=""; - const schoolTitle=document.createElement("div");schoolTitle.className="am-group";schoolTitle.textContent=T("imgSchools"); - host.append(schoolTitle,...data.schools.map(s=>editor({...s,name:s.name},"schools"))); - const userTitle=document.createElement("div");userTitle.className="am-group";userTitle.textContent=T("imgUsers"); - const filter=document.createElement("input");filter.className="am-inp";filter.type="search";filter.placeholder=T("imgSearchUsers"); - const users=document.createElement("div");users.className="img-quota-users"; - const draw=()=>{const term=filter.value.trim().toLowerCase();users.innerHTML="";data.users.filter(u=>(u.displayName+" "+u.username).toLowerCase().includes(term)).forEach(u=>users.append(editor(u,"users")))}; - filter.oninput=draw;host.append(userTitle,filter,users);draw(); + /* stap 1: kies een school (of Systeem) - geen eindeloze platte lijsten */ + const pick=document.createElement("select");pick.className="am-sel img-quota-pick"; + pick.append(new Option(T("imgPickSchool"),"")); + data.schools.forEach(s=>pick.append(new Option(s.name,String(s.id)))); + pick.append(new Option(T("imgSystemScope"),"none")); + pick.value=imageQuotaSchool; + if(pick.value!==imageQuotaSchool) imageQuotaSchool=""; /* school verdwenen */ + pick.onchange=()=>{imageQuotaSchool=pick.value;imageQuotaFilter="";if(pick.value)imageQuotaOpen.add("school:"+pick.value);renderImageStoragePanel(panel)}; + host.append(pick); + if(!imageQuotaSchool){ + const hint=document.createElement("div");hint.className="guestnote";hint.textContent=T("amPickSchoolHint"); + host.append(hint); + return; + } + const system=imageQuotaSchool==="none"; + const school=system?null:data.schools.find(s=>String(s.id)===imageQuotaSchool); + let users=data.users.filter(u=>system?u.schoolId==null:Number(u.schoolId)===Number(imageQuotaSchool)); + /* zoeken binnen de gekozen school */ + const filter=document.createElement("input");filter.className="am-inp";filter.type="search"; + filter.placeholder=T("imgSearchUsers");filter.value=imageQuotaFilter; + filter.oninput=()=>{imageQuotaFilter=filter.value;draw()}; + host.append(filter); + const cardHost=document.createElement("div"); + host.append(cardHost); + const draw=()=>{ + cardHost.innerHTML=""; + const term=imageQuotaFilter.trim().toLowerCase(); + const shown=users.filter(u=>(u.displayName+" "+u.username).toLowerCase().includes(term)); + const card=amGroupCard({ + icon:"🏫", + title:system?T("imgSystemScope"):school.name, + summary:(school?usage(school)+" · ":"")+shown.length+" "+T("imgUsers").toLowerCase(), + key:"school:"+imageQuotaSchool, + openSet:imageQuotaOpen, editSet:imageQuotaOpen, + edit:school?{key:"s:"+school.id,tools:editorTools(school,"schools")}:null, + subs:shown.map(userSub), + }); + cardHost.append(card); + }; + draw(); }catch(err){host.textContent="";status.textContent=err.message} })(); }