diff --git a/sync/src/db.ts b/sync/src/db.ts index 0f6f62a..94a18b6 100644 --- a/sync/src/db.ts +++ b/sync/src/db.ts @@ -324,6 +324,58 @@ export async function syncAT20ToAtemschutz(pool: Pool): Promise { log(`AT20-Sync: ${result.rowCount ?? 0} atemschutz_traeger rows upserted`); } +/** + * Populates atemschutz_traeger from the untersuchungen table: + * 1) Latest "Atemschutz Leistungstest" with bestanden → leistungstest_datum / leistungstest_bestanden + * 2) Latest "Atemschutztauglichkeit" → untersuchung_datum / untersuchung_ergebnis + * Must run AFTER syncToDatabase() has committed. + */ +export async function syncUntersuchungenToAtemschutz(pool: Pool): Promise { + // --- Leistungstest (Finnentest) --- + const ltResult = await pool.query( + `INSERT INTO atemschutz_traeger (id, user_id, leistungstest_datum, leistungstest_gueltig_bis, leistungstest_bestanden) + SELECT uuid_generate_v4(), u.user_id, u.max_datum, u.max_datum + INTERVAL '1 year', true + FROM ( + SELECT user_id, MAX(datum) AS max_datum + FROM untersuchungen + WHERE TRIM(art) = 'Atemschutz Leistungstest' + AND TRIM(ergebnis) ILIKE '%bestanden%' + GROUP BY user_id + ) u + ON CONFLICT (user_id) DO UPDATE + SET leistungstest_datum = EXCLUDED.leistungstest_datum, + leistungstest_gueltig_bis = EXCLUDED.leistungstest_gueltig_bis, + leistungstest_bestanden = true, + updated_at = NOW()` + ); + log(`Untersuchungen→Atemschutz: ${ltResult.rowCount ?? 0} Leistungstest rows synced`); + + // --- Atemschutztauglichkeit (G26 medical exam) --- + // Map FDISK ergebnis text to the DB enum (tauglich / bedingt_tauglich / nicht_tauglich) + const atResult = await pool.query( + `INSERT INTO atemschutz_traeger (id, user_id, untersuchung_datum, untersuchung_ergebnis) + SELECT uuid_generate_v4(), sub.user_id, sub.datum, + CASE + WHEN sub.ergebnis ILIKE '%nicht%tauglich%' THEN 'nicht_tauglich' + WHEN sub.ergebnis ILIKE '%bedingt%tauglich%' THEN 'bedingt_tauglich' + WHEN sub.ergebnis ILIKE '%tauglich%' THEN 'tauglich' + ELSE NULL + END + FROM ( + SELECT DISTINCT ON (user_id) user_id, datum, ergebnis + FROM untersuchungen + WHERE TRIM(art) = 'Atemschutztauglichkeit' + AND datum IS NOT NULL + ORDER BY user_id, datum DESC + ) sub + ON CONFLICT (user_id) DO UPDATE + SET untersuchung_datum = EXCLUDED.untersuchung_datum, + untersuchung_ergebnis = COALESCE(EXCLUDED.untersuchung_ergebnis, atemschutz_traeger.untersuchung_ergebnis), + updated_at = NOW()` + ); + log(`Untersuchungen→Atemschutz: ${atResult.rowCount ?? 0} Atemschutztauglichkeit rows synced`); +} + async function syncAusbildungen( client: PoolClient, ausbildungen: FdiskAusbildung[], diff --git a/sync/src/index.ts b/sync/src/index.ts index 74e0a61..d92bd84 100644 --- a/sync/src/index.ts +++ b/sync/src/index.ts @@ -2,7 +2,7 @@ import 'dotenv/config'; import * as http from 'http'; import { Pool } from 'pg'; import { scrapeAll } from './scraper'; -import { syncToDatabase, syncAT20ToAtemschutz } from './db'; +import { syncToDatabase, syncAT20ToAtemschutz, syncUntersuchungenToAtemschutz } from './db'; // In-memory log ring buffer — last 500 lines captured from all modules const LOG_BUFFER_MAX = 500; @@ -71,6 +71,7 @@ async function runSync(force = false): Promise { await syncToDatabase(pool, members, ausbildungen, befoerderungen, untersuchungen, fahrgenehmigungen, force); log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen, ${befoerderungen.length} Beförderungen, ${untersuchungen.length} Untersuchungen, ${fahrgenehmigungen.length} Fahrgenehmigungen`); await syncAT20ToAtemschutz(pool); + await syncUntersuchungenToAtemschutz(pool); } finally { syncRunning = false; await pool.end(); diff --git a/sync/src/scraper.ts b/sync/src/scraper.ts index 914f02a..10a8cd7 100644 --- a/sync/src/scraper.ts +++ b/sync/src/scraper.ts @@ -1087,14 +1087,12 @@ async function scrapeMemberUntersuchungen( const results: FdiskUntersuchung[] = []; for (const row of dataRows) { - const valueCols: string[] = []; - for (let ci = dateColIdx + 1; ci < row.cells.length; ci++) { - const v = cellText(row.cells[ci]); - if (v !== null) valueCols.push(v); - } - const anmerkungen = valueCols[0] ?? null; - const art = valueCols[1] ?? null; - const ergebnis = valueCols[2] ?? null; + // Fixed column offsets from the date column. + // FDISK table layout: [date] sep [anmerkungen] sep [art] sep [ergebnis] sep [buttons] + // Data columns are at +2, +4, +6 from dateColIdx (separator cols in between). + const anmerkungen = cellText(row.cells[dateColIdx + 2]) ?? null; + const art = cellText(row.cells[dateColIdx + 4]) ?? null; + const ergebnis = cellText(row.cells[dateColIdx + 6]) ?? null; if (!art) continue; const datum = parseDate(row.cells[dateColIdx]); const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;