change dat format in member overview, sync exams to atemschutz tool, rework member detail page
This commit is contained in:
@@ -171,9 +171,9 @@ class AtemschutzController {
|
||||
user_id: item.user_id,
|
||||
typ: 'atemschutz_expiry',
|
||||
titel: item.untersuchung_status === 'abgelaufen'
|
||||
? 'G26 Untersuchung abgelaufen'
|
||||
: 'G26 Untersuchung läuft bald ab',
|
||||
nachricht: `Ihre G26 Untersuchung ${item.untersuchung_status === 'abgelaufen' ? 'ist abgelaufen' : 'läuft bald ab'}.`,
|
||||
? 'Atemschutztauglichkeitsuntersuchung abgelaufen'
|
||||
: 'Atemschutztauglichkeitsuntersuchung läuft bald ab',
|
||||
nachricht: `Ihre Atemschutztauglichkeitsuntersuchung ${item.untersuchung_status === 'abgelaufen' ? 'ist abgelaufen' : 'läuft bald ab'}.`,
|
||||
schwere: item.untersuchung_status === 'abgelaufen' ? 'fehler' : 'warnung',
|
||||
quell_typ: 'atemschutz_untersuchung',
|
||||
quell_id: item.id,
|
||||
|
||||
@@ -45,6 +45,8 @@ class MemberController {
|
||||
search,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortDir,
|
||||
} = req.query as Record<string, string | undefined>;
|
||||
|
||||
// Arrays can be sent as ?status[]=aktiv&status[]=jugend or CSV
|
||||
@@ -62,6 +64,8 @@ class MemberController {
|
||||
dienstgrad: normalizeArray(dienstgradParam) as any,
|
||||
page: page ? parseInt(page, 10) || 1 : 1,
|
||||
pageSize: pageSize ? (parseInt(pageSize, 10) === 0 ? 0 : Math.min(parseInt(pageSize, 10) || 25, 100)) : 25,
|
||||
sortBy,
|
||||
sortDir: sortDir === 'desc' ? 'desc' : sortDir === 'asc' ? 'asc' : undefined,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AtemschutzTraeger {
|
||||
id: string;
|
||||
user_id: string;
|
||||
atemschutz_lehrgang: boolean;
|
||||
lehrgang_theorie_only: boolean;
|
||||
lehrgang_datum: Date | null;
|
||||
untersuchung_datum: Date | null;
|
||||
untersuchung_gueltig_bis: Date | null;
|
||||
|
||||
@@ -166,6 +166,8 @@ export interface MemberFilters {
|
||||
dienstgrad?: DienstgradEnum[];
|
||||
page?: number; // 1-based
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user