diff --git a/conversor_relatorio_estruturas.html b/conversor_relatorio_estruturas.html new file mode 100644 index 0000000..8e97b2c --- /dev/null +++ b/conversor_relatorio_estruturas.html @@ -0,0 +1,695 @@ + + + + + + Conversor de Lista de Peças PDF para Excel + + + + + + +
+
+
+

Extrator & Conversor de Lista de Peças (Corrigido)

+

Conversão 100% no navegador (Client-Side) com regras ajustadas

+
+
Autônomo / Sem Servidor
+
+ +
+ + + +

Arraste e solte o arquivo PDF da Lista de Peças Estruturada aqui

+
Selecionar PDF do Computador
+ +
+ +
+
+ Aguardando arquivo PDF... +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
OFFaseMarca (Nº Peça)Descrição (Nome)Composto?QuantidadeMaterial PrincipalPerfil Principal (Maior Comp.)Comp. Max (mm)Peso Unit. (kg)Peso Total (kg)Tratamento Superficial
+ Nenhum dado extraído ainda. Carregue um PDF acima. +
+
+ +
+
+ + + + diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 2085289..7830485 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -1,16 +1,118 @@ - -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Upload, FileSpreadsheet } from 'lucide-react'; +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 { toast } from 'sonner'; import * as XLSX from 'xlsx'; -const AdvanceSteelConverter: React.FC = () => { +export interface ExtractedPiece { + of: string; + fase: string; + marca: string; + descricao: string; + isComposed: 'SIM' | 'NÃO'; + quantidade: number; + material: string; + perfilPrincipal: string; + comprimentoMax: number | string; + pesoUnit: number | string; + pesoTotal: number | string; + tratamentoSuperficial: string; +} + +interface ComponentItem { + quantidade: number; + perfil: string; + material: string; + comprimento: number; + pesoTotal: number; +} + +interface AssemblyItem { + rawMark: string; + of: string; + fase: string; + marca: string; + numeroPeca: string; + descricao: string; + quantidade: number; + pesoUnitario: number; + pesoTotal: number; + components: ComponentItem[]; +} + +interface PdfItem { + text: string; + x: number; + y: number; + page: number; +} + +interface AdvanceSteelConverterProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +interface PdfTextItem { + str: string; + transform: number[]; +} + +interface PdfPage { + getTextContent: () => Promise<{ items: PdfTextItem[] }>; +} + +interface PdfDocument { + numPages: number; + getPage: (pageNumber: number) => Promise; +} + +interface PdfJsLib { + GlobalWorkerOptions: { workerSrc: string }; + getDocument: (options: { data: ArrayBuffer }) => { promise: Promise }; +} + +// Carregador dinâmico do PDF.js +const loadPdfJs = async (): Promise => { + const win = window as unknown as { pdfjsLib?: PdfJsLib }; + if (win.pdfjsLib) { + return win.pdfjsLib; + } + + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + 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'; + resolve(pdfjs); + } else { + reject(new Error('pdfjsLib não foi encontrado após o carregamento.')); + } + }; + script.onerror = () => reject(new Error('Falha ao carregar a biblioteca PDF.js via CDN.')); + document.head.appendChild(script); + }); +}; + +const AdvanceSteelConverterContent: React.FC = () => { const [selectedFile, setSelectedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); - const [divideBy1000, setDivideBy1000] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [extractedData, setExtractedData] = useState([]); + const [logs, setLogs] = useState([]); + const [statusText, setStatusText] = useState('Aguardando arquivo PDF...'); + const [currentFileName, setCurrentFileName] = useState('Lista_Pecas'); + + const fileInputRef = useRef(null); + + const addLog = (msg: string) => { + setLogs((prev) => [...prev, `> ${msg}`]); + }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -25,9 +127,8 @@ const AdvanceSteelConverter: React.FC = () => { const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); - const files = e.dataTransfer.files; - if (files.length > 0) { - handleFile(files[0]); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFile(e.dataTransfer.files[0]); } }; @@ -37,287 +138,588 @@ const AdvanceSteelConverter: React.FC = () => { } }; - const handleFile = (file: File) => { - const validTypes = [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.ms-excel' - ]; - - if (!validTypes.includes(file.type)) { - toast.error('Por favor, selecione um arquivo Excel (.xlsx ou .xls)'); - return; + const parseNumeric = (valStr: string | number): number => { + 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]; + } } - - setSelectedFile(file); - toast.success('Arquivo selecionado com sucesso!'); + const num = parseFloat(clean); + return isNaN(num) ? 0 : num; }; - const processFile = () => { - if (!selectedFile) { - toast.error('Por favor, selecione um arquivo primeiro.'); + const processPdfLines = (items: PdfItem[]) => { + 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: PdfItem[][] = []; + let currentLine: PdfItem[] = []; + let lastY: number | null = null; + let lastPage: number | null = null; + + items.forEach((item) => { + if (lastPage === null || item.page !== lastPage || Math.abs(item.y - (lastY ?? item.y)) > 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); + + addLog(`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(/-/g, ''); + addLog(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); + break; + } + } + + const assemblies: AssemblyItem[] = []; + let currentAssembly: AssemblyItem | null = null; + const processedMarks = new Set(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineText = line.map((it) => it.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) { + 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; + 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'; + } + } + + currentAssembly = { + rawMark: firstToken, + of: ofCode || defaultOf, + fase: phaseCode, + marca: pieceNumber, + numeroPeca: pieceNumber, + descricao: mainDesc || 'ESTRUTURA', + quantidade: mainQuant, + pesoUnitario: 0, + pesoTotal: 0, + components: [] + }; + assemblies.push(currentAssembly); + } else { + if (currentAssembly) { + let compQuant = 1; + let compProfile = ''; + 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; + } + + 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: string[] = []; + 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]); + } + 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 { + if (currentAssembly && line.length <= 3) { + const possibleTotal = parseNumeric(line[0].text); + if (possibleTotal > 10 && currentAssembly.pesoTotal === 0) { + currentAssembly.pesoTotal = possibleTotal; + } + } + } + } + + addLog(`Total de Peças Principais processadas: ${assemblies.length}`); + + const finalPieces: ExtractedPiece[] = 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'; + } + + const finalPesoTotal = asm.pesoTotal > 0 ? asm.pesoTotal : sumComponentsWeight; + const 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' + }; + }); + + setExtractedData(finalPieces); + setStatusText(`${finalPieces.length} peças principais extraídas com sucesso!`); + addLog(`Tabela renderizada com ${finalPieces.length} registros.`); + }; + + const handleFile = async (file: File) => { + if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) { + toast.error('Por favor, selecione um arquivo no formato PDF válido.'); return; } - const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = new Uint8Array(e.target?.result as ArrayBuffer); - const workbook = XLSX.read(data, { type: 'array' }); - const firstSheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheetName]; - const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; + setSelectedFile(file); + const baseName = file.name.replace(/\.[^/.]+$/, ''); + setCurrentFileName(baseName); + setStatusText(`Processando: ${file.name}...`); + setLogs([]); + setIsProcessing(true); - const newSheetData: any[][] = []; - newSheetData.push(['Marca', 'Qtde', 'Descrição', 'Mat.', 'Comp.', 'Larg.', 'P.Un.', 'P.Tot.']); - - let fileNamePrefix: string | null = null; - const processedMarks = new Set(); + addLog(`Carregando PDF: ${file.name}`); - for (let i = 0; i < json.length; i++) { - const row = json[i]; - if (!row || row.length === 0) continue; + try { + const pdfjs = await loadPdfJs(); + const arrayBuffer = await file.arrayBuffer(); + const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise; + addLog(`PDF carregado com sucesso! Total de páginas: ${pdf.numPages}`); - const marcaCompleta = String(row[0] || ''); - - // Primeiro tenta o formato com fase: B118-4-2 - let mainMarkMatch = marcaCompleta.match(/^(B\d+)-(\d+)-(\d+)$/); - let prefixo: string; - let numeroMarca: number; - - if (mainMarkMatch) { - // Formato com fase: B118-4-2 - const ofNumber = mainMarkMatch[1]; // B118 - const faseNumber = mainMarkMatch[2]; // 4 - const marcaNumber = mainMarkMatch[3]; // 2 - prefixo = `${ofNumber}-`; // B118- - numeroMarca = parseInt(marcaNumber, 10); // 2 - } else { - // Tenta o formato sem fase: B118-2 - mainMarkMatch = marcaCompleta.match(/^(B\d+-)(\d+)$/); - if (mainMarkMatch) { - prefixo = mainMarkMatch[1]; // B118- - numeroMarca = parseInt(mainMarkMatch[2], 10); // 2 - } + const allItems: PdfItem[] = []; + 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 + }); } - - if (mainMarkMatch) { - - if (numeroMarca >= 999 || processedMarks.has(numeroMarca)) { - continue; - } - processedMarks.add(numeroMarca); - - if (!fileNamePrefix) { - fileNamePrefix = prefixo; - } - - const qtde = row[5]; - const descricao = row[6]; - const larg = row[14]; - - let mat = ''; - let compRaw = ''; - let maxComp = 0; - - // Busca o maior comprimento entre todas as sub-linhas da marca - for (let j = i + 1; j < json.length; j++) { - const subRow = json[j]; - if (!subRow || !subRow[0]) continue; - - const subMarkCompleta = String(subRow[0]); - - // Verifica formato com fase: B118-4-2 - let subMarkMatch = subMarkCompleta.match(/^(B\d+)-(\d+)-(\d+)$/); - let subNumeroMarca: number | null = null; - - if (subMarkMatch) { - // Formato com fase - subNumeroMarca = parseInt(subMarkMatch[3], 10); - } else { - // Verifica formato sem fase: B118-2 - subMarkMatch = subMarkCompleta.match(/^(B\d+-)(\d+)$/); - if (subMarkMatch) { - subNumeroMarca = parseInt(subMarkMatch[2], 10); - } - } - - // Se encontrou uma nova marca principal, para a busca - if (subMarkMatch && subNumeroMarca !== null && subNumeroMarca < 999 && !processedMarks.has(subNumeroMarca)) { - break; - } - - // Se é uma sub-linha da marca atual, verifica o comprimento - if (subMarkMatch) { - const currentComp = Number(String(subRow[13] || 0).replace(',', '.')); - if (!isNaN(currentComp) && currentComp > maxComp) { - maxComp = currentComp; - mat = subRow[12]; - compRaw = subRow[13]; - } - } - } - - let pTotSum = 0; - for (let j = i + 1; j < json.length; j++) { - const subRow = json[j]; - if (!subRow || !subRow[0]) continue; - - const subMarkCompleta = String(subRow[0]); - - // Verifica formato com fase: B118-4-2 - let subMarkMatch = subMarkCompleta.match(/^(B\d+)-(\d+)-(\d+)$/); - let subNumeroMarca: number | null = null; - - if (subMarkMatch) { - // Formato com fase - subNumeroMarca = parseInt(subMarkMatch[3], 10); - } else { - // Verifica formato sem fase: B118-2 - subMarkMatch = subMarkCompleta.match(/^(B\d+-)(\d+)$/); - if (subMarkMatch) { - subNumeroMarca = parseInt(subMarkMatch[2], 10); - } - } - - if (subMarkMatch && subNumeroMarca !== null && subNumeroMarca < 999 && !processedMarks.has(subNumeroMarca)) { - break; - } - - if (subMarkMatch) { - const pTotSubItemRaw = Number(String(subRow[16] || 0).replace(',', '.')); - const pTotSubItemValue = divideBy1000 ? pTotSubItemRaw / 1000 : pTotSubItemRaw; - if (!isNaN(pTotSubItemValue)) { - pTotSum += pTotSubItemValue; - } - } - } - - const qtdeValue = Number(String(qtde || 0).replace(',', '.')); - const compValue = Number(String(compRaw || 0).replace(',', '.')); - - let pUnCalculated = 0; - if (qtdeValue !== 0) { - pUnCalculated = pTotSum / qtdeValue; - } - - const compRounded = Math.round(compValue); - const pUnRounded = Math.round(pUnCalculated); - const pTotRounded = Math.round(pTotSum); - - newSheetData.push([ - numeroMarca, - qtde, - descricao, - mat, - isNaN(compRounded) ? '' : compRounded, - larg, - isNaN(pUnRounded) ? '' : pUnRounded, - isNaN(pTotRounded) ? '' : pTotRounded - ]); - } - } - - if (newSheetData.length <= 1) { - throw new Error("Nenhuma linha válida foi encontrada para conversão."); - } - if (!fileNamePrefix) { - throw new Error("Não foi possível determinar o prefixo para o nome do arquivo."); - } - - const newWorksheet = XLSX.utils.aoa_to_sheet(newSheetData); - const newWorkbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, 'Lista de Peças'); - - const newFileName = `${fileNamePrefix}Lista de Peças.xlsx`; - XLSX.writeFile(newWorkbook, newFileName); - - toast.success('Arquivo convertido e baixado com sucesso!'); - setSelectedFile(null); - - } catch (error) { - console.error('Erro no processamento:', error); - toast.error(`Erro ao processar o arquivo: ${(error as Error).message}`); + }); } - }; - reader.onerror = () => { - toast.error('Não foi possível ler o arquivo.'); - }; + 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}`); + setStatusText('Falha no processamento.'); + } finally { + setIsProcessing(false); + } + }; - reader.readAsArrayBuffer(selectedFile); + const exportToExcel = () => { + 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); + addLog(`Arquivo Excel exportado: ${outFileName}`); + toast.success(`Planilha Excel "${outFileName}" gerada com sucesso!`); }; return ( - - - - - Conversor Advance Steel - -

- Converta a "Lista de Peças - Estruturada" para o formato final. -

-
- -
document.getElementById('advance-file-input')?.click()} - > - - - {selectedFile ? ( -
-

- Arquivo selecionado: -

-

{selectedFile.name}

-
- ) : ( -
-

- Clique para carregar ou arraste e solte a planilha -

-

- Formatos suportados: .xlsx, .xls -

+
+ {/* Upload Zone */} +
fileInputRef.current?.click()} + > + + + {selectedFile ? ( +
+

Arquivo selecionado:

+

{selectedFile.name}

+

+ ({(selectedFile.size / 1024).toFixed(1)} KB) — Clique para trocar +

+
+ ) : ( +
+

+ Arraste e solte o arquivo PDF da Lista de Peças Estruturada aqui +

+

Suporta relatórios originais do Advance Steel em PDF

+ +
+ )} +
+ + {/* Actions & Status Bar */} +
+
+ + {statusText} + + {isProcessing && ( +
+ + Processando PDF...
)}
-
- setDivideBy1000(checked === true)} - /> - -
- +
+ + {/* Table Preview */} + + + + + Peças Extraídas ({extractedData.length}) + + + + + + + + + + + + + + + + + + + + + + + {extractedData.length === 0 ? ( + + + + ) : ( + extractedData.map((row, idx) => ( + + + + + + + + + + + + + + + )) + )} + +
OFFaseMarcaDescriçãoComposto?QtdMaterialPerfil PrincipalComp. Max (mm)Peso Unit. (kg)Peso Total (kg)Tratamento
+ Nenhum dado extraído ainda. Carregue um PDF de Lista de Peças acima. +
{row.of}{row.fase}{row.marca}{row.descricao} + + {row.isComposed} + + {row.quantidade}{row.material}{row.perfilPrincipal}{row.comprimentoMax}{row.pesoUnit}{row.pesoTotal}{row.tratamentoSuperficial}
+
+
+
+ + {/* Terminal Log Panel */} + {logs.length > 0 && ( + + + + + Log de Processamento + + + + +
+ {logs.map((logLine, idx) => ( +
{logLine}
+ ))} +
+
+
+
+ )} +
+ ); +}; + +export const AdvanceSteelConverterModal: React.FC = ({ open, onOpenChange }) => { + if (open === undefined || onOpenChange === undefined) return null; + + return ( + + + +
+
+ + + Conversor Advance Steel (PDF para Excel) + + + Conversão 100% no navegador (Client-Side) de relatórios estruturados do Advance Steel em PDF para Excel (.xlsx) + +
+ + Autônomo / Sem Servidor + +
+
+ +
+ +
+
+
+ ); +}; + +const AdvanceSteelConverter: React.FC = ({ open, onOpenChange }) => { + if (open !== undefined && onOpenChange !== undefined) { + return ; + } + + return ( + + +
+
+ + + Conversor Advance Steel (PDF para Excel) + +

+ Conversão 100% no navegador (Client-Side) de relatórios estruturados em PDF para Excel (.xlsx) +

+
+ + Autônomo / Sem Servidor + +
+
+ +
); diff --git a/src/pages/ConversoresDados.tsx b/src/pages/ConversoresDados.tsx index 358011c..71e6e5e 100644 --- a/src/pages/ConversoresDados.tsx +++ b/src/pages/ConversoresDados.tsx @@ -174,22 +174,7 @@ const ConversoresDados = () => { ); } - if (showAdvanceSteelConverter) { - return ( -
-
- -
- -
- ); - } + return (
@@ -373,6 +358,12 @@ const ConversoresDados = () => { open={showGenericConverter} onOpenChange={setShowGenericConverter} /> + + {/* Modal de Conversão Advance Steel (PDF para Excel) */} +
); };