mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-02 00:10:50 +03:00
fix(web-test): scope readSpreadsheet to current form, improve headers
Two fixes: - Scope iframe scanning to current form container (prevents reading stale data from other open report tabs) - Improved header detection: use LAST row before data as detail header, previous row as group header with fill-forward for merged cells. Fixes two-level headers like "Начальный остаток / Долг клиента" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
366378c4b5
commit
5d18bf4501
@@ -438,12 +438,35 @@ export async function readTable({ maxRows = 20, offset = 0 } = {}) {
|
|||||||
*/
|
*/
|
||||||
export async function readSpreadsheet() {
|
export async function readSpreadsheet() {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
|
const formNum = await page.evaluate(detectFormScript());
|
||||||
|
|
||||||
|
// Collect iframe indices that belong to the current form's spreadsheet container
|
||||||
|
const iframeIndices = await page.evaluate(`(() => {
|
||||||
|
const prefix = 'form${formNum ?? 0}_';
|
||||||
|
const allIframes = [...document.querySelectorAll('iframe')];
|
||||||
|
const indices = [];
|
||||||
|
for (let i = 0; i < allIframes.length; i++) {
|
||||||
|
const f = allIframes[i];
|
||||||
|
if (f.offsetWidth < 100) continue;
|
||||||
|
let el = f.parentElement, found = false;
|
||||||
|
for (let d = 0; el && d < 30; d++, el = el.parentElement) {
|
||||||
|
if (el.id && el.id.startsWith(prefix)) { found = true; break; }
|
||||||
|
}
|
||||||
|
if (found) indices.push(i);
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
})()`);
|
||||||
|
|
||||||
const frames = page.frames();
|
const frames = page.frames();
|
||||||
const allCells = new Map();
|
const allCells = new Map();
|
||||||
|
|
||||||
for (let fi = 1; fi < frames.length; fi++) {
|
// Map page iframe indices to frame objects (frame 0 = main, iframes start at 1+)
|
||||||
|
for (const iframeIdx of iframeIndices) {
|
||||||
|
// Playwright frames: frame[0] is main, frame[1..N] map to iframes in DOM order
|
||||||
|
const frame = frames[iframeIdx + 1];
|
||||||
|
if (!frame) continue;
|
||||||
try {
|
try {
|
||||||
const cells = await frames[fi].evaluate(`(() => {
|
const cells = await frame.evaluate(`(() => {
|
||||||
const cells = [];
|
const cells = [];
|
||||||
document.querySelectorAll('div[x]').forEach(d => {
|
document.querySelectorAll('div[x]').forEach(d => {
|
||||||
const span = d.querySelector('span');
|
const span = d.querySelector('span');
|
||||||
@@ -488,46 +511,62 @@ export async function readSpreadsheet() {
|
|||||||
const hasNumber = (row) => row.some(c => /^[\d\s\u00a0]/.test(c) && /\d/.test(c));
|
const hasNumber = (row) => row.some(c => /^[\d\s\u00a0]/.test(c) && /\d/.test(c));
|
||||||
const nonEmpty = (row) => row.filter(c => c !== '').length;
|
const nonEmpty = (row) => row.filter(c => c !== '').length;
|
||||||
|
|
||||||
// 1. Find header row: row with most non-empty cells BEFORE first row with numbers
|
// 1. Find first data row (first row with numbers)
|
||||||
let headerIdx = -1;
|
let firstDataIdx = rows.length;
|
||||||
let bestCount = 0;
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
if (hasNumber(rows[i])) break;
|
if (hasNumber(rows[i])) { firstDataIdx = i; break; }
|
||||||
const cnt = nonEmpty(rows[i]);
|
|
||||||
if (cnt >= bestCount) { bestCount = cnt; headerIdx = i; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headerIdx === -1 || bestCount < 2) return { rows, total: rows.length };
|
// 2. Find header rows: scan backwards from data, pick last row with ≥3 cells as detail header
|
||||||
|
let detailIdx = -1;
|
||||||
const headerRow = rows[headerIdx];
|
for (let i = firstDataIdx - 1; i >= 0; i--) {
|
||||||
|
if (nonEmpty(rows[i]) >= 3) { detailIdx = i; break; }
|
||||||
// 2. Check for group header row (row just before header, sparse text entries)
|
|
||||||
let groupRow = null;
|
|
||||||
if (headerIdx > 0) {
|
|
||||||
const prev = rows[headerIdx - 1];
|
|
||||||
const prevCnt = nonEmpty(prev);
|
|
||||||
if (prevCnt > 0 && prevCnt < bestCount) groupRow = prev;
|
|
||||||
}
|
}
|
||||||
|
if (detailIdx === -1) return { rows, total: rows.length };
|
||||||
|
|
||||||
// 3. Build column names; disambiguate duplicates with group prefix
|
// Group header: row before detail with ≥2 non-empty cells
|
||||||
const nameCounts = {};
|
let groupIdx = -1;
|
||||||
|
if (detailIdx > 0 && nonEmpty(rows[detailIdx - 1]) >= 2) groupIdx = detailIdx - 1;
|
||||||
|
|
||||||
|
const detailRow = rows[detailIdx];
|
||||||
|
const groupRow = groupIdx >= 0 ? rows[groupIdx] : null;
|
||||||
|
|
||||||
|
// 3. Build column names by merging group + detail rows
|
||||||
|
// Fill-forward group names across empty columns (merged cells)
|
||||||
|
const groupFilled = new Array(maxCol + 1).fill('');
|
||||||
|
if (groupRow) {
|
||||||
|
let cur = '';
|
||||||
for (let c = 0; c <= maxCol; c++) {
|
for (let c = 0; c <= maxCol; c++) {
|
||||||
const n = headerRow[c];
|
if (groupRow[c]) cur = groupRow[c];
|
||||||
if (n) nameCounts[n] = (nameCounts[n] || 0) + 1;
|
groupFilled[c] = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each column: use detail name if available, else group name
|
||||||
|
// Prefix with group when duplicates exist in detail row
|
||||||
|
const detailCounts = {};
|
||||||
|
for (let c = 0; c <= maxCol; c++) {
|
||||||
|
const n = detailRow[c];
|
||||||
|
if (n) detailCounts[n] = (detailCounts[n] || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colNames = [];
|
const colNames = [];
|
||||||
let curGroup = '';
|
|
||||||
for (let c = 0; c <= maxCol; c++) {
|
for (let c = 0; c <= maxCol; c++) {
|
||||||
if (groupRow && groupRow[c]) curGroup = groupRow[c];
|
const detail = detailRow[c];
|
||||||
const name = headerRow[c];
|
const group = groupFilled[c];
|
||||||
if (!name) { colNames.push(null); continue; }
|
if (detail) {
|
||||||
colNames.push(nameCounts[name] > 1 && curGroup ? `${curGroup} / ${name}` : name);
|
// Use group prefix if duplicate detail names or if group differs from detail
|
||||||
|
const needPrefix = group && group !== detail && (detailCounts[detail] > 1 || (groupRow && groupRow[c] === ''));
|
||||||
|
colNames.push(needPrefix ? `${group} / ${detail}` : detail);
|
||||||
|
} else if (group) {
|
||||||
|
colNames.push(group);
|
||||||
|
} else {
|
||||||
|
colNames.push(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Skip sub-header rows after header (text-only rows before first numeric row)
|
// 4. Data starts at firstDataIdx
|
||||||
let dataStart = headerIdx + 1;
|
const dataStart = firstDataIdx;
|
||||||
while (dataStart < rows.length && !hasNumber(rows[dataStart])) dataStart++;
|
|
||||||
|
|
||||||
// 5. Convert data rows to objects
|
// 5. Convert data rows to objects
|
||||||
const data = [];
|
const data = [];
|
||||||
@@ -551,7 +590,7 @@ export async function readSpreadsheet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. Meta: title, params, filters from rows before header
|
// 6. Meta: title, params, filters from rows before header
|
||||||
const metaEnd = groupRow ? headerIdx - 1 : headerIdx;
|
const metaEnd = groupIdx >= 0 ? groupIdx : detailIdx;
|
||||||
let title = '';
|
let title = '';
|
||||||
const meta = [];
|
const meta = [];
|
||||||
for (let i = 0; i < metaEnd; i++) {
|
for (let i = 0; i < metaEnd; i++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user