| null> {
+): Promise<{ rows: Array<{ cells: string[] }>; dateColIdx: number } | null> {
await frame_goto(frame, url);
const landed = frame.url();
@@ -827,13 +937,29 @@ async function navigateAndGetTableRows(
const fdcRows = allRows.filter(r => r.tableClass.includes('FdcLayList'));
const resultRows = fdcRows.length > 0 ? fdcRows : allRows;
- const mapped = resultRows.map(r => ({ cells: r.cells }));
+ // Strip \u00A0 (non-breaking space) from all cell values and trim
+ const mapped = resultRows.map(r => ({
+ cells: r.cells.map(c => c.replace(/\u00A0/g, ' ').trim()),
+ }));
- // Filter: only keep rows where cells[0] looks like a DD.MM.YYYY date
+ // Find date column dynamically: look for a DD.MM.YYYY pattern in any column
const datePattern = /^\d{2}\.\d{2}\.\d{4}$/;
- const dataRows = mapped.filter(r => datePattern.test(r.cells[0]?.trim() ?? ''));
+ let dateColIdx = -1;
+ for (const r of mapped) {
+ for (let ci = 0; ci < r.cells.length; ci++) {
+ if (datePattern.test(r.cells[ci] ?? '')) {
+ dateColIdx = ci;
+ break;
+ }
+ }
+ if (dateColIdx >= 0) break;
+ }
- log(` → ${allRows.length} total rows, ${fdcRows.length} FdcLayList rows, ${dataRows.length} data rows (with date in cells[0])`);
+ const dataRows = dateColIdx >= 0
+ ? mapped.filter(r => datePattern.test(r.cells[dateColIdx] ?? ''))
+ : [];
+
+ log(` → ${allRows.length} total rows, ${fdcRows.length} FdcLayList rows, ${dataRows.length} data rows (date in col ${dateColIdx})`);
// Debug: dump HTML when no data rows found
if (dataRows.length === 0) {
@@ -841,7 +967,7 @@ async function navigateAndGetTableRows(
await dumpHtml(frame, `navigateAndGetTableRows_${urlSlug}`);
}
- return dataRows;
+ return { rows: dataRows, dateColIdx };
}
/**
@@ -857,13 +983,19 @@ async function scrapeMemberBefoerderungen(
+ `?search=1&searchid_mitgliedschaften=${idMitgliedschaft}&id_personen=${idPersonen}`
+ `&id_mitgliedschaften=${idMitgliedschaft}&searchid_personen=${idPersonen}&searchid_maskmode=`;
- const rows = await navigateAndGetTableRows(frame, url);
- if (!rows) return [];
+ const result = await navigateAndGetTableRows(frame, url);
+ if (!result) return [];
+ const { rows, dateColIdx } = result;
const results: FdiskBefoerderung[] = [];
for (const row of rows) {
- const datum = parseDate(row.cells[0]);
- const dienstgrad = cellText(row.cells[1]) ?? '';
+ const datum = parseDate(row.cells[dateColIdx]);
+ // The next non-empty column after the date holds the Dienstgrad
+ let dienstgrad = '';
+ for (let ci = dateColIdx + 1; ci < row.cells.length; ci++) {
+ const v = cellText(row.cells[ci]);
+ if (v) { dienstgrad = v; break; }
+ }
const syncKey = `${standesbuchNr}::${dienstgrad}::${datum ?? ''}`;
results.push({ standesbuchNr, datum, dienstgrad, syncKey });
}
@@ -885,22 +1017,32 @@ async function scrapeMemberUntersuchungen(
+ `?search=1&searchid_mitgliedschaften=${idMitgliedschaft}&id_personen=${idPersonen}`
+ `&id_mitgliedschaften=${idMitgliedschaft}&searchid_personen=${idPersonen}&searchid_maskmode=`;
- const rows = await navigateAndGetTableRows(frame, url);
- if (!rows) return [];
+ const result = await navigateAndGetTableRows(frame, url);
+ if (!result) return [];
+ const { rows, dateColIdx } = result;
const results: FdiskUntersuchung[] = [];
for (const row of rows) {
- // Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
- const art = cellText(row.cells[2]);
+ // Collect non-empty values from columns after the date column
+ 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);
+ }
+ // Original layout: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
+ // With spacer columns the date may not be at 0; use relative offsets from collected values
+ const anmerkungen = valueCols[0] ?? null;
+ const art = valueCols[1] ?? null;
+ const ergebnis = valueCols[2] ?? null;
if (!art) continue;
- const datum = parseDate(row.cells[0]);
+ const datum = parseDate(row.cells[dateColIdx]);
const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;
results.push({
standesbuchNr,
datum,
- anmerkungen: cellText(row.cells[1]),
+ anmerkungen,
art,
- ergebnis: cellText(row.cells[3]),
+ ergebnis,
syncKey,
});
}
@@ -911,8 +1053,9 @@ async function scrapeMemberUntersuchungen(
/**
* Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries.
- * This page is a ListEdit page with a different structure than normal list pages.
- * Uses its own page evaluation to read | headers + | // |