From 482bd276bdc0562a2bc53d92a0e15a906bfded39 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Fri, 11 Sep 2026 11:00:44 +0000 Subject: [PATCH] fix(conversores): calibrar extracao de quantidades, comprimentos e pesos no conversor advance steel --- public/conversor_relatorio_estruturas.html | 334 +++++++++++------ .../conversores/AdvanceSteelConverter.tsx | 335 ++++++++++++------ 2 files changed, 452 insertions(+), 217 deletions(-) diff --git a/public/conversor_relatorio_estruturas.html b/public/conversor_relatorio_estruturas.html index 8e97b2c..673092a 100644 --- a/public/conversor_relatorio_estruturas.html +++ b/public/conversor_relatorio_estruturas.html @@ -398,7 +398,7 @@ let lastPage = null; items.forEach(item => { - if (lastPage === null || item.page !== lastPage || Math.abs(item.y - lastY) > 3.5) { + if (lastPage === null || item.page !== lastPage || Math.abs(item.y - (lastY ?? item.y)) > 3.5) { if (currentLine.length > 0) { lines.push(currentLine); } @@ -416,148 +416,216 @@ let defaultOf = ""; for (const line of lines) { const lineText = line.map(i => i.text).join(' '); - const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9\-]+)/i); + const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9\-]+)/i) || lineText.match(/Projeto:\s*([A-Za-z0-9\-]+)/i); if (matchTrabalho) { - defaultOf = matchTrabalho[1].replace('-', ''); + defaultOf = matchTrabalho[1].replace(/-/g, ''); log(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); break; } } + // Calibração inicial das colunas X + const colX = { + marca: 30, + qtde: 100, + nome: 180, + qualidade: 290, + comprimento: 370, + largura: 430, + pesoPeca: 490, + totalPeso: 550, + superficie: 610, + totalSuperficie: 670 + }; + + for (const line of lines) { + const lineText = line.map(i => i.text).join(' ').toLowerCase(); + if (lineText.includes('marca') && (lineText.includes('qtde') || lineText.includes('quant'))) { + for (const item of line) { + const t = item.text.toLowerCase().trim(); + if (t === 'marca') colX.marca = item.x; + else if (t.startsWith('qtd') || t.startsWith('quant')) colX.qtde = item.x; + else if (t === 'nome') colX.nome = item.x; + else if (t.startsWith('qualid')) colX.qualidade = item.x; + else if (t.startsWith('comprim')) colX.comprimento = item.x; + else if (t.startsWith('largura')) colX.largura = item.x; + else if (t.includes('peso da') || t.includes('da peça') || t.includes('piece')) colX.pesoPeca = item.x; + else if (t.includes('total peso') || (t === 'total' && item.x < 600)) colX.totalPeso = item.x; + else if (t.includes('total superf') || (t === 'total' && item.x >= 600)) colX.totalSuperficie = item.x; + else if (t.startsWith('superf')) colX.superficie = item.x; + } + } + } + + const getNearestCol = (x) => { + let bestCol = ''; + let minDiff = Infinity; + for (const [colName, targetX] of Object.entries(colX)) { + const diff = Math.abs(x - targetX); + if (diff < minDiff) { + minDiff = diff; + bestCol = colName; + } + } + return bestCol; + }; + const assemblies = []; let currentAssembly = null; - // Filtro estrito para evitar linhas duplicadas/ruídos de componentes repetidos na listagem - const processedMarks = new Set(); - for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineText = line.map(i => i.text).join(' '); - if (lineText.includes('Lista de Peças') || lineText.includes('Cliente:') || lineText.includes('Marca Quant Nome') || lineText.includes('Superfície')) { + if ( + lineText.includes('Lista de Peças') || + lineText.includes('Cliente:') || + lineText.includes('Marca Quant Nome') || + lineText.includes('Superfície') || + lineText.includes('Trabalho:') + ) { continue; } - const firstToken = line[0].text; - const markMatch = firstToken.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstToken.match(/^([A-Za-z0-9]+)-(\d+)$/); + let markStr = ''; + let markTokensConsumed = 0; - if (markMatch) { - const parts = firstToken.split('-'); - let ofCode = ""; - let phaseCode = "0"; - let pieceNumber = ""; + const firstTok = line[0].text.trim(); + if (firstTok.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstTok.match(/^([A-Za-z0-9]+)-(\d+)$/)) { + markStr = firstTok; + markTokensConsumed = 1; + } else { + let candidate = ''; + let found3 = null; + let found2 = null; - if (parts.length === 3) { - ofCode = parts[0]; - phaseCode = parts[1]; - pieceNumber = parts[2]; - } else if (parts.length === 2) { - ofCode = parts[0]; - phaseCode = "0"; - pieceNumber = parts[1]; + for (let k = 0; k < Math.min(line.length, 6); k++) { + candidate += line[k].text.trim(); + if (candidate.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/)) { + found3 = { mark: candidate, count: k + 1 }; + break; + } + if (candidate.match(/^([A-Za-z0-9]+)-(\d+)$/) && !found2) { + found2 = { mark: candidate, count: k + 1 }; + } } + if (found3) { + markStr = found3.mark; + markTokensConsumed = found3.count; + } else if (found2) { + markStr = found2.mark; + markTokensConsumed = found2.count; + } + } + + if (markStr) { + const parts = markStr.split('-'); + const ofCode = parts.length === 3 ? parts[0] : (parts.length === 2 ? parts[0] : defaultOf); + const phaseCode = parts.length === 3 ? parts[1] : '0'; + const pieceNumber = parts.length === 3 ? parts[2] : parts[1]; + const pieceNumInt = parseInt(pieceNumber, 10); const isComponent = !isNaN(pieceNumInt) && pieceNumInt >= 1000; + const remainingItems = line.slice(markTokensConsumed); if (!isComponent) { - // Evitar duplicidade caso a mesma marca principal apareça mais de uma vez agrupada - if (processedMarks.has(firstToken)) { + // Se a marca for idêntica à da peça principal atual, é a linha de detalhe da peça simples! + if (currentAssembly && currentAssembly.rawMark === markStr) { + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qualidade' && !currentAssembly.material) { + currentAssembly.material = it.text; + } else if (col === 'comprimento' && valNum > 0) { + currentAssembly.comprimento = valNum; + } else if (col === 'pesoPeca' && valNum > 0) { + currentAssembly.pesoUnitario = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + currentAssembly.pesoTotal = valNum; + } + } continue; } - processedMarks.add(firstToken); + // Nova Peça Principal let mainQuant = 1; - let mainDesc = ""; + const descTokens = []; - if (line.length >= 2) { - const qVal = parseInt(line[1].text, 10); - if (!isNaN(qVal)) { + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const qVal = parseInt(it.text, 10); + if (col === 'qtde' && !isNaN(qVal) && qVal > 0) { mainQuant = qVal; - // CORREÇÃO 2: Garantir que a descrição seja única, pegando apenas o primeiro token descritivo limpo (ex: W 310x38.7) - let rawDescTokens = line.slice(2).map(item => item.text); - if (rawDescTokens.length > 0) { - mainDesc = rawDescTokens[0]; // Nome único/perfil inicial da peça principal - // Se o perfil tiver complemento separado por espaço (ex: W 310x38.7), agrupar se necessário ou manter limpo - if (rawDescTokens.length > 1 && (rawDescTokens[1].includes('x') || rawDescTokens[1].includes('Pl') || !isNaN(parseFloat(rawDescTokens[1])))) { - mainDesc = rawDescTokens.slice(0, 2).join(' '); - } - } - } else { - let rawDescTokens = line.slice(1).map(item => item.text); - mainDesc = rawDescTokens.length > 0 ? rawDescTokens[0] : 'ESTRUTURA'; + } else if (col === 'nome' || (!it.text.match(/^[0-9.,]+$/) && descTokens.length === 0)) { + descTokens.push(it.text); + } + } + + if (mainQuant === 1 && remainingItems.length > 0) { + const firstNum = remainingItems.find((it) => /^[0-9]+$/.test(it.text.trim())); + if (firstNum) { + const parsedQ = parseInt(firstNum.text.trim(), 10); + if (parsedQ > 0 && parsedQ < 1000) mainQuant = parsedQ; } } currentAssembly = { - rawMark: firstToken, + rawMark: markStr, of: ofCode || defaultOf, fase: phaseCode, - marca: pieceNumber, // CORREÇÃO 1: Apenas o número da peça (última numeração) + marca: pieceNumber, numeroPeca: pieceNumber, - descricao: mainDesc || 'ESTRUTURA', + descricao: descTokens.join(' ') || 'ESTRUTURA', quantidade: mainQuant, + material: '', + comprimento: 0, pesoUnitario: 0, pesoTotal: 0, + subtotalPeso: 0, + isComposed: 'NÃO', components: [] }; assemblies.push(currentAssembly); } else { - // Componente subordinado + // Componente com marca >= 1000 (Peça Composta) if (currentAssembly) { + currentAssembly.isComposed = 'SIM'; let compQuant = 1; - let compProfile = ""; - let compMaterial = "A36"; + const compDescTokens = []; + let compMaterial = 'A36'; let compLength = 0; let compWeightTotal = 0; - let compTokens = line.map(it => it.text); - if (compTokens.length >= 2) { - const qTest = parseInt(compTokens[1], 10); - if (!isNaN(qTest)) compQuant = qTest; - } + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + const t = it.text.trim(); - let matIndex = -1; - for (let k = 1; k < compTokens.length; k++) { - const tok = compTokens[k].toUpperCase(); - if (tok.includes('A572') || tok.includes('A36') || tok.includes('A500') || tok.includes('A106') || tok.includes('GR') || tok.includes('INOX') || tok.includes('SAE')) { - matIndex = k; - compMaterial = compTokens[k]; - if (k + 1 < compTokens.length && (compTokens[k+1].toUpperCase().includes('GR') || compTokens[k+1] === '50')) { - compMaterial += ' ' + compTokens[k+1]; - } - break; + if (col === 'qtde') { + const qVal = parseInt(t, 10); + if (!isNaN(qVal)) compQuant = qVal; + } else if (col === 'nome') { + compDescTokens.push(t); + } else if (col === 'qualidade') { + compMaterial = t; + } else if (col === 'comprimento' && valNum > 0) { + compLength = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + compWeightTotal = valNum; } } - if (matIndex > 2) { - compProfile = compTokens.slice(2, matIndex).join(' '); - } else if (compTokens.length >= 3) { - compProfile = compTokens[2]; - } - - const afterMatTokens = matIndex !== -1 ? compTokens.slice(matIndex + 1) : compTokens.slice(3); - const numericValues = []; - afterMatTokens.forEach(t => { - const cleanT = t.replace(/[()]/g, ''); - if (/[0-9]/.test(cleanT) && !cleanT.toUpperCase().includes('GR')) { - numericValues.push(cleanT); + if (compWeightTotal === 0) { + const nums = remainingItems.map((it) => parseNumeric(it.text)).filter((n) => n > 0); + if (nums.length >= 2) { + compWeightTotal = nums[nums.length - 1]; } - }); - - if (numericValues.length >= 1) { - compLength = parseNumeric(numericValues[0]); - } - // O peso total do componente geralmente é o último ou penúltimo valor numérico da linha de detalhe - if (numericValues.length >= 3) { - compWeightTotal = parseNumeric(numericValues[numericValues.length - 1]); - } else if (numericValues.length === 2) { - compWeightTotal = parseNumeric(numericValues[1]); } currentAssembly.components.push({ quantidade: compQuant, - perfil: compProfile, + perfil: compDescTokens.join(' ') || currentAssembly.descricao, material: compMaterial, comprimento: compLength, pesoTotal: compWeightTotal @@ -565,11 +633,62 @@ } } } else { - // Linha de subtotal ou peso geral da peça principal - if (currentAssembly && line.length <= 3) { - const possibleTotal = parseNumeric(line[0].text); - if (possibleTotal > 10 && currentAssembly.pesoTotal === 0) { - currentAssembly.pesoTotal = possibleTotal; + // Linhas sem marca explícita + if (currentAssembly) { + if (lineText.includes('M13') || lineText.startsWith('-') || lineText.includes('ASTM') || lineText.includes('10.9')) { + currentAssembly.isComposed = 'SIM'; + let compLength = 0; + let compWeightTotal = 0; + let compQuant = 1; + + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qtde') { + const q = parseInt(it.text, 10); + if (!isNaN(q)) compQuant = q; + } else if (col === 'comprimento' && valNum > 0) { + compLength = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + compWeightTotal = valNum; + } + } + + currentAssembly.components.push({ + quantidade: compQuant, + perfil: lineText.split(' ')[0] || 'ACESSÓRIO', + material: '10.9', + comprimento: compLength, + pesoTotal: compWeightTotal + }); + } else if (lineText.includes('W ') || lineText.includes('C ') || lineText.includes('Pl ')) { + let compLength = 0; + let compWeightTotal = 0; + let compMaterial = 'A36'; + + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qualidade') compMaterial = it.text; + else if (col === 'comprimento' && valNum > 0) compLength = valNum; + else if (col === 'totalPeso' && valNum > 0) compWeightTotal = valNum; + } + + currentAssembly.components.push({ + quantidade: 1, + perfil: lineText.split(' ')[0] + ' ' + (lineText.split(' ')[1] || ''), + material: compMaterial, + comprimento: compLength, + pesoTotal: compWeightTotal + }); + } else { + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'totalPeso' && valNum > 0) { + currentAssembly.subtotalPeso = valNum; + } + } } } } @@ -578,14 +697,14 @@ log(`Total de Peças Principais processadas: ${assemblies.length}`); extractedData = assemblies.map(asm => { - const hasComponents = asm.components.length > 0; - + const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0; let perfilPrincipal = asm.descricao; - let materialPrincipal = "A36"; - let maxComp = 0; - let sumComponentsWeight = 0; + let materialPrincipal = asm.material || 'A36'; + let maxComp = asm.comprimento || 0; + let totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal; - if (hasComponents) { + if (isComposed) { + let sumComponentsWeight = 0; let bestComp = asm.components[0]; asm.components.forEach(c => { if (c.comprimento > maxComp) { @@ -595,28 +714,31 @@ sumComponentsWeight += c.pesoTotal; }); - perfilPrincipal = bestComp.perfil || asm.descricao; - materialPrincipal = bestComp.material || "A36"; + if (bestComp) { + perfilPrincipal = bestComp.perfil || asm.descricao; + materialPrincipal = bestComp.material || 'A36'; + } + + if (!totalPeso || totalPeso === 0) { + totalPeso = sumComponentsWeight; + } } - // Definir peso total final (prioriza o subtotal extraído do relatório) - let finalPesoTotal = asm.pesoTotal > 0 ? asm.pesoTotal : sumComponentsWeight; - - // CORREÇÃO 3: Peso Unitário = Peso Total dividido pela quantidade da peça principal - let finalPesoUnit = asm.quantidade > 0 ? (finalPesoTotal / asm.quantidade) : finalPesoTotal; + // Regra de Ouro: Peso Unitário = Peso Total dividido pela quantidade da peça principal + const pesoUnit = asm.quantidade > 0 ? (totalPeso / asm.quantidade) : totalPeso; return { of: asm.of, fase: asm.fase, marca: asm.marca, descricao: asm.descricao, - isComposed: hasComponents ? "SIM" : "NÃO", + isComposed: isComposed ? "SIM" : "NÃO", quantidade: asm.quantidade, material: materialPrincipal, perfilPrincipal: perfilPrincipal, comprimentoMax: maxComp > 0 ? maxComp : "-", - pesoUnit: finalPesoUnit > 0 ? finalPesoUnit.toFixed(2) : "-", - pesoTotal: finalPesoTotal > 0 ? finalPesoTotal.toFixed(2) : "-", + pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(2) : "-", + pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : "-", tratamentoSuperficial: "pintura" }; }); diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 6392d56..563f846 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -39,8 +39,12 @@ interface AssemblyItem { numeroPeca: string; descricao: string; quantidade: number; + material: string; + comprimento: number; pesoUnitario: number; pesoTotal: number; + subtotalPeso: number; + isComposed: 'SIM' | 'NÃO'; components: ComponentItem[]; } @@ -203,7 +207,7 @@ const AdvanceSteelConverterContent: React.FC = () => { let defaultOf = ''; for (const line of lines) { const lineText = line.map((i) => i.text).join(' '); - const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9-]+)/i); + const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9-]+)/i) || lineText.match(/Projeto:\s*([A-Za-z0-9-]+)/i); if (matchTrabalho) { defaultOf = matchTrabalho[1].replace(/-/g, ''); addLog(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); @@ -211,9 +215,55 @@ const AdvanceSteelConverterContent: React.FC = () => { } } + // Calibração inicial das colunas X para o layout padrão do Advance Steel + const colX = { + marca: 30, + qtde: 100, + nome: 180, + qualidade: 290, + comprimento: 370, + largura: 430, + pesoPeca: 490, + totalPeso: 550, + superficie: 610, + totalSuperficie: 670 + }; + + // Tentar recalibrar X dinamicamente a partir dos títulos das colunas + for (const line of lines) { + const lineText = line.map((i) => i.text).join(' ').toLowerCase(); + if (lineText.includes('marca') && (lineText.includes('qtde') || lineText.includes('quant'))) { + for (const item of line) { + const t = item.text.toLowerCase().trim(); + if (t === 'marca') colX.marca = item.x; + else if (t.startsWith('qtd') || t.startsWith('quant')) colX.qtde = item.x; + else if (t === 'nome') colX.nome = item.x; + else if (t.startsWith('qualid')) colX.qualidade = item.x; + else if (t.startsWith('comprim')) colX.comprimento = item.x; + else if (t.startsWith('largura')) colX.largura = item.x; + else if (t.includes('peso da') || t.includes('da peça') || t.includes('piece')) colX.pesoPeca = item.x; + else if (t.includes('total peso') || (t === 'total' && item.x < 600)) colX.totalPeso = item.x; + else if (t.includes('total superf') || (t === 'total' && item.x >= 600)) colX.totalSuperficie = item.x; + else if (t.startsWith('superf')) colX.superficie = item.x; + } + } + } + + const getNearestCol = (x: number): string => { + let bestCol = ''; + let minDiff = Infinity; + for (const [colName, targetX] of Object.entries(colX)) { + const diff = Math.abs(x - targetX); + if (diff < minDiff) { + minDiff = diff; + bestCol = colName; + } + } + return bestCol; + }; + const assemblies: AssemblyItem[] = []; let currentAssembly: AssemblyItem | null = null; - const processedMarks = new Set(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -223,150 +273,153 @@ const AdvanceSteelConverterContent: React.FC = () => { lineText.includes('Lista de Peças') || lineText.includes('Cliente:') || lineText.includes('Marca Quant Nome') || - lineText.includes('Superfície') + lineText.includes('Superfície') || + lineText.includes('Trabalho:') ) { continue; } - let firstToken = line[0].text.trim(); + // 1. Identificar se a linha inicia com marca ou aglutinação de tokens (ex: B133-1-20) + let markStr = ''; + let markTokensConsumed = 0; + + const firstTok = line[0].text.trim(); + if (firstTok.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstTok.match(/^([A-Za-z0-9]+)-(\d+)$/)) { + markStr = firstTok; + markTokensConsumed = 1; + } else { + let candidate = ''; + let found3: { mark: string; count: number } | null = null; + let found2: { mark: string; count: number } | null = null; - // MONTAGEM INTELIGENTE DE TOKENS SEPARADOS (Ex: ["B132", "-", "1", "-", "1"] -> "B132-1-1") - if (!firstToken.includes('-') && line.length >= 3) { - let assembled = ''; for (let k = 0; k < Math.min(line.length, 6); k++) { - assembled += line[k].text.trim(); - if (assembled.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || assembled.match(/^([A-Za-z0-9]+)-(\d+)$/)) { - firstToken = assembled; + candidate += line[k].text.trim(); + if (candidate.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/)) { + found3 = { mark: candidate, count: k + 1 }; break; } + if (candidate.match(/^([A-Za-z0-9]+)-(\d+)$/) && !found2) { + found2 = { mark: candidate, count: k + 1 }; + } + } + + if (found3) { + markStr = found3.mark; + markTokensConsumed = found3.count; + } else if (found2) { + markStr = found2.mark; + markTokensConsumed = found2.count; } } - const markMatch = firstToken.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstToken.match(/^([A-Za-z0-9]+)-(\d+)$/); - - if (markMatch) { - const parts = firstToken.split('-'); - let ofCode = ''; - let phaseCode = '0'; - let pieceNumber = ''; - - if (parts.length === 3) { - ofCode = parts[0]; - phaseCode = parts[1]; - pieceNumber = parts[2]; - } else if (parts.length === 2) { - ofCode = parts[0]; - phaseCode = '0'; - pieceNumber = parts[1]; - } + if (markStr) { + const parts = markStr.split('-'); + const ofCode = parts.length === 3 ? parts[0] : (parts.length === 2 ? parts[0] : defaultOf); + const phaseCode = parts.length === 3 ? parts[1] : '0'; + const pieceNumber = parts.length === 3 ? parts[2] : parts[1]; const pieceNumInt = parseInt(pieceNumber, 10); const isComponent = !isNaN(pieceNumInt) && pieceNumInt >= 1000; + const remainingItems = line.slice(markTokensConsumed); if (!isComponent) { - if (processedMarks.has(firstToken)) { + // Se a marca for idêntica à da peça principal atual, é a linha de detalhe da peça simples! + if (currentAssembly && currentAssembly.rawMark === markStr) { + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qualidade' && !currentAssembly.material) { + currentAssembly.material = it.text; + } else if (col === 'comprimento' && valNum > 0) { + currentAssembly.comprimento = valNum; + } else if (col === 'pesoPeca' && valNum > 0) { + currentAssembly.pesoUnitario = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + currentAssembly.pesoTotal = valNum; + } + } continue; } - processedMarks.add(firstToken); + // Nova Peça Principal let mainQuant = 1; - let mainDesc = ''; + const descTokens: string[] = []; - if (line.length >= 2) { - const qVal = parseInt(line[1].text, 10); - if (!isNaN(qVal)) { + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const qVal = parseInt(it.text, 10); + if (col === 'qtde' && !isNaN(qVal) && qVal > 0) { mainQuant = qVal; - const rawDescTokens = line.slice(2).map((item) => item.text); - if (rawDescTokens.length > 0) { - mainDesc = rawDescTokens[0]; - if ( - rawDescTokens.length > 1 && - (rawDescTokens[1].includes('x') || rawDescTokens[1].includes('Pl') || !isNaN(parseFloat(rawDescTokens[1]))) - ) { - mainDesc = rawDescTokens.slice(0, 2).join(' '); - } - } - } else { - const rawDescTokens = line.slice(1).map((item) => item.text); - mainDesc = rawDescTokens.length > 0 ? rawDescTokens[0] : 'ESTRUTURA'; + } else if (col === 'nome' || (!it.text.match(/^[0-9.,]+$/) && descTokens.length === 0)) { + descTokens.push(it.text); + } + } + + if (mainQuant === 1 && remainingItems.length > 0) { + const firstNum = remainingItems.find((it) => /^[0-9]+$/.test(it.text.trim())); + if (firstNum) { + const parsedQ = parseInt(firstNum.text.trim(), 10); + if (parsedQ > 0 && parsedQ < 1000) mainQuant = parsedQ; } } currentAssembly = { - rawMark: firstToken, + rawMark: markStr, of: ofCode || defaultOf, fase: phaseCode, marca: pieceNumber, numeroPeca: pieceNumber, - descricao: mainDesc || 'ESTRUTURA', + descricao: descTokens.join(' ') || 'ESTRUTURA', quantidade: mainQuant, + material: '', + comprimento: 0, pesoUnitario: 0, pesoTotal: 0, + subtotalPeso: 0, + isComposed: 'NÃO', components: [] }; assemblies.push(currentAssembly); } else { + // Componente com marca >= 1000 (Peça Composta) if (currentAssembly) { + currentAssembly.isComposed = 'SIM'; let compQuant = 1; - let compProfile = ''; + const compDescTokens: string[] = []; let compMaterial = 'A36'; let compLength = 0; let compWeightTotal = 0; - const compTokens = line.map((it) => it.text); - if (compTokens.length >= 2) { - const qTest = parseInt(compTokens[1], 10); - if (!isNaN(qTest)) compQuant = qTest; - } + for (const it of remainingItems) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + const t = it.text.trim(); - let matIndex = -1; - for (let k = 1; k < compTokens.length; k++) { - const tok = compTokens[k].toUpperCase(); - if ( - tok.includes('A572') || - tok.includes('A36') || - tok.includes('A500') || - tok.includes('A106') || - tok.includes('GR') || - tok.includes('INOX') || - tok.includes('SAE') - ) { - matIndex = k; - compMaterial = compTokens[k]; - if (k + 1 < compTokens.length && (compTokens[k + 1].toUpperCase().includes('GR') || compTokens[k + 1] === '50')) { - compMaterial += ' ' + compTokens[k + 1]; - } - break; + if (col === 'qtde') { + const qVal = parseInt(t, 10); + if (!isNaN(qVal)) compQuant = qVal; + } else if (col === 'nome') { + compDescTokens.push(t); + } else if (col === 'qualidade') { + compMaterial = t; + } else if (col === 'comprimento' && valNum > 0) { + compLength = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + compWeightTotal = valNum; } } - if (matIndex > 2) { - compProfile = compTokens.slice(2, matIndex).join(' '); - } else if (compTokens.length >= 3) { - compProfile = compTokens[2]; - } - - const afterMatTokens = matIndex !== -1 ? compTokens.slice(matIndex + 1) : compTokens.slice(3); - const numericValues: string[] = []; - afterMatTokens.forEach((t) => { - const cleanT = t.replace(/[()]/g, ''); - if (/[0-9]/.test(cleanT) && !cleanT.toUpperCase().includes('GR')) { - numericValues.push(cleanT); + // Fallback caso coordenadas não tenham capturado peso total do componente + if (compWeightTotal === 0) { + const nums = remainingItems.map((it) => parseNumeric(it.text)).filter((n) => n > 0); + if (nums.length >= 2) { + compWeightTotal = nums[nums.length - 1]; } - }); - - if (numericValues.length >= 1) { - compLength = parseNumeric(numericValues[0]); - } - if (numericValues.length >= 3) { - compWeightTotal = parseNumeric(numericValues[numericValues.length - 1]); - } else if (numericValues.length === 2) { - compWeightTotal = parseNumeric(numericValues[1]); } currentAssembly.components.push({ quantidade: compQuant, - perfil: compProfile, + perfil: compDescTokens.join(' ') || currentAssembly.descricao, material: compMaterial, comprimento: compLength, pesoTotal: compWeightTotal @@ -374,10 +427,64 @@ const AdvanceSteelConverterContent: React.FC = () => { } } } else { - if (currentAssembly && line.length <= 3) { - const possibleTotal = parseNumeric(line[0].text); - if (possibleTotal > 10 && currentAssembly.pesoTotal === 0) { - currentAssembly.pesoTotal = possibleTotal; + // Linhas sem marca explícita (subitens, parafusos '-' ou linha de subtotal) + if (currentAssembly) { + if (lineText.includes('M13') || lineText.startsWith('-') || lineText.includes('ASTM') || lineText.includes('10.9')) { + currentAssembly.isComposed = 'SIM'; + let compLength = 0; + let compWeightTotal = 0; + let compQuant = 1; + + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qtde') { + const q = parseInt(it.text, 10); + if (!isNaN(q)) compQuant = q; + } else if (col === 'comprimento' && valNum > 0) { + compLength = valNum; + } else if (col === 'totalPeso' && valNum > 0) { + compWeightTotal = valNum; + } + } + + currentAssembly.components.push({ + quantidade: compQuant, + perfil: lineText.split(' ')[0] || 'ACESSÓRIO', + material: '10.9', + comprimento: compLength, + pesoTotal: compWeightTotal + }); + } else if (lineText.includes('W ') || lineText.includes('C ') || lineText.includes('Pl ')) { + // Perfil principal de conjunto composto + let compLength = 0; + let compWeightTotal = 0; + let compMaterial = 'A36'; + + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'qualidade') compMaterial = it.text; + else if (col === 'comprimento' && valNum > 0) compLength = valNum; + else if (col === 'totalPeso' && valNum > 0) compWeightTotal = valNum; + } + + currentAssembly.components.push({ + quantidade: 1, + perfil: lineText.split(' ')[0] + ' ' + (lineText.split(' ')[1] || ''), + material: compMaterial, + comprimento: compLength, + pesoTotal: compWeightTotal + }); + } else { + // Linha com totais do conjunto (ex: 3,5 na coluna Total Peso) + for (const it of line) { + const col = getNearestCol(it.x); + const valNum = parseNumeric(it.text); + if (col === 'totalPeso' && valNum > 0) { + currentAssembly.subtotalPeso = valNum; + } + } } } } @@ -386,14 +493,14 @@ const AdvanceSteelConverterContent: React.FC = () => { addLog(`Total de Peças Principais extraídas: ${assemblies.length}`); const finalPieces: ExtractedPiece[] = assemblies.map((asm) => { - const hasComponents = asm.components.length > 0; - + const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0; let perfilPrincipal = asm.descricao; - let materialPrincipal = 'A36'; - let maxComp = 0; - let sumComponentsWeight = 0; + let materialPrincipal = asm.material || 'A36'; + let maxComp = asm.comprimento || 0; + let totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal; - if (hasComponents) { + if (isComposed) { + let sumComponentsWeight = 0; let bestComp = asm.components[0]; asm.components.forEach((c) => { if (c.comprimento > maxComp) { @@ -403,25 +510,31 @@ const AdvanceSteelConverterContent: React.FC = () => { sumComponentsWeight += c.pesoTotal; }); - perfilPrincipal = bestComp.perfil || asm.descricao; - materialPrincipal = bestComp.material || 'A36'; + if (bestComp) { + perfilPrincipal = bestComp.perfil || asm.descricao; + materialPrincipal = bestComp.material || 'A36'; + } + + if (!totalPeso || totalPeso === 0) { + totalPeso = sumComponentsWeight; + } } - const finalPesoTotal = asm.pesoTotal > 0 ? asm.pesoTotal : sumComponentsWeight; - const finalPesoUnit = asm.quantidade > 0 ? finalPesoTotal / asm.quantidade : finalPesoTotal; + // Regra de Ouro: Peso Unitário = Peso Total dividido pela Quantidade da peça principal + const pesoUnit = asm.quantidade > 0 ? totalPeso / asm.quantidade : totalPeso; return { of: asm.of, fase: asm.fase, marca: asm.marca, descricao: asm.descricao, - isComposed: hasComponents ? 'SIM' : 'NÃO', + isComposed: isComposed ? 'SIM' : 'NÃO', quantidade: asm.quantidade, material: materialPrincipal, perfilPrincipal: perfilPrincipal, comprimentoMax: maxComp > 0 ? maxComp : '-', - pesoUnit: finalPesoUnit > 0 ? finalPesoUnit.toFixed(2) : '-', - pesoTotal: finalPesoTotal > 0 ? finalPesoTotal.toFixed(2) : '-', + pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(2) : '-', + pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : '-', tratamentoSuperficial: 'pintura' }; });