diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index b1bf7cca..19310abd 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -232,7 +232,8 @@ const READ_FORM_FN = `function readForm(p) { const textEl = box.querySelector('.gridBoxText'); const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; if (text) { - columns.push(text); + const r = box.getBoundingClientRect(); + columns.push({ text, x: r.x, right: r.x + r.width, y: r.y, h: r.height }); } else { // Unnamed column — check if data cells contain checkboxes const firstLine = body?.querySelector('.gridLine'); @@ -241,18 +242,47 @@ const READ_FORM_FN = `function readForm(p) { const idx = visibleHeaders.indexOf(box); const cells = [...firstLine.children].filter(c => c.offsetWidth > 0); if (cells[idx]?.querySelector('.checkbox')) { - columns.push('(checkbox)'); + columns.push({ text: '(checkbox)', x: 0, right: 0, y: 0, h: 0 }); } } } }); + // Expand single merged headers with multiple data sub-rows (e.g. "Субконто Дт" → 1/2/3) + const firstLine = body?.querySelector('.gridLine'); + if (firstLine && columns.length > 0) { + const xGrp = new Map(); + columns.forEach(c => { + const k = Math.round(c.x) + ':' + Math.round(c.right); + if (!xGrp.has(k)) xGrp.set(k, []); + xGrp.get(k).push(c); + }); + for (const [k, hdrs] of xGrp) { + if (hdrs.length !== 1) continue; + let cnt = 0; + [...firstLine.children].forEach(box => { + if (box.offsetWidth === 0) return; + const r = box.getBoundingClientRect(); + const cx = r.x + r.width / 2; + if (cx >= hdrs[0].x && cx < hdrs[0].right) cnt++; + }); + if (cnt > 1) { + const base = hdrs[0]; + const baseIdx = columns.indexOf(base); + columns.splice(baseIdx, 1); + for (let si = 0; si < cnt; si++) { + columns.splice(baseIdx + si, 0, { text: base.text + ' ' + (si + 1), x: base.x, right: base.right, y: 0, h: 0 }); + } + } + } + } } + const colNames = columns.map(c => c.text); const rowCount = body ? body.querySelectorAll('.gridLine').length : 0; // Visual label from group title (e.g. "Входящие:" for grid "Входящие") const titleEl = document.getElementById(p + name + '#title_div') || document.getElementById(p + 'Группа' + name + '#title_div'); const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null; - return { name, columns, rowCount, ...(label ? { label } : {}) }; + return { name, columns: colNames, rowCount, ...(label ? { label } : {}) }; }); result.tables = tables; // Backward compat: table = first grid summary @@ -536,15 +566,91 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto const cells = [...firstLine.children].filter(c => c.offsetWidth > 0); if (cells[idx]?.querySelector('.checkbox')) { const r = box.getBoundingClientRect(); - columns.push({ text: '(checkbox)', x: r.x, w: r.width, right: r.x + r.width }); + columns.push({ text: '(checkbox)', x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height }); } } return; } const r = box.getBoundingClientRect(); - columns.push({ text, x: r.x, w: r.width, right: r.x + r.width }); + columns.push({ text, x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height }); }); + // Multi-row grid support: detect stacked/merged headers. + // Group headers by X-range. For each group, count data sub-rows from first line. + // - Stacked headers (2+ headers at same X) with multiple data rows → match by Y-order + // - Single merged header with multiple data rows → expand to numbered columns (e.g. "Субконто Дт 1") + const xGroups = new Map(); + columns.forEach(c => { + const key = Math.round(c.x) + ':' + Math.round(c.right); + if (!xGroups.has(key)) xGroups.set(key, []); + xGroups.get(key).push(c); + }); + for (const [, hdrs] of xGroups) hdrs.sort((a, b) => a.y - b.y); + + const firstDataLine = body?.querySelector('.gridLine'); + const subRowMap = new Map(); + if (firstDataLine) { + [...firstDataLine.children].forEach(box => { + if (box.offsetWidth === 0) return; + const r = box.getBoundingClientRect(); + const cx = r.x + r.width / 2; + for (const [key, hdrs] of xGroups) { + const h0 = hdrs[0]; + if (cx >= h0.x && cx < h0.right) { + if (!subRowMap.has(key)) subRowMap.set(key, []); + subRowMap.get(key).push({ y: r.y }); + break; + } + } + }); + for (const [, subs] of subRowMap) subs.sort((a, b) => a.y - b.y); + } + + const multiRowGroups = new Map(); + for (const [key, hdrs] of xGroups) { + const subs = subRowMap.get(key); + if (!subs || subs.length <= 1) continue; + if (hdrs.length >= 2) { + multiRowGroups.set(key, hdrs); + } else if (hdrs.length === 1 && subs.length > 1) { + const base = hdrs[0]; + const baseIdx = columns.indexOf(base); + columns.splice(baseIdx, 1); + const expanded = []; + for (let si = 0; si < subs.length; si++) { + const numbered = { + text: base.text + ' ' + (si + 1), + x: base.x, w: base.w, right: base.right, + y: base.y + si, h: base.h / subs.length, _subIdx: si + }; + columns.splice(baseIdx + si, 0, numbered); + expanded.push(numbered); + } + multiRowGroups.set(key, expanded); + } + } + + function matchColumn(cellX, cellW, cellY) { + const cx = cellX + cellW / 2; + for (const [key, hdrs] of multiRowGroups) { + const h0 = hdrs[0]; + if (cx >= h0.x && cx < h0.right) { + const subs = subRowMap.get(key); + if (subs) { + const subIdx = subs.findIndex(s => Math.abs(s.y - cellY) < 5); + if (subIdx >= 0 && subIdx < hdrs.length) return hdrs[subIdx]; + } + let best = hdrs[0], bestDist = Infinity; + for (const h of hdrs) { + const dist = Math.abs(cellY - h.y); + if (dist < bestDist) { bestDist = dist; best = h; } + } + return best; + } + } + return columns.find(c => cx >= c.x && cx < c.right); + } + // Extract data rows from gridBody const allLines = body.querySelectorAll('.gridLine'); const total = allLines.length; @@ -566,10 +672,9 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto val = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; if (!val) return; } - // Match cell to column by X-coordinate overlap + // Match cell to column by X+Y overlap (multi-row aware) const r = box.getBoundingClientRect(); - const cx = r.x + r.width / 2; - const col = columns.find(c => cx >= c.x && cx < c.right); + const col = matchColumn(r.x, r.width, r.y); if (col) { row[col.text] = row[col.text] ? row[col.text] + ' / ' + val : val; }