update
This commit is contained in:
@@ -141,6 +141,7 @@ interface NextcloudChatMessage {
|
|||||||
messageParameters: Record<string, any>;
|
messageParameters: Record<string, any>;
|
||||||
reactions: Record<string, any>;
|
reactions: Record<string, any>;
|
||||||
reactionsSelf: string[];
|
reactionsSelf: string[];
|
||||||
|
parent?: NextcloudChatMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> {
|
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 ?? {},
|
messageParameters: m.messageParameters ?? {},
|
||||||
reactions: m.reactions ?? {},
|
reactions: m.reactions ?? {},
|
||||||
reactionsSelf: m.reactionsSelf ?? [],
|
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) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||||
|
|||||||
@@ -616,9 +616,62 @@ async function scrapeAusbildungenFromDetailPage(frame: Frame, member: FdiskMembe
|
|||||||
return ausbildungen;
|
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.
|
* 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(
|
async function scrapeMemberBefoerderungen(
|
||||||
frame: Frame,
|
frame: Frame,
|
||||||
@@ -629,41 +682,25 @@ async function scrapeMemberBefoerderungen(
|
|||||||
): Promise<FdiskBefoerderung[]> {
|
): Promise<FdiskBefoerderung[]> {
|
||||||
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx`
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx`
|
||||||
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
+ `?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[] = [];
|
const results: FdiskBefoerderung[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
try {
|
const dienstgrad = cellText(row.cells[1]);
|
||||||
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
|
if (!dienstgrad) continue;
|
||||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
const datum = parseDate(row.cells[0]);
|
||||||
trs.map((tr) => {
|
const syncKey = `${standesbuchNr}::${dienstgrad}::${datum ?? ''}`;
|
||||||
const cells = Array.from(tr.querySelectorAll('td'));
|
results.push({ standesbuchNr, datum, dienstgrad, syncKey });
|
||||||
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);
|
|
||||||
if (!dienstgrad) continue;
|
|
||||||
const datum = parseDate(row.datum);
|
|
||||||
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})`);
|
|
||||||
}
|
}
|
||||||
|
log(` Beförderungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||||
|
for (const b of results) log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
async function scrapeMemberUntersuchungen(
|
async function scrapeMemberUntersuchungen(
|
||||||
frame: Frame,
|
frame: Frame,
|
||||||
@@ -674,54 +711,33 @@ async function scrapeMemberUntersuchungen(
|
|||||||
): Promise<FdiskUntersuchung[]> {
|
): Promise<FdiskUntersuchung[]> {
|
||||||
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx`
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx`
|
||||||
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
+ `?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[] = [];
|
const results: FdiskUntersuchung[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
try {
|
// Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
|
||||||
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
|
const art = cellText(row.cells[2]);
|
||||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
if (!art) continue;
|
||||||
trs.map((tr) => {
|
const datum = parseDate(row.cells[0]);
|
||||||
const cells = Array.from(tr.querySelectorAll('td'));
|
const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;
|
||||||
const cell = (i: number) => (cells[i]?.textContent ?? '').trim();
|
results.push({
|
||||||
// Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
|
standesbuchNr,
|
||||||
return {
|
datum,
|
||||||
datum: cell(0),
|
anmerkungen: cellText(row.cells[1]),
|
||||||
anmerkungen: cell(1),
|
art,
|
||||||
art: cell(2),
|
ergebnis: cellText(row.cells[3]),
|
||||||
ergebnis: cell(3),
|
syncKey,
|
||||||
};
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const art = cellText(row.art);
|
|
||||||
if (!art) continue;
|
|
||||||
const datum = parseDate(row.datum);
|
|
||||||
const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;
|
|
||||||
results.push({
|
|
||||||
standesbuchNr,
|
|
||||||
datum,
|
|
||||||
anmerkungen: cellText(row.anmerkungen),
|
|
||||||
art,
|
|
||||||
ergebnis: cellText(row.ergebnis),
|
|
||||||
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})`);
|
|
||||||
}
|
}
|
||||||
|
log(` Untersuchungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||||
|
for (const u of results) log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
async function scrapeMemberFahrgenehmigungen(
|
async function scrapeMemberFahrgenehmigungen(
|
||||||
frame: Frame,
|
frame: Frame,
|
||||||
@@ -732,63 +748,29 @@ async function scrapeMemberFahrgenehmigungen(
|
|||||||
): Promise<FdiskFahrgenehmigung[]> {
|
): Promise<FdiskFahrgenehmigung[]> {
|
||||||
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx`
|
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx`
|
||||||
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
|
+ `?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[] = [];
|
const results: FdiskFahrgenehmigung[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
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
|
// Columns: 0=Ausstellungsdatum, 1=Gültig bis, 2=Behörde, 3=Nummer, 4=Fahrgenehmigungsklasse
|
||||||
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
|
const klasse = cellText(row.cells[4]);
|
||||||
trs.map((tr) => {
|
if (!klasse) continue;
|
||||||
const cells = Array.from(tr.querySelectorAll('td'));
|
const ausstellungsdatum = parseDate(row.cells[0]);
|
||||||
const cellVal = (i: number): string => {
|
const syncKey = `${standesbuchNr}::${klasse}::${ausstellungsdatum ?? ''}`;
|
||||||
const cell = cells[i];
|
results.push({
|
||||||
if (!cell) return '';
|
standesbuchNr,
|
||||||
const input = cell.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
|
ausstellungsdatum,
|
||||||
if (input) return input.value?.trim() ?? '';
|
gueltigBis: parseDate(row.cells[1]),
|
||||||
const select = cell.querySelector('select') as HTMLSelectElement | null;
|
behoerde: cellText(row.cells[2]),
|
||||||
if (select) {
|
nummer: cellText(row.cells[3]),
|
||||||
const opt = select.options[select.selectedIndex];
|
klasse,
|
||||||
return (opt?.text || opt?.value || '').trim();
|
syncKey,
|
||||||
}
|
});
|
||||||
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);
|
|
||||||
if (!klasse) continue;
|
|
||||||
const ausstellungsdatum = parseDate(row.ausstellungsdatum);
|
|
||||||
const syncKey = `${standesbuchNr}::${klasse}::${ausstellungsdatum ?? ''}`;
|
|
||||||
results.push({
|
|
||||||
standesbuchNr,
|
|
||||||
ausstellungsdatum,
|
|
||||||
gueltigBis: parseDate(row.gueltigBis),
|
|
||||||
behoerde: cellText(row.behoerde),
|
|
||||||
nummer: cellText(row.nummer),
|
|
||||||
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})`);
|
|
||||||
}
|
}
|
||||||
|
log(` Fahrgenehmigungen for StNr ${standesbuchNr}: ${results.length} rows`);
|
||||||
|
for (const f of results) log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user