Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
6 changed files with 439 additions and 11 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.57-beta
|
0.3.58-beta
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,23 @@
|
||||||
box-shadow:0 2px 8px rgba(30,50,90,.15);
|
box-shadow:0 2px 8px rgba(30,50,90,.15);
|
||||||
}
|
}
|
||||||
.pupil-empty{margin:auto; color:var(--muted); font-size:16px; font-weight:600;}
|
.pupil-empty{margin:auto; color:var(--muted); font-size:16px; font-weight:600;}
|
||||||
|
/* ---------- Ouderportaal ---------- */
|
||||||
|
/* ouders zien geen whiteboard: alleen het portaal met hun kinderen */
|
||||||
|
#parentView{display:none;}
|
||||||
|
body.parent-mode #board,body.parent-mode #fab,body.parent-mode #fabBoardsBtn,
|
||||||
|
body.parent-mode #boardStrip,body.parent-mode #fabmenu,
|
||||||
|
body.parent-mode #hud,body.parent-mode #btnFolders,body.parent-mode #btnSave{display:none !important;}
|
||||||
|
body.parent-mode #parentView{
|
||||||
|
display:block; position:absolute; inset:var(--tbar) 0 0 0;
|
||||||
|
overflow:auto; padding:24px 18px; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.parent-inner{
|
||||||
|
max-width:720px; margin:0 auto; display:flex; flex-direction:column; gap:10px;
|
||||||
|
}
|
||||||
|
.parent-title{margin:0 0 4px; font-size:22px;}
|
||||||
|
.parent-inner .am-row{flex-wrap:wrap;}
|
||||||
|
.parent-inner .am-row-tools{flex-basis:100%; display:flex; flex-direction:column; gap:8px;}
|
||||||
|
.parent-inner .am-row-tools:empty{display:none;}
|
||||||
/* floating add-button (FAB) */
|
/* floating add-button (FAB) */
|
||||||
#fab{
|
#fab{
|
||||||
position:fixed; right:24px; bottom:24px; z-index:5600; width:60px; height:60px;
|
position:fixed; right:24px; bottom:24px; z-index:5600; width:60px; height:60px;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
|
|
||||||
<div id="board"></div>
|
<div id="board"></div>
|
||||||
<div id="pupilView"></div>
|
<div id="pupilView"></div>
|
||||||
|
<div id="parentView"></div>
|
||||||
<button id="fab">+</button>
|
<button id="fab">+</button>
|
||||||
<button id="fabBoardsBtn">🗂</button>
|
<button id="fabBoardsBtn">🗂</button>
|
||||||
<div id="boardStrip"></div>
|
<div id="boardStrip"></div>
|
||||||
|
|
@ -108,12 +109,12 @@
|
||||||
<div class="ed-tabs">
|
<div class="ed-tabs">
|
||||||
<button type="button" class="active" data-tab="login" data-i18n="tabLogin"></button>
|
<button type="button" class="active" data-tab="login" data-i18n="tabLogin"></button>
|
||||||
<button type="button" data-tab="link" data-i18n="tabLinkCode"></button>
|
<button type="button" data-tab="link" data-i18n="tabLinkCode"></button>
|
||||||
|
<button type="button" data-tab="parentreg" data-i18n="tabParentReg"></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="loginBox">
|
<div id="loginBox">
|
||||||
<select id="liSchool"></select>
|
<select id="liSchool"></select>
|
||||||
<input type="text" id="liName" maxlength="30" autocomplete="username">
|
<input type="text" id="liName" maxlength="30" autocomplete="username">
|
||||||
<input type="password" id="liPass" maxlength="64" autocomplete="current-password">
|
<input type="password" id="liPass" maxlength="64" autocomplete="current-password">
|
||||||
<div id="loginMsg" class="formMsg"></div>
|
|
||||||
<div class="lrow">
|
<div class="lrow">
|
||||||
<button class="tbtn" id="btnLogin" data-i18n="loginBtn"></button>
|
<button class="tbtn" id="btnLogin" data-i18n="loginBtn"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -130,6 +131,19 @@
|
||||||
<button class="tbtn ghost" id="btnLink" data-i18n="linkBtn"></button>
|
<button class="tbtn ghost" id="btnLink" data-i18n="linkBtn"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="parentRegBox" style="display:none;">
|
||||||
|
<div class="lisub" data-i18n="parentRegIntro"></div>
|
||||||
|
<input type="text" id="prName" maxlength="30" autocomplete="username">
|
||||||
|
<div class="lrow">
|
||||||
|
<input type="password" id="prPass" maxlength="64" autocomplete="new-password">
|
||||||
|
<button class="tbtn ghost pwtoggle" type="button" id="btnPrPassToggle" data-i18n="pwShow"></button>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="prPass2" maxlength="64" autocomplete="new-password">
|
||||||
|
<div class="lrow">
|
||||||
|
<button class="tbtn ghost" id="btnParentReg" data-i18n="parentRegBtn"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loginMsg" class="formMsg"></div>
|
||||||
<div class="guestnote" data-i18n="guestNote"></div>
|
<div class="guestnote" data-i18n="guestNote"></div>
|
||||||
<button class="tbtn ghost" type="button" id="btnGuestView" style="margin-top:10px;">🎨 <span data-i18n="stView"></span></button>
|
<button class="tbtn ghost" type="button" id="btnGuestView" style="margin-top:10px;">🎨 <span data-i18n="stView"></span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -268,6 +282,7 @@
|
||||||
<script src="js/widgets/learning.js"></script>
|
<script src="js/widgets/learning.js"></script>
|
||||||
<script src="js/board.js"></script>
|
<script src="js/board.js"></script>
|
||||||
<script src="js/pupil.js"></script>
|
<script src="js/pupil.js"></script>
|
||||||
|
<script src="js/parent.js"></script>
|
||||||
<script src="js/admin.js"></script>
|
<script src="js/admin.js"></script>
|
||||||
<script src="js/image-catalog.js"></script>
|
<script src="js/image-catalog.js"></script>
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
/* huidige klas-toewijzingen (leerling-omgeving), per klas-id - opnieuw
|
/* huidige klas-toewijzingen (leerling-omgeving), per klas-id - opnieuw
|
||||||
opgehaald bij elke reload() zoals CLASSES/USERS hierboven */
|
opgehaald bij elke reload() zoals CLASSES/USERS hierboven */
|
||||||
let ASSIGNMENTS = {};
|
let ASSIGNMENTS = {};
|
||||||
|
/* openstaande aanmeldverzoeken van ouders (thuiskind -> deze school) */
|
||||||
|
let LINK_REQUESTS = [];
|
||||||
/* resultaat van een laatste multireset (klas/school): blijft zichtbaar
|
/* resultaat van een laatste multireset (klas/school): blijft zichtbaar
|
||||||
totdat de beheerder het venster zelf sluit, zodat wachtwoorden niet
|
totdat de beheerder het venster zelf sluit, zodat wachtwoorden niet
|
||||||
meteen weer verdwijnen na een reload() */
|
meteen weer verdwijnen na een reload() */
|
||||||
|
|
@ -175,7 +177,12 @@
|
||||||
const results = await Promise.all(CLASSES.map(c=>api("/assignments/class/"+c.id).catch(()=>({assignment:null}))));
|
const results = await Promise.all(CLASSES.map(c=>api("/assignments/class/"+c.id).catch(()=>({assignment:null}))));
|
||||||
CLASSES.forEach((c,i)=>{ ASSIGNMENTS[c.id] = results[i].assignment; });
|
CLASSES.forEach((c,i)=>{ ASSIGNMENTS[c.id] = results[i].assignment; });
|
||||||
}
|
}
|
||||||
}catch(e){ USERS = []; CLASSES = []; ASSIGNMENTS = {}; }
|
LINK_REQUESTS = [];
|
||||||
|
if(currentUser.role!=="super" || selSchool){
|
||||||
|
const lq = currentUser.role==="super" ? "?school="+selSchool : "";
|
||||||
|
LINK_REQUESTS = ((await api("/admin/link-requests"+lq).catch(()=>({}))).requests) || [];
|
||||||
|
}
|
||||||
|
}catch(e){ USERS = []; CLASSES = []; ASSIGNMENTS = {}; LINK_REQUESTS = []; }
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,6 +297,16 @@
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
/* eenmalige ouder-code waarmee een ouder dit kind aan het eigen
|
||||||
|
ouderaccount koppelt (ouderportaal); een eerdere code vervalt */
|
||||||
|
actions.appendChild(dangerBtn("👪 " + T("amParentCode"), "amParentCodeWarn", async ()=>{
|
||||||
|
try{
|
||||||
|
const r = await api("/admin/users/"+u.id+"/parent-code", { body:{} });
|
||||||
|
rowResults.set(u.id, { label: T("amResultParentCode"), value: r.parentCode });
|
||||||
|
openRows.add(u.id);
|
||||||
|
reload();
|
||||||
|
}catch(e){ msg(e.message); }
|
||||||
|
}));
|
||||||
}else{
|
}else{
|
||||||
if(u.pending && u.linkCode){
|
if(u.pending && u.linkCode){
|
||||||
tools.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode));
|
tools.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode));
|
||||||
|
|
@ -811,7 +828,9 @@
|
||||||
wordt tellen alle groepen mee zodat een treffer nooit verstopt zit. */
|
wordt tellen alle groepen mee zodat een treffer nooit verstopt zit. */
|
||||||
function applyUserFilter(panel){
|
function applyUserFilter(panel){
|
||||||
const q = userFilter.trim().toLowerCase();
|
const q = userFilter.trim().toLowerCase();
|
||||||
const list = panel.querySelector(".am-list");
|
/* expliciet de gebruikerslijst: het Aanmeldingen-blok erboven is ook een
|
||||||
|
.am-list maar valt buiten het zoekfilter en de groepslogica */
|
||||||
|
const list = panel.querySelector(".am-userlist") || panel.querySelector(".am-list");
|
||||||
if(!list) return;
|
if(!list) return;
|
||||||
let curGroup = null, groupVisible = false;
|
let curGroup = null, groupVisible = false;
|
||||||
const closeGroup = ()=>{
|
const closeGroup = ()=>{
|
||||||
|
|
@ -842,7 +861,73 @@
|
||||||
Leerlingen krijgen een aparte behandeling: voor schoolbeheerder/super gegroepeerd
|
Leerlingen krijgen een aparte behandeling: voor schoolbeheerder/super gegroepeerd
|
||||||
per klas (met een inline toevoegrij per klas), voor groepsleiding een platte lijst
|
per klas (met een inline toevoegrij per klas), voor groepsleiding een platte lijst
|
||||||
(die dankzij de server-side scope toch al beperkt is tot de eigen klas(sen)). */
|
(die dankzij de server-side scope toch al beperkt is tot de eigen klas(sen)). */
|
||||||
|
/* aanmeldverzoek van een ouder (thuiskind -> deze school): de school kiest
|
||||||
|
adopteren (thuisaccount wordt de schoolleerling, evt. met klas en - bij
|
||||||
|
naamconflict - een andere gebruikersnaam), koppelen aan een bestaande
|
||||||
|
leerling, of afwijzen */
|
||||||
|
function linkRequestRow(r){
|
||||||
|
const row = h("div","am-row");
|
||||||
|
row.appendChild(h("span","am-name","🧒 " + r.pupil.displayName
|
||||||
|
+ (r.pupil.displayName!==r.pupil.username ? ` (${r.pupil.username})` : "")));
|
||||||
|
row.appendChild(h("span","am-role", `${T("amRequestVia")} 👪 ${r.parent.displayName}`));
|
||||||
|
const tools = h("div","am-row-tools");
|
||||||
|
|
||||||
|
const adoptWrap = h("div","am-inline");
|
||||||
|
const cs = classSel(null);
|
||||||
|
const un = h("input","am-inp");
|
||||||
|
un.placeholder = T("amAdoptUserPh");
|
||||||
|
un.style.display = "none";
|
||||||
|
un.addEventListener("keydown", ev=>ev.stopPropagation());
|
||||||
|
const adopt = h("button","am-btn", T("amAdopt"));
|
||||||
|
adopt.type = "button";
|
||||||
|
adopt.addEventListener("click", async ()=>{
|
||||||
|
const body = { action:"adopt", classId: cs.value ? +cs.value : null };
|
||||||
|
if(un.style.display!=="none" && un.value.trim()) body.username = un.value.trim();
|
||||||
|
try{ await api("/admin/link-requests/"+r.id+"/resolve", { body }); reload(); }
|
||||||
|
catch(e){
|
||||||
|
/* naamconflict binnen de school: veld tonen om direct een andere
|
||||||
|
gebruikersnaam mee te geven */
|
||||||
|
if(e.status===409){ un.style.display = ""; un.focus(); }
|
||||||
|
msg(e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adoptWrap.appendChild(cs);
|
||||||
|
adoptWrap.appendChild(un);
|
||||||
|
adoptWrap.appendChild(adopt);
|
||||||
|
tools.appendChild(field("amAdopt", adoptWrap));
|
||||||
|
|
||||||
|
const matchWrap = h("div","am-inline");
|
||||||
|
const ms = h("select","am-sel");
|
||||||
|
fillPupilSelect(ms, USERS.filter(u=>u.role==="pupil"));
|
||||||
|
const mb = h("button","am-btn", T("amMatchBtn"));
|
||||||
|
mb.type = "button";
|
||||||
|
mb.addEventListener("click", async ()=>{
|
||||||
|
if(!ms.value) return;
|
||||||
|
try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"match", pupilId: +ms.value } }); reload(); }
|
||||||
|
catch(e){ msg(e.message); }
|
||||||
|
});
|
||||||
|
matchWrap.appendChild(ms);
|
||||||
|
matchWrap.appendChild(mb);
|
||||||
|
tools.appendChild(field("amMatchBtn", matchWrap));
|
||||||
|
|
||||||
|
const actions = h("div","am-actions");
|
||||||
|
actions.appendChild(dangerBtn("🗑 " + T("amReject"), "amRejectWarn", async ()=>{
|
||||||
|
try{ await api("/admin/link-requests/"+r.id+"/resolve", { body:{ action:"reject" } }); reload(); }
|
||||||
|
catch(e){ msg(e.message); }
|
||||||
|
}));
|
||||||
|
tools.appendChild(field("amFieldActions", actions));
|
||||||
|
row.appendChild(tools);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGebruikersPanel(panel){
|
function renderGebruikersPanel(panel){
|
||||||
|
/* openstaande aanmeldingen bovenaan: dit vraagt actie van de school */
|
||||||
|
if(LINK_REQUESTS.length){
|
||||||
|
panel.appendChild(h("div","am-group", `${T("amLinkRequests")} (${LINK_REQUESTS.length})`));
|
||||||
|
const rl = h("div","am-reqlist am-list");
|
||||||
|
LINK_REQUESTS.forEach(r=>rl.appendChild(linkRequestRow(r)));
|
||||||
|
panel.appendChild(rl);
|
||||||
|
}
|
||||||
const filterBar = h("div","am-add");
|
const filterBar = h("div","am-add");
|
||||||
const filterInp = h("input","am-inp am-filter");
|
const filterInp = h("input","am-inp am-filter");
|
||||||
filterInp.type = "search";
|
filterInp.type = "search";
|
||||||
|
|
@ -853,8 +938,8 @@
|
||||||
filterBar.appendChild(filterInp);
|
filterBar.appendChild(filterInp);
|
||||||
panel.appendChild(filterBar);
|
panel.appendChild(filterBar);
|
||||||
|
|
||||||
const list = h("div","am-list");
|
const list = h("div","am-list am-userlist");
|
||||||
const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]];
|
const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")], ["parent", T("amParents")]];
|
||||||
staffGroups.forEach(([role, label])=>{
|
staffGroups.forEach(([role, label])=>{
|
||||||
const us = USERS.filter(u=>u.role===role);
|
const us = USERS.filter(u=>u.role===role);
|
||||||
if(!us.length) return;
|
if(!us.length) return;
|
||||||
|
|
|
||||||
|
|
@ -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.57-beta";
|
const VERSION = "0.3.58-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -204,7 +204,22 @@ const I18N = {
|
||||||
errPwMatch:"Wachtwoorden komen niet overeen.",
|
errPwMatch:"Wachtwoorden komen niet overeen.",
|
||||||
codePh:"koppelcode",
|
codePh:"koppelcode",
|
||||||
roleSuper:"Systeemmanager", roleAdmin:"Schoolbeheerder",
|
roleSuper:"Systeemmanager", roleAdmin:"Schoolbeheerder",
|
||||||
roleTeacher:"Groepsleiding", rolePupil:"Leerling",
|
roleTeacher:"Groepsleiding", rolePupil:"Leerling", roleParent:"Ouder",
|
||||||
|
tabParentReg:"Ouders",
|
||||||
|
parentRegIntro:"Maak een ouderaccount en volg je kind - ook als de school (nog) niet meedoet:",
|
||||||
|
parentRegBtn:"Account maken",
|
||||||
|
parentTitle:"Mijn kinderen",
|
||||||
|
parentNoChildren:"Nog geen kinderen gekoppeld.",
|
||||||
|
parentAddChild:"+ Kind", parentChildNamePh:"naam van je kind…", parentAddBtn:"Toevoegen",
|
||||||
|
parentPwNote:"Bewaar dit wachtwoord: je kind logt ermee in (schoolveld leeg laten).",
|
||||||
|
parentLinkTitle:"+ Ouder-code", parentLinkPh:"ouder-code van school…", parentLinkBtn:"Koppel",
|
||||||
|
parentLinkNote:"Vraag de school om een ouder-code voor je kind.",
|
||||||
|
parentHome:"thuis",
|
||||||
|
parentApply:"🏫 Aanmelden", parentApplied:"aangemeld bij", parentApplyBtn:"Meld aan",
|
||||||
|
parentSchoolPh:"zoek je school…",
|
||||||
|
parentUnlink:"Ontkoppel",
|
||||||
|
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
|
||||||
|
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
|
||||||
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
|
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
|
||||||
pupilWatching:"meekijken",
|
pupilWatching:"meekijken",
|
||||||
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
|
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
|
||||||
|
|
@ -219,6 +234,13 @@ const I18N = {
|
||||||
amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.",
|
amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.",
|
||||||
amDeleteWarn:"Verwijdert dit account definitief, inclusief borden en woorden.",
|
amDeleteWarn:"Verwijdert dit account definitief, inclusief borden en woorden.",
|
||||||
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
|
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
|
||||||
|
amParents:"Ouders",
|
||||||
|
amParentCode:"Ouder-code", amResultParentCode:"Ouder-code",
|
||||||
|
amParentCodeWarn:"Maakt een eenmalige ouder-code aan; een eerdere code vervalt.",
|
||||||
|
amLinkRequests:"Aanmeldingen", amRequestVia:"via",
|
||||||
|
amAdopt:"Adopteer", amAdoptUserPh:"andere gebruikersnaam (bij conflict)…",
|
||||||
|
amMatchPick:"— koppel aan bestaande leerling… —", amMatchBtn:"Koppel",
|
||||||
|
amReject:"Afwijzen", amRejectWarn:"Wijst de aanmelding af; de ouder ziet het verzoek verdwijnen.",
|
||||||
amSchoolNamePh:"naam van de school…", amNewSchoolConfirm:"Aanmaken:",
|
amSchoolNamePh:"naam van de school…", amNewSchoolConfirm:"Aanmaken:",
|
||||||
amDeleteSchoolWarn:"Verwijdert deze school met alle klassen en gebruikers. Dit kan niet ongedaan worden gemaakt.",
|
amDeleteSchoolWarn:"Verwijdert deze school met alle klassen en gebruikers. Dit kan niet ongedaan worden gemaakt.",
|
||||||
amClassNamePh:"naam van de klas…",
|
amClassNamePh:"naam van de klas…",
|
||||||
|
|
@ -429,7 +451,22 @@ const I18N = {
|
||||||
errPwMatch:"Passwords don't match.",
|
errPwMatch:"Passwords don't match.",
|
||||||
codePh:"link code",
|
codePh:"link code",
|
||||||
roleSuper:"System manager", roleAdmin:"School administrator",
|
roleSuper:"System manager", roleAdmin:"School administrator",
|
||||||
roleTeacher:"Group leader", rolePupil:"Pupil",
|
roleTeacher:"Group leader", rolePupil:"Pupil", roleParent:"Parent",
|
||||||
|
tabParentReg:"Parents",
|
||||||
|
parentRegIntro:"Create a parent account and follow your child - even if the school doesn't take part (yet):",
|
||||||
|
parentRegBtn:"Create account",
|
||||||
|
parentTitle:"My children",
|
||||||
|
parentNoChildren:"No children linked yet.",
|
||||||
|
parentAddChild:"+ Child", parentChildNamePh:"your child's name…", parentAddBtn:"Add",
|
||||||
|
parentPwNote:"Keep this password: your child logs in with it (leave the school field empty).",
|
||||||
|
parentLinkTitle:"+ Parent code", parentLinkPh:"parent code from school…", parentLinkBtn:"Link",
|
||||||
|
parentLinkNote:"Ask the school for a parent code for your child.",
|
||||||
|
parentHome:"home",
|
||||||
|
parentApply:"🏫 Apply", parentApplied:"applied to", parentApplyBtn:"Apply",
|
||||||
|
parentSchoolPh:"search your school…",
|
||||||
|
parentUnlink:"Unlink",
|
||||||
|
parentUnlinkWarn:"Removes only the link; your child's account remains.",
|
||||||
|
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
|
||||||
pupilEmpty:"Your teacher hasn't set up anything for you yet.",
|
pupilEmpty:"Your teacher hasn't set up anything for you yet.",
|
||||||
pupilWatching:"watching",
|
pupilWatching:"watching",
|
||||||
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",
|
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",
|
||||||
|
|
@ -444,6 +481,13 @@ const I18N = {
|
||||||
amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.",
|
amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.",
|
||||||
amDeleteWarn:"Deletes this account permanently, including boards and words.",
|
amDeleteWarn:"Deletes this account permanently, including boards and words.",
|
||||||
amResultPw:"New password", amResultCode:"New link code",
|
amResultPw:"New password", amResultCode:"New link code",
|
||||||
|
amParents:"Parents",
|
||||||
|
amParentCode:"Parent code", amResultParentCode:"Parent code",
|
||||||
|
amParentCodeWarn:"Generates a one-time parent code; a previous code stops working.",
|
||||||
|
amLinkRequests:"Enrolments", amRequestVia:"via",
|
||||||
|
amAdopt:"Adopt", amAdoptUserPh:"different username (on conflict)…",
|
||||||
|
amMatchPick:"— link to existing pupil… —", amMatchBtn:"Link",
|
||||||
|
amReject:"Reject", amRejectWarn:"Rejects the enrolment; the request disappears for the parent.",
|
||||||
amSchoolNamePh:"name of the school…", amNewSchoolConfirm:"Create:",
|
amSchoolNamePh:"name of the school…", amNewSchoolConfirm:"Create:",
|
||||||
amDeleteSchoolWarn:"Deletes this school with all its classes and users. This cannot be undone.",
|
amDeleteSchoolWarn:"Deletes this school with all its classes and users. This cannot be undone.",
|
||||||
amClassNamePh:"name of the class…",
|
amClassNamePh:"name of the class…",
|
||||||
|
|
@ -494,6 +538,9 @@ function applyI18n(){
|
||||||
document.getElementById("liCode").placeholder = T("codePh");
|
document.getElementById("liCode").placeholder = T("codePh");
|
||||||
document.getElementById("liPassNew").placeholder = T("pwNewPh");
|
document.getElementById("liPassNew").placeholder = T("pwNewPh");
|
||||||
document.getElementById("liPassNew2").placeholder = T("pwConfirmPh");
|
document.getElementById("liPassNew2").placeholder = T("pwConfirmPh");
|
||||||
|
document.getElementById("prName").placeholder = T("namePh");
|
||||||
|
document.getElementById("prPass").placeholder = T("pwNewPh");
|
||||||
|
document.getElementById("prPass2").placeholder = T("pwConfirmPh");
|
||||||
document.getElementById("ownPwCurrent").placeholder = T("pwCurrentPh");
|
document.getElementById("ownPwCurrent").placeholder = T("pwCurrentPh");
|
||||||
document.getElementById("ownPw").placeholder = T("pwNewPh");
|
document.getElementById("ownPw").placeholder = T("pwNewPh");
|
||||||
document.getElementById("ownPw2").placeholder = T("pwConfirmPh");
|
document.getElementById("ownPw2").placeholder = T("pwConfirmPh");
|
||||||
|
|
@ -652,11 +699,22 @@ async function hydrateFromServer(){
|
||||||
if(currentUser && currentUser.role === "pupil"){
|
if(currentUser && currentUser.role === "pupil"){
|
||||||
THEMES = {nl:[],en:[]};
|
THEMES = {nl:[],en:[]};
|
||||||
GENERAL_EXTRA = {nl:[],en:[]};
|
GENERAL_EXTRA = {nl:[],en:[]};
|
||||||
|
stopParentView();
|
||||||
startPupilView();
|
startPupilView();
|
||||||
document.dispatchEvent(new CustomEvent("userchange"));
|
document.dispatchEvent(new CustomEvent("userchange"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* ouders krijgen het portaal (parent.js) i.p.v. het whiteboard */
|
||||||
|
if(currentUser && currentUser.role === "parent"){
|
||||||
|
THEMES = {nl:[],en:[]};
|
||||||
|
GENERAL_EXTRA = {nl:[],en:[]};
|
||||||
stopPupilView();
|
stopPupilView();
|
||||||
|
startParentView();
|
||||||
|
document.dispatchEvent(new CustomEvent("userchange"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopPupilView();
|
||||||
|
stopParentView();
|
||||||
const res = await api("/me/data");
|
const res = await api("/me/data");
|
||||||
const d = res.data || {};
|
const d = res.data || {};
|
||||||
DATA_REV = typeof res.rev === "number" ? res.rev : 0;
|
DATA_REV = typeof res.rev === "number" ? res.rev : 0;
|
||||||
|
|
@ -671,6 +729,7 @@ async function hydrateFromServer(){
|
||||||
function resetToGuest(){
|
function resetToGuest(){
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
stopPupilView();
|
stopPupilView();
|
||||||
|
stopParentView();
|
||||||
THEMES = {nl:[],en:[]};
|
THEMES = {nl:[],en:[]};
|
||||||
GENERAL_EXTRA = {nl:[],en:[]};
|
GENERAL_EXTRA = {nl:[],en:[]};
|
||||||
ANCHOR_LIB = [];
|
ANCHOR_LIB = [];
|
||||||
|
|
@ -686,7 +745,7 @@ const loginWrap = document.getElementById("loginWrap");
|
||||||
const loginMsg = document.getElementById("loginMsg");
|
const loginMsg = document.getElementById("loginMsg");
|
||||||
const settingsWrap = document.getElementById("settingsWrap");
|
const settingsWrap = document.getElementById("settingsWrap");
|
||||||
const settingsMsg = document.getElementById("settingsMsg");
|
const settingsMsg = document.getElementById("settingsMsg");
|
||||||
const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==="teacher" ? "roleTeacher" : "rolePupil";
|
const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==="teacher" ? "roleTeacher" : r==="parent" ? "roleParent" : "rolePupil";
|
||||||
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);
|
||||||
|
|
@ -703,11 +762,12 @@ function updateUserUI(){
|
||||||
document.getElementById("ownPwBlock").style.display = can("password.own.change") ? "flex" : "none";
|
document.getElementById("ownPwBlock").style.display = can("password.own.change") ? "flex" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* login-modal tabs: "inloggen" vs "koppelcode activeren" */
|
/* login-modal tabs: "inloggen" / "koppelcode activeren" / "ouderaccount maken" */
|
||||||
function switchLoginTab(tab){
|
function switchLoginTab(tab){
|
||||||
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab));
|
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab));
|
||||||
document.getElementById("loginBox").style.display = tab==="login" ? "block" : "none";
|
document.getElementById("loginBox").style.display = tab==="login" ? "block" : "none";
|
||||||
document.getElementById("linkBox").style.display = tab==="link" ? "block" : "none";
|
document.getElementById("linkBox").style.display = tab==="link" ? "block" : "none";
|
||||||
|
document.getElementById("parentRegBox").style.display = tab==="parentreg" ? "block" : "none";
|
||||||
}
|
}
|
||||||
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));
|
||||||
|
|
@ -851,6 +911,24 @@ function pwToggle(btnId, ...fieldIds){
|
||||||
}
|
}
|
||||||
pwToggle("btnLiPassToggle", "liPassNew", "liPassNew2");
|
pwToggle("btnLiPassToggle", "liPassNew", "liPassNew2");
|
||||||
pwToggle("btnOwnPwToggle", "ownPwCurrent", "ownPw", "ownPw2");
|
pwToggle("btnOwnPwToggle", "ownPwCurrent", "ownPw", "ownPw2");
|
||||||
|
pwToggle("btnPrPassToggle", "prPass", "prPass2");
|
||||||
|
|
||||||
|
/* ouderaccount registreren: direct ingelogd, parent.js toont daarna het portaal */
|
||||||
|
document.getElementById("btnParentReg").addEventListener("click", async ()=>{
|
||||||
|
const btn = document.getElementById("btnParentReg");
|
||||||
|
const username = document.getElementById("prName").value.trim();
|
||||||
|
const password = document.getElementById("prPass").value;
|
||||||
|
const password2 = document.getElementById("prPass2").value;
|
||||||
|
loginMsg.classList.remove("ok");
|
||||||
|
if(password !== password2){ loginMsg.textContent = T("errPwMatch"); return; }
|
||||||
|
await withBusy(btn, async ()=>{
|
||||||
|
try{
|
||||||
|
await afterLogin(await api("/auth/register-parent", { body:{ username, password } }));
|
||||||
|
document.getElementById("prPass").value = "";
|
||||||
|
document.getElementById("prPass2").value = "";
|
||||||
|
}catch(e){ loginMsg.textContent = e.message; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* account activeren met een koppelcode */
|
/* account activeren met een koppelcode */
|
||||||
document.getElementById("btnLink").addEventListener("click", async ()=>{
|
document.getElementById("btnLink").addEventListener("click", async ()=>{
|
||||||
|
|
|
||||||
233
public/js/parent.js
Normal file
233
public/js/parent.js
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
/* teach - ouderportaal: ouders (role parent) zien geen whiteboard maar een
|
||||||
|
portaal met hun gekoppelde kinderen. Per kind: school/klas (of "thuis"),
|
||||||
|
voortgangstijdlijn, en voor thuiskinderen een aanmelding bij een school
|
||||||
|
(doorzoekbare lijst; de school handelt het verzoek af in het beheer).
|
||||||
|
Kinderen komen erbij door ze zelf aan te maken (thuiskind, wachtwoord
|
||||||
|
eenmalig zichtbaar) of via een eenmalige ouder-code van de school. */
|
||||||
|
"use strict";
|
||||||
|
const parentView = document.getElementById("parentView");
|
||||||
|
/* opengeklapte voortgang/aanmeldformulieren en eenmalige resultaten
|
||||||
|
(kind-wachtwoord) blijven behouden over her-renders van het portaal */
|
||||||
|
const parentOpen = new Set();
|
||||||
|
const parentResults = new Map();
|
||||||
|
let PARENT_SCHOOLS = [];
|
||||||
|
|
||||||
|
function prH(tag, cls, text){
|
||||||
|
const el = document.createElement(tag);
|
||||||
|
if(cls) el.className = cls;
|
||||||
|
if(text != null) el.textContent = text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
/* arm-then-confirm zoals dangerBtn in het beheer: eerste klik wapent ("zeker?"),
|
||||||
|
tweede klik voert uit, na 4s ontwapent hij vanzelf */
|
||||||
|
function prDangerBtn(label, warnKey, fn){
|
||||||
|
const b = prH("button","am-btn am-danger", label);
|
||||||
|
b.type = "button";
|
||||||
|
if(warnKey) b.title = T(warnKey);
|
||||||
|
let timer = null;
|
||||||
|
b.addEventListener("click", async ()=>{
|
||||||
|
if(!timer){
|
||||||
|
b.classList.add("armed");
|
||||||
|
b.textContent = "⚠ " + T("amSure");
|
||||||
|
timer = setTimeout(()=>{ timer = null; b.classList.remove("armed"); b.textContent = label; }, 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(timer); timer = null;
|
||||||
|
b.classList.remove("armed"); b.textContent = label;
|
||||||
|
await fn(b);
|
||||||
|
});
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
function prMsg(t){
|
||||||
|
const m = parentView.querySelector(".am-msg");
|
||||||
|
if(m) m.textContent = t || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parentReload(){
|
||||||
|
let children = [];
|
||||||
|
try{ children = (await api("/parent/children")).children || []; }
|
||||||
|
catch(e){ /* render toont dan een lege lijst; melding via prMsg hieronder */ }
|
||||||
|
renderParentView(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentChildRow(c){
|
||||||
|
const row = prH("div","am-row");
|
||||||
|
row.appendChild(prH("span","am-name","🧒 " + c.displayName + (c.displayName!==c.username ? ` (${c.username})` : "")));
|
||||||
|
const where = c.schoolName
|
||||||
|
? c.schoolName + (c.className ? ` · ${c.className}` : "")
|
||||||
|
: T("parentHome") + (c.appliedTo ? ` · ${T("parentApplied")} ${c.appliedTo}` : "");
|
||||||
|
row.appendChild(prH("span","am-role", where));
|
||||||
|
|
||||||
|
const tools = prH("div","am-row-tools");
|
||||||
|
const r = parentResults.get(c.id);
|
||||||
|
if(r){
|
||||||
|
const res = prH("div","am-result");
|
||||||
|
res.appendChild(prH("span","am-result-lbl", r.label + ": "));
|
||||||
|
const code = prH("code", null, r.value);
|
||||||
|
res.appendChild(code);
|
||||||
|
res.appendChild(prH("div","guestnote", T("parentPwNote")));
|
||||||
|
tools.appendChild(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* voortgangstijdlijn (zelfde regelopbouw als het beheer: widget, score, tijd) */
|
||||||
|
const progHolder = prH("div","am-list");
|
||||||
|
async function loadProgress(){
|
||||||
|
progHolder.innerHTML = "";
|
||||||
|
let events = [];
|
||||||
|
try{ events = (await api("/progress/pupil/"+c.id)).events || []; }
|
||||||
|
catch(e){ progHolder.appendChild(prH("div","guestnote", e.message)); return; }
|
||||||
|
if(!events.length){ progHolder.appendChild(prH("div","guestnote", T("parentProgressEmpty"))); return; }
|
||||||
|
events.forEach(ev=>{
|
||||||
|
const line = prH("div","am-row");
|
||||||
|
line.appendChild(prH("span","am-name", T("wg_"+ev.widgetType) || ev.widgetType));
|
||||||
|
const scoreParts = [];
|
||||||
|
if(ev.attempts) scoreParts.push(`${ev.correct}/${ev.attempts} ${T("mGoed")}`);
|
||||||
|
if(ev.stars) scoreParts.push("★".repeat(ev.stars));
|
||||||
|
line.appendChild(prH("span","am-code", scoreParts.join(" · ")));
|
||||||
|
line.appendChild(prH("span","am-code", new Date(ev.createdAt).toLocaleString(LANG==="nl" ? "nl-NL" : "en-GB")));
|
||||||
|
progHolder.appendChild(line);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const progBtn = prH("button","am-btn", T("parentProgress"));
|
||||||
|
progBtn.type = "button";
|
||||||
|
progBtn.addEventListener("click", ()=>{
|
||||||
|
const key = "prog:"+c.id;
|
||||||
|
if(parentOpen.has(key)){ parentOpen.delete(key); progHolder.innerHTML = ""; }
|
||||||
|
else{ parentOpen.add(key); loadProgress(); }
|
||||||
|
});
|
||||||
|
const actions = prH("div","am-actions");
|
||||||
|
actions.appendChild(progBtn);
|
||||||
|
|
||||||
|
/* thuiskind zonder lopende aanmelding: aanmelden bij een school via een
|
||||||
|
doorzoekbare lijst (input + datalist gevuld uit de publieke /schools) */
|
||||||
|
const applyHolder = prH("div","am-add");
|
||||||
|
applyHolder.style.display = "none";
|
||||||
|
if(!c.schoolId && !c.appliedTo){
|
||||||
|
const applyBtn = prH("button","am-btn", T("parentApply"));
|
||||||
|
applyBtn.type = "button";
|
||||||
|
const inp = prH("input","am-inp");
|
||||||
|
inp.placeholder = T("parentSchoolPh");
|
||||||
|
inp.setAttribute("list","parentSchoolList");
|
||||||
|
const go = prH("button","am-btn", T("parentApplyBtn"));
|
||||||
|
go.type = "button";
|
||||||
|
go.addEventListener("click", async ()=>{
|
||||||
|
const s = PARENT_SCHOOLS.find(x=>x.name.toLowerCase()===inp.value.trim().toLowerCase());
|
||||||
|
if(!s){ prMsg(T("parentSchoolPh")); return; }
|
||||||
|
try{ await api(`/parent/children/${c.id}/apply`, { body:{ schoolId: s.id } }); parentOpen.delete("apply:"+c.id); parentReload(); }
|
||||||
|
catch(e){ prMsg(e.message); }
|
||||||
|
});
|
||||||
|
applyHolder.appendChild(inp);
|
||||||
|
applyHolder.appendChild(go);
|
||||||
|
applyBtn.addEventListener("click", ()=>{
|
||||||
|
const key = "apply:"+c.id;
|
||||||
|
if(parentOpen.has(key)) parentOpen.delete(key); else parentOpen.add(key);
|
||||||
|
applyHolder.style.display = parentOpen.has(key) ? "" : "none";
|
||||||
|
if(parentOpen.has(key)) inp.focus();
|
||||||
|
});
|
||||||
|
applyHolder.style.display = parentOpen.has("apply:"+c.id) ? "" : "none";
|
||||||
|
actions.appendChild(applyBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.appendChild(prDangerBtn(T("parentUnlink"), "parentUnlinkWarn", async ()=>{
|
||||||
|
try{ await api("/parent/children/"+c.id, { method:"DELETE" }); parentResults.delete(c.id); parentReload(); }
|
||||||
|
catch(e){ prMsg(e.message); }
|
||||||
|
}));
|
||||||
|
tools.appendChild(actions);
|
||||||
|
tools.appendChild(applyHolder);
|
||||||
|
tools.appendChild(progHolder);
|
||||||
|
row.appendChild(tools);
|
||||||
|
if(parentOpen.has("prog:"+c.id)) loadProgress();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ingeklapt formulier ("+ …"-knop) zoals addForm in het beheer */
|
||||||
|
function parentCollapsedForm(label, build){
|
||||||
|
const wrap = prH("div","am-add-wrap");
|
||||||
|
const toggle = prH("button","tbtn ghost", label);
|
||||||
|
toggle.type = "button";
|
||||||
|
const f = prH("div","am-add");
|
||||||
|
f.style.display = "none";
|
||||||
|
toggle.addEventListener("click", ()=>{
|
||||||
|
f.style.display = f.style.display==="none" ? "" : "none";
|
||||||
|
const first = f.querySelector("input");
|
||||||
|
if(f.style.display!=="none" && first) first.focus();
|
||||||
|
});
|
||||||
|
build(f);
|
||||||
|
wrap.appendChild(toggle);
|
||||||
|
wrap.appendChild(f);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderParentView(children){
|
||||||
|
parentView.innerHTML = "";
|
||||||
|
const inner = prH("div","parent-inner");
|
||||||
|
inner.appendChild(prH("h2","parent-title", T("parentTitle")));
|
||||||
|
inner.appendChild(prH("div","am-msg"));
|
||||||
|
|
||||||
|
const list = prH("div","am-list");
|
||||||
|
if(!children.length) list.appendChild(prH("div","guestnote", T("parentNoChildren")));
|
||||||
|
children.forEach(c=>list.appendChild(parentChildRow(c)));
|
||||||
|
inner.appendChild(list);
|
||||||
|
|
||||||
|
/* datalist voor de doorzoekbare schoolkeuze (gedeeld door alle rijen) */
|
||||||
|
const dl = document.createElement("datalist");
|
||||||
|
dl.id = "parentSchoolList";
|
||||||
|
PARENT_SCHOOLS.forEach(s=>dl.appendChild(new Option(s.name)));
|
||||||
|
inner.appendChild(dl);
|
||||||
|
|
||||||
|
/* kind aanmaken (thuiskind): naam -> account zonder school + wachtwoord */
|
||||||
|
inner.appendChild(parentCollapsedForm(T("parentAddChild"), f=>{
|
||||||
|
const inp = prH("input","am-inp");
|
||||||
|
inp.placeholder = T("parentChildNamePh");
|
||||||
|
inp.maxLength = 30;
|
||||||
|
const go = prH("button","am-btn", T("parentAddBtn"));
|
||||||
|
go.type = "button";
|
||||||
|
go.addEventListener("click", async ()=>{
|
||||||
|
try{
|
||||||
|
const r = await api("/parent/children", { body:{ name: inp.value.trim() } });
|
||||||
|
parentResults.set(r.child.id, { label: T("amFieldPw"), value: r.child.password });
|
||||||
|
parentReload();
|
||||||
|
}catch(e){ prMsg(e.message); }
|
||||||
|
});
|
||||||
|
inp.addEventListener("keydown", ev=>{ if(ev.key==="Enter") go.click(); });
|
||||||
|
f.appendChild(inp);
|
||||||
|
f.appendChild(go);
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* schoolkind koppelen met de eenmalige ouder-code van de school */
|
||||||
|
inner.appendChild(parentCollapsedForm(T("parentLinkTitle"), f=>{
|
||||||
|
f.appendChild(prH("div","guestnote", T("parentLinkNote")));
|
||||||
|
const inp = prH("input","am-inp");
|
||||||
|
inp.placeholder = T("parentLinkPh");
|
||||||
|
inp.maxLength = 9;
|
||||||
|
inp.style.textTransform = "uppercase";
|
||||||
|
const go = prH("button","am-btn", T("parentLinkBtn"));
|
||||||
|
go.type = "button";
|
||||||
|
go.addEventListener("click", async ()=>{
|
||||||
|
try{
|
||||||
|
await api("/parent/children/link", { body:{ code: inp.value.trim().toUpperCase() } });
|
||||||
|
inp.value = "";
|
||||||
|
parentReload();
|
||||||
|
}catch(e){ prMsg(e.message); }
|
||||||
|
});
|
||||||
|
inp.addEventListener("keydown", ev=>{ if(ev.key==="Enter") go.click(); });
|
||||||
|
f.appendChild(inp);
|
||||||
|
f.appendChild(go);
|
||||||
|
}));
|
||||||
|
|
||||||
|
parentView.appendChild(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startParentView(){
|
||||||
|
document.body.classList.add("parent-mode");
|
||||||
|
parentOpen.clear();
|
||||||
|
parentResults.clear();
|
||||||
|
(async ()=>{
|
||||||
|
try{ PARENT_SCHOOLS = await api("/schools"); }catch(e){ PARENT_SCHOOLS = []; }
|
||||||
|
parentReload();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
function stopParentView(){
|
||||||
|
document.body.classList.remove("parent-mode");
|
||||||
|
parentView.innerHTML = "";
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue