178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
import { Pool } from 'pg';
|
|
import { FdiskMember, FdiskAusbildung } from './types';
|
|
|
|
function log(msg: string) {
|
|
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: string): string | null {
|
|
const map: Record<string, string> = {
|
|
// 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;
|
|
}
|
|
|
|
export async function syncToDatabase(
|
|
pool: Pool,
|
|
members: FdiskMember[],
|
|
ausbildungen: FdiskAusbildung[]
|
|
): Promise<void> {
|
|
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<{ user_id: string }>(
|
|
`SELECT mp.user_id
|
|
FROM mitglieder_profile mp
|
|
JOIN users u ON u.id = mp.user_id
|
|
WHERE mp.fdisk_standesbuch_nr = $1
|
|
AND u.last_login_at IS NOT NULL`,
|
|
[member.standesbuchNr]
|
|
);
|
|
|
|
let userId: string | null = null;
|
|
|
|
if (profileResult.rows.length > 0) {
|
|
userId = profileResult.rows[0].user_id;
|
|
} else {
|
|
// Fallback: match by name (case-insensitive), only logged-in users
|
|
const nameResult = await client.query<{ id: string }>(
|
|
`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)
|
|
AND u.last_login_at IS NOT NULL`,
|
|
[member.vorname, member.zuname]
|
|
);
|
|
|
|
if (nameResult.rows.length > 1) {
|
|
log(`WARN: skipping ${member.vorname} ${member.zuname} (Standesbuch-Nr ${member.standesbuchNr}) — duplicate name match (${nameResult.rows.length} users)`);
|
|
} else if (nameResult.rows.length === 1) {
|
|
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<{ user_id: string }>(
|
|
`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();
|
|
}
|
|
}
|