teach/public/js/image-catalog.js
Ramon c3492ab2ca
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
feat: Opslag per school met groepskaart-component (v0.3.88-beta)
- Nieuw herbruikbaar groepskaart-component (amGroupCard): één kader om items die bij elkaar horen - koprij met grijs icoon, titel, samenvatting-als-uitklaptoggle en Bewerken; compacte subrijen; open-status overleeft re-renders (injecteerbare Set zodat ook andere modules hem gebruiken)
- Opslag-tab volledig herbouwd: eerst één school kiezen (of "Systeem (zonder school)" voor systeemmanagers/ouders/losse accounts) i.p.v. alle scholen én alle gebruikers plat met altijd-open editors
- Na keuze één kaart: schoolnaam + quota-samenvatting in de kop (schoolquota bewerken klapt uit), daaronder alleen de gebruikers van díe school als compacte regels; bewerkvelden pas na "Bewerken" per rij; zoeken binnen de school
- "Standaard"-optie benoemt nu de echte rolstandaard ("Standaard (200 MB)") via defaultBytes
- Mobiel: beheer-rijen stapelen niet meer verticaal (kaartkop en subrijen wrappen netjes; subrij ± twee compacte regels i.p.v. >100px)
- Geverifieerd in headless Chromium (13 checks, o.a. dat schoolkeuze en open bewerkrij een her-render overleven - module-state) + screenshots 390px/desktop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 14:20:41 +02:00

451 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* teach - doorzoekbare afbeeldingscatalogus voor whiteboard en plaatjeskiezers.
Bestanden staan op de server; op het bord bewaren we alleen een stabiele URL. */
"use strict";
let IMAGE_CATALOG = {images:[],themes:[],folders:[],quota:{},uploadScope:"personal"};
let imageCatalogWrap = null, imageCatalogSelect = null, imageCatalogDeleteArmed = false;
const imageBytes = value=>{
const n=Number(value||0);
if(n<1024*1024)return Math.max(0,Math.round(n/1024))+" KB";
return (n/(1024*1024)).toLocaleString(LANG,{maximumFractionDigits:1})+" MB";
};
const imageScopeName = scope=>T(scope==="global"?"imgScopeGlobal":scope==="school"?"imgScopeSchool":"imgScopePersonal");
const imageThemeName = theme=>LANG==="nl"?theme.nameNl:theme.nameEn;
const imageFileName = file=>(file.name||T("imgUntitled")).replace(/\.[^.]+$/,"").slice(0,80);
async function uploadImageToCatalog(file, options={}){
if(!currentUser)throw new Error(T("imgLoginRequired"));
if(!file||!file.size)throw new Error(T("imgInvalidFile"));
if(file.size>50*1024*1024)throw new Error(T("imgFileTooLarge"));
const params=new URLSearchParams({
name:(options.name||imageFileName(file)).slice(0,80),
folder:options.folder||"",
themes:(options.themeIds||[]).join(",")
});
const response=await fetch("/api/images?"+params,{
method:"POST",credentials:"same-origin",
headers:{"Content-Type":file.type||"application/octet-stream","x-file-name":encodeURIComponent(file.name||"")},
body:file
});
const json=await response.json().catch(()=>({}));
if(!response.ok)throw new Error(json.error||T("imgUploadFailed"));
return json.image;
}
async function loadImageCatalog(){
IMAGE_CATALOG=await api("/images");
refreshWordPictureCatalog();
return IMAGE_CATALOG;
}
function refreshWordPictureCatalog(){
const wordCatalog=languagePictureCatalog(LANG);
IMAGE_CATALOG.images=(IMAGE_CATALOG.images||[]).filter(image=>!image.wordPicture);
IMAGE_CATALOG.themes=(IMAGE_CATALOG.themes||[]).filter(theme=>!theme.wordPicture);
IMAGE_CATALOG.images=wordCatalog.images.concat(IMAGE_CATALOG.images);
IMAGE_CATALOG.themes=wordCatalog.themes.concat(IMAGE_CATALOG.themes);
}
function ensureImageCatalog(){
if(imageCatalogWrap)return imageCatalogWrap;
const wrap=document.createElement("div");
wrap.id="imageCatalogWrap";wrap.className="modalwrap";wrap.setAttribute("role","dialog");wrap.setAttribute("aria-modal","true");
wrap.innerHTML=`
<div class="modal image-catalog-modal">
<div class="gallery-head"><h2 class="ic-title"></h2><button class="wclose ic-close" type="button">✕</button></div>
<div class="ic-tools">
<input class="ic-search" type="search" autocomplete="off">
<select class="ic-theme-filter"></select>
<select class="ic-scope-filter"></select>
<select class="ic-folder-filter"></select>
<button class="tbtn ghost ic-folders" type="button"></button>
</div>
<div class="ic-word-themes"></div>
<div class="ic-quota"></div>
<details class="ic-add">
<summary></summary>
<div class="ic-add-grid">
<label class="ic-field"><span class="ic-upload-folder-label"></span><select class="ic-upload-folder"></select></label>
<label class="ic-field"><span class="ic-upload-themes-label"></span><select class="ic-upload-themes" multiple></select></label>
<label class="ed-upload ic-upload"><span></span><input type="file" accept="image/png,image/jpeg,image/gif,image/webp,image/avif" multiple></label>
</div>
<div class="ic-theme-new"><input type="text" maxlength="80"><button class="tbtn ghost" type="button"></button></div>
</details>
<div class="ic-status" role="status"></div>
<div class="ic-grid"></div>
<section class="ic-editor" hidden>
<h3></h3>
<div class="ic-editor-grid">
<label class="ic-field"><span class="ic-name-label"></span><input class="ic-edit-name" maxlength="80"></label>
<label class="ic-field"><span class="ic-folder-label"></span><input class="ic-edit-folder" maxlength="200"></label>
<label class="ic-field"><span class="ic-themes-label"></span><select class="ic-edit-themes" multiple></select></label>
</div>
<div class="ic-editor-actions"><button class="tbtn ic-save" type="button"></button><button class="tbtn danger ic-delete" type="button"></button><button class="tbtn ghost ic-cancel" type="button"></button></div>
</section>
</div>`;
document.body.append(wrap);imageCatalogWrap=wrap;
const q=s=>wrap.querySelector(s);
q(".ic-close").onclick=closeImageCatalog;
wrap.onclick=e=>{if(e.target===wrap)closeImageCatalog()};
q(".ic-search").oninput=renderImageCatalogGrid;
q(".ic-theme-filter").onchange=()=>{renderImageCatalogGrid();renderLanguageThemeChips()};
q(".ic-scope-filter").onchange=renderImageCatalogGrid;
q(".ic-folder-filter").onchange=renderImageCatalogGrid;
q(".ic-folders").onclick=openImageFolderExplorer;
q(".ic-cancel").onclick=()=>{q(".ic-editor").hidden=true;imageCatalogDeleteArmed=false};
q(".ic-save").onclick=saveCatalogImage;
q(".ic-delete").onclick=deleteCatalogImage;
q(".ic-theme-new button").onclick=createCatalogTheme;
q(".ic-upload input").onchange=async e=>{
const files=[...e.target.files];e.target.value="";
if(!files.length)return;
const folder=q(".ic-upload-folder").value;
const themeIds=[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value);
setImageCatalogStatus(T("imgUploading"));
try{
for(const file of files)await uploadImageToCatalog(file,{folder,themeIds});
await loadImageCatalog();renderImageCatalog();setImageCatalogStatus(T("imgUploaded"),true);
}catch(err){setImageCatalogStatus(err.message)}
};
return wrap;
}
function setImageCatalogStatus(text,ok=false){
if(!imageCatalogWrap)return;
const status=imageCatalogWrap.querySelector(".ic-status");
status.textContent=text||"";status.classList.toggle("ok",!!ok);
}
function closeImageCatalog(){
if(imageCatalogWrap)imageCatalogWrap.classList.remove("open");
imageCatalogSelect=null;imageCatalogDeleteArmed=false;
}
function catalogOwnFolders(){
const scope=IMAGE_CATALOG.uploadScope;
const paths=new Set([""]);
IMAGE_CATALOG.folders.filter(f=>f.scope===scope&&f.canManage).forEach(f=>paths.add(f.path));
IMAGE_CATALOG.images.filter(i=>i.scope===scope&&i.canManage&&i.folder).forEach(i=>paths.add(i.folder));
return [...paths].sort((a,b)=>a.localeCompare(b,LANG));
}
function fillImageSelect(select,items,value,label){
select.innerHTML="";
if(label!=null)select.append(new Option(label,""));
items.forEach(item=>select.append(new Option(item.label,item.value)));
if([...select.options].some(o=>o.value===String(value)))select.value=String(value);
}
function fillThemeSelect(select,selected=[],includeAll=false,asset=null){
const current=new Set((selected||[]).map(Number));select.innerHTML="";
if(includeAll)select.append(new Option(T("imgAllThemes"),""));
IMAGE_CATALOG.themes.filter(theme=>(includeAll||!theme.filterOnly)&&(!asset||theme.scope==="global"
||(asset.scope==="school"&&theme.scope==="school")
||(asset.scope==="personal"&&theme.scope==="personal")))
.forEach(theme=>{
const option=new Option(imageThemeName(theme),theme.id);
option.selected=current.has(theme.id);select.append(option);
});
}
function renderLanguageThemeChips(){
if(!imageCatalogWrap)return;
const host=imageCatalogWrap.querySelector(".ic-word-themes");
const select=imageCatalogWrap.querySelector(".ic-theme-filter");
const themes=IMAGE_CATALOG.themes.filter(theme=>theme.languageBuiltin);
host.innerHTML="";host.hidden=!themes.length;
if(!themes.length)return;
const label=document.createElement("strong");label.textContent=T("imgWordThemes");host.append(label);
themes.forEach(theme=>{
const button=document.createElement("button");button.type="button";
button.textContent=(theme.icon||"🖼️")+" "+imageThemeName(theme);
button.classList.toggle("active",select.value===String(theme.id));
button.onclick=()=>{
select.value=select.value===String(theme.id)?"":String(theme.id);
renderImageCatalogGrid();renderLanguageThemeChips();
};
host.append(button);
});
}
function applyImageCatalogLabels(){
const q=s=>imageCatalogWrap.querySelector(s);
q(".ic-title").textContent=T("imgCatalog");
q(".ic-close").title=T("close");
q(".ic-search").placeholder=T("imgSearch");
q(".ic-folders").textContent="🗂 "+T("imgFolders");
q(".ic-add summary").textContent=" "+T("imgUpload");
q(".ic-upload span").textContent=T("imgChooseFiles");
q(".ic-upload-folder-label").textContent=T("imgFolder");
q(".ic-upload-themes-label").textContent=T("imgThemes");
q(".ic-theme-new input").placeholder=T("imgNewTheme");
q(".ic-theme-new button").textContent=T("imgAddTheme");
q(".ic-editor h3").textContent=T("imgEdit");
q(".ic-name-label").textContent=T("imgName");
q(".ic-folder-label").textContent=T("imgFolder");
q(".ic-themes-label").textContent=T("imgThemes");
q(".ic-save").textContent=T("save");
q(".ic-delete").textContent=T("exDelete");
q(".ic-cancel").textContent=T("cancel");
}
function renderImageCatalog(){
const q=s=>imageCatalogWrap.querySelector(s);
refreshWordPictureCatalog();
applyImageCatalogLabels();
const themeValue=q(".ic-theme-filter").value,scopeValue=q(".ic-scope-filter").value,folderValue=q(".ic-folder-filter").value;
fillThemeSelect(q(".ic-theme-filter"),[],true);
fillImageSelect(q(".ic-scope-filter"),[
{value:"global",label:imageScopeName("global")},
{value:"school",label:imageScopeName("school")},
{value:"personal",label:imageScopeName("personal")}
],scopeValue,T("imgAllScopes"));
const allFolders=[...new Set(IMAGE_CATALOG.images.map(i=>i.folder).concat(IMAGE_CATALOG.folders.map(f=>f.path)).filter(Boolean))]
.sort((a,b)=>a.localeCompare(b,LANG)).map(path=>({value:path,label:path}));
fillImageSelect(q(".ic-folder-filter"),allFolders,folderValue,T("imgAllFolders"));
if(themeValue&&[...q(".ic-theme-filter").options].some(o=>o.value===themeValue))q(".ic-theme-filter").value=themeValue;
const ownFolders=catalogOwnFolders().map(path=>({value:path,label:path||T("imgFolderRoot")}));
fillImageSelect(q(".ic-upload-folder"),ownFolders,q(".ic-upload-folder").value);
fillThemeSelect(q(".ic-upload-themes"),[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value),false,{scope:IMAGE_CATALOG.uploadScope});
renderLanguageThemeChips();
const quota=IMAGE_CATALOG.quota||{},limit=quota.limitBytes;
let text=T("imgQuotaUsed").replace("{used}",imageBytes(quota.usedBytes));
text+=" · "+(limit==null?T("imgUnlimited"):T("imgQuotaOf").replace("{limit}",imageBytes(limit)));
if(quota.schoolLimitBytes!=null)text+=" · "+T("imgSchoolQuota").replace("{used}",imageBytes(quota.schoolUsedBytes)).replace("{limit}",imageBytes(quota.schoolLimitBytes));
q(".ic-quota").textContent=text;
renderImageCatalogGrid();
}
function renderImageCatalogGrid(){
if(!imageCatalogWrap)return;
const q=s=>imageCatalogWrap.querySelector(s),grid=q(".ic-grid");
const search=q(".ic-search").value.trim().toLowerCase(),theme=+q(".ic-theme-filter").value||null;
const scope=q(".ic-scope-filter").value,folder=q(".ic-folder-filter").value;
const themeNames=new Map(IMAGE_CATALOG.themes.map(t=>[t.id,imageThemeName(t)]));
const images=IMAGE_CATALOG.images.filter(image=>{
const hay=[image.name,image.folder,...(image.tags||[]),...image.themes.map(id=>themeNames.get(id)||"")].join(" ").toLowerCase();
return(!search||hay.includes(search))&&(!theme||image.themes.includes(theme))
&&(!scope||image.scope===scope)&&(!folder||image.folder===folder||image.folder.startsWith(folder+"/"));
});
grid.innerHTML="";
images.forEach(image=>{
const card=document.createElement("article");card.className="ic-card";
const choose=document.createElement("button");choose.type="button";choose.className="ic-choose";
const img=document.createElement("img");img.src=image.src;img.alt=image.name;img.loading="lazy";
const name=document.createElement("strong");name.textContent=image.name;
const meta=document.createElement("span");meta.textContent=[imageScopeName(image.scope),image.folder].filter(Boolean).join(" · ");
choose.append(img,name,meta);choose.onclick=()=>{if(imageCatalogSelect){imageCatalogSelect(image.src,image);closeImageCatalog()}else if(image.canManage)openCatalogImageEditor(image)};
card.append(choose);
if(image.canManage){
const edit=document.createElement("button");edit.type="button";edit.className="ic-card-edit";edit.textContent="✏";
edit.title=T("imgEdit");edit.onclick=()=>openCatalogImageEditor(image);card.append(edit);
}
grid.append(card);
});
if(!images.length){const empty=document.createElement("p");empty.className="gallery-empty";empty.textContent=T("imgNoResults");grid.append(empty)}
}
function openCatalogImageEditor(image){
const q=s=>imageCatalogWrap.querySelector(s),editor=q(".ic-editor");
editor.hidden=false;editor.dataset.id=image.id;q(".ic-edit-name").value=image.name;q(".ic-edit-folder").value=image.folder;
fillThemeSelect(q(".ic-edit-themes"),image.themes,false,image);
q(".ic-delete").hidden=!image.canDelete;q(".ic-delete").textContent=T("exDelete");
imageCatalogDeleteArmed=false;editor.scrollIntoView({block:"nearest"});
}
async function saveCatalogImage(){
const q=s=>imageCatalogWrap.querySelector(s),editor=q(".ic-editor"),id=+editor.dataset.id;
const themeIds=[...q(".ic-edit-themes").selectedOptions].map(o=>+o.value);
try{
await api("/images/"+id,{method:"PATCH",body:{name:q(".ic-edit-name").value,folder:q(".ic-edit-folder").value,themeIds}});
await loadImageCatalog();editor.hidden=true;renderImageCatalog();setImageCatalogStatus(T("imgSaved"),true);
}catch(err){setImageCatalogStatus(err.message)}
}
async function deleteCatalogImage(){
const q=s=>imageCatalogWrap.querySelector(s),button=q(".ic-delete");
if(!imageCatalogDeleteArmed){imageCatalogDeleteArmed=true;button.textContent="⚠ "+T("exConfirm");return}
try{
await api("/images/"+q(".ic-editor").dataset.id,{method:"DELETE"});
await loadImageCatalog();q(".ic-editor").hidden=true;renderImageCatalog();setImageCatalogStatus(T("imgDeleted"),true);
}catch(err){setImageCatalogStatus(err.message)}
imageCatalogDeleteArmed=false;
}
async function createCatalogTheme(){
const input=imageCatalogWrap.querySelector(".ic-theme-new input"),name=input.value.trim();
if(!name)return;
try{
await api("/images/themes",{method:"POST",body:{name}});
input.value="";await loadImageCatalog();renderImageCatalog();setImageCatalogStatus(T("imgThemeAdded"),true);
}catch(err){setImageCatalogStatus(err.message)}
}
function imageScopeBranches(){
const scopes=["global"];
if(currentUser&&currentUser.schoolId!=null)scopes.push("school");
scopes.push("personal");
return scopes.map(scope=>({scope,name:(scope==="global"?"🌐 ":scope==="school"?"🏫 ":"👤 ")+imageScopeName(scope)}));
}
function openImageFolderExplorer(){
const branches=imageScopeBranches(),branchFor=path=>branches.find(b=>b.name===path[0]);
const join=path=>path.join("/");
const sameScope=(item,branch)=>item.scope===branch.scope;
const adapter={
title:()=>T("imgFolders"),allowRootItems:false,
list(path){
if(!path.length)return{folders:branches.map(b=>({name:b.name,count:IMAGE_CATALOG.images.filter(i=>sameScope(i,b)).length,fixed:true})),items:[]};
const branch=branchFor(path);if(!branch)return{folders:[],items:[]};
const rel=join(path.slice(1)),prefix=rel?rel+"/":"",folderNames=new Map();
const paths=IMAGE_CATALOG.folders.filter(f=>sameScope(f,branch)).map(f=>f.path)
.concat(IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)).map(i=>i.folder)).filter(Boolean);
paths.forEach(full=>{if(rel&&full!==rel&&!full.startsWith(prefix))return;const rest=rel?full.slice(prefix.length):full;if(!rest)return;const seg=rest.split("/")[0],child=prefix+seg;folderNames.set(seg,IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)&&(i.folder===child||i.folder.startsWith(child+"/"))).length)});
const items=IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)&&i.folder===rel).map(image=>({
key:image.id,name:image.name,movable:image.canManage,deletable:image.canDelete,
tile(){const im=document.createElement("img");im.className="ic-explorer-thumb";im.src=image.src;im.alt="";im.loading="lazy";return im}
}));
return{folders:[...folderNames].map(([name,count])=>({name,count})),items};
},
async createFolder(path,name){
const branch=branchFor(path);if(!branch||branch.scope!==IMAGE_CATALOG.uploadScope)return T("imgNoManage");
try{await api("/images/folders",{method:"POST",body:{path:join([...path.slice(1),name])}});await loadImageCatalog();return null}catch(err){return err.message}
},
async renameFolder(path,oldName,newName){return this.moveFolder([...path,oldName],path,newName)},
async moveFolder(srcPath,targetPath,newName){
const source=branchFor(srcPath),target=branchFor(targetPath);
if(!source||!target||source.scope!==target.scope)return T("exWrongBranch");
const from=join(srcPath.slice(1)),folder=IMAGE_CATALOG.folders.find(f=>f.scope===source.scope&&f.path===from);
if(!folder||!folder.canManage)return T("imgNoManage");
const to=join([...targetPath.slice(1),newName||srcPath.at(-1)]);
try{await api("/images/folders/"+folder.id,{method:"PATCH",body:{path:to}});await loadImageCatalog();return null}catch(err){return err.message}
},
async deleteFolder(path,name){
const branch=branchFor(path),full=join([...path.slice(1),name]);
const folder=branch&&IMAGE_CATALOG.folders.find(f=>f.scope===branch.scope&&f.path===full);
if(!folder||!folder.canManage)return T("imgNoManage");
try{await api("/images/folders/"+folder.id,{method:"DELETE"});await loadImageCatalog();return null}catch(err){return err.message}
},
async renameItem(id,name){
try{await api("/images/"+id,{method:"PATCH",body:{name}});await loadImageCatalog();return null}catch(err){return err.message}
},
async moveItem(id,targetPath){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id),branch=branchFor(targetPath);
if(!image||!branch||image.scope!==branch.scope||!image.canManage)return T("imgNoManage");
try{await api("/images/"+id,{method:"PATCH",body:{folder:join(targetPath.slice(1))}});await loadImageCatalog();return null}catch(err){return err.message}
},
async deleteItem(id){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id);if(!image?.canDelete)return T("imgNoManage");
try{await api("/images/"+id,{method:"DELETE"});await loadImageCatalog();return null}catch(err){return err.message}
},
openItem(id){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id);if(!image)return T("imgNoResults");
if(imageCatalogSelect)imageCatalogSelect(image.src,image);closeImageCatalog();return null;
}
};
imageCatalogWrap.style.visibility="hidden";
openExplorer(adapter,()=>{imageCatalogWrap.style.visibility="";if(imageCatalogWrap.classList.contains("open"))renderImageCatalog()});
}
async function openImageCatalog(onSelect){
if(!currentUser){alert(T("imgLoginRequired"));return}
const wrap=ensureImageCatalog();imageCatalogSelect=onSelect||null;setImageCatalogStatus(T("imgLoading"));
wrap.classList.add("open");
try{await loadImageCatalog();renderImageCatalog();setImageCatalogStatus("")}
catch(err){setImageCatalogStatus(err.message)}
}
/* 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 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);
/* 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";
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)):"";
amount.placeholder=T("imgQuotaMb");amount.hidden=mode.value!=="custom";mode.onchange=()=>amount.hidden=mode.value!=="custom";
const save=document.createElement("button");save.className="am-btn";save.type="button";save.textContent=T("save");
save.onclick=async()=>{
const limitBytes=mode.value==="default"?null:mode.value==="unlimited"?-1:Math.round(Number(amount.value)*1024*1024);
if(mode.value==="custom"&&(!Number.isFinite(limitBytes)||limitBytes<0)){status.textContent=T("imgQuotaInvalid");return}
try{await api("/images/admin/"+type+"/"+item.id+"/quota",{method:"PATCH",body:{limitBytes}});renderImageStoragePanel(panel)}
catch(err){status.textContent=err.message}
};
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="";
/* 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}
})();
}
const imageToolbarButton=document.getElementById("btnImages");
if(imageToolbarButton)imageToolbarButton.addEventListener("click",()=>openImageCatalog());
function refreshImageToolbar(){if(imageToolbarButton)imageToolbarButton.title=T("imgCatalog")}
document.addEventListener("langchange",()=>{refreshImageToolbar();if(imageCatalogWrap?.classList.contains("open"))renderImageCatalog()});
refreshImageToolbar();