diff --git a/public/conversor_tecnometal.html b/public/conversor_tecnometal.html new file mode 100644 index 0000000..829dd85 --- /dev/null +++ b/public/conversor_tecnometal.html @@ -0,0 +1,692 @@ + + + + + + Conversor Tecnometal (PDF para Excel) + + + + + + +
+
+
+

Conversor Tecnometal (PDF para Excel)

+

Leitura e conversão 100% no navegador de Listas de Conjuntos Tecnometal

+
+
Autônomo / Client-Side
+
+ +
+

Arraste e solte o arquivo PDF da Lista de Conjuntos Tecnometal aqui

+
Selecionar PDF do Computador
+ +
+ +
+
+ Aguardando arquivo PDF... +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
OFFaseMarcaDescriçãoComposto?QtdMaterialPerfil PrincipalComp. Max (mm)Peso Unit. (kg)Peso Total (kg)Tratamento
+ Nenhum dado extraído ainda. Selecione um arquivo PDF acima. +
+
+ +
+
> Sistema pronto. Aguardando arquivo...
+
+
+ + + + diff --git a/src/components/conversores/TecnometalConverter.tsx b/src/components/conversores/TecnometalConverter.tsx index 8f761eb..5ad8aaa 100644 --- a/src/components/conversores/TecnometalConverter.tsx +++ b/src/components/conversores/TecnometalConverter.tsx @@ -1,601 +1,784 @@ -import React, { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import React, { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Input } from '@/components/ui/input'; -import { Upload, FileText, Download, Trash2, Plus, RefreshCw, Settings, CheckCircle, Clock } from 'lucide-react'; +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, Terminal, FileText, AlertTriangle } from 'lucide-react'; import { toast } from 'sonner'; import * as XLSX from 'xlsx'; -import { jsonCodeManager } from '@/utils/jsonCodeManager'; -import { apiKeyManager } from '@/utils/apiKeyManager'; -import { useWebhookConfigs } from '@/hooks/useWebhookConfigs'; -import WebhookConfigManager from './WebhookConfigManager'; -interface ProcessedRow { - of_number: string; - etapa_fase: string; +export interface ExtractedPiece { + of: string; + fase: string; marca: string; descricao: string; - quantidade: string; - peso_unitario: string; - peso_total: string; - tratamento_superficial: string; + isComposed: 'SIM' | 'NÃO'; + quantidade: number; material: string; - perfil_principal: string; - tem_componentes: string; - marca_componente: string; - descricao_componente: string; - perfil_componente: string; - peso_unitario_componente: string; - quantidade_por_peca: string; + perfilPrincipal: string; + comprimentoMax: number | string; + pesoUnit: number | string; + pesoTotal: number | string; + tratamentoSuperficial: string; } -const TecnometalConverter: React.FC = () => { - const [selectedFile, setSelectedFile] = useState(null); - const [isProcessing, setIsProcessing] = useState(false); - const [processedData, setProcessedData] = useState([]); - const [showTable, setShowTable] = useState(false); - const [editingCell, setEditingCell] = useState<{ row: number; col: string } | null>(null); - const [debugInfo, setDebugInfo] = useState(''); - const [tecnometalConfig, setTecnometalConfig] = useState(null); - const [showWebhookConfig, setShowWebhookConfig] = useState(false); - const [selectedWebhookConfig, setSelectedWebhookConfig] = useState(null); - const [currentProcessing, setCurrentProcessing] = useState(null); - const [isMonitoring, setIsMonitoring] = useState(false); +interface ComponentItem { + pos: string; + descricao: string; + quantidade: number; + comprimento: number; + material: string; + pesoUnitario: number; + pesoTotal: number; +} - const { createFileProcessing, updateFileProcessing, fileProcessings } = useWebhookConfigs(); +interface AssemblyItem { + rawMark: string; + of: string; + fase: string; + marca: string; + descricao: string; + quantidade: number; + material: string; + perfilPrincipal: string; + comprimento: number; + pesoUnitario: number; + pesoTotal: number; + isComposed: 'SIM' | 'NÃO'; + components: ComponentItem[]; +} - const columnHeaders = [ - 'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade', - 'peso_unitario', 'peso_total', 'tratamento_superficial', 'material', - 'perfil_principal', 'tem_componentes', 'marca_componente', 'descricao_componente', - 'perfil_componente', 'peso_unitario_componente', 'quantidade_por_peca' - ]; +interface PdfItem { + text: string; + x: number; + y: number; + page: number; +} - const columnLabels = { - of_number: 'OF', - etapa_fase: 'Fase', - marca: 'Marca', - descricao: 'Descrição', - quantidade: 'Qtd', - peso_unitario: 'Peso Un.', - peso_total: 'Peso Total', - tratamento_superficial: 'Tratamento', - material: 'Material', - perfil_principal: 'Perfil Principal', - tem_componentes: 'Tem Comp.', - marca_componente: 'Marca Comp.', - descricao_componente: 'Desc. Comp.', - perfil_componente: 'Perfil Comp.', - peso_unitario_componente: 'Peso Un. Comp.', - quantidade_por_peca: 'Qtd por Peça' - }; +interface TecnometalConverterProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; +} - useEffect(() => { - const loadTecnometalConfig = async () => { - try { - const config = await jsonCodeManager.getJsonCodeByName('Tecnometal'); - if (config) { - setTecnometalConfig(config); - setDebugInfo(prev => prev + 'Configuração Tecnometal carregada com sucesso!\n'); - } else { - setDebugInfo(prev => prev + 'AVISO: Configuração "Tecnometal" não encontrada no sistema.\n'); - } - } catch (error) { - console.error('Erro ao carregar configuração Tecnometal:', error); - setDebugInfo(prev => prev + `ERRO ao carregar configuração: ${error}\n`); +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 robusto do PDF.js +const ensurePdfJs = async (): Promise => { + 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'; + resolve(pdfjs); + } else { + reject(new Error('pdfjsLib não foi encontrado após carregar a CDN.')); } }; + script.onerror = () => reject(new Error('Falha ao carregar script PDF.js via CDN.')); + document.head.appendChild(script); + }); +}; - loadTecnometalConfig(); +const normalizeMaterial = (rawMat: string): string => { + if (!rawMat) return 'A36'; + const clean = rawMat.trim().toUpperCase(); + if (clean.includes('A572') || clean.includes('A-572') || clean.includes('A572GR5') || clean.includes('GR5')) { + return 'A572-GR 50'; + } + if (clean.includes('A36') || clean.includes('A-36')) { + return 'A36'; + } + if (clean.includes('1020') || clean.includes('SAE1020') || clean.includes('SAE 1020')) { + return 'SAE 1020'; + } + return clean; +}; + +const TecnometalConverterContent: React.FC = () => { + const [selectedFile, setSelectedFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [extractedData, setExtractedData] = useState([]); + const [logs, setLogs] = useState(['> Aguardando seleção da lista PDF do Tecnometal...']); + const [statusText, setStatusText] = useState('Aguardando arquivo PDF...'); + const [currentFileName, setCurrentFileName] = useState('Lista_Tecnometal'); + + const fileInputRef = useRef(null); + const logEndRef = useRef(null); + + useEffect(() => { + ensurePdfJs().catch((err) => console.warn('Preload PDF.js notice:', err)); }, []); - const handleFileSelect = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - const allowedTypes = [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.ms-excel', - 'application/pdf' - ]; - - if (allowedTypes.includes(file.type)) { - setSelectedFile(file); - setDebugInfo(''); - setShowTable(false); - setProcessedData([]); - setCurrentProcessing(null); - toast.success('Arquivo selecionado com sucesso!'); + const addLog = (msg: string) => { + setLogs((prev) => [...prev, `> ${msg}`]); + setTimeout(() => { + logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 50); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFile(e.dataTransfer.files[0]); + } + }; + + const handleFileInput = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + handleFile(e.target.files[0]); + } + }; + + const parseNumeric = (valStr: string | number): number => { + if (!valStr && valStr !== 0) return 0; + let clean = valStr.toString().trim(); + if (clean.includes(',') && clean.includes('.')) { + if (clean.indexOf('.') < clean.indexOf(',')) { + clean = clean.replace(/\./g, '').replace(',', '.'); } else { - toast.error('Tipo de arquivo não suportado. Use .xlsx, .xls ou .pdf'); + 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; }; - const sendToWebhook = async (file: File, webhookConfig: any) => { - try { - const formData = new FormData(); - formData.append('file', file); + const processPdfLines = (items: PdfItem[]) => { + // 1. Agrupar em linhas ordenadas por página, Y descendente e X ascendente + 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 response = await fetch(webhookConfig.link_envio, { - method: 'POST', - body: formData, - }); + const lines: PdfItem[][] = []; + let currentLine: PdfItem[] = []; + let lastY: number | null = null; + let lastPage: number | null = null; - if (!response.ok) { - throw new Error(`Erro no webhook: ${response.status} ${response.statusText}`); + 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); - return await response.json(); - } catch (error) { - console.error('Erro ao enviar para webhook:', error); - throw error; - } - }; + addLog(`Total de linhas identificadas no PDF: ${lines.length}`); - const checkWebhookStatus = async (webhookConfig: any, processingId: string) => { - try { - const response = await fetch(`${webhookConfig.link_recebimento}/${processingId}`, { - method: 'GET', - }); + // 2. Extrair OF e Fase do título/cabeçalho (Ex: "B121-FASE4-LISTA DE CONJUNTOS-R00") + let defaultOf = ''; + let defaultFase = '1'; - if (!response.ok) { - throw new Error(`Erro ao verificar status: ${response.status}`); + for (const line of lines) { + const lineText = line.map((i) => i.text).join(' '); + const matchTitulo = + lineText.match(/^([A-Za-z0-9]+)-FASE\s*(\d+)/i) || + lineText.match(/([A-Za-z0-9]+)-FASE\s*(\d+)/i) || + lineText.match(/([A-Za-z0-9]+)-F(\d+)/i); + if (matchTitulo) { + defaultOf = matchTitulo[1]; + defaultFase = matchTitulo[2]; + addLog(`OF detectada no cabeçalho: ${defaultOf} | Fase: ${defaultFase}`); + break; } - - return await response.json(); - } catch (error) { - console.error('Erro ao verificar status:', error); - return null; } - }; - const startMonitoring = async (processingId: string) => { - if (!selectedWebhookConfig) return; + // 3. Calibração das colunas X padrão do Tecnometal + const colX = { + marca: 35, + pos: 95, + descricao: 175, + qtde: 255, + largura: 300, + espessura: 340, + comprimento: 395, + material: 450, + pesoUnit: 510, + pesoTotal: 570, + superficieTotal: 640 + }; - setIsMonitoring(true); - - const monitorInterval = setInterval(async () => { - const status = await checkWebhookStatus(selectedWebhookConfig, processingId); - - if (status) { - if (status.completed) { - await updateFileProcessing(processingId, { - status: 'concluido', - completed_at: new Date().toISOString(), - download_url: status.download_url - }); - - setCurrentProcessing({ - ...currentProcessing, - status: 'concluido', - download_url: status.download_url - }); - - setIsMonitoring(false); - clearInterval(monitorInterval); - toast.success('Arquivo processado e disponível para download!'); - } else if (status.error) { - await updateFileProcessing(processingId, { - status: 'erro' - }); - - setIsMonitoring(false); - clearInterval(monitorInterval); - toast.error('Erro no processamento do arquivo'); + // Recalibração dinâmica baseada no cabeçalho da tabela + for (const line of lines) { + const lineText = line.map((i) => i.text).join(' ').toLowerCase(); + if (lineText.includes('marca') && lineText.includes('pos')) { + for (const item of line) { + const t = item.text.toLowerCase().trim(); + if (t === 'marca') colX.marca = item.x; + else if (t.startsWith('pos')) colX.pos = item.x; + else if (t.startsWith('descr')) colX.descricao = item.x; + else if (t.startsWith('qtd')) colX.qtde = item.x; + else if (t.startsWith('lar')) colX.largura = item.x; + else if (t.startsWith('esp')) colX.espessura = item.x; + else if (t.startsWith('comp')) colX.comprimento = item.x; + else if (t.startsWith('mat')) colX.material = item.x; + else if (t.startsWith('p.un') || t === 'p.un.') colX.pesoUnit = item.x; + else if (t.startsWith('p.tot') || t === 'p.tot.') colX.pesoTotal = item.x; + else if (t.startsWith('s.tot')) colX.superficieTotal = item.x; } } - }, 5000); // Verifica a cada 5 segundos + } - // Para o monitoramento após 10 minutos - setTimeout(() => { - clearInterval(monitorInterval); - setIsMonitoring(false); - }, 600000); + 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; + + 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 CONJUNTOS') || + lineText.includes('Marca Pos. Descrição') || + lineText.includes('P.Un. P.Tot. S.Tot.') || + lineText.includes('B121-FASE') + ) { + continue; + } + + // Mapear elementos da linha nas colunas correspondentes (descartando S.Tot) + const rowByCol: Record = {}; + for (const it of line) { + const col = getNearestCol(it.x); + if (col === 'superficieTotal') continue; // S.Tot riscado / ignorado + + if (!rowByCol[col]) { + rowByCol[col] = it.text.trim(); + } else { + rowByCol[col] += ' ' + it.text.trim(); + } + } + + const rawMarca = (rowByCol.marca || '').trim(); + const rawPos = (rowByCol.pos || '').trim(); + + // Testa se rawMarca contém uma marca principal (ex: B121-4-1, B121-4-10, B121-4-11) + const markMatch = rawMarca.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || rawMarca.match(/^([A-Za-z0-9]+)-(\d+)$/); + + if (markMatch) { + const parts = rawMarca.split('-'); + const ofCode = parts.length === 3 ? parts[0] : defaultOf; + const phaseCode = parts.length === 3 ? parts[1] : defaultFase; + const pieceNumber = parts.length === 3 ? parts[2] : parts[1]; + + const qVal = parseInt(rowByCol.qtde || '1', 10); + const mainQuant = isNaN(qVal) || qVal <= 0 ? 1 : qVal; + + const pUnVal = parseNumeric(rowByCol.pesoUnit || 0); + const pTotVal = parseNumeric(rowByCol.pesoTotal || 0); + const compVal = parseNumeric(rowByCol.comprimento || 0); + const matVal = rowByCol.material ? normalizeMaterial(rowByCol.material) : ''; + + // Se a coluna Pos é igual à Marca ou se tem Comprimento e Material preenchidos na linha: + const isSimple = rawPos === rawMarca || (compVal > 0 && !!matVal); + + currentAssembly = { + rawMark: rawMarca, + of: ofCode || defaultOf, + fase: phaseCode || defaultFase, + marca: pieceNumber, + descricao: rowByCol.descricao || 'ESTRUTURA', + quantidade: mainQuant, + material: matVal, + perfilPrincipal: rowByCol.descricao || 'ESTRUTURA', + comprimento: compVal, + pesoUnitario: pUnVal, + pesoTotal: pTotVal, + isComposed: isSimple ? 'NÃO' : 'NÃO', + components: [] + }; + + assemblies.push(currentAssembly); + } else if (rawPos && currentAssembly) { + // Linha de componente subordinado (Marca vazia, Pos preenchida com marca filha ex: B121-4-1037) + currentAssembly.isComposed = 'SIM'; + + const compQuant = parseInt(rowByCol.qtde || '1', 10) || 1; + const compLength = parseNumeric(rowByCol.comprimento || 0); + const compMaterial = rowByCol.material ? normalizeMaterial(rowByCol.material) : 'A36'; + const compPUn = parseNumeric(rowByCol.pesoUnit || 0); + const compPTot = parseNumeric(rowByCol.pesoTotal || 0); + + currentAssembly.components.push({ + pos: rawPos, + descricao: rowByCol.descricao || '', + quantidade: compQuant, + comprimento: compLength, + material: compMaterial, + pesoUnitario: compPUn, + pesoTotal: compPTot + }); + } + } + + addLog(`Total de Peças Principais Tecnometal extraídas: ${assemblies.length}`); + + // 4. Formatação Final das Peças + const finalPieces: ExtractedPiece[] = assemblies.map((asm) => { + const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0; + let perfilPrincipal = asm.descricao; + let materialPrincipal = asm.material || 'A36'; + let maxComp = asm.comprimento || 0; + let pesoUnit = asm.pesoUnitario; + let pesoTot = asm.pesoTotal; + + if (isComposed) { + // REGRA DE OURO TECNOMETAL: + // "para peças que tem varios componentes, quantidade, descricao (mesmo que perfil principal), + // peso unitario e peso total estarao na mesma linha. Para o comprimento e o material é necessario + // ver na lista de componentes aquele com maior comprimento e respectivamente na mesma linha obter tambem o material." + let bestComp = asm.components[0]; + maxComp = bestComp.comprimento; + + for (const comp of asm.components) { + if (comp.comprimento > maxComp) { + maxComp = comp.comprimento; + bestComp = comp; + } + } + + if (bestComp) { + materialPrincipal = bestComp.material || 'A36'; + perfilPrincipal = asm.descricao; // perfil principal da linha do conjunto + } + + if (pesoTot === 0 && asm.components.length > 0) { + pesoTot = asm.components.reduce((acc, c) => acc + c.pesoTotal, 0); + } + if (pesoUnit === 0 && asm.quantidade > 0) { + pesoUnit = pesoTot / asm.quantidade; + } + } else { + // Peça simples monopeça + if (pesoTot === 0 && pesoUnit > 0) { + pesoTot = pesoUnit * asm.quantidade; + } + if (pesoUnit === 0 && pesoTot > 0 && asm.quantidade > 0) { + pesoUnit = pesoTot / asm.quantidade; + } + } + + return { + of: asm.of, + fase: asm.fase, + marca: asm.marca, + descricao: asm.descricao, + isComposed: isComposed ? 'SIM' : 'NÃO', + quantidade: asm.quantidade, + material: materialPrincipal, + perfilPrincipal: perfilPrincipal, + comprimentoMax: maxComp > 0 ? maxComp : '-', + pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(1) : '-', + pesoTotal: pesoTot > 0 ? pesoTot.toFixed(0) : '-', + tratamentoSuperficial: 'pintura' + }; + }); + + setExtractedData(finalPieces); + + if (finalPieces.length === 0) { + setStatusText('Nenhuma peça principal identificada.'); + addLog('AVISO: Nenhuma marca bateu com a máscara (ex: B121-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 Tecnometal.`); + toast.success(`${finalPieces.length} peças Tecnometal extraídas do PDF!`); + } }; - const processFile = async () => { - if (!selectedFile) { - toast.error('Por favor, selecione um arquivo para processar.'); - return; - } - - if (!selectedWebhookConfig) { - toast.error('Por favor, selecione uma configuração de webhook.'); + 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; } + setSelectedFile(file); + const baseName = file.name.replace(/\.[^/.]+$/, ''); + setCurrentFileName(baseName); + setStatusText(`Processando: ${file.name}...`); + setLogs([`> Carregando arquivo Tecnometal: ${file.name}`]); setIsProcessing(true); - setDebugInfo('Enviando arquivo para webhook...\n'); - + try { - // Criar registro de processamento - const processing = { - webhook_config_id: selectedWebhookConfig.id, - file_name: selectedFile.name, - file_type: selectedFile.type, - status: 'enviando' - }; + const pdfjs = await ensurePdfJs(); + const arrayBuffer = await file.arrayBuffer(); + const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise; + addLog(`PDF aberto! Total de páginas: ${pdf.numPages}`); - const success = await createFileProcessing(processing); - if (!success) { - throw new Error('Erro ao criar registro de processamento'); + 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 + }); + } + }); } - // Enviar arquivo para webhook - const webhookResponse = await sendToWebhook(selectedFile, selectedWebhookConfig); - - setDebugInfo(prev => prev + 'Arquivo enviado com sucesso!\n'); - setDebugInfo(prev => prev + `ID do processamento: ${webhookResponse.processing_id}\n`); - - // Atualizar status para enviado - const currentFileProcessing = fileProcessings.find(fp => - fp.file_name === selectedFile.name && fp.status === 'enviando' - ); - - if (currentFileProcessing) { - await updateFileProcessing(currentFileProcessing.id, { - status: 'processando' - }); - - setCurrentProcessing({ - ...currentFileProcessing, - status: 'processando' - }); - - // Iniciar monitoramento - startMonitoring(currentFileProcessing.id); - } - - toast.success('Arquivo enviado para processamento!'); - - } catch (error) { - console.error('Erro ao processar arquivo:', error); - setDebugInfo(prev => prev + `ERRO: ${error}\n`); - toast.error('Erro ao processar o arquivo. Verifique as configurações de webhook.'); + addLog(`Total de fragmentos de texto extraídos: ${allItems.length}`); + processPdfLines(allItems); + } catch (err) { + console.error(err); + const errMsg = err instanceof Error ? err.message : 'Erro desconhecido'; + toast.error(`Erro ao processar PDF: ${errMsg}`); + addLog(`ERRO CRÍTICO: ${errMsg}`); + setStatusText('Falha no processamento.'); } finally { setIsProcessing(false); } }; - const downloadProcessedFile = async () => { - if (!currentProcessing?.download_url) { - toast.error('URL de download não disponível.'); - return; - } + const exportToExcel = () => { + if (extractedData.length === 0) return; - try { - const response = await fetch(currentProcessing.download_url); - if (!response.ok) { - throw new Error('Erro ao baixar arquivo'); - } + 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 blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `processed_${currentProcessing.file_name}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(excelRows); - toast.success('Arquivo baixado com sucesso!'); - } catch (error) { - console.error('Erro ao baixar arquivo:', error); - toast.error('Erro ao baixar o arquivo processado.'); - } - }; + 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 } + ]; - const processFileLocally = async (jsonData: any[], ofNumber: string, etapaFase: string) => { - const linhasFinais: ProcessedRow[] = []; - let dadosPecaMae: any = null; - let componentesDoGrupo: any[] = []; - - for (const row of jsonData) { - const isEmptyRow = Object.values(row).every(val => - val === null || val === undefined || val === '' - ); - - if (isEmptyRow) continue; - - if (row['Marca']) { - if (dadosPecaMae) { - const pecaMaeFormatada = { - of_number: ofNumber, - etapa_fase: etapaFase, - marca: formatarMarca(dadosPecaMae['Marca']), - descricao: dadosPecaMae['Descrição'] || '', - quantidade: String(dadosPecaMae['Qtde'] || ''), - peso_unitario: formatarDecimal(dadosPecaMae['P.Un.'] || ''), - peso_total: formatarDecimal(dadosPecaMae['P.Tot.'] || ''), - tratamento_superficial: '-', - material: componentesDoGrupo.length > 0 ? - getMaterial(componentesDoGrupo[0]['Comp. Mat.'] || '') : '', - perfil_principal: dadosPecaMae['Descrição'] || '', - tem_componentes: componentesDoGrupo.length > 0 ? 'true' : 'false' - }; - - if (componentesDoGrupo.length > 0) { - for (const comp of componentesDoGrupo) { - linhasFinais.push({ - ...pecaMaeFormatada, - marca_componente: formatarMarca(comp['Pos.'] || ''), - descricao_componente: comp['Descrição'] || '', - perfil_componente: comp['Descrição'] || '', - peso_unitario_componente: formatarDecimal(comp['P.Un.'] || ''), - quantidade_por_peca: String(comp['Qtde'] || '') - }); - } - } else { - linhasFinais.push({ - ...pecaMaeFormatada, - marca_componente: '', - descricao_componente: '', - perfil_componente: '', - peso_unitario_componente: '', - quantidade_por_peca: '' - }); - } - } - - dadosPecaMae = row; - componentesDoGrupo = []; - } else if (row['Pos.'] && dadosPecaMae) { - componentesDoGrupo.push(row); - } - } - - return linhasFinais; - }; - - const extractMetadataFromFilename = (filename: string) => { - console.log('Extraindo metadados do nome do arquivo:', filename); - const ofNumber = filename.split('-')[0]; - const faseMatch = filename.match(/FASE-(\d+)/i); - const etapaFase = faseMatch ? faseMatch[1] : '1'; - - console.log('Metadados extraídos:', { ofNumber, etapaFase }); - return { ofNumber, etapaFase }; - }; - - const formatarMarca = (marca: string): string => { - if (typeof marca === 'string' && marca.includes('-')) { - return marca.split('-').pop() || marca; - } - return marca; - }; - - const formatarDecimal = (numero: any): string => { - try { - return String(numero).replace(',', '.'); - } catch { - return String(numero); - } - }; - - const getMaterial = (matStr: string): string => { - if (typeof matStr === 'string') { - const parts = matStr.split(' '); - return parts[parts.length - 1] || ''; - } - return ''; - }; - - const handleCellEdit = (rowIndex: number, column: string, value: string) => { - const newData = [...processedData]; - newData[rowIndex] = { ...newData[rowIndex], [column]: value }; - setProcessedData(newData); - }; - - const deleteRow = (rowIndex: number) => { - const newData = processedData.filter((_, index) => index !== rowIndex); - setProcessedData(newData); - toast.success('Linha excluída com sucesso!'); - }; - - const addNewRow = () => { - const newRow: ProcessedRow = { - of_number: '', - etapa_fase: '', - marca: '', - descricao: '', - quantidade: '', - peso_unitario: '', - peso_total: '', - tratamento_superficial: '-', - material: '', - perfil_principal: '', - tem_componentes: 'false', - marca_componente: '', - descricao_componente: '', - perfil_componente: '', - peso_unitario_componente: '', - quantidade_por_peca: '' - }; - - setProcessedData([...processedData, newRow]); - toast.success('Nova linha adicionada!'); - }; - - const generateCSV = () => { - if (processedData.length === 0) { - toast.error('Nenhum dado para gerar o CSV.'); - return; - } - - try { - const csvContent = [ - columnHeaders.join(','), - ...processedData.map(row => - columnHeaders.map(col => row[col as keyof ProcessedRow]).join(',') - ) - ].join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - - const { ofNumber, etapaFase } = extractMetadataFromFilename(selectedFile?.name || 'arquivo'); - const fileName = `CSV_FINAL_${ofNumber}_FASE-${etapaFase}.csv`; - - link.setAttribute('href', URL.createObjectURL(blob)); - link.setAttribute('download', fileName); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - toast.success(`Arquivo ${fileName} gerado com sucesso!`); - } catch (error) { - console.error('Erro ao gerar CSV:', error); - toast.error('Erro ao gerar o arquivo CSV.'); - } + XLSX.utils.book_append_sheet(wb, ws, 'Lista_Pecas'); + const outFileName = `${currentFileName}_Corrigido.xlsx`; + XLSX.writeFile(wb, outFileName); + addLog(`Planilha Excel baixada: ${outFileName}`); + toast.success(`Planilha Excel "${outFileName}" gerada com sucesso!`); }; return (
- - - - - Processador Tecnometal com Webhook + {/* 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 Conjuntos Tecnometal aqui +

+

Suporta relatórios originais do Tecnometal em PDF

+
+ )} +
+ + {/* Actions & Status Bar */} +
+
+ + {statusText} + + {isProcessing && ( +
+ + Processando PDF Tecnometal... +
+ )} +
+ + +
+ + {/* Table Preview */} + + + + + Peças Extraídas Tecnometal ({extractedData.length}) - - {showWebhookConfig && ( - { - setSelectedWebhookConfig(config); - setShowWebhookConfig(false); - toast.success('Configuração selecionada!'); - }} - /> - )} - - {selectedWebhookConfig && ( - - -
-
-

Configuração Ativa:

-

- Envio: {selectedWebhookConfig.link_envio} -

-
- -
-
-
- )} - -
- -
- - -
-
- - - - {currentProcessing && ( - - -
-
-

Status do Processamento:

-
- {currentProcessing.status === 'processando' ? ( - <> - - Processando... - - ) : currentProcessing.status === 'concluido' ? ( - <> - - Concluído - + + + + + + + + + + + + + + + + + + + + + {extractedData.length === 0 ? ( + + + + ) : ( + extractedData.map((row, idx) => ( + + + + + + + + + + + + + + + )) + )} + +
OFFaseMarcaDescriçãoComposto?QtdMaterialPerfil PrincipalComp. Max (mm)Peso Unit. (kg)Peso Total (kg)Tratamento
+ {selectedFile ? ( +
+ +

Nenhuma peça foi identificada no PDF Tecnometal.

+

+ Verifique os logs abaixo ou se o arquivo é uma Lista de Conjuntos válida. +

+
) : ( - {currentProcessing.status} + 'Nenhum dado extraído ainda. Carregue um PDF de Lista de Conjuntos Tecnometal acima.' )} - - - {currentProcessing.status === 'concluido' && currentProcessing.download_url && ( - - )} - - - - )} - - {debugInfo && ( -
-

Log de Processamento:

-
-                {debugInfo}
-              
-
- )} +
{row.of}{row.fase}{row.marca}{row.descricao} + + {row.isComposed} + + {row.quantidade}{row.material}{row.perfilPrincipal}{row.comprimentoMax}{row.pesoUnit}{row.pesoTotal}{row.tratamentoSuperficial}
+
- {isMonitoring && ( - - -
- - Monitorando processamento... + {/* Terminal Log Panel */} + + + + + Log de Processamento e Diagnóstico Tecnometal + + {logs.length} eventos + + + +
+ {logs.map((logLine, idx) => ( +
{logLine}
+ ))} +
- - - )} + + +
); }; +export const TecnometalConverterModal: React.FC = ({ open, onOpenChange }) => { + if (open === undefined || onOpenChange === undefined) return null; + + return ( + + + +
+
+ + + Conversor Tecnometal (PDF para Excel) + + + Conversão 100% no navegador (Client-Side) de relatórios do Tecnometal em PDF para Excel (.xlsx) + +
+ + Autônomo / Sem Servidor + +
+
+ +
+ +
+
+
+ ); +}; + +const TecnometalConverter: React.FC = ({ open, onOpenChange }) => { + if (open !== undefined && onOpenChange !== undefined) { + return ; + } + + return ( + + +
+
+ + + Conversor Tecnometal (PDF para Excel) + +

+ Conversão 100% no navegador (Client-Side) de Listas de Conjuntos do Tecnometal em PDF para Excel (.xlsx) +

+
+ + Autônomo / Sem Servidor + +
+
+ + + +
+ ); +}; + export default TecnometalConverter;