Files
dashboard/sync/dist/db.js
Matthias Hochmeister 02797554aa fix: commit pre-compiled dist and simplify sync Dockerfile
Server npm proxy silently drops devDependencies, making TypeScript
unavailable in Docker. Solution: compile locally and commit dist/.
Dockerfile now only needs prod deps + Playwright, both of which
install cleanly via the public registry.

Also fix TS2688/TS2304 errors: add DOM to tsconfig lib and cast
querySelectorAll results to Element inside $$eval callbacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 13:51:48 +01:00

136 lines
5.9 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.syncToDatabase = syncToDatabase;
function log(msg) {
console.log(`[db] ${new Date().toISOString()} ${msg}`);
}
/**
* Map FDISK Dienstgrad (abbreviation or full name) to the DB enum value.
* Returns null if no match found — the field will be left unchanged.
*/
function mapDienstgrad(raw) {
const map = {
// Abbreviations
'fa': 'Feuerwehranwärter',
'fm': 'Feuerwehrmann',
'ff': 'Feuerwehrfrau',
'ofm': 'Oberfeuerwehrmann',
'off': 'Oberfeuerwehrfrau',
'hfm': 'Hauptfeuerwehrmann',
'hff': 'Hauptfeuerwehrfrau',
'lm': 'Löschmeister',
'olm': 'Oberlöschmeister',
'hlm': 'Hauptlöschmeister',
'bm': 'Brandmeister',
'obm': 'Oberbrandmeister',
'hbm': 'Hauptbrandmeister',
'bi': 'Brandinspektor',
'obi': 'Oberbrandinspektor',
'boi': 'Brandoberinspektor',
'bam': 'Brandamtmann',
// Full names (pass-through if already matching)
'feuerwehranwärter': 'Feuerwehranwärter',
'feuerwehrmann': 'Feuerwehrmann',
'feuerwehrfrau': 'Feuerwehrfrau',
'oberfeuerwehrmann': 'Oberfeuerwehrmann',
'oberfeuerwehrfrau': 'Oberfeuerwehrfrau',
'hauptfeuerwehrmann': 'Hauptfeuerwehrmann',
'hauptfeuerwehrfrau': 'Hauptfeuerwehrfrau',
'löschmeister': 'Löschmeister',
'oberlöschmeister': 'Oberlöschmeister',
'hauptlöschmeister': 'Hauptlöschmeister',
'brandmeister': 'Brandmeister',
'oberbrandmeister': 'Oberbrandmeister',
'hauptbrandmeister': 'Hauptbrandmeister',
'brandinspektor': 'Brandinspektor',
'oberbrandinspektor': 'Oberbrandinspektor',
'brandoberinspektor': 'Brandoberinspektor',
'brandamtmann': 'Brandamtmann',
};
return map[raw.trim().toLowerCase()] ?? null;
}
async function syncToDatabase(pool, members, ausbildungen) {
const client = await pool.connect();
try {
await client.query('BEGIN');
let updated = 0;
let skipped = 0;
for (const member of members) {
// Find the matching mitglieder_profile by fdisk_standesbuch_nr first,
// then fall back to matching by name (given_name + family_name)
const profileResult = await client.query(`SELECT mp.user_id
FROM mitglieder_profile mp
WHERE mp.fdisk_standesbuch_nr = $1`, [member.standesbuchNr]);
let userId = null;
if (profileResult.rows.length > 0) {
userId = profileResult.rows[0].user_id;
}
else {
// Fallback: match by name (case-insensitive)
const nameResult = await client.query(`SELECT u.id
FROM users u
JOIN mitglieder_profile mp ON mp.user_id = u.id
WHERE LOWER(u.given_name) = LOWER($1)
AND LOWER(u.family_name) = LOWER($2)
LIMIT 1`, [member.vorname, member.zuname]);
if (nameResult.rows.length > 0) {
userId = nameResult.rows[0].id;
// Store the Standesbuch-Nr now that we found a match
await client.query(`UPDATE mitglieder_profile SET fdisk_standesbuch_nr = $1 WHERE user_id = $2`, [member.standesbuchNr, userId]);
log(`Linked ${member.vorname} ${member.zuname} → Standesbuch-Nr ${member.standesbuchNr}`);
}
}
if (!userId) {
skipped++;
continue;
}
// Update mitglieder_profile with FDISK data
const dienstgrad = mapDienstgrad(member.dienstgrad);
await client.query(`UPDATE mitglieder_profile SET
fdisk_standesbuch_nr = $1,
status = $2,
eintrittsdatum = COALESCE($3::date, eintrittsdatum),
austrittsdatum = $4::date,
geburtsdatum = COALESCE($5::date, geburtsdatum),
${dienstgrad ? 'dienstgrad = $6,' : ''}
updated_at = NOW()
WHERE user_id = ${dienstgrad ? '$7' : '$6'}`, dienstgrad
? [member.standesbuchNr, member.status, member.eintrittsdatum, member.abmeldedatum, member.geburtsdatum, dienstgrad, userId]
: [member.standesbuchNr, member.status, member.eintrittsdatum, member.abmeldedatum, member.geburtsdatum, userId]);
updated++;
}
log(`Members: ${updated} updated, ${skipped} skipped (no dashboard account)`);
// Upsert Ausbildungen
let ausbildungUpserted = 0;
let ausbildungSkipped = 0;
for (const ausb of ausbildungen) {
// Find user_id by standesbuch_nr
const result = await client.query(`SELECT user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr = $1`, [ausb.standesbuchNr]);
if (result.rows.length === 0) {
ausbildungSkipped++;
continue;
}
const userId = result.rows[0].user_id;
await client.query(`INSERT INTO ausbildung (user_id, kursname, kurs_datum, ablaufdatum, ort, bemerkung, fdisk_sync_key)
VALUES ($1, $2, $3::date, $4::date, $5, $6, $7)
ON CONFLICT (user_id, fdisk_sync_key) DO UPDATE SET
kursname = EXCLUDED.kursname,
kurs_datum = EXCLUDED.kurs_datum,
ablaufdatum = EXCLUDED.ablaufdatum,
ort = EXCLUDED.ort,
bemerkung = EXCLUDED.bemerkung,
updated_at = NOW()`, [userId, ausb.kursname, ausb.kursDatum, ausb.ablaufdatum, ausb.ort, ausb.bemerkung, ausb.syncKey]);
ausbildungUpserted++;
}
await client.query('COMMIT');
log(`Ausbildungen: ${ausbildungUpserted} upserted, ${ausbildungSkipped} skipped`);
}
catch (err) {
await client.query('ROLLBACK');
throw err;
}
finally {
client.release();
}
}