fix(sync): fix Untersuchungen column parsing and sync Leistungstest/Atemschutztauglichkeit dates to atemschutz profile

This commit is contained in:
Matthias Hochmeister
2026-04-19 17:08:29 +02:00
parent 0a5402a9e5
commit 54a110d17b
3 changed files with 60 additions and 9 deletions

View File

@@ -324,6 +324,58 @@ export async function syncAT20ToAtemschutz(pool: Pool): Promise<void> {
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<void> {
// --- 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[],

View File

@@ -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<void> {
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();

View File

@@ -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 ?? ''}`;