update
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
-- Migration 032: Add FDISK-scraped profile fields to mitglieder_profile
|
-- 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 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 beruf VARCHAR(255);
|
||||||
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS wohnort VARCHAR(128);
|
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS wohnort VARCHAR(128);
|
||||||
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS plz VARCHAR(16);
|
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS plz VARCHAR(16);
|
||||||
|
|||||||
2
backend/src/database/migrations/036_widen_geschlecht.sql
Normal file
2
backend/src/database/migrations/036_widen_geschlecht.sql
Normal 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);
|
||||||
@@ -87,7 +87,7 @@ export interface MitgliederProfile {
|
|||||||
|
|
||||||
// FDISK-synced extended profile fields
|
// FDISK-synced extended profile fields
|
||||||
geburtsort: string | null;
|
geburtsort: string | null;
|
||||||
geschlecht: string | null;
|
geschlecht: string | null; // full text, e.g. 'männlich', 'weiblich'
|
||||||
beruf: string | null;
|
beruf: string | null;
|
||||||
wohnort: string | null;
|
wohnort: string | null;
|
||||||
plz: string | null;
|
plz: string | null;
|
||||||
|
|||||||
@@ -92,20 +92,36 @@ export async function scrapeAll(username: string, password: string): Promise<{
|
|||||||
member.wohnort = profileFields.wohnort;
|
member.wohnort = profileFields.wohnort;
|
||||||
member.plz = profileFields.plz;
|
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
|
// Ausbildungen
|
||||||
const quals = await scrapeAusbildungenFromDetailPage(mainFrame, member);
|
const quals = await scrapeAusbildungenFromDetailPage(mainFrame, member);
|
||||||
ausbildungen.push(...quals);
|
ausbildungen.push(...quals);
|
||||||
|
|
||||||
// Beförderungen
|
// Beförderungen
|
||||||
const befos = await scrapeMemberBefoerderungen(mainFrame, member.standesbuchNr);
|
const befos = idMitgliedschaft
|
||||||
|
? await scrapeMemberBefoerderungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
|
||||||
|
: [];
|
||||||
befoerderungen.push(...befos);
|
befoerderungen.push(...befos);
|
||||||
|
|
||||||
// Untersuchungen
|
// Untersuchungen
|
||||||
const unters = await scrapeMemberUntersuchungen(mainFrame, member.standesbuchNr);
|
const unters = idMitgliedschaft
|
||||||
|
? await scrapeMemberUntersuchungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
|
||||||
|
: [];
|
||||||
untersuchungen.push(...unters);
|
untersuchungen.push(...unters);
|
||||||
|
|
||||||
// Fahrgenehmigungen
|
// Fahrgenehmigungen
|
||||||
const fahrg = await scrapeMemberFahrgenehmigungen(mainFrame, member.standesbuchNr);
|
const fahrg = idMitgliedschaft
|
||||||
|
? await scrapeMemberFahrgenehmigungen(mainFrame, member.standesbuchNr, idMitgliedschaft, idInstanzen, idFeuerwehren)
|
||||||
|
: [];
|
||||||
fahrgenehmigungen.push(...fahrg);
|
fahrgenehmigungen.push(...fahrg);
|
||||||
|
|
||||||
log(` ${member.vorname} ${member.zuname}: ${quals.length} Ausbildungen, ${befos.length} Beförderungen, ${unters.length} Untersuchungen, ${fahrg.length} Fahrgenehmigungen`);
|
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.
|
* 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[]> {
|
async function scrapeMemberBefoerderungen(
|
||||||
// Find sidebar link to Beförderungen
|
frame: Frame,
|
||||||
const link = frame.locator('a[href*="befoerderungenList.aspx"], a[href*="BefoerderungenList.aspx"]').first();
|
standesbuchNr: string,
|
||||||
const hasLink = await link.isVisible().catch(() => false);
|
idMitgliedschaft: string,
|
||||||
if (!hasLink) {
|
idInstanzen: string,
|
||||||
log(` No Beförderungen link for StNr ${standesbuchNr}`);
|
idFeuerwehren: string,
|
||||||
return [];
|
): Promise<FdiskBefoerderung[]> {
|
||||||
}
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx`
|
||||||
|
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
||||||
const href = await link.getAttribute('href');
|
|
||||||
if (!href) return [];
|
|
||||||
|
|
||||||
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
|
|
||||||
await frame_goto(frame, url);
|
await frame_goto(frame, url);
|
||||||
|
|
||||||
const results: FdiskBefoerderung[] = [];
|
const results: FdiskBefoerderung[] = [];
|
||||||
@@ -643,7 +655,7 @@ async function scrapeMemberBefoerderungen(frame: Frame, standesbuchNr: string):
|
|||||||
log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
|
log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
|
||||||
}
|
}
|
||||||
} catch {
|
} 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;
|
return results;
|
||||||
@@ -653,18 +665,15 @@ async function scrapeMemberBefoerderungen(frame: Frame, standesbuchNr: string):
|
|||||||
* Navigate to the Untersuchungen sub-page and scrape all medical exams.
|
* 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.
|
* Keeps all rows (one per art+datum); DB stores all, queries filter latest per category.
|
||||||
*/
|
*/
|
||||||
async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string): Promise<FdiskUntersuchung[]> {
|
async function scrapeMemberUntersuchungen(
|
||||||
const link = frame.locator('a[href*="UntersuchungenList.aspx"]').first();
|
frame: Frame,
|
||||||
const hasLink = await link.isVisible().catch(() => false);
|
standesbuchNr: string,
|
||||||
if (!hasLink) {
|
idMitgliedschaft: string,
|
||||||
log(` No Untersuchungen link for StNr ${standesbuchNr}`);
|
idInstanzen: string,
|
||||||
return [];
|
idFeuerwehren: string,
|
||||||
}
|
): Promise<FdiskUntersuchung[]> {
|
||||||
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx`
|
||||||
const href = await link.getAttribute('href');
|
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
||||||
if (!href) return [];
|
|
||||||
|
|
||||||
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
|
|
||||||
await frame_goto(frame, url);
|
await frame_goto(frame, url);
|
||||||
|
|
||||||
const results: FdiskUntersuchung[] = [];
|
const results: FdiskUntersuchung[] = [];
|
||||||
@@ -704,7 +713,7 @@ async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string):
|
|||||||
log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
|
log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
|
||||||
}
|
}
|
||||||
} catch {
|
} 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;
|
return results;
|
||||||
@@ -714,18 +723,15 @@ async function scrapeMemberUntersuchungen(frame: Frame, standesbuchNr: string):
|
|||||||
* Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries.
|
* Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries.
|
||||||
* This is an inline-edit (ListEdit) page — values are in <input> fields.
|
* This is an inline-edit (ListEdit) page — values are in <input> fields.
|
||||||
*/
|
*/
|
||||||
async function scrapeMemberFahrgenehmigungen(frame: Frame, standesbuchNr: string): Promise<FdiskFahrgenehmigung[]> {
|
async function scrapeMemberFahrgenehmigungen(
|
||||||
const link = frame.locator('a[href*="Ges_fahrgenehmigungenListEdit.aspx"], a[href*="ges_fahrgenehmigungenListEdit.aspx"]').first();
|
frame: Frame,
|
||||||
const hasLink = await link.isVisible().catch(() => false);
|
standesbuchNr: string,
|
||||||
if (!hasLink) {
|
idMitgliedschaft: string,
|
||||||
log(` No Fahrgenehmigungen link for StNr ${standesbuchNr}`);
|
idInstanzen: string,
|
||||||
return [];
|
idFeuerwehren: string,
|
||||||
}
|
): Promise<FdiskFahrgenehmigung[]> {
|
||||||
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx`
|
||||||
const href = await link.getAttribute('href');
|
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
||||||
if (!href) return [];
|
|
||||||
|
|
||||||
const url = href.startsWith('http') ? href : new URL(href, frame.url()).toString();
|
|
||||||
await frame_goto(frame, url);
|
await frame_goto(frame, url);
|
||||||
|
|
||||||
const results: FdiskFahrgenehmigung[] = [];
|
const results: FdiskFahrgenehmigung[] = [];
|
||||||
@@ -741,7 +747,6 @@ async function scrapeMemberFahrgenehmigungen(frame: Frame, standesbuchNr: string
|
|||||||
const cellVal = (i: number): string => {
|
const cellVal = (i: number): string => {
|
||||||
const cell = cells[i];
|
const cell = cells[i];
|
||||||
if (!cell) return '';
|
if (!cell) return '';
|
||||||
// Prefer input value, then select text, then textContent
|
|
||||||
const input = cell.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
|
const input = cell.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
|
||||||
if (input) return input.value?.trim() ?? '';
|
if (input) return input.value?.trim() ?? '';
|
||||||
const select = cell.querySelector('select') as HTMLSelectElement | null;
|
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 ?? ''}`);
|
log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
|
||||||
}
|
}
|
||||||
} catch {
|
} 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;
|
return results;
|
||||||
|
|||||||
Reference in New Issue
Block a user