update
This commit is contained in:
@@ -141,6 +141,7 @@ interface NextcloudChatMessage {
|
||||
messageParameters: Record<string, any>;
|
||||
reactions: Record<string, any>;
|
||||
reactionsSelf: string[];
|
||||
parent?: NextcloudChatMessage;
|
||||
}
|
||||
|
||||
async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> {
|
||||
@@ -259,6 +260,20 @@ async function getMessages(token: string, loginName: string, appPassword: string
|
||||
messageParameters: m.messageParameters ?? {},
|
||||
reactions: m.reactions ?? {},
|
||||
reactionsSelf: m.reactionsSelf ?? [],
|
||||
...(m.parent ? { parent: {
|
||||
id: m.parent.id,
|
||||
token: m.parent.token,
|
||||
actorType: m.parent.actorType,
|
||||
actorId: m.parent.actorId,
|
||||
actorDisplayName: m.parent.actorDisplayName,
|
||||
message: m.parent.message,
|
||||
timestamp: m.parent.timestamp,
|
||||
messageType: m.parent.messageType ?? '',
|
||||
systemMessage: m.parent.systemMessage ?? '',
|
||||
messageParameters: m.parent.messageParameters ?? {},
|
||||
reactions: m.parent.reactions ?? {},
|
||||
reactionsSelf: m.parent.reactionsSelf ?? [],
|
||||
}} : {}),
|
||||
}));
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
|
||||
@@ -616,9 +616,62 @@ async function scrapeAusbildungenFromDetailPage(frame: Frame, member: FdiskMembe
|
||||
return ausbildungen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a sub-section URL and wait for any data table.
|
||||
* Logs the actual URL and title so wrong-page issues are visible.
|
||||
* Returns all <tr> rows from the first table found, or null if none.
|
||||
*/
|
||||
async function navigateAndGetTableRows(
|
||||
frame: Frame,
|
||||
url: string,
|
||||
): Promise<Array<{ cells: string[] }> | null> {
|
||||
await frame_goto(frame, url);
|
||||
|
||||
const landed = frame.url();
|
||||
const title = await frame.title().catch(() => '');
|
||||
log(` → landed: ${landed} | title: "${title}"`);
|
||||
|
||||
// Check for FDISK error pages
|
||||
if (landed.includes('BLError') || landed.includes('support.aspx') || title.toLowerCase().includes('fehler')) {
|
||||
log(` → ERROR page, skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try table.FdcLayList first, then any table with tbody rows
|
||||
const selectors = ['table.FdcLayList', 'table'];
|
||||
for (const sel of selectors) {
|
||||
const exists = await frame.$(sel).then(el => !!el).catch(() => false);
|
||||
if (!exists) continue;
|
||||
|
||||
const rows = await frame.$$eval(`${sel} tbody tr`, (trs) =>
|
||||
trs.map((tr) => ({
|
||||
cells: Array.from(tr.querySelectorAll('td')).map(td => {
|
||||
const input = td.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
|
||||
if (input) return input.value?.trim() ?? '';
|
||||
const select = td.querySelector('select') as HTMLSelectElement | null;
|
||||
if (select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
return (opt?.text || opt?.value || '').trim();
|
||||
}
|
||||
return td.textContent?.trim() ?? '';
|
||||
}),
|
||||
}))
|
||||
).catch(() => [] as Array<{ cells: string[] }>);
|
||||
|
||||
if (rows.length > 0) {
|
||||
log(` → found ${rows.length} rows via "${sel}"`);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
// No table rows found — page might be empty or structured differently
|
||||
const bodyText = await frame.evaluate(() => document.body?.textContent?.slice(0, 300) ?? '').catch(() => '');
|
||||
log(` → no table rows found. Body preview: ${bodyText.replace(/\s+/g, ' ')}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Beförderungen sub-page and scrape all promotions.
|
||||
* URL is constructed from the mitgliedschaft ID extracted from PersonenForm URL.
|
||||
*/
|
||||
async function scrapeMemberBefoerderungen(
|
||||
frame: Frame,
|
||||
@@ -629,41 +682,25 @@ async function scrapeMemberBefoerderungen(
|
||||
): 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 rows = await navigateAndGetTableRows(frame, url);
|
||||
if (!rows) return [];
|
||||
|
||||
const results: FdiskBefoerderung[] = [];
|
||||
|
||||
try {
|
||||
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
|
||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
||||
trs.map((tr) => {
|
||||
const cells = Array.from(tr.querySelectorAll('td'));
|
||||
const cell = (i: number) => (cells[i]?.textContent ?? '').trim();
|
||||
return { datum: cell(0), dienstgrad: cell(1) };
|
||||
})
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const dienstgrad = cellText(row.dienstgrad);
|
||||
const dienstgrad = cellText(row.cells[1]);
|
||||
if (!dienstgrad) continue;
|
||||
const datum = parseDate(row.datum);
|
||||
const datum = parseDate(row.cells[0]);
|
||||
const syncKey = `${standesbuchNr}::${dienstgrad}::${datum ?? ''}`;
|
||||
results.push({ standesbuchNr, datum, dienstgrad, syncKey });
|
||||
}
|
||||
log(` Beförderungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||
for (const b of results) {
|
||||
log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
|
||||
}
|
||||
} catch {
|
||||
log(` WARN: could not parse Beförderungen table for StNr ${standesbuchNr} (url: ${url})`);
|
||||
}
|
||||
|
||||
for (const b of results) log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -674,54 +711,33 @@ async function scrapeMemberUntersuchungen(
|
||||
): 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 rows = await navigateAndGetTableRows(frame, url);
|
||||
if (!rows) return [];
|
||||
|
||||
const results: FdiskUntersuchung[] = [];
|
||||
|
||||
try {
|
||||
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
|
||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
||||
trs.map((tr) => {
|
||||
const cells = Array.from(tr.querySelectorAll('td'));
|
||||
const cell = (i: number) => (cells[i]?.textContent ?? '').trim();
|
||||
// Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
|
||||
return {
|
||||
datum: cell(0),
|
||||
anmerkungen: cell(1),
|
||||
art: cell(2),
|
||||
ergebnis: cell(3),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const art = cellText(row.art);
|
||||
// Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
|
||||
const art = cellText(row.cells[2]);
|
||||
if (!art) continue;
|
||||
const datum = parseDate(row.datum);
|
||||
const datum = parseDate(row.cells[0]);
|
||||
const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;
|
||||
results.push({
|
||||
standesbuchNr,
|
||||
datum,
|
||||
anmerkungen: cellText(row.anmerkungen),
|
||||
anmerkungen: cellText(row.cells[1]),
|
||||
art,
|
||||
ergebnis: cellText(row.ergebnis),
|
||||
ergebnis: cellText(row.cells[3]),
|
||||
syncKey,
|
||||
});
|
||||
}
|
||||
log(` Untersuchungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||
for (const u of results) {
|
||||
log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
|
||||
}
|
||||
} catch {
|
||||
log(` WARN: could not parse Untersuchungen table for StNr ${standesbuchNr} (url: ${url})`);
|
||||
}
|
||||
|
||||
for (const u of results) log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -732,63 +748,29 @@ async function scrapeMemberFahrgenehmigungen(
|
||||
): 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 rows = await navigateAndGetTableRows(frame, url);
|
||||
if (!rows) return [];
|
||||
|
||||
const results: FdiskFahrgenehmigung[] = [];
|
||||
|
||||
try {
|
||||
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
|
||||
|
||||
// ListEdit pages: each data row has inline <input> fields instead of plain text.
|
||||
// Columns: 0=Ausstellungsdatum, 1=Gültig bis, 2=Behörde, 3=Nummer, 4=Fahrgenehmigungsklasse
|
||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
||||
trs.map((tr) => {
|
||||
const cells = Array.from(tr.querySelectorAll('td'));
|
||||
const cellVal = (i: number): string => {
|
||||
const cell = cells[i];
|
||||
if (!cell) return '';
|
||||
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;
|
||||
if (select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
return (opt?.text || opt?.value || '').trim();
|
||||
}
|
||||
return cell.textContent?.trim() ?? '';
|
||||
};
|
||||
return {
|
||||
ausstellungsdatum: cellVal(0),
|
||||
gueltigBis: cellVal(1),
|
||||
behoerde: cellVal(2),
|
||||
nummer: cellVal(3),
|
||||
klasse: cellVal(4),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const klasse = cellText(row.klasse);
|
||||
// Columns: 0=Ausstellungsdatum, 1=Gültig bis, 2=Behörde, 3=Nummer, 4=Fahrgenehmigungsklasse
|
||||
const klasse = cellText(row.cells[4]);
|
||||
if (!klasse) continue;
|
||||
const ausstellungsdatum = parseDate(row.ausstellungsdatum);
|
||||
const ausstellungsdatum = parseDate(row.cells[0]);
|
||||
const syncKey = `${standesbuchNr}::${klasse}::${ausstellungsdatum ?? ''}`;
|
||||
results.push({
|
||||
standesbuchNr,
|
||||
ausstellungsdatum,
|
||||
gueltigBis: parseDate(row.gueltigBis),
|
||||
behoerde: cellText(row.behoerde),
|
||||
nummer: cellText(row.nummer),
|
||||
gueltigBis: parseDate(row.cells[1]),
|
||||
behoerde: cellText(row.cells[2]),
|
||||
nummer: cellText(row.cells[3]),
|
||||
klasse,
|
||||
syncKey,
|
||||
});
|
||||
}
|
||||
log(` Fahrgenehmigungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||
for (const f of results) {
|
||||
log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
|
||||
}
|
||||
} catch {
|
||||
log(` WARN: could not parse Fahrgenehmigungen table for StNr ${standesbuchNr} (url: ${url})`);
|
||||
}
|
||||
|
||||
for (const f of results) log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user