change dat format in member overview, sync exams to atemschutz tool, rework member detail page

This commit is contained in:
Matthias Hochmeister
2026-04-20 10:32:20 +02:00
parent d5291360c9
commit 752dfe474c
16 changed files with 874 additions and 182 deletions

View File

@@ -229,19 +229,46 @@ class AtemschutzService {
*/
async syncLehrgangFromKurse(): Promise<{ processed: number }> {
try {
const result = await pool.query(`
INSERT INTO atemschutz_traeger (id, user_id, atemschutz_lehrgang, lehrgang_datum)
SELECT uuid_generate_v4(), a.user_id, true, MIN(a.kurs_datum)
// Pass 1: Full lehrgang — AT20/AGL with 'mit Erfolg' or 'mit ausgezeichnetem Erfolg'
const fullResult = await pool.query(`
INSERT INTO atemschutz_traeger (id, user_id, atemschutz_lehrgang, lehrgang_datum, lehrgang_theorie_only)
SELECT uuid_generate_v4(), a.user_id, true, MIN(a.kurs_datum), false
FROM ausbildung a
WHERE a.kurs_kurzbezeichnung = 'AT20'
AND a.erfolgscode = 'mit Erfolg'
WHERE (
(TRIM(a.kurs_kurzbezeichnung) = 'AT20' AND TRIM(a.erfolgscode) IN ('mit Erfolg', 'mit ausgezeichnetem Erfolg'))
OR
(TRIM(a.kurs_kurzbezeichnung) = 'AGL' AND TRIM(a.erfolgscode) IN ('mit Erfolg', 'mit ausgezeichnetem Erfolg'))
)
GROUP BY a.user_id
ON CONFLICT (user_id) DO UPDATE
SET atemschutz_lehrgang = true,
lehrgang_theorie_only = false,
lehrgang_datum = COALESCE(atemschutz_traeger.lehrgang_datum, EXCLUDED.lehrgang_datum),
updated_at = NOW()
`);
const processed = result.rowCount ?? 0;
// Pass 2: Theory-only — AT20 with 'mit Erfolg Theorie', only for users without a full lehrgang
const theorieResult = await pool.query(`
INSERT INTO atemschutz_traeger (id, user_id, atemschutz_lehrgang, lehrgang_datum, lehrgang_theorie_only)
SELECT uuid_generate_v4(), a.user_id, true, MIN(a.kurs_datum), true
FROM ausbildung a
WHERE TRIM(a.kurs_kurzbezeichnung) = 'AT20'
AND TRIM(a.erfolgscode) = 'mit Erfolg Theorie'
AND NOT EXISTS (
SELECT 1 FROM atemschutz_traeger at2
WHERE at2.user_id = a.user_id
AND at2.atemschutz_lehrgang = true
AND at2.lehrgang_theorie_only = false
)
GROUP BY a.user_id
ON CONFLICT (user_id) DO UPDATE
SET atemschutz_lehrgang = true,
lehrgang_theorie_only = true,
lehrgang_datum = COALESCE(atemschutz_traeger.lehrgang_datum, EXCLUDED.lehrgang_datum),
updated_at = NOW()
`);
const processed = (fullResult.rowCount ?? 0) + (theorieResult.rowCount ?? 0);
logger.info('AT20 Atemschutz-Sync abgeschlossen', { processed });
return { processed };
} catch (error) {

View File

@@ -133,6 +133,8 @@ class MemberService {
dienstgrad,
page = 1,
pageSize = 25,
sortBy,
sortDir,
} = filters ?? {};
const conditions: string[] = ['u.is_active = TRUE'];
@@ -165,6 +167,20 @@ class MemberService {
const whereClause = `WHERE ${conditions.join(' AND ')}`;
const fetchAll = pageSize === 0;
// Build ORDER BY — whitelist columns to prevent SQL injection
const SORT_COLUMNS: Record<string, string> = {
family_name: 'u.family_name',
eintrittsdatum: 'mp.eintrittsdatum',
dienstgrad: 'mp.dienstgrad',
status: 'mp.status',
fdisk_standesbuch_nr: 'mp.fdisk_standesbuch_nr',
};
const defaultOrder = 'u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST';
const dir = sortDir === 'desc' ? 'DESC' : 'ASC';
const orderBy = sortBy && SORT_COLUMNS[sortBy]
? `${SORT_COLUMNS[sortBy]} ${dir} NULLS LAST, u.family_name ASC NULLS LAST`
: defaultOrder;
let dataQuery: string;
if (fetchAll) {
dataQuery = `
@@ -186,7 +202,7 @@ class MemberService {
FROM users u
LEFT JOIN mitglieder_profile mp ON mp.user_id = u.id
${whereClause}
ORDER BY u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST
ORDER BY ${orderBy}
`;
} else {
const offset = (page - 1) * pageSize;
@@ -209,7 +225,7 @@ class MemberService {
FROM users u
LEFT JOIN mitglieder_profile mp ON mp.user_id = u.id
${whereClause}
ORDER BY u.family_name ASC NULLS LAST, u.given_name ASC NULLS LAST
ORDER BY ${orderBy}
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
`;
values.push(pageSize, offset);