This commit is contained in:
Matthias Hochmeister
2026-03-13 21:27:07 +01:00
parent 0d4e7b480d
commit b3266afbf8
4 changed files with 54 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
-- Migration 032: Add FDISK-scraped profile fields to mitglieder_profile
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS geburtsort VARCHAR(128);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS geschlecht VARCHAR(1);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS geschlecht VARCHAR(32);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS beruf VARCHAR(255);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS wohnort VARCHAR(128);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS plz VARCHAR(16);

View File

@@ -0,0 +1,2 @@
-- Migration 036: Widen geschlecht column (was VARCHAR(1), stores full text like 'männlich')
ALTER TABLE mitglieder_profile ALTER COLUMN geschlecht TYPE VARCHAR(32);

View File

@@ -87,7 +87,7 @@ export interface MitgliederProfile {
// FDISK-synced extended profile fields
geburtsort: string | null;
geschlecht: string | null;
geschlecht: string | null; // full text, e.g. 'männlich', 'weiblich'
beruf: string | null;
wohnort: string | null;
plz: string | null;

View File

@@ -92,20 +92,36 @@ export async function scrapeAll(username: string, password: string): Promise<{
member.wohnort = profileFields.wohnort;
member.plz = profileFields.plz;
// Extract mitgliedschaft params from the current URL for constructing sub-section URLs.
// PersonenForm.aspx is in the personen module; sub-sections are in mitgliedschaften module.
// The links to Beförderungen/Untersuchungen/Fahrgenehmigungen live in the navigation
// frame (not the content mainFrame), so we construct the URLs directly.
const currentUrl = mainFrame.url();
const urlObj = new URL(currentUrl);
const idMitgliedschaft = urlObj.searchParams.get('id_mitgliedschaften');
const idInstanzen = urlObj.searchParams.get('id_instanzen') ?? ID_INSTANZEN;
const idFeuerwehren = urlObj.searchParams.get('id_feuerwehren') ?? ID_FEUERWEHREN;
// Ausbildungen
const quals = await scrapeAusbildungenFromDetailPage(mainFrame, member);
ausbildungen.push(...quals);
// Beförderungen
const befos = await scrapeMemberBefoerderungen(mainFrame, member.standesbuchNr);
const befos = idMitgliedschaft
? await scrapeMemberBefoerderungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
: [];
befoerderungen.push(...befos);
// Untersuchungen
const unters = await scrapeMemberUntersuchungen(mainFrame, member.standesbuchNr);
const unters = idMitgliedschaft
? await scrapeMemberUntersuchungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
: [];
untersuchungen.push(...unters);
// Fahrgenehmigungen
const fahrg = await scrapeMemberFahrgenehmigungen(mainFrame, member.standesbuchNr);
const fahrg = idMitgliedschaft
? await scrapeMemberFahrgenehmigungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
: [];
fahrgenehmigungen.push(...fahrg);
log(` ${member.vorname} ${member.zuname}: ${quals.length} Ausbildungen, ${befos.length} Beförderungen, ${unters.length} Untersuchungen, ${fahrg.length} Fahrgenehmigungen`);
@@ -602,21 +618,17 @@ async function scrapeAusbildungenFromDetailPage(frame: Frame, member: FdiskMembe
/**
* Navigate to the Beförderungen sub-page and scrape all promotions.
* Navigates back to the member detail page afterwards.
* URL is constructed from the mitgliedschaft ID extracted from PersonenForm URL.
*/
async function scrapeMemberBefoerderungen(frame: Frame, standesbuchNr: string): Promise<FdiskBefoerderung[]> {
// Find sidebar link to Beförderungen
const link = frame.locator('a[href*="befoerderungenList.aspx"], a[href*="BefoerderungenList.aspx"]').first();
const hasLink = await link.isVisible().catch(() => false);
if (!hasLink) {
log(` No Beförderungen link for StNr ${standesbuchNr}`);
return [];
}
const href = await link.getAttribute('href');
if (!href) return [];
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
async function scrapeMemberBefoerderungen(
frame: Frame,
standesbuchNr: string,
idMitgliedschaft: string,
idInstanzen: string,
idFeuerwehren: string,
): Promise<FdiskBefoerderung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const results: FdiskBefoerderung[] = [];
@@ -643,7 +655,7 @@ async function scrapeMemberBefoerderungen(frame: Frame, standesbuchNr: string):
log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
}
} catch {
log(` WARN: could not parse Beförderungen table for StNr ${standesbuchNr}`);
log(` WARN: could not parse Beförderungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results;
@@ -653,18 +665,15 @@ async function scrapeMemberBefoerderungen(frame: Frame, standesbuchNr: string):
* Navigate to the Untersuchungen sub-page and scrape all medical exams.
* Keeps all rows (one per art+datum); DB stores all, queries filter latest per category.
*/
async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string): Promise<FdiskUntersuchung[]> {
const link = frame.locator('a[href*="UntersuchungenList.aspx"]').first();
const hasLink = await link.isVisible().catch(() => false);
if (!hasLink) {
log(` No Untersuchungen link for StNr ${standesbuchNr}`);
return [];
}
const href = await link.getAttribute('href');
if (!href) return [];
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
async function scrapeMemberUntersuchungen(
frame: Frame,
standesbuchNr: string,
idMitgliedschaft: string,
idInstanzen: string,
idFeuerwehren: string,
): Promise<FdiskUntersuchung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const results: FdiskUntersuchung[] = [];
@@ -704,7 +713,7 @@ async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string):
log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
}
} catch {
log(` WARN: could not parse Untersuchungen table for StNr ${standesbuchNr}`);
log(` WARN: could not parse Untersuchungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results;
@@ -714,18 +723,15 @@ async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string):
* Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries.
* This is an inline-edit (ListEdit) page — values are in <input> fields.
*/
async function scrapeMemberFahrgenehmigungen(frame: Frame, standesbuchNr: string): Promise<FdiskFahrgenehmigung[]> {
const link = frame.locator('a[href*="Ges_fahrgenehmigungenListEdit.aspx"], a[href*="ges_fahrgenehmigungenListEdit.aspx"]').first();
const hasLink = await link.isVisible().catch(() => false);
if (!hasLink) {
log(` No Fahrgenehmigungen link for StNr ${standesbuchNr}`);
return [];
}
const href = await link.getAttribute('href');
if (!href) return [];
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
async function scrapeMemberFahrgenehmigungen(
frame: Frame,
standesbuchNr: string,
idMitgliedschaft: string,
idInstanzen: string,
idFeuerwehren: string,
): Promise<FdiskFahrgenehmigung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const results: FdiskFahrgenehmigung[] = [];
@@ -741,7 +747,6 @@ async function scrapeMemberFahrgenehmigungen(frame: Frame, standesbuchNr: string
const cellVal = (i: number): string => {
const cell = cells[i];
if (!cell) return '';
// Prefer input value, then select text, then textContent
const input = cell.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
if (input) return input.value?.trim() ?? '';
const select = cell.querySelector('select') as HTMLSelectElement | null;
@@ -781,7 +786,7 @@ async function scrapeMemberFahrgenehmigungen(frame: Frame, standesbuchNr: string
log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
}
} catch {
log(` WARN: could not parse Fahrgenehmigungen table for StNr ${standesbuchNr}`);
log(` WARN: could not parse Fahrgenehmigungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results;