fix: toon gekoppelde ouders in de gebruikerslijst van de school (v0.3.59-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 28s

- Ouders (school_id NULL) vielen buiten elke schoolgescopeerde gebruikerslijst; /admin/users levert nu ook de ouders die aan leerlingen van de school gekoppeld zijn (groepsleiding: alleen ouders van leerlingen uit eigen klassen)
- Ouder-rijen tonen de namen van hun gekoppelde kinderen (childNames)
- Servertest voor de gekoppelde-ouders-lijst

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
This commit is contained in:
Ramon 2026-07-16 00:23:51 +02:00
parent 5f9ffbfdde
commit 42fc323ab1
5 changed files with 52 additions and 2 deletions

View file

@ -1 +1 @@
0.3.58-beta
0.3.59-beta

View file

@ -205,6 +205,10 @@
if(u.role==="pupil"){
const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId));
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : "")));
}else if(u.role==="parent"){
/* de school ziet bij elke ouder direct om welke kinderen het gaat */
const kids = (u.childNames||[]).join(", ");
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (kids ? ` · 🧒 ${kids}` : "")));
}else{
const names = (u.classIds||[])
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.58-beta";
const VERSION = "0.3.59-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;

View file

@ -389,11 +389,20 @@ export default async function api(app) {
// teacher de eigen school maar leerlingen alleen uit de eigen klas(sen).
app.get('/admin/users', async (req, reply) => {
need(req, reply, PERMISSIONS['users.manage']);
// Ouders hebben school_id NULL en vallen daardoor buiten elke schoollijst;
// een school ziet wél de ouders die aan haar eigen leerlingen gekoppeld zijn.
const linkedParents = async (schoolId, extra = '', params = []) => (await pool.query(
`SELECT DISTINCT p.* FROM users p
JOIN parent_children pc ON pc.parent_id = p.id
JOIN users k ON k.id = pc.pupil_id
WHERE k.school_id = $1 ${extra} ORDER BY lower(p.username)`,
[schoolId, ...params])).rows;
let rows;
if (req.user.allRoles.includes('super')) {
rows = (req.query.school
? await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.query.school])
: await pool.query('SELECT * FROM users ORDER BY school_id NULLS FIRST, role, lower(username)')).rows;
if (req.query.school) rows = rows.concat(await linkedParents(req.query.school));
} else if (teacherOnly(req)) {
rows = (await pool.query(
`SELECT * FROM users
@ -401,8 +410,11 @@ export default async function api(app) {
AND (role != 'pupil' OR class_id IN (SELECT class_id FROM class_teachers WHERE user_id = $2))
ORDER BY role, lower(username)`,
[req.user.school_id, req.user.id])).rows;
rows = rows.concat(await linkedParents(req.user.school_id,
'AND k.class_id IN (SELECT class_id FROM class_teachers WHERE user_id = $2)', [req.user.id]));
} else {
rows = (await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.user.school_id])).rows;
rows = rows.concat(await linkedParents(req.user.school_id));
}
// extra stafrollen per gebruiker erbij ophalen (in één keer, niet per rij)
const roleRows = rows.length
@ -422,11 +434,24 @@ export default async function api(app) {
const key = String(r.user_id);
classesByUser.set(key, [...(classesByUser.get(key) || []), Number(r.class_id)]);
});
// kindernamen bij ouder-rijen, zodat de school ziet wie bij wie hoort
const parentIds = rows.filter((u) => u.role === 'parent').map((u) => u.id);
const kidRows = parentIds.length
? (await pool.query(
`SELECT pc.parent_id, k.display_name, k.username FROM parent_children pc
JOIN users k ON k.id = pc.pupil_id WHERE pc.parent_id = ANY($1)`, [parentIds])).rows
: [];
const kidsByParent = new Map();
kidRows.forEach((r) => {
const key = String(r.parent_id);
kidsByParent.set(key, [...(kidsByParent.get(key) || []), r.display_name || r.username]);
});
return { users: rows.map((u) => ({
...publicUser({ ...u, extra_roles: extraByUser.get(String(u.id)) || [] }),
linkCode: u.link_code || null,
everLoggedIn: !!u.last_login_at,
classIds: classesByUser.get(String(u.id)) || [],
...(u.role === 'parent' ? { childNames: kidsByParent.get(String(u.id)) || [] } : {}),
})) };
});

View file

@ -288,6 +288,27 @@ test('afwijzen verwijdert alleen het verzoek; andere school mag niet afhandelen'
await app.close(); await app2.close();
});
test('schoolbeheerder ziet gekoppelde ouders (met kindernamen) in de gebruikerslijst', async () => {
const { app } = await makeApp({ user: admin, respond: (sql) => {
if (sql.startsWith('SELECT * FROM users WHERE school_id')) return { rows: [
{ id: 50, username: 'lena', role: 'pupil', school_id: 2, class_id: 5 },
] };
if (sql.includes('JOIN parent_children pc ON pc.parent_id = p.id')) return { rows: [
{ id: 20, username: 'ouder', display_name: 'Ouder', role: 'parent', school_id: null, class_id: null },
] };
if (sql.includes('FROM parent_children pc') && sql.includes('WHERE pc.parent_id = ANY')) return { rows: [
{ parent_id: 20, display_name: 'Lena', username: 'lena' },
] };
return undefined;
} });
const res = await app.inject({ method: 'GET', url: '/api/admin/users', cookies });
assert.equal(res.statusCode, 200, res.body);
const p = res.json().users.find((u) => u.role === 'parent');
assert.ok(p, 'ouder hoort in de schoollijst te staan');
assert.deepEqual(p.childNames, ['Lena']);
await app.close();
});
test('thuiskind logt voortgang met de gekoppelde ouder als leerkracht', async () => {
const { app, calls } = await makeApp({ user: homeChild, respond: (sql) => {
if (sql.includes('FROM parent_children WHERE pupil_id')) return { rows: [{ parent_id: 20 }] };