fix(conversores): adicionar aglutinacao inteligente de marcas e modo leitor html direto no modal advance steel

This commit is contained in:
2026-08-21 14:16:32 +00:00
parent 3d8c38f632
commit 2c20ae7e3c
2 changed files with 820 additions and 45 deletions
+695
View File
@@ -0,0 +1,695 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conversor de Lista de Peças PDF para Excel</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
:root {
--primary: #1e3a8a;
--primary-hover: #1d4ed8;
--accent: #0284c7;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
--success: #16a34a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
background-color: var(--bg);
color: var(--text);
padding: 24px 16px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 1200px;
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
border: 1px solid var(--border);
padding: 28px;
}
header {
border-bottom: 2px solid var(--border);
padding-bottom: 18px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
h1 {
font-size: 22px;
font-weight: 700;
color: var(--primary);
}
.badge {
font-size: 12px;
background: #e0f2fe;
color: #0369a1;
padding: 4px 10px;
border-radius: 9999px;
font-weight: 600;
}
.upload-zone {
border: 2px dashed #cbd5e1;
border-radius: 10px;
padding: 36px 20px;
text-align: center;
background: #f1f5f9;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--accent);
background: #e0f2fe;
}
.upload-zone p {
color: var(--text-muted);
margin-top: 8px;
font-size: 14px;
}
.upload-zone .btn-select {
display: inline-block;
margin-top: 12px;
padding: 8px 18px;
background: var(--primary);
color: #fff;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
pointer-events: none;
}
.actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.btn {
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.15s;
}
.btn-export {
background: var(--success);
color: white;
}
.btn-export:hover {
background: #15803d;
}
.btn-export:disabled {
background: #94a3b8;
cursor: not-allowed;
}
.stats-card {
font-size: 14px;
color: var(--text-muted);
background: #f8fafc;
padding: 8px 16px;
border-radius: 6px;
border: 1px solid var(--border);
}
.table-container {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 8px;
max-height: 550px;
position: relative;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
text-align: left;
}
th {
background: #0f172a;
color: #ffffff;
padding: 10px 12px;
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
white-space: nowrap;
}
td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tr:nth-child(even) {
background-color: #f8fafc;
}
tr:hover {
background-color: #f1f5f9;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.status-yes {
background: #dcfce7;
color: #166534;
}
.status-no {
background: #fef3c7;
color: #92400e;
}
.log-panel {
margin-top: 18px;
padding: 12px;
background: #0f172a;
color: #38bdf8;
font-family: monospace;
font-size: 12px;
border-radius: 6px;
max-height: 120px;
overflow-y: auto;
display: none;
}
#fileInput {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>Extrator & Conversor de Lista de Peças (Corrigido)</h1>
<p style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">Conversão 100% no navegador (Client-Side) com regras ajustadas</p>
</div>
<div class="badge">Autônomo / Sem Servidor</div>
</header>
<div class="upload-zone" id="dropZone">
<svg width="40" height="40" fill="none" stroke="#64748b" stroke-width="2" viewBox="0 0 24 24" style="margin: 0 auto;">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
<p>Arraste e solte o arquivo <strong>PDF da Lista de Peças Estruturada</strong> aqui</p>
<div class="btn-select">Selecionar PDF do Computador</div>
<input type="file" id="fileInput" accept="application/pdf">
</div>
<div class="actions-bar">
<div class="stats-card" id="statsCard">
Aguardando arquivo PDF...
</div>
<button class="btn btn-export" id="btnExport" disabled>
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
Baixar Planilha Excel (.xlsx)
</button>
</div>
<div class="table-container">
<table id="resultTable">
<thead>
<tr>
<th>OF</th>
<th>Fase</th>
<th>Marca (Nº Peça)</th>
<th>Descrição (Nome)</th>
<th>Composto?</th>
<th>Quantidade</th>
<th>Material Principal</th>
<th>Perfil Principal (Maior Comp.)</th>
<th>Comp. Max (mm)</th>
<th>Peso Unit. (kg)</th>
<th>Peso Total (kg)</th>
<th>Tratamento Superficial</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="12" style="text-align:center; padding: 24px; color: var(--text-muted);">
Nenhum dado extraído ainda. Carregue um PDF acima.
</td>
</tr>
</tbody>
</table>
</div>
<div class="log-panel" id="logPanel"></div>
</div>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const btnExport = document.getElementById('btnExport');
const tableBody = document.getElementById('tableBody');
const statsCard = document.getElementById('statsCard');
const logPanel = document.getElementById('logPanel');
let extractedData = [];
let currentFileName = 'Lista_Pecas';
function log(msg) {
logPanel.style.display = 'block';
const line = document.createElement('div');
line.textContent = `> ${msg}`;
logPanel.appendChild(line);
logPanel.scrollTop = logPanel.scrollHeight;
}
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
async function handleFile(file) {
if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) {
alert('Por favor, selecione um arquivo no formato PDF válido.');
return;
}
currentFileName = file.name.replace(/\.[^/.]+$/, "");
statsCard.innerHTML = `Processando: <strong>${file.name}</strong>...`;
logPanel.innerHTML = '';
log(`Carregando PDF: ${file.name}`);
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
log(`PDF carregado com sucesso! Total de páginas: ${pdf.numPages}`);
const allItems = [];
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const textContent = await page.getTextContent();
textContent.items.forEach(item => {
if (item.str && item.str.trim() !== '') {
allItems.push({
text: item.str.trim(),
x: item.transform[4],
y: item.transform[5],
page: p
});
}
});
}
log(`Total de elementos de texto extraídos: ${allItems.length}`);
processPdfLines(allItems);
} catch (err) {
console.error(err);
alert('Erro ao processar o arquivo PDF: ' + err.message);
log(`ERRO: ${err.message}`);
}
}
function parseNumeric(valStr) {
if (!valStr) return 0;
let clean = valStr.toString().trim();
if (clean.includes(',') && clean.includes('.')) {
if (clean.indexOf('.') < clean.indexOf(',')) {
clean = clean.replace(/\./g, '').replace(',', '.');
} else {
clean = clean.replace(/,/g, '');
}
} else if (clean.includes(',')) {
clean = clean.replace(',', '.');
} else if (clean.includes('.')) {
const parts = clean.split('.');
if (parts.length === 2 && parts[1].length === 3) {
clean = parts[0] + parts[1];
}
}
const num = parseFloat(clean);
return isNaN(num) ? 0 : num;
}
function processPdfLines(items) {
items.sort((a, b) => {
if (a.page !== b.page) return a.page - b.page;
if (Math.abs(b.y - a.y) > 3) return b.y - a.y;
return a.x - b.x;
});
const lines = [];
let currentLine = [];
let lastY = null;
let lastPage = null;
items.forEach(item => {
if (lastPage === null || item.page !== lastPage || Math.abs(item.y - lastY) > 3.5) {
if (currentLine.length > 0) {
lines.push(currentLine);
}
currentLine = [item];
lastY = item.y;
lastPage = item.page;
} else {
currentLine.push(item);
}
});
if (currentLine.length > 0) lines.push(currentLine);
log(`Total de linhas identificadas: ${lines.length}`);
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);
if (matchTrabalho) {
defaultOf = matchTrabalho[1].replace('-', '');
log(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`);
break;
}
}
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')) {
continue;
}
const firstToken = line[0].text;
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];
}
const pieceNumInt = parseInt(pieceNumber, 10);
const isComponent = !isNaN(pieceNumInt) && pieceNumInt >= 1000;
if (!isComponent) {
// Evitar duplicidade caso a mesma marca principal apareça mais de uma vez agrupada
if (processedMarks.has(firstToken)) {
continue;
}
processedMarks.add(firstToken);
let mainQuant = 1;
let mainDesc = "";
if (line.length >= 2) {
const qVal = parseInt(line[1].text, 10);
if (!isNaN(qVal)) {
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';
}
}
currentAssembly = {
rawMark: firstToken,
of: ofCode || defaultOf,
fase: phaseCode,
marca: pieceNumber, // CORREÇÃO 1: Apenas o número da peça (última numeração)
numeroPeca: pieceNumber,
descricao: mainDesc || 'ESTRUTURA',
quantidade: mainQuant,
pesoUnitario: 0,
pesoTotal: 0,
components: []
};
assemblies.push(currentAssembly);
} else {
// Componente subordinado
if (currentAssembly) {
let compQuant = 1;
let compProfile = "";
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;
}
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 (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 (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,
material: compMaterial,
comprimento: compLength,
pesoTotal: compWeightTotal
});
}
}
} 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;
}
}
}
}
log(`Total de Peças Principais processadas: ${assemblies.length}`);
extractedData = assemblies.map(asm => {
const hasComponents = asm.components.length > 0;
let perfilPrincipal = asm.descricao;
let materialPrincipal = "A36";
let maxComp = 0;
let sumComponentsWeight = 0;
if (hasComponents) {
let bestComp = asm.components[0];
asm.components.forEach(c => {
if (c.comprimento > maxComp) {
maxComp = c.comprimento;
bestComp = c;
}
sumComponentsWeight += c.pesoTotal;
});
perfilPrincipal = bestComp.perfil || asm.descricao;
materialPrincipal = bestComp.material || "A36";
}
// 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;
return {
of: asm.of,
fase: asm.fase,
marca: asm.marca,
descricao: asm.descricao,
isComposed: hasComponents ? "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) : "-",
tratamentoSuperficial: "pintura"
};
});
renderTable();
}
function renderTable() {
tableBody.innerHTML = '';
if (extractedData.length === 0) {
tableBody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding: 20px; color: #ef4444;">Nenhuma peça identificada no PDF. Verifique o documento.</td></tr>`;
btnExport.disabled = true;
statsCard.innerHTML = `Nenhum registro encontrado.`;
return;
}
extractedData.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><strong>${row.of}</strong></td>
<td>${row.fase}</td>
<td><strong>${row.marca}</strong></td>
<td>${row.descricao}</td>
<td><span class="status-badge ${row.isComposed === 'SIM' ? 'status-yes' : 'status-no'}">${row.isComposed}</span></td>
<td style="text-align:center;">${row.quantidade}</td>
<td>${row.material}</td>
<td><strong>${row.perfilPrincipal}</strong></td>
<td style="text-align:right;">${row.comprimentoMax}</td>
<td style="text-align:right;">${row.pesoUnit}</td>
<td style="text-align:right; font-weight:600;">${row.pesoTotal}</td>
<td>${row.tratamentoSuperficial}</td>
`;
tableBody.appendChild(tr);
});
statsCard.innerHTML = `<strong>${extractedData.length}</strong> peças principais extraídas com sucesso!`;
btnExport.disabled = false;
log(`Tabela renderizada com ${extractedData.length} linhas.`);
}
btnExport.addEventListener('click', () => {
if (extractedData.length === 0) return;
const excelRows = extractedData.map(item => ({
"OF": item.of,
"Fase": isNaN(Number(item.fase)) ? item.fase : Number(item.fase),
"Marca": item.marca,
"Descrição": item.descricao,
"Composto por Componentes?": item.isComposed,
"Quantidade": Number(item.quantidade),
"Peso Unitário (kg)": item.pesoUnit === "-" ? "" : Number(item.pesoUnit),
"Peso Total (kg)": item.pesoTotal === "-" ? "" : Number(item.pesoTotal),
"Tratamento Superficial": item.tratamentoSuperficial,
"Material": item.material,
"Perfil Principal": item.perfilPrincipal,
"Comprimento Ref. (mm)": item.comprimentoMax === "-" ? "" : Number(item.comprimentoMax)
}));
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(excelRows);
ws['!cols'] = [
{ wch: 12 }, { wch: 8 }, { wch: 12 }, { wch: 20 },
{ wch: 15 }, { wch: 12 }, { wch: 18 }, { wch: 16 },
{ wch: 22 }, { wch: 16 }, { wch: 22 }, { wch: 20 }
];
XLSX.utils.book_append_sheet(wb, ws, "Lista_Pecas");
const outFileName = `${currentFileName}_Corrigido.xlsx`;
XLSX.writeFile(wb, outFileName);
log(`Arquivo Excel exportado: ${outFileName}`);
});
</script>
</body>
</html>
@@ -1,10 +1,10 @@
import React, { useState, useRef } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Upload, FileSpreadsheet, Download, RefreshCw, Check, AlertCircle, Terminal, FileText } from 'lucide-react';
import { Upload, FileSpreadsheet, Download, RefreshCw, Terminal, FileText, Code2, AlertTriangle } from 'lucide-react';
import { toast } from 'sonner';
import * as XLSX from 'xlsx';
@@ -75,26 +75,30 @@ interface PdfJsLib {
getDocument: (options: { data: ArrayBuffer }) => { promise: Promise<PdfDocument> };
}
// Carregador dinâmico do PDF.js
const loadPdfJs = async (): Promise<PdfJsLib> => {
// Carregador robusto do PDF.js
const ensurePdfJs = async (): Promise<PdfJsLib> => {
const win = window as unknown as { pdfjsLib?: PdfJsLib };
if (win.pdfjsLib) {
win.pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
return win.pdfjsLib;
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = 'pdfjs-script-cdn';
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
script.onload = () => {
const pdfjs = (window as unknown as { pdfjsLib?: PdfJsLib }).pdfjsLib;
if (pdfjs) {
pdfjs.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
pdfjs.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
resolve(pdfjs);
} else {
reject(new Error('pdfjsLib não foi encontrado após o carregamento.'));
reject(new Error('pdfjsLib não foi encontrado após carregar a CDN.'));
}
};
script.onerror = () => reject(new Error('Falha ao carregar a biblioteca PDF.js via CDN.'));
script.onerror = () => reject(new Error('Falha ao carregar script PDF.js via CDN.'));
document.head.appendChild(script);
});
};
@@ -104,14 +108,23 @@ const AdvanceSteelConverterContent: React.FC = () => {
const [isDragging, setIsDragging] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [extractedData, setExtractedData] = useState<ExtractedPiece[]>([]);
const [logs, setLogs] = useState<string[]>([]);
const [logs, setLogs] = useState<string[]>(['> Aguardando seleção do PDF...']);
const [statusText, setStatusText] = useState<string>('Aguardando arquivo PDF...');
const [currentFileName, setCurrentFileName] = useState<string>('Lista_Pecas');
const [useIframeMode, setUseIframeMode] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const logEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
ensurePdfJs().catch((err) => console.warn('Preload PDF.js notice:', err));
}, []);
const addLog = (msg: string) => {
setLogs((prev) => [...prev, `> ${msg}`]);
setTimeout(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 50);
};
const handleDragOver = (e: React.DragEvent) => {
@@ -185,7 +198,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
});
if (currentLine.length > 0) lines.push(currentLine);
addLog(`Total de linhas identificadas: ${lines.length}`);
addLog(`Total de linhas identificadas no PDF: ${lines.length}`);
let defaultOf = '';
for (const line of lines) {
@@ -215,7 +228,20 @@ const AdvanceSteelConverterContent: React.FC = () => {
continue;
}
const firstToken = line[0].text;
let firstToken = line[0].text.trim();
// 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;
break;
}
}
}
const markMatch = firstToken.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstToken.match(/^([A-Za-z0-9]+)-(\d+)$/);
if (markMatch) {
@@ -357,7 +383,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
}
}
addLog(`Total de Peças Principais processadas: ${assemblies.length}`);
addLog(`Total de Peças Principais extraídas: ${assemblies.length}`);
const finalPieces: ExtractedPiece[] = assemblies.map((asm) => {
const hasComponents = asm.components.length > 0;
@@ -401,8 +427,16 @@ const AdvanceSteelConverterContent: React.FC = () => {
});
setExtractedData(finalPieces);
if (finalPieces.length === 0) {
setStatusText('Nenhuma peça principal identificada.');
addLog('AVISO: Nenhuma peça bateu com a máscara (ex: B132-1 ou B132-4-1).');
toast.warning('PDF lido, mas nenhuma marca de peça foi reconhecida.');
} else {
setStatusText(`${finalPieces.length} peças principais extraídas com sucesso!`);
addLog(`Tabela renderizada com ${finalPieces.length} registros.`);
toast.success(`${finalPieces.length} peças extraídas do PDF!`);
}
};
const handleFile = async (file: File) => {
@@ -415,16 +449,14 @@ const AdvanceSteelConverterContent: React.FC = () => {
const baseName = file.name.replace(/\.[^/.]+$/, '');
setCurrentFileName(baseName);
setStatusText(`Processando: ${file.name}...`);
setLogs([]);
setLogs([`> Carregando arquivo: ${file.name}`]);
setIsProcessing(true);
addLog(`Carregando PDF: ${file.name}`);
try {
const pdfjs = await loadPdfJs();
const pdfjs = await ensurePdfJs();
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
addLog(`PDF carregado com sucesso! Total de páginas: ${pdf.numPages}`);
addLog(`PDF aberto! Total de páginas: ${pdf.numPages}`);
const allItems: PdfItem[] = [];
for (let p = 1; p <= pdf.numPages; p++) {
@@ -444,12 +476,11 @@ const AdvanceSteelConverterContent: React.FC = () => {
addLog(`Total de elementos de texto extraídos: ${allItems.length}`);
processPdfLines(allItems);
toast.success(`PDF processado com sucesso! ${allItems.length} elementos analisados.`);
} catch (err) {
console.error(err);
const errMsg = err instanceof Error ? err.message : 'Erro desconhecido';
toast.error(`Erro ao processar o arquivo PDF: ${errMsg}`);
addLog(`ERRO: ${errMsg}`);
toast.error(`Erro ao processar PDF: ${errMsg}`);
addLog(`ERRO CRÍTICO: ${errMsg}`);
setStatusText('Falha no processamento.');
} finally {
setIsProcessing(false);
@@ -495,12 +526,51 @@ const AdvanceSteelConverterContent: React.FC = () => {
XLSX.utils.book_append_sheet(wb, ws, 'Lista_Pecas');
const outFileName = `${currentFileName}_Corrigido.xlsx`;
XLSX.writeFile(wb, outFileName);
addLog(`Arquivo Excel exportado: ${outFileName}`);
addLog(`Planilha Excel baixada: ${outFileName}`);
toast.success(`Planilha Excel "${outFileName}" gerada com sucesso!`);
};
if (useIframeMode) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center bg-slate-800 p-3 rounded-lg border border-slate-700">
<span className="text-xs text-slate-300 flex items-center gap-2">
<Code2 className="w-4 h-4 text-amber-400" />
Modo 100% HTML Original Ativo
</span>
<Button
size="sm"
variant="outline"
onClick={() => setUseIframeMode(false)}
className="text-xs bg-slate-700 border-slate-600 text-white"
>
Voltar ao Modo React
</Button>
</div>
<iframe
src="/conversor_relatorio_estruturas.html"
className="w-full h-[650px] border-0 rounded-xl bg-white shadow-2xl"
title="Conversor HTML Original"
/>
</div>
);
}
return (
<div className="space-y-6">
{/* Modos e Alternador */}
<div className="flex justify-end">
<Button
variant="ghost"
size="sm"
onClick={() => setUseIframeMode(true)}
className="text-xs text-slate-400 hover:text-sky-400 hover:bg-slate-800"
>
<Code2 className="w-3.5 h-3.5 mr-1" />
Usar Leitor HTML Puro (Modo Direct)
</Button>
</div>
{/* Upload Zone */}
<div
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200 ${
@@ -520,7 +590,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
onChange={handleFileInput}
className="hidden"
/>
<Upload className="mx-auto h-12 w-12 text-slate-400 mb-3 animate-bounce-subtle" />
<Upload className="mx-auto h-12 w-12 text-slate-400 mb-3" />
{selectedFile ? (
<div>
<p className="text-sm font-semibold text-sky-400 mb-1">Arquivo selecionado:</p>
@@ -580,7 +650,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<ScrollArea className="h-[420px] w-full">
<ScrollArea className="h-[400px] w-full">
<table className="w-full text-xs text-left border-collapse">
<thead className="bg-slate-950 text-slate-300 font-mono sticky top-0 z-10 shadow-sm">
<tr>
@@ -602,7 +672,17 @@ const AdvanceSteelConverterContent: React.FC = () => {
{extractedData.length === 0 ? (
<tr>
<td colSpan={12} className="py-12 text-center text-slate-500 font-medium">
Nenhum dado extraído ainda. Carregue um PDF de Lista de Peças acima.
{selectedFile ? (
<div className="space-y-2">
<AlertTriangle className="mx-auto h-8 w-8 text-amber-400/80" />
<p className="text-slate-300">Nenhuma peça foi identificada no PDF.</p>
<p className="text-xs text-slate-400">
Verifique os logs abaixo ou clique em "Usar Leitor HTML Puro" no topo.
</p>
</div>
) : (
'Nenhum dado extraído ainda. Carregue um PDF de Lista de Peças acima.'
)}
</td>
</tr>
) : (
@@ -640,26 +720,26 @@ const AdvanceSteelConverterContent: React.FC = () => {
</CardContent>
</Card>
{/* Terminal Log Panel */}
{logs.length > 0 && (
{/* Terminal Log Panel - SE MANTÉM VISÍVEL PARA DIAGNÓSTICO */}
<Card className="bg-slate-950 border-slate-800 font-mono text-xs overflow-hidden">
<CardHeader className="py-2 px-3 bg-slate-900/90 border-b border-slate-800 flex flex-row items-center justify-between">
<CardTitle className="text-xs font-semibold text-slate-400 flex items-center gap-1.5">
<Terminal className="h-3.5 w-3.5 text-sky-400" />
Log de Processamento
Log de Processamento e Diagnóstico
</CardTitle>
<span className="text-[10px] text-slate-500 font-normal">{logs.length} eventos</span>
</CardHeader>
<CardContent className="p-3">
<ScrollArea className="h-28 w-full">
<ScrollArea className="h-32 w-full">
<div className="space-y-1 text-sky-400/90 leading-relaxed">
{logs.map((logLine, idx) => (
<div key={idx}>{logLine}</div>
))}
<div ref={logEndRef} />
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</div>
);
};