Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
8 changed files with 383 additions and 94 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.59-beta
|
0.3.60-beta
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,8 @@
|
||||||
/* instellingen is breder dan het inlogscherm: het beheerpaneel (tabblad) heeft
|
/* instellingen is breder dan het inlogscherm: het beheerpaneel (tabblad) heeft
|
||||||
de ruimte nodig van wat vroeger een eigen, brede modal was (#adminModal) */
|
de ruimte nodig van wat vroeger een eigen, brede modal was (#adminModal) */
|
||||||
#settingsModal{width:min(760px,94vw); max-height:86vh; overflow:auto;}
|
#settingsModal{width:min(760px,94vw); max-height:86vh; overflow:auto;}
|
||||||
#settingsAccountPanel{max-width:400px;}
|
.st-section[data-section="account"]{max-width:420px;}
|
||||||
|
.st-section[data-section="account"] .am-h3:first-of-type{margin-top:0;}
|
||||||
#loginModal input,#settingsModal input{
|
#loginModal input,#settingsModal input{
|
||||||
width:100%; border:1px solid var(--line); border-radius:var(--radius);
|
width:100%; border:1px solid var(--line); border-radius:var(--radius);
|
||||||
height:var(--control-h); padding:0 12px;
|
height:var(--control-h); padding:0 12px;
|
||||||
|
|
@ -530,15 +531,76 @@
|
||||||
.guestnote{font-size:13px; color:var(--muted); margin-top:12px; line-height:1.45;}
|
.guestnote{font-size:13px; color:var(--muted); margin-top:12px; line-height:1.45;}
|
||||||
/* instellingen: neutrale kaarten per sectie (.am-reset, hergebruikt van het
|
/* instellingen: neutrale kaarten per sectie (.am-reset, hergebruikt van het
|
||||||
beheerpaneel) i.p.v. het speelse bord-uiterlijk - dit scherm is voor volwassenen */
|
beheerpaneel) i.p.v. het speelse bord-uiterlijk - dit scherm is voor volwassenen */
|
||||||
#settingsAccountPanel .am-h3:first-of-type{margin-top:0;}
|
|
||||||
#settingsModal .am-reset .lrow{margin:0;}
|
#settingsModal .am-reset .lrow{margin:0;}
|
||||||
#settingsModal .am-reset .guestnote{margin-top:2px;}
|
#settingsModal .am-reset .guestnote{margin-top:2px;}
|
||||||
.st-footer{
|
.st-footer{
|
||||||
display:flex; justify-content:space-between; align-items:center; gap:8px;
|
display:flex; justify-content:space-between; align-items:center; gap:8px;
|
||||||
margin-top:16px; padding-top:12px; border-top:1.5px solid var(--line);
|
margin-top:12px; padding-top:12px; border-top:1.5px solid var(--line);
|
||||||
font-size:11px; color:var(--muted);
|
font-size:11px; color:var(--muted); flex:none;
|
||||||
}
|
}
|
||||||
#settingsModal .am-name{font-size:16px;}
|
#settingsModal .am-name{font-size:16px;}
|
||||||
|
/* ---------- Instellingen-schil: nav links, inhoud rechts, zoeken boven ----- */
|
||||||
|
.st-modal{
|
||||||
|
width:min(980px,96vw); height:min(700px,92vh); max-height:92vh;
|
||||||
|
display:flex; flex-direction:column; overflow:hidden; padding:18px 22px;
|
||||||
|
}
|
||||||
|
.st-head{display:flex; align-items:center; gap:14px; flex:none; padding-bottom:12px;}
|
||||||
|
.st-head h2{margin:0; font-size:19px;}
|
||||||
|
#stSearch{
|
||||||
|
flex:1; max-width:360px; margin-left:auto; min-width:120px;
|
||||||
|
height:var(--control-h); padding:0 12px; font-size:14px;
|
||||||
|
border:1.5px solid var(--line); border-radius:10px;
|
||||||
|
background:var(--surface-2); color:var(--ink); outline:none;
|
||||||
|
}
|
||||||
|
#stSearch:focus{border-color:var(--accent);}
|
||||||
|
.st-body{display:flex; flex:1; min-height:0; border-top:1.5px solid var(--line);}
|
||||||
|
#stNav{
|
||||||
|
width:200px; flex:none; overflow-y:auto; padding:12px 12px 12px 0;
|
||||||
|
border-right:1.5px solid var(--line);
|
||||||
|
display:flex; flex-direction:column; gap:2px; align-content:flex-start;
|
||||||
|
}
|
||||||
|
.st-nav-group{
|
||||||
|
font-size:10.5px; font-weight:800; color:var(--muted);
|
||||||
|
text-transform:uppercase; letter-spacing:.5px; margin:12px 6px 4px;
|
||||||
|
}
|
||||||
|
.st-nav-group:first-child{margin-top:0;}
|
||||||
|
.st-nav-btn{
|
||||||
|
display:flex; gap:9px; align-items:center; border:none; background:transparent;
|
||||||
|
padding:8px 10px; border-radius:9px; cursor:pointer; text-align:left;
|
||||||
|
font-size:14px; font-weight:650; color:var(--ink);
|
||||||
|
}
|
||||||
|
.st-nav-btn:hover{background:var(--surface-3);}
|
||||||
|
.st-nav-btn.active{background:var(--accent-soft); color:var(--accent-ink);}
|
||||||
|
.st-nav-ico{flex:none; width:20px; text-align:center;}
|
||||||
|
#stContent{flex:1; min-width:0; overflow-y:auto; padding:14px 4px 14px 18px;}
|
||||||
|
#stSecHead{margin-bottom:12px;}
|
||||||
|
#stSecHead:empty{display:none;}
|
||||||
|
.st-sec-title{font-size:17px; font-weight:800;}
|
||||||
|
.st-sec-sub{font-size:12.5px; color:var(--muted); margin-top:2px;}
|
||||||
|
/* zoekresultaten: gegroepeerd per sectie, klik = navigeren */
|
||||||
|
#stSearchResults{display:flex; flex-direction:column; gap:2px;}
|
||||||
|
.st-result{
|
||||||
|
display:flex; gap:9px; align-items:center; border:none; background:transparent;
|
||||||
|
padding:8px 10px; border-radius:9px; cursor:pointer; text-align:left;
|
||||||
|
font-size:14px; color:var(--ink); width:100%;
|
||||||
|
}
|
||||||
|
.st-result:hover{background:var(--surface-3);}
|
||||||
|
.st-result-lbl{font-weight:650; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
||||||
|
.st-result-sub{margin-left:auto; flex:none; font-size:12px; color:var(--muted);}
|
||||||
|
/* smal scherm: nav wordt een horizontale strook boven de inhoud */
|
||||||
|
@media (max-width:720px){
|
||||||
|
.st-modal{padding:14px;}
|
||||||
|
.st-body{flex-direction:column;}
|
||||||
|
#stNav{
|
||||||
|
width:auto; flex-direction:row; overflow-x:auto; overflow-y:hidden;
|
||||||
|
border-right:none; border-bottom:1.5px solid var(--line);
|
||||||
|
padding:8px 0; gap:4px;
|
||||||
|
}
|
||||||
|
.st-nav-group{display:none;}
|
||||||
|
.st-nav-btn{flex:none; padding:7px 10px;}
|
||||||
|
#stContent{padding:12px 2px;}
|
||||||
|
#stSearch{max-width:none;}
|
||||||
|
}
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
Letter game widget
|
Letter game widget
|
||||||
==========================================================*/
|
==========================================================*/
|
||||||
|
|
@ -719,9 +781,6 @@
|
||||||
.ed-tabs{
|
.ed-tabs{
|
||||||
display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:14px;
|
display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:14px;
|
||||||
}
|
}
|
||||||
/* de instellingen-balk bevat nu ook alle beheer-tabs: laten omlopen op
|
|
||||||
smalle schermen i.p.v. overlopen */
|
|
||||||
#settingsTabs{flex-wrap:wrap;}
|
|
||||||
.ed-tabs button{
|
.ed-tabs button{
|
||||||
border:none; border-radius:0; padding:10px 15px 11px; font:inherit; font-weight:700;
|
border:none; border-radius:0; padding:10px 15px 11px; font:inherit; font-weight:700;
|
||||||
font-size:14px; background:transparent; color:var(--muted); cursor:pointer;
|
font-size:14px; background:transparent; color:var(--muted); cursor:pointer;
|
||||||
|
|
|
||||||
|
|
@ -149,19 +149,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modalwrap" id="settingsWrap">
|
<div class="modalwrap" id="settingsWrap">
|
||||||
<div class="modal" id="settingsModal">
|
<div class="modal st-modal" id="settingsModal">
|
||||||
<div class="am-head">
|
<div class="st-head">
|
||||||
<h2 data-i18n="settingsTitle"></h2>
|
<h2 data-i18n="settingsTitle"></h2>
|
||||||
|
<input type="search" id="stSearch" maxlength="60" autocomplete="off">
|
||||||
<button class="wclose" id="btnSettingsClose">✕</button>
|
<button class="wclose" id="btnSettingsClose">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ed-tabs" id="settingsTabs">
|
<div class="st-body">
|
||||||
<button type="button" class="active" data-tab="account" data-i18n="stAccount"></button>
|
<nav id="stNav"></nav>
|
||||||
<button type="button" data-tab="view" data-i18n="stView"></button>
|
<div id="stContent">
|
||||||
<span id="adminTabsSlot" style="display:none;"></span>
|
<div id="stSecHead"></div>
|
||||||
</div>
|
<div id="stSearchResults" style="display:none;"></div>
|
||||||
|
|
||||||
<div id="settingsAccountPanel">
|
<div class="st-section" data-section="account" style="display:none;">
|
||||||
<div class="am-reset">
|
<div class="am-reset">
|
||||||
<div class="am-name" id="helloName"></div>
|
<div class="am-name" id="helloName"></div>
|
||||||
<div class="am-role" id="helloRole"></div>
|
<div class="am-role" id="helloRole"></div>
|
||||||
|
|
@ -196,9 +197,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="guestnote" data-i18n="storedNote"></div>
|
<div class="guestnote" data-i18n="storedNote"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settingsViewPanel" style="display:none;">
|
<div class="st-section" data-section="view" style="display:none;">
|
||||||
<div class="vw-row">
|
<div class="vw-row">
|
||||||
<span class="vw-lbl" data-i18n="vwTheme"></span>
|
<span class="vw-lbl" data-i18n="vwTheme"></span>
|
||||||
<div class="seg" id="segTheme">
|
<div class="seg" id="segTheme">
|
||||||
|
|
@ -244,10 +245,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="guestnote" data-i18n="vwNote"></div>
|
<div class="guestnote" data-i18n="vwNote"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settingsAdminPanel" style="display:none;">
|
<div class="st-section" data-section="admin" style="display:none;">
|
||||||
<div id="adminModal"></div>
|
<div id="adminModal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="st-footer">
|
<div class="st-footer">
|
||||||
|
|
@ -259,6 +262,7 @@
|
||||||
|
|
||||||
<script src="js/core.js"></script>
|
<script src="js/core.js"></script>
|
||||||
<script src="js/permissions.js"></script>
|
<script src="js/permissions.js"></script>
|
||||||
|
<script src="js/settings.js"></script>
|
||||||
<script src="js/data.js"></script>
|
<script src="js/data.js"></script>
|
||||||
<script src="js/explorer.js"></script>
|
<script src="js/explorer.js"></script>
|
||||||
<script src="js/picker.js"></script>
|
<script src="js/picker.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -117,46 +117,84 @@
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* de beheer-tabs staan als losse knoppen in de ene instellingen-tabbalk
|
/* ---- beheer-secties in het instellingen-register (settings.js) ------------
|
||||||
(#adminTabsSlot in core.js/index.html) - geen geneste tabbalk meer */
|
Elke sectie meldt zichzelf aan met rol-zichtbaarheid, een activate() en
|
||||||
const adminTabDefs = ()=>{
|
zoekgegevens; de schil bouwt daar navigatie + zoeken uit. Per rol alleen
|
||||||
const defs = [];
|
de essentie: groepsleiding ziet gebruikers/toewijzingen/voortgang,
|
||||||
if(currentUser && currentUser.role==="super"){ defs.push(["scholen", T("amOverarching")]); defs.push(["opslag", T("imgStorageAdmin")]); }
|
schoolbeheer ook klassen, de systeemmanager ook scholen en opslag. */
|
||||||
defs.push(["klassen", T("amClasses")]);
|
const ADMIN_SECTIONS = [
|
||||||
defs.push(["gebruikers", T("amUsers")]);
|
{ tab:"gebruikers", icon:"👥", group:"school", order:10, labelKey:"amUsers", subKey:"stUsersSub",
|
||||||
defs.push(["toewijzingen", T("amAssignments")]);
|
show:()=>true,
|
||||||
defs.push(["voortgang", T("amProgress")]);
|
keywords:()=>[T("amPupils"), T("roleTeacher"), T("roleAdmin"), T("amParents"), T("amLinkRequests"),
|
||||||
return defs;
|
T("amNewPw"), T("amParentCode"), "leerling","pupil","ouder","parent","wachtwoord","password",
|
||||||
};
|
"koppelcode","aanmelding","enrolment"],
|
||||||
function buildAdminTabs(){
|
search:q=>USERS
|
||||||
const slot = document.getElementById("adminTabsSlot");
|
.filter(u=>`${u.displayName} ${u.username}`.toLowerCase().includes(q))
|
||||||
slot.innerHTML = "";
|
.map(u=>({ icon: u.role==="pupil" ? "🧒" : u.role==="parent" ? "👪" : "🧑🏫",
|
||||||
if(!currentUser || !can("admin.access")) return;
|
label:u.displayName, sub:T(roleKey(u.role)),
|
||||||
adminTabDefs().forEach(([t, label])=>{
|
go:()=>goSettingsSection("gebruikers", { filter:u.displayName }) })) },
|
||||||
const b = h("button", null, label);
|
{ tab:"klassen", icon:"🏷", group:"school", order:11, labelKey:"amClasses", subKey:"stKlassenSub",
|
||||||
b.type = "button";
|
show:()=>can("classes.manage"),
|
||||||
b.dataset.tab = "admin";
|
keywords:()=>[T("amNewClass"), T("amResetClass"), "klas","class","groep","group"],
|
||||||
b.dataset.admintab = t;
|
search:q=>CLASSES
|
||||||
b.addEventListener("click", async ()=>{
|
.filter(c=>c.name.toLowerCase().includes(q))
|
||||||
adminTab = t;
|
.map(c=>({ icon:"🏷", label:c.name, sub:T("amClasses"), go:()=>goSettingsSection("klassen") })) },
|
||||||
/* data (opnieuw) laden bij binnenkomst in het beheer-deel; wisselen
|
{ tab:"toewijzingen", icon:"🎯", group:"school", order:12, labelKey:"amAssignments", subKey:"stToewijzSub",
|
||||||
tussen beheer-tabs onderling hergebruikt de geladen data */
|
show:()=>can("assignments.manage"),
|
||||||
const adminVisible = document.getElementById("settingsAdminPanel").style.display !== "none";
|
keywords:()=>[T("amModeWatch"), T("amModeWork"), "bord","board","delen","share","toewijzen","assign","widget"] },
|
||||||
if(!adminVisible){
|
{ tab:"voortgang", icon:"📈", group:"school", order:13, labelKey:"amProgress", subKey:"stVoortgangSub",
|
||||||
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
show:()=>can("progress.view"),
|
||||||
await reload();
|
keywords:()=>["voortgang","progress","score","resultaten","results","tijdlijn","timeline"] },
|
||||||
}
|
{ tab:"scholen", icon:"🏫", group:"system", order:30, labelKey:"amOverarching", subKey:"stScholenSub",
|
||||||
switchSettingsTab("admin");
|
show:()=>currentUser.role==="super",
|
||||||
switchAdminTab(t);
|
keywords:()=>[T("amNewSchool"), T("amNewSuper"), "school","systeemmanager","system manager"],
|
||||||
});
|
search:q=>SCHOOLS
|
||||||
slot.appendChild(b);
|
.filter(s=>s.name.toLowerCase().includes(q))
|
||||||
});
|
.map(s=>({ icon:"🏫", label:s.name, sub:T("amSchools"), go:()=>goSettingsSection("scholen") })) },
|
||||||
|
{ tab:"opslag", icon:"🖼", group:"system", order:31, labelKey:"stOpslag", subKey:"stOpslagSub",
|
||||||
|
show:()=>currentUser.role==="super",
|
||||||
|
keywords:()=>["opslag","storage","afbeeldingen","images","quota"] },
|
||||||
|
];
|
||||||
|
const adminTabDefs = ()=>ADMIN_SECTIONS.filter(d=>d.show()).map(d=>[d.tab, T(d.labelKey)]);
|
||||||
|
/* data één keer per bezoek aan Instellingen laden; secties onderling
|
||||||
|
hergebruiken de lading (in-flight promise voorkomt dubbel laden) */
|
||||||
|
let loadPromise = null;
|
||||||
|
function ensureLoaded(force){
|
||||||
|
if(force) loadPromise = null;
|
||||||
|
if(!loadPromise){
|
||||||
|
loadPromise = (async ()=>{
|
||||||
|
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
||||||
|
await reload();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return loadPromise;
|
||||||
}
|
}
|
||||||
|
/* settings.js roept dit bij het openen aan: verse data + zoekindex vullen */
|
||||||
|
window.adminPrefetch = ()=>{ ensureLoaded(true).catch(()=>{}); };
|
||||||
|
ADMIN_SECTIONS.forEach(d=>{
|
||||||
|
registerSettingsSection({
|
||||||
|
id:d.tab, icon:d.icon, group:d.group, order:d.order,
|
||||||
|
label:()=>T(d.labelKey), sub:()=>T(d.subKey),
|
||||||
|
visible:()=>!!currentUser && can("admin.access") && d.show(),
|
||||||
|
async activate(payload){
|
||||||
|
await ensureLoaded();
|
||||||
|
if(d.tab==="gebruikers" && payload && payload.filter !== undefined) userFilter = payload.filter;
|
||||||
|
showAdminHost();
|
||||||
|
switchAdminTab(d.tab);
|
||||||
|
if(d.tab==="gebruikers" && payload && payload.filter !== undefined){
|
||||||
|
const panel = modal.querySelector('[data-panel="gebruikers"]');
|
||||||
|
const inp = panel && panel.querySelector(".am-filter");
|
||||||
|
if(inp){ inp.value = userFilter; applyUserFilter(panel); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keywords:d.keywords,
|
||||||
|
searchItems:d.search,
|
||||||
|
});
|
||||||
|
});
|
||||||
document.addEventListener("userchange", ()=>{
|
document.addEventListener("userchange", ()=>{
|
||||||
if(!currentUser) resetResults = null;
|
if(!currentUser) resetResults = null;
|
||||||
buildAdminTabs();
|
loadPromise = null;
|
||||||
});
|
});
|
||||||
document.addEventListener("langchange", buildAdminTabs);
|
|
||||||
|
|
||||||
async function reload(){
|
async function reload(){
|
||||||
try{
|
try{
|
||||||
|
|
@ -1007,7 +1045,6 @@
|
||||||
|
|
||||||
function switchAdminTab(tab){
|
function switchAdminTab(tab){
|
||||||
adminTab = tab;
|
adminTab = tab;
|
||||||
document.querySelectorAll("#adminTabsSlot button").forEach(b=>b.classList.toggle("active", b.dataset.admintab===tab));
|
|
||||||
modal.querySelectorAll(".am-tabpanel").forEach(p=>{ p.style.display = p.dataset.panel===tab ? "block" : "none"; });
|
modal.querySelectorAll(".am-tabpanel").forEach(p=>{ p.style.display = p.dataset.panel===tab ? "block" : "none"; });
|
||||||
/* het Systeem-tabblad is school-onafhankelijk: daar is de schoolkiezer
|
/* het Systeem-tabblad is school-onafhankelijk: daar is de schoolkiezer
|
||||||
alleen maar verwarrend, dus die verbergen we er */
|
alleen maar verwarrend, dus die verbergen we er */
|
||||||
|
|
|
||||||
|
|
@ -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.3.59-beta";
|
const VERSION = "0.3.60-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -184,6 +184,17 @@ const I18N = {
|
||||||
login:"Inloggen", loginBtn:"Inloggen", registerBtn:"Account maken", logout:"Uitloggen",
|
login:"Inloggen", loginBtn:"Inloggen", registerBtn:"Account maken", logout:"Uitloggen",
|
||||||
settingsTitle:"Instellingen", tabLogin:"Inloggen", tabLinkCode:"Koppelcode", busy:"Bezig…",
|
settingsTitle:"Instellingen", tabLogin:"Inloggen", tabLinkCode:"Koppelcode", busy:"Bezig…",
|
||||||
stAccount:"Account", stPassword:"Wachtwoord", stSession:"Sessie",
|
stAccount:"Account", stPassword:"Wachtwoord", stSession:"Sessie",
|
||||||
|
stSearchPh:"🔎 Zoeken in instellingen…", stNoResults:"Niets gevonden.",
|
||||||
|
stOpenSection:"open dit onderdeel",
|
||||||
|
stGroupPersonal:"Persoonlijk", stGroupSchool:"School", stGroupSystem:"Systeem",
|
||||||
|
stAccountSub:"Je gegevens, wachtwoord en reservekopieën.",
|
||||||
|
stViewSub:"Weergave op dit apparaat: thema, taal en menubalk.",
|
||||||
|
stUsersSub:"Accounts beheren: leerlingen, staf, ouders en aanmeldingen.",
|
||||||
|
stKlassenSub:"Klassen aanmaken, koppelen en beheren.",
|
||||||
|
stToewijzSub:"Bepaal wat een klas of leerling te zien krijgt.",
|
||||||
|
stVoortgangSub:"Volg per leerling de resultaten in de tijd.",
|
||||||
|
stScholenSub:"Scholen en systeemmanagers, over alle scholen heen.",
|
||||||
|
stOpslag:"Opslag", stOpslagSub:"Afbeeldingencatalogus en opslagruimte per school.",
|
||||||
stBackups:"Reservekopieën", backupsNote:"Automatisch bewaarde eerdere versies van je borden en woorden.",
|
stBackups:"Reservekopieën", backupsNote:"Automatisch bewaarde eerdere versies van je borden en woorden.",
|
||||||
backupsEmpty:"Nog geen reservekopieën.", backupRestore:"herstel", backupRestoreConfirm:"zeker?",
|
backupsEmpty:"Nog geen reservekopieën.", backupRestore:"herstel", backupRestoreConfirm:"zeker?",
|
||||||
backupRestored:"Versie hersteld ✓",
|
backupRestored:"Versie hersteld ✓",
|
||||||
|
|
@ -431,6 +442,17 @@ const I18N = {
|
||||||
login:"Log in", loginBtn:"Log in", registerBtn:"Create account", logout:"Log out",
|
login:"Log in", loginBtn:"Log in", registerBtn:"Create account", logout:"Log out",
|
||||||
settingsTitle:"Settings", tabLogin:"Log in", tabLinkCode:"Link code", busy:"Please wait…",
|
settingsTitle:"Settings", tabLogin:"Log in", tabLinkCode:"Link code", busy:"Please wait…",
|
||||||
stAccount:"Account", stPassword:"Password", stSession:"Session",
|
stAccount:"Account", stPassword:"Password", stSession:"Session",
|
||||||
|
stSearchPh:"🔎 Search settings…", stNoResults:"Nothing found.",
|
||||||
|
stOpenSection:"open this section",
|
||||||
|
stGroupPersonal:"Personal", stGroupSchool:"School", stGroupSystem:"System",
|
||||||
|
stAccountSub:"Your details, password and backups.",
|
||||||
|
stViewSub:"Display on this device: theme, language and toolbar.",
|
||||||
|
stUsersSub:"Manage accounts: pupils, staff, parents and enrolments.",
|
||||||
|
stKlassenSub:"Create, link and manage classes.",
|
||||||
|
stToewijzSub:"Choose what a class or pupil gets to see.",
|
||||||
|
stVoortgangSub:"Follow each pupil's results over time.",
|
||||||
|
stScholenSub:"Schools and system managers, across all schools.",
|
||||||
|
stOpslag:"Storage", stOpslagSub:"Image catalogue and storage per school.",
|
||||||
stBackups:"Backups", backupsNote:"Automatically kept earlier versions of your boards and words.",
|
stBackups:"Backups", backupsNote:"Automatically kept earlier versions of your boards and words.",
|
||||||
backupsEmpty:"No backups yet.", backupRestore:"restore", backupRestoreConfirm:"sure?",
|
backupsEmpty:"No backups yet.", backupRestore:"restore", backupRestoreConfirm:"sure?",
|
||||||
backupRestored:"Version restored ✓",
|
backupRestored:"Version restored ✓",
|
||||||
|
|
@ -601,9 +623,13 @@ let DATA_REV = 0;
|
||||||
let saveConflict = false;
|
let saveConflict = false;
|
||||||
let saveInFlight = false;
|
let saveInFlight = false;
|
||||||
function canPersist(){
|
function canPersist(){
|
||||||
/* leerlingen en ouders slaan geen eigen borden-data op; het ouderportaal
|
/* ouders slaan geen eigen borden-data op (het portaal werkt met gekoppelde
|
||||||
werkt straks met gekoppelde kind-accounts i.p.v. een eigen blob */
|
kind-accounts); schoolleerlingen ook niet (zij zien de toewijzing van de
|
||||||
return !!currentUser && currentUser.role !== "pupil" && currentUser.role !== "parent";
|
leerkracht). Thuiskinderen (leerling zónder school, ouderportaal) hebben
|
||||||
|
wél een eigen whiteboard met eigen opslag. */
|
||||||
|
if(!currentUser || currentUser.role === "parent") return false;
|
||||||
|
if(currentUser.role === "pupil") return !currentUser.schoolId;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
/* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save
|
/* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save
|
||||||
verstuurt alleen die secties i.p.v. altijd de complete blob (die MB's kan
|
verstuurt alleen die secties i.p.v. altijd de complete blob (die MB's kan
|
||||||
|
|
@ -694,9 +720,11 @@ function generalExtraFromData(data){
|
||||||
return (data && data.generalExtra) ? data.generalExtra : {nl:[], en:[]};
|
return (data && data.generalExtra) ? data.generalExtra : {nl:[], en:[]};
|
||||||
}
|
}
|
||||||
async function hydrateFromServer(){
|
async function hydrateFromServer(){
|
||||||
/* leerlingen hebben geen eigen borden - zij zien enkel wat de leerkracht
|
/* schoolleerlingen hebben geen eigen borden - zij zien enkel wat de
|
||||||
heeft toegewezen (zie pupil.js), nooit het whiteboard */
|
leerkracht heeft toegewezen (zie pupil.js), nooit het whiteboard.
|
||||||
if(currentUser && currentUser.role === "pupil"){
|
Thuiskinderen (leerling zonder school, ouderportaal) vallen hier expres
|
||||||
|
buiten: zij krijgen hieronder het volledige whiteboard met eigen opslag. */
|
||||||
|
if(currentUser && currentUser.role === "pupil" && currentUser.schoolId){
|
||||||
THEMES = {nl:[],en:[]};
|
THEMES = {nl:[],en:[]};
|
||||||
GENERAL_EXTRA = {nl:[],en:[]};
|
GENERAL_EXTRA = {nl:[],en:[]};
|
||||||
stopParentView();
|
stopParentView();
|
||||||
|
|
@ -749,12 +777,8 @@ const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==
|
||||||
function updateUserUI(){
|
function updateUserUI(){
|
||||||
document.getElementById("userLbl").textContent = currentUser ? currentUser.displayName : T("login");
|
document.getElementById("userLbl").textContent = currentUser ? currentUser.displayName : T("login");
|
||||||
document.getElementById("btnUser").classList.toggle("on", !!currentUser);
|
document.getElementById("btnUser").classList.toggle("on", !!currentUser);
|
||||||
/* de beheer-tabs (Systeem/Klassen/Gebruikers/...) staan als losse knoppen
|
/* welke instellingen-secties zichtbaar zijn bepaalt settings.js per rol
|
||||||
in de ene tabbalk; admin.js vult de slot, hier alleen zichtbaarheid */
|
(sectieregister); hier alleen nog de accountgegevens zelf */
|
||||||
const adminSlot = document.getElementById("adminTabsSlot");
|
|
||||||
const showAdmin = can("admin.access");
|
|
||||||
adminSlot.style.display = showAdmin ? "contents" : "none";
|
|
||||||
if(!showAdmin && adminSlot.querySelector("button.active")) switchSettingsTab("account");
|
|
||||||
if(currentUser){
|
if(currentUser){
|
||||||
document.getElementById("helloName").textContent = currentUser.displayName;
|
document.getElementById("helloName").textContent = currentUser.displayName;
|
||||||
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
|
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
|
||||||
|
|
@ -772,18 +796,6 @@ function switchLoginTab(tab){
|
||||||
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>{
|
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>{
|
||||||
b.addEventListener("click", ()=>switchLoginTab(b.dataset.tab));
|
b.addEventListener("click", ()=>switchLoginTab(b.dataset.tab));
|
||||||
});
|
});
|
||||||
/* instellingen-tabs: één balk met account / weergave / de beheer-tabs
|
|
||||||
(die laatste worden door admin.js in #adminTabsSlot gezet en zelf
|
|
||||||
actief gemarkeerd via switchAdminTab) */
|
|
||||||
function switchSettingsTab(tab){
|
|
||||||
document.querySelectorAll("#settingsTabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab && !b.dataset.admintab));
|
|
||||||
document.getElementById("settingsAccountPanel").style.display = tab==="account" ? "block" : "none";
|
|
||||||
document.getElementById("settingsViewPanel").style.display = tab==="view" ? "block" : "none";
|
|
||||||
document.getElementById("settingsAdminPanel").style.display = tab==="admin" ? "block" : "none";
|
|
||||||
}
|
|
||||||
/* de beheer-tabs hebben hun eigen klik-handlers in admin.js (moeten eerst data laden) */
|
|
||||||
document.querySelector('#settingsTabs button[data-tab="account"]').addEventListener("click", ()=>switchSettingsTab("account"));
|
|
||||||
document.querySelector('#settingsTabs button[data-tab="view"]').addEventListener("click", ()=>switchSettingsTab("view"));
|
|
||||||
async function loadSchools(){
|
async function loadSchools(){
|
||||||
const sel = document.getElementById("liSchool");
|
const sel = document.getElementById("liSchool");
|
||||||
try{
|
try{
|
||||||
|
|
@ -792,15 +804,8 @@ async function loadSchools(){
|
||||||
list.map(x=>`<option value="${x.id}">${x.name.replace(/</g,"<")}</option>`).join("");
|
list.map(x=>`<option value="${x.id}">${x.name.replace(/</g,"<")}</option>`).join("");
|
||||||
}catch(e){ sel.innerHTML = `<option value="">${T("schoolSel")}</option>`; }
|
}catch(e){ sel.innerHTML = `<option value="">${T("schoolSel")}</option>`; }
|
||||||
}
|
}
|
||||||
function openSettings(){
|
/* openSettings() zelf woont in settings.js (sectieregister + zoeken);
|
||||||
settingsMsg.textContent = ""; settingsMsg.classList.remove("ok");
|
de knoppen hieronder roepen hem via window aan */
|
||||||
/* zonder account is er geen Account-tab: dan direct naar Weergave
|
|
||||||
(zo blijven taal/contrast/thema ook als gast bereikbaar) */
|
|
||||||
document.querySelector('#settingsTabs button[data-tab="account"]').style.display = currentUser ? "" : "none";
|
|
||||||
switchSettingsTab(currentUser ? "account" : "view");
|
|
||||||
loadBackups();
|
|
||||||
settingsWrap.classList.add("open");
|
|
||||||
}
|
|
||||||
/* reservekopieën (Instellingen → Account): automatisch bewaarde eerdere
|
/* reservekopieën (Instellingen → Account): automatisch bewaarde eerdere
|
||||||
versies van de borddata, met een arm-then-confirm herstelknop zoals de
|
versies van de borddata, met een arm-then-confirm herstelknop zoals de
|
||||||
verwijderknop in de boardStrip */
|
verwijderknop in de boardStrip */
|
||||||
|
|
|
||||||
171
public/js/settings.js
Normal file
171
public/js/settings.js
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
/* teach - instellingen-schil: één sectieregister als enige bron van waarheid.
|
||||||
|
Elke sectie meldt zich aan met zichtbaarheid per rol, een activate() en
|
||||||
|
zoekgegevens; de schil bouwt daar de navigatie (gegroepeerd: Persoonlijk /
|
||||||
|
School / Systeem), de sectiekop en de zoekfunctie uit op. Zo bepaalt de rol
|
||||||
|
van de ingelogde gebruiker vanzelf wat er te zien is: een gast alleen
|
||||||
|
Weergave, een leerling/ouder Account + Weergave, staf ook de beheerdelen
|
||||||
|
(admin.js registreert die zelf - de schil kent het beheer niet van binnen).
|
||||||
|
De zoekbalk zoekt door sectienamen, instelling-trefwoorden én data
|
||||||
|
(gebruikers/klassen/scholen via searchItems van de secties). */
|
||||||
|
"use strict";
|
||||||
|
const ST_SECTIONS = [];
|
||||||
|
function registerSettingsSection(sec){ ST_SECTIONS.push(sec); }
|
||||||
|
|
||||||
|
(function(){
|
||||||
|
const wrap = document.getElementById("settingsWrap");
|
||||||
|
const nav = document.getElementById("stNav");
|
||||||
|
const content = document.getElementById("stContent");
|
||||||
|
const secHead = document.getElementById("stSecHead");
|
||||||
|
const searchInp = document.getElementById("stSearch");
|
||||||
|
const resultsEl = document.getElementById("stSearchResults");
|
||||||
|
let activeSection = null;
|
||||||
|
|
||||||
|
const el = (tag, cls, text)=>{
|
||||||
|
const e = document.createElement(tag);
|
||||||
|
if(cls) e.className = cls;
|
||||||
|
if(text != null) e.textContent = text;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
const visibleSections = ()=>
|
||||||
|
ST_SECTIONS.filter(s=>s.visible()).sort((a,b)=>(a.order||99)-(b.order||99));
|
||||||
|
|
||||||
|
/* ---- ingebouwde secties: Account en Weergave ------------------------------ */
|
||||||
|
registerSettingsSection({
|
||||||
|
id:"account", icon:"👤", group:"personal", order:1,
|
||||||
|
label:()=>T("stAccount"), sub:()=>T("stAccountSub"),
|
||||||
|
visible:()=>!!currentUser,
|
||||||
|
activate(){ showDomSection("account"); loadBackups(); },
|
||||||
|
keywords:()=>[T("stPassword"), T("stBackups"), T("stSession"), T("logout"),
|
||||||
|
"wachtwoord","password","reservekopie","backup","herstel","restore",
|
||||||
|
"uitloggen","logout","sessie","session","naam","name"],
|
||||||
|
});
|
||||||
|
registerSettingsSection({
|
||||||
|
id:"view", icon:"🎨", group:"personal", order:2,
|
||||||
|
label:()=>T("stView"), sub:()=>T("stViewSub"),
|
||||||
|
visible:()=>true,
|
||||||
|
activate(){ showDomSection("view"); },
|
||||||
|
keywords:()=>[T("vwTheme"), T("vwContrast"), T("vwLang"), T("vwTbar"), T("vwMotion"), T("vwGlass"),
|
||||||
|
"thema","theme","donker","dark","licht","light","contrast","taal","language",
|
||||||
|
"menubalk","toolbar","animaties","animations","glas","glass"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/* de drie DOM-secties (account/view/admin-host); beheer-secties delen de
|
||||||
|
admin-host, admin.js wisselt daarbinnen zelf van paneel */
|
||||||
|
function showDomSection(id){
|
||||||
|
resultsEl.style.display = "none";
|
||||||
|
content.querySelectorAll(".st-section").forEach(s=>{
|
||||||
|
s.style.display = s.dataset.section===id ? "" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.showAdminHost = ()=>showDomSection("admin");
|
||||||
|
|
||||||
|
const GROUPS = [["personal","stGroupPersonal"], ["school","stGroupSchool"], ["system","stGroupSystem"]];
|
||||||
|
function buildNav(){
|
||||||
|
nav.innerHTML = "";
|
||||||
|
const secs = visibleSections();
|
||||||
|
GROUPS.forEach(([g, key])=>{
|
||||||
|
const inGroup = secs.filter(s=>s.group===g);
|
||||||
|
if(!inGroup.length) return;
|
||||||
|
/* groepskoppen pas als er echt iets te groeperen valt (staf); een
|
||||||
|
gast/leerling met 1-2 secties krijgt een kale, rustige lijst */
|
||||||
|
if(secs.length > 3) nav.appendChild(el("div","st-nav-group", T(key)));
|
||||||
|
inGroup.forEach(s=>{
|
||||||
|
const b = el("button","st-nav-btn");
|
||||||
|
b.type = "button";
|
||||||
|
b.dataset.section = s.id;
|
||||||
|
b.appendChild(el("span","st-nav-ico", s.icon));
|
||||||
|
b.appendChild(el("span","st-nav-lbl", s.label()));
|
||||||
|
b.addEventListener("click", ()=>goSection(s.id));
|
||||||
|
nav.appendChild(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goSection(id, payload){
|
||||||
|
const s = ST_SECTIONS.find(x=>x.id===id && x.visible());
|
||||||
|
if(!s) return;
|
||||||
|
activeSection = id;
|
||||||
|
searchInp.value = "";
|
||||||
|
nav.querySelectorAll(".st-nav-btn").forEach(b=>b.classList.toggle("active", b.dataset.section===id));
|
||||||
|
secHead.innerHTML = "";
|
||||||
|
secHead.appendChild(el("div","st-sec-title", s.icon + " " + s.label()));
|
||||||
|
if(s.sub) secHead.appendChild(el("div","st-sec-sub", s.sub()));
|
||||||
|
content.scrollTop = 0;
|
||||||
|
await s.activate(payload);
|
||||||
|
}
|
||||||
|
window.goSettingsSection = goSection;
|
||||||
|
|
||||||
|
/* ---- zoeken ---------------------------------------------------------------
|
||||||
|
drie lagen per zichtbare sectie: (1) sectienaam, (2) trefwoorden van de
|
||||||
|
instellingen erin, (3) live data (searchItems: gebruikers/klassen/scholen).
|
||||||
|
Resultaten vervangen de inhoud; een klik navigeert (en filtert waar kan). */
|
||||||
|
function doSearch(){
|
||||||
|
const q = searchInp.value.trim().toLowerCase();
|
||||||
|
if(!q){
|
||||||
|
if(activeSection) goSection(activeSection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nav.querySelectorAll(".st-nav-btn").forEach(b=>b.classList.remove("active"));
|
||||||
|
secHead.innerHTML = "";
|
||||||
|
resultsEl.innerHTML = "";
|
||||||
|
content.querySelectorAll(".st-section").forEach(s=>{ s.style.display = "none"; });
|
||||||
|
let total = 0;
|
||||||
|
visibleSections().forEach(s=>{
|
||||||
|
const hits = [];
|
||||||
|
if(s.label().toLowerCase().includes(q)){
|
||||||
|
hits.push({ icon:s.icon, label:s.label(), sub:T("stOpenSection"), go:()=>goSection(s.id) });
|
||||||
|
}else{
|
||||||
|
const kw = (s.keywords ? s.keywords() : []).find(k=>String(k).toLowerCase().includes(q));
|
||||||
|
if(kw) hits.push({ icon:s.icon, label:String(kw), sub:s.label(), go:()=>goSection(s.id) });
|
||||||
|
}
|
||||||
|
if(s.searchItems) hits.push(...s.searchItems(q));
|
||||||
|
if(!hits.length) return;
|
||||||
|
total += hits.length;
|
||||||
|
resultsEl.appendChild(el("div","st-nav-group", s.label()));
|
||||||
|
hits.slice(0, 8).forEach(hit=>{
|
||||||
|
const row = el("button","st-result");
|
||||||
|
row.type = "button";
|
||||||
|
row.appendChild(el("span","st-nav-ico", hit.icon || s.icon));
|
||||||
|
row.appendChild(el("span","st-result-lbl", hit.label));
|
||||||
|
if(hit.sub) row.appendChild(el("span","st-result-sub", hit.sub));
|
||||||
|
row.addEventListener("click", ()=>hit.go());
|
||||||
|
resultsEl.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if(!total) resultsEl.appendChild(el("div","guestnote", T("stNoResults")));
|
||||||
|
resultsEl.style.display = "";
|
||||||
|
}
|
||||||
|
searchInp.addEventListener("input", doSearch);
|
||||||
|
searchInp.addEventListener("keydown", e=>{
|
||||||
|
e.stopPropagation();
|
||||||
|
if(e.key==="Escape"){ searchInp.value = ""; doSearch(); }
|
||||||
|
if(e.key==="Enter"){ const first = resultsEl.querySelector(".st-result"); if(first) first.click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- openen ---------------------------------------------------------------- */
|
||||||
|
window.openSettings = function(section){
|
||||||
|
const msg = document.getElementById("settingsMsg");
|
||||||
|
msg.textContent = ""; msg.classList.remove("ok");
|
||||||
|
searchInp.value = "";
|
||||||
|
searchInp.placeholder = T("stSearchPh");
|
||||||
|
buildNav();
|
||||||
|
goSection(section || (currentUser ? "account" : "view"));
|
||||||
|
/* beheerdata alvast op de achtergrond laden zodat zoeken op namen van
|
||||||
|
gebruikers/klassen/scholen meteen wat oplevert (admin.js bewaakt zelf
|
||||||
|
dat er maar één keer geladen wordt) */
|
||||||
|
if(currentUser && can("admin.access") && window.adminPrefetch) window.adminPrefetch();
|
||||||
|
wrap.classList.add("open");
|
||||||
|
};
|
||||||
|
document.addEventListener("langchange", ()=>{
|
||||||
|
searchInp.placeholder = T("stSearchPh");
|
||||||
|
if(wrap.classList.contains("open")){ buildNav(); if(activeSection) goSection(activeSection); }
|
||||||
|
});
|
||||||
|
document.addEventListener("userchange", ()=>{
|
||||||
|
if(wrap.classList.contains("open")){
|
||||||
|
buildNav();
|
||||||
|
const secs = visibleSections();
|
||||||
|
if(!secs.some(s=>s.id===activeSection)) goSection(secs[0] ? secs[0].id : "view");
|
||||||
|
else goSection(activeSection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
@ -200,9 +200,12 @@ export default async function api(app) {
|
||||||
if (!r.rows[0]) return fail(reply, 409, 'in een ander tabblad bewerkt');
|
if (!r.rows[0]) return fail(reply, 409, 'in een ander tabblad bewerkt');
|
||||||
return { ok: true, rev: Number(r.rows[0].data_rev) };
|
return { ok: true, rev: Number(r.rows[0].data_rev) };
|
||||||
};
|
};
|
||||||
|
// schoolleerlingen hebben geen eigen borden-data; thuiskinderen (pupil
|
||||||
|
// zonder school, ouderportaal) wél - die mogen ook hun reservekopieën zien
|
||||||
|
const schoolPupil = (req) => req.user.role === 'pupil' && req.user.school_id != null;
|
||||||
app.get('/me/data/versions', async (req, reply) => {
|
app.get('/me/data/versions', async (req, reply) => {
|
||||||
need(req, reply);
|
need(req, reply);
|
||||||
if (req.user.role === 'pupil') return fail(reply, 403, 'geen rechten');
|
if (schoolPupil(req)) return fail(reply, 403, 'geen rechten');
|
||||||
const rows = (await pool.query(
|
const rows = (await pool.query(
|
||||||
'SELECT id, created_at FROM user_data_versions WHERE user_id = $1 ORDER BY created_at DESC',
|
'SELECT id, created_at FROM user_data_versions WHERE user_id = $1 ORDER BY created_at DESC',
|
||||||
[req.user.id])).rows;
|
[req.user.id])).rows;
|
||||||
|
|
@ -210,7 +213,7 @@ export default async function api(app) {
|
||||||
});
|
});
|
||||||
app.post('/me/data/restore', async (req, reply) => {
|
app.post('/me/data/restore', async (req, reply) => {
|
||||||
need(req, reply);
|
need(req, reply);
|
||||||
if (req.user.role === 'pupil') return fail(reply, 403, 'geen rechten');
|
if (schoolPupil(req)) return fail(reply, 403, 'geen rechten');
|
||||||
const { versionId } = req.body ?? {};
|
const { versionId } = req.body ?? {};
|
||||||
if (!Number.isInteger(versionId)) return fail(reply, 400, 'ongeldige versie');
|
if (!Number.isInteger(versionId)) return fail(reply, 400, 'ongeldige versie');
|
||||||
const v = (await pool.query(
|
const v = (await pool.query(
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,16 @@ test('thuiskind logt voortgang met de gekoppelde ouder als leerkracht', async ()
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('thuiskind mag reservekopieën zien, schoolleerling niet', async () => {
|
||||||
|
const { app } = await makeApp({ user: homeChild });
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/me/data/versions', cookies });
|
||||||
|
assert.equal(res.statusCode, 200, res.body);
|
||||||
|
const { app: app2 } = await makeApp({ user: { ...homeChild, school_id: 2 } });
|
||||||
|
const res2 = await app2.inject({ method: 'GET', url: '/api/me/data/versions', cookies });
|
||||||
|
assert.equal(res2.statusCode, 403);
|
||||||
|
await app.close(); await app2.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('gekoppelde ouder ziet de voortgang van het kind, een vreemde ouder niet', async () => {
|
test('gekoppelde ouder ziet de voortgang van het kind, een vreemde ouder niet', async () => {
|
||||||
const { app } = await makeApp({ user: parent, respond: (sql) => {
|
const { app } = await makeApp({ user: parent, respond: (sql) => {
|
||||||
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [homeChild] };
|
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [homeChild] };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue