import React, { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Upload, FileSpreadsheet, Download, RefreshCw, Terminal, FileText, Code2, AlertTriangle, Boxes, Paintbrush, CheckCircle2, Info } from 'lucide-react'; import { toast } from 'sonner'; import * as XLSX from 'xlsx'; import { useAppLabels } from '@/hooks/useAppLabels'; import { usePecas } from '@/hooks/usePecas'; import { supabase } from '@/integrations/supabase/client'; import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService'; import { ResultadoAnaliseImportacao } from '@/types/importDiff'; import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal'; export interface ComponentItem { marca: string; quantidade: number; perfil: string; material: string; comprimento: number; pesoUnitario?: number; pesoTotal: number; } 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; components?: ComponentItem[]; } interface AssemblyItem { rawMark: string; of: string; fase: string; marca: string; numeroPeca: string; descricao: string; quantidade: number; material: string; comprimento: number; pesoUnitario: number; pesoTotal: number; subtotalPeso: number; isComposed: 'SIM' | 'NÃO'; 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 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); }); }; const AdvanceSteelConverterContent: React.FC = () => { const { labels, isComponente } = useAppLabels(); const { importPecas } = usePecas(); const [selectedFile, setSelectedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [isDirectImporting, setIsDirectImporting] = useState(false); const [extractedData, setExtractedData] = useState([]); const [headerOf, setHeaderOf] = useState(''); const [headerFase, setHeaderFase] = useState('1'); const [tratamentoGlobal, setTratamentoGlobal] = useState<'pintura' | 'galvanizacao'>('pintura'); const [showLogs, setShowLogs] = useState(false); const [logs, setLogs] = useState(['> Aguardando seleção do PDF...']); const [statusText, setStatusText] = useState('Aguardando arquivo PDF...'); const [currentFileName, setCurrentFileName] = useState('Lista_Pecas'); const [useIframeMode, setUseIframeMode] = useState(false); const [analiseDiff, setAnaliseDiff] = useState(null); const [componentesMapDiff, setComponentesMapDiff] = useState>(new Map()); const [showDiffModal, setShowDiffModal] = useState(false); const fileInputRef = useRef(null); const logEndRef = useRef(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 buscarTratamentoOF = async (ofCode: string): Promise<'pintura' | 'galvanizacao'> => { if (!ofCode) return 'pintura'; try { const cleanOf = ofCode.trim(); const { data } = await supabase .from('ordens_fabricacao') .select('num_of, tratamento_final') .or(`num_of.eq.${cleanOf},num_of.ilike.%${cleanOf}%`) .limit(1); if (data && data.length > 0 && data[0].tratamento_final) { const trat = data[0].tratamento_final.toLowerCase(); if (trat.includes('galv')) { return 'galvanizacao'; } return 'pintura'; } } catch (err) { console.warn('Aviso ao buscar tratamento da OF:', err); } return 'pintura'; }; const handleTratamentoChange = (novoTratamento: 'pintura' | 'galvanizacao') => { setTratamentoGlobal(novoTratamento); setExtractedData((prev) => prev.map((item) => ({ ...item, tratamentoSuperficial: novoTratamento })) ); }; const handleOfChange = async (newOf: string) => { setHeaderOf(newOf); const trat = await buscarTratamentoOF(newOf); setTratamentoGlobal(trat); setExtractedData((prev) => prev.map((item) => ({ ...item, of: newOf, tratamentoSuperficial: trat })) ); }; const handleFaseChange = (newFase: string) => { setHeaderFase(newFase); setExtractedData((prev) => prev.map((item) => ({ ...item, fase: newFase })) ); }; 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) 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; }; const processPdfLines = async (items: PdfItem[]) => { // 1. Agrupamento robusto de linhas por Clustering de Coordenada Y em cada página const pagesMap = new Map(); items.forEach((it) => { if (!pagesMap.has(it.page)) pagesMap.set(it.page, []); pagesMap.get(it.page)!.push(it); }); const lines: PdfItem[][] = []; const Y_TOLERANCE = 6.0; // Tolerância vertical em pixels para agrupar na mesma linha visual const pageNumbers = Array.from(pagesMap.keys()).sort((a, b) => a - b); for (const pNum of pageNumbers) { const pageItems = pagesMap.get(pNum)!; interface LineCluster { ySum: number; count: number; avgY: number; items: PdfItem[]; } const clusters: LineCluster[] = []; // Ordena por Y decrescente (do topo da página para o rodapé) pageItems.sort((a, b) => b.y - a.y); for (const item of pageItems) { let bestCluster: LineCluster | null = null; let minDiff = Infinity; for (const cluster of clusters) { const diff = Math.abs(item.y - cluster.avgY); if (diff <= Y_TOLERANCE && diff < minDiff) { minDiff = diff; bestCluster = cluster; } } if (bestCluster) { bestCluster.items.push(item); bestCluster.ySum += item.y; bestCluster.count += 1; bestCluster.avgY = bestCluster.ySum / bestCluster.count; } else { clusters.push({ ySum: item.y, count: 1, avgY: item.y, items: [item] }); } } // Ordena as linhas do topo para o rodapé da página clusters.sort((a, b) => b.avgY - a.avgY); // Dentro de cada linha, ordena da esquerda para a direita (X crescente) for (const cluster of clusters) { cluster.items.sort((a, b) => a.x - b.x); lines.push(cluster.items); } } addLog(`Total de linhas identificadas no PDF: ${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) || lineText.match(/Projeto:\s*([A-Za-z0-9-]+)/i); if (matchTrabalho) { defaultOf = matchTrabalho[1].replace(/-/g, ''); addLog(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); setHeaderOf(defaultOf); break; } } const KNOWN_MATERIALS = [ 'A572-GR 50', 'A572-GR50', 'A572-GR.50', 'A572 GR50', 'A572GR50', 'A572GR5', 'A572', 'A36', 'ASTM A36', 'ASTM A572', '10.9', '8.8', 'SAE 1020', 'SAE 1045', 'INOX', '304', '316' ]; const normalizeLineText = (lineStr: string): string => { let s = lineStr.trim(); s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)\s*-\s*(\d+)/g, '$1-$2-$3'); s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)/g, '$1-$2'); s = s.replace(/A572\s*-\s*GR\s*50/gi, 'A572-GR 50'); s = s.replace(/A572\s*-\s*GR/gi, 'A572-GR'); return s; }; const assemblies: AssemblyItem[] = []; let currentAssembly: AssemblyItem | null = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; let rawLineText = line.map((it) => it.text).join(' ').trim(); const lineText = normalizeLineText(rawLineText); // Ignorar cabeçalhos e metadados conhecidos if ( lineText.includes('LISTA DE PEÇAS') || lineText.includes('LISTA ESTRUTURADA') || lineText.includes('Lista de Peças') || lineText.includes('DetailDwgExtract') || lineText.includes('Cliente:') || lineText.includes('Desenhado') || lineText.includes('Superfície') || lineText.includes('Trabalho:') || lineText.includes('Kg/piece') || lineText.includes('( mm)') || lineText.toLowerCase().includes('peso total:') || (lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome')) ) { continue; } // 1. Linha Mestre de Peça Principal (ex: "B134-13-1 90 W 150x13.0" ou "B134-13-2 72 W 150x13.0") // A primeira linha de cada registro contém a Marca, a Quantidade Total da Peça e o Nome/Perfil const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/); if (masterMatch) { const fullMark = masterMatch[1]; const quantFromLine = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1; const descCandidate = masterMatch[3].trim(); const parts = fullMark.split('-'); const pieceNumber = parts[parts.length - 1]; const pieceNumInt = parseInt(pieceNumber, 10); // Se o número da peça for menor que 1000, é uma peça principal mestre if (pieceNumInt < 1000) { const hasMaterial = KNOWN_MATERIALS.some((m) => descCandidate.includes(m)); // A linha mestre não possui indicação de material na descrição nem pesos if (!hasMaterial) { const ofCode = parts.length >= 2 ? parts[0] : defaultOf; const phaseCode = parts.length >= 3 ? parts[1] : (parts.length === 2 ? parts[0] : '1'); if (phaseCode && phaseCode !== '0' && isNaN(Number(phaseCode)) === false) { setHeaderFase(phaseCode); } currentAssembly = { rawMark: fullMark, of: ofCode || defaultOf, fase: phaseCode || '1', marca: pieceNumber, numeroPeca: pieceNumber, descricao: descCandidate || 'ESTRUTURA', quantidade: quantFromLine > 0 ? quantFromLine : 1, material: '', comprimento: 0, pesoUnitario: 0, pesoTotal: 0, subtotalPeso: 0, isComposed: 'NÃO', components: [] }; assemblies.push(currentAssembly); continue; } } } // 2. Linha com Subtotal da Marca (ex: "1.973,10 101,691" ou "408,70 20,434" ou "81 3,07") const tokens = lineText.split(/\s+/); const allTokensNumeric = tokens.length > 0 && tokens.every((tok) => /^[0-9.,]+$/.test(tok)); if (allTokensNumeric && tokens.length <= 3) { if (currentAssembly && (!currentAssembly.subtotalPeso || currentAssembly.subtotalPeso === 0)) { const subP = parseNumeric(tokens[0]); if (subP > 0) { currentAssembly.subtotalPeso = subP; } } continue; } // 3. Linha de Detalhe de Monopeça ou Componente Subordinado let itemMark = ''; let itemQuant = 1; let remaining = lineText; const mMark = remaining.match(/^([A-Za-z0-9]+(?:-\d+)+|-)\s+(\d+)\s+/); if (mMark) { itemMark = mMark[1]; itemQuant = parseInt(mMark[2], 10); remaining = remaining.substring(mMark[0].length).trim(); } else if (remaining.startsWith('-')) { itemMark = '-'; remaining = remaining.substring(1).trim(); const mQ = remaining.match(/^(\d+)\s+/); if (mQ) { itemQuant = parseInt(mQ[1], 10); remaining = remaining.substring(mQ[0].length).trim(); } } // Localizar Material dentro de remaining let detectedMaterial = ''; let matIdx = -1; let matLen = 0; for (const mat of KNOWN_MATERIALS) { const idx = remaining.indexOf(mat); if (idx !== -1 && idx > matIdx) { detectedMaterial = mat; matIdx = idx; matLen = mat.length; } } let itemDesc = ''; let afterMat = ''; if (matIdx !== -1) { itemDesc = remaining.substring(0, matIdx).trim(); afterMat = remaining.substring(matIdx + matLen).trim(); } else { const parts = remaining.split(/\s+/); const firstNumIdx = parts.findIndex((p) => /^[0-9.,]+$/.test(p)); if (firstNumIdx > 0) { itemDesc = parts.slice(0, firstNumIdx).join(' '); afterMat = parts.slice(firstNumIdx).join(' '); } else { itemDesc = remaining; } } const numTokens = afterMat.split(/\s+/).filter((tok) => /^[0-9.,]+$/.test(tok)); const nums = numTokens.map(parseNumeric); let itemComp = 0; let itemPUn = 0; let itemPesoTot = 0; if (nums.length >= 1) { itemComp = nums[0]; // O comprimento é sempre o primeiro valor numérico após o material } if (nums.length === 3) { itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length === 4) { itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length === 5) { itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length >= 6) { itemPUn = nums[2]; itemPesoTot = nums[3]; } if (currentAssembly) { // Extrai o número final da marca do item (ex: de "B134-13-1" -> 1, de "B134-13-1000" -> 1000, de "B133-13-1015" -> 1015, de "1000" -> 1000) const lastNumberMatch = itemMark.match(/(?:^|-)(\d+)$/); const itemNum = lastNumberMatch ? parseInt(lastNumberMatch[1], 10) : (parseInt(itemMark, 10) || 0); // Um item é SUBCOMPONENTE apenas se sua numeração de marca for >= faixaComponenteMin (1000) OU se for explicitamente '-' const isSubComp = itemMark === '-' || itemNum >= (labels.faixaComponenteMin || 1000); if (isSubComp) { currentAssembly.isComposed = 'SIM'; let cleanCompMark = itemMark; const subMatch = itemMark.match(/-?(\d{4,})$/) || itemMark.match(/(\d{4,})/); if (subMatch) { cleanCompMark = subMatch[1]; } else if (itemMark === '-' || !/^\d{4}$/.test(itemMark)) { cleanCompMark = String((labels.faixaComponenteMin || 1000) + currentAssembly.components.length); } currentAssembly.components.push({ marca: cleanCompMark, quantidade: itemQuant, perfil: itemDesc || currentAssembly.descricao, material: detectedMaterial || 'A36', comprimento: itemComp, pesoUnitario: itemPUn, pesoTotal: itemPesoTot }); } else { // Monopeça (peça simples sem componentes subordinados) // Preenche os dados técnicos da peça SEM sobrescrever a quantidade mestre if (!currentAssembly.material && detectedMaterial) { currentAssembly.material = detectedMaterial; } if (itemComp > 0 && (!currentAssembly.comprimento || currentAssembly.comprimento === 0)) { currentAssembly.comprimento = itemComp; } if (itemPUn > 0 && (!currentAssembly.pesoUnitario || currentAssembly.pesoUnitario === 0)) { currentAssembly.pesoUnitario = itemPUn; } if (itemPesoTot > 0 && (!currentAssembly.pesoTotal || currentAssembly.pesoTotal === 0)) { currentAssembly.pesoTotal = itemPesoTot; } } } } addLog(`Total de Peças Principais extraídas: ${assemblies.length}`); const targetOf = defaultOf || headerOf || (assemblies[0] ? assemblies[0].of : ''); const detectedTratamento = await buscarTratamentoOF(targetOf); setTratamentoGlobal(detectedTratamento); addLog(`Tratamento superficial obtido automaticamente da OF ${targetOf}: ${detectedTratamento === 'galvanizacao' ? 'Galvanizado' : 'Pintura'}`); 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 totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal; if (isComposed) { let sumComponentsWeight = 0; let sumUnitComponentsWeight = 0; let bestComp = asm.components[0]; maxComp = 0; asm.components.forEach((c) => { if (c.comprimento > maxComp) { maxComp = c.comprimento; bestComp = c; } sumComponentsWeight += c.pesoTotal; if (c.pesoUnitario && c.pesoUnitario > 0) { sumUnitComponentsWeight += c.pesoUnitario * c.quantidade; } }); if (bestComp) { perfilPrincipal = bestComp.perfil; materialPrincipal = bestComp.material; } if (sumComponentsWeight > 0) { totalPeso = sumComponentsWeight; } // Se a quantidade do assembly principal veio 1 mas a soma unitária x total dá a razão if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) { const ratio = Math.round(totalPeso / sumUnitComponentsWeight); if (ratio > 1) { asm.quantidade = ratio; } } } else { // Monopeça: se totalPeso não veio, calcula if (!totalPeso || totalPeso === 0) { totalPeso = (asm.pesoUnitario || 0) * asm.quantidade; } // Fallback de segurança para monopeça caso a quantidade mestre não tenha sido lida e veio 1 if (asm.quantidade === 1 && asm.pesoUnitario > 0 && totalPeso > asm.pesoUnitario) { const ratio = Math.round(totalPeso / asm.pesoUnitario); if (ratio > 1) { asm.quantidade = ratio; } } } // Regra de Ouro: Peso Unitário = Peso Total dividido pela Quantidade da peça principal const pesoUnit = asm.quantidade > 0 ? totalPeso / asm.quantidade : (asm.pesoUnitario || totalPeso); 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(2) : '-', pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : '-', tratamentoSuperficial: detectedTratamento, components: isComposed ? asm.components : [] }; }); 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) => { 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: ${file.name}`]); setIsProcessing(true); try { 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 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 }); } }); } addLog(`Total de elementos de texto extraídos: ${allItems.length}`); await 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 exportToExcel = () => { if (extractedData.length === 0) return; // Aba 1: Lista consolidada de Peças const excelRowsPecas = 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 || tratamentoGlobal, Material: item.material, 'Perfil Principal': item.perfilPrincipal, 'Comprimento Ref. (mm)': item.comprimentoMax === '-' ? '' : Number(item.comprimentoMax) })); // Aba 2: Lista detalhada de Componentes const excelRowsComponentes: any[] = []; extractedData.forEach((item) => { if (item.components && item.components.length > 0) { item.components.forEach((comp) => { const qtdPorPeca = Number(comp.quantidade) || 1; const qtdTotal = qtdPorPeca * Number(item.quantidade); excelRowsComponentes.push({ OF: item.of, Fase: isNaN(Number(item.fase)) ? item.fase : Number(item.fase), 'Peça Principal': item.marca, 'Marca Componente': comp.marca || '-', 'Descrição Componente': comp.perfil || item.descricao, Material: comp.material || item.material, 'Comprimento (mm)': comp.comprimento || '', 'Qtd / Peça': qtdPorPeca, 'Qtd Total no Lote': qtdTotal, 'Peso Unitário (kg)': comp.pesoUnitario || '', 'Peso Total (kg)': comp.pesoTotal || '' }); }); } }); // Aba 3: Formato Plano Oficial para Importação TrackSteel const excelRowsImportacao: any[] = []; extractedData.forEach((item) => { if (item.components && item.components.length > 0) { item.components.forEach((comp) => { excelRowsImportacao.push({ of_number: item.of, etapa_fase: item.fase, marca: item.marca, descricao: item.descricao, quantidade: Number(item.quantidade), peso_unitario: item.pesoUnit === '-' ? 0 : Number(item.pesoUnit), peso_total: item.pesoTotal === '-' ? 0 : Number(item.pesoTotal), tratamento_superficial: item.tratamentoSuperficial || tratamentoGlobal, material: item.material, perfil_principal: item.perfilPrincipal, tem_componentes: true, marca_componente: comp.marca || '', descricao_componente: comp.perfil || item.descricao, perfil_componente: comp.perfil || item.descricao, peso_unitario_componente: comp.pesoUnitario || 0, quantidade_por_peca: Number(comp.quantidade) || 1 }); }); } else { excelRowsImportacao.push({ of_number: item.of, etapa_fase: item.fase, marca: item.marca, descricao: item.descricao, quantidade: Number(item.quantidade), peso_unitario: item.pesoUnit === '-' ? 0 : Number(item.pesoUnit), peso_total: item.pesoTotal === '-' ? 0 : Number(item.pesoTotal), tratamento_superficial: item.tratamentoSuperficial || tratamentoGlobal, material: item.material, perfil_principal: item.perfilPrincipal, tem_componentes: false, marca_componente: '', descricao_componente: '', perfil_componente: '', peso_unitario_componente: 0, quantidade_por_peca: 0 }); } }); const wb = XLSX.utils.book_new(); const wsPecas = XLSX.utils.json_to_sheet(excelRowsPecas); wsPecas['!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, wsPecas, 'Lista_Pecas'); if (excelRowsComponentes.length > 0) { const wsComp = XLSX.utils.json_to_sheet(excelRowsComponentes); wsComp['!cols'] = [ { wch: 12 }, { wch: 8 }, { wch: 16 }, { wch: 18 }, { wch: 22 }, { wch: 16 }, { wch: 18 }, { wch: 12 }, { wch: 16 }, { wch: 18 }, { wch: 16 } ]; XLSX.utils.book_append_sheet(wb, wsComp, 'Lista_Componentes'); } const wsImport = XLSX.utils.json_to_sheet(excelRowsImportacao); XLSX.utils.book_append_sheet(wb, wsImport, 'Importacao_TrackSteel'); const outFileName = `${headerOf || currentFileName}_Fase_${headerFase || '1'}_Lista_Pecas_AdvanceSteel.xlsx`; XLSX.writeFile(wb, outFileName); addLog(`Planilha Excel baixada com ${excelRowsPecas.length} peças e ${excelRowsComponentes.length} componentes: ${outFileName}`); toast.success(`Planilha gerada com sucesso: ${outFileName}`); }; const handleDirectImport = async () => { if (extractedData.length === 0) return; setIsDirectImporting(true); try { const importacaoRows: any[] = []; extractedData.forEach((item) => { if (item.components && item.components.length > 0) { item.components.forEach((comp) => { importacaoRows.push({ of_number: item.of, etapa_fase: item.fase, marca: item.marca, descricao: item.descricao, quantidade: Number(item.quantidade), peso_unitario: item.pesoUnit === '-' ? 0 : Number(item.pesoUnit), peso_total: item.pesoTotal === '-' ? 0 : Number(item.pesoTotal), tratamento_superficial: item.tratamentoSuperficial || tratamentoGlobal, material: item.material, perfil_principal: item.perfilPrincipal, tem_componentes: true, marca_componente: comp.marca || '', descricao_componente: comp.perfil || item.descricao, perfil_componente: comp.perfil || item.descricao, peso_unitario_componente: comp.pesoUnitario || 0, quantidade_por_peca: Number(comp.quantidade) || 1 }); }); } else { importacaoRows.push({ of_number: item.of, etapa_fase: item.fase, marca: item.marca, descricao: item.descricao, quantidade: Number(item.quantidade), peso_unitario: item.pesoUnit === '-' ? 0 : Number(item.pesoUnit), peso_total: item.pesoTotal === '-' ? 0 : Number(item.pesoTotal), tratamento_superficial: item.tratamentoSuperficial || tratamentoGlobal, material: item.material, perfil_principal: item.perfilPrincipal, tem_componentes: false, marca_componente: '', descricao_componente: '', perfil_componente: '', peso_unitario_componente: 0, quantidade_por_peca: 0 }); } }); toast.info('Verificando existência e status de produção das peças...'); const analise = await analisarConflitosImportacao(importacaoRows); const { componentesMap } = consolidarPecasEComponentes(importacaoRows); if (analise.totalComDivergencia > 0 || analise.totalIdenticas > 0 || analise.totalComApontamento > 0) { setAnaliseDiff(analise); setComponentesMapDiff(componentesMap); setShowDiffModal(true); setIsDirectImporting(false); return; } await importPecas(importacaoRows); toast.success(`${extractedData.length} peças e seus componentes foram importados com sucesso!`); addLog(`✅ Importação concluída: ${extractedData.length} peças gravadas no banco.`); } catch (err: any) { console.error('Erro na importação direta:', err); toast.error(`Falha ao importar: ${err?.message || 'Erro no banco'}`); } finally { setIsDirectImporting(false); } }; const totalPesoGeral = extractedData.reduce((acc, curr) => { const val = typeof curr.pesoTotal === 'number' ? curr.pesoTotal : parseFloat(String(curr.pesoTotal)) || 0; return acc + val; }, 0); const totalPecasCompostas = extractedData.filter((p) => p.isComposed === 'SIM').length; const totalPecasSimples = extractedData.filter((p) => p.isComposed === 'NÃO').length; return (
{/* Banner Principal de Conversão */}
Conversor de Listas Advance Steel
Converte a Lista Estruturada de Peças do Advance Steel para o padrão oficial de importação do TrackSteel.
{/* Indicador Automático de Tratamento Superficial da OF */}
Tratamento Superficial: Automático da OF
{tratamentoGlobal === 'galvanizacao' ? 'Galvanizado (OF)' : 'Pintura (OF)'}
{/* Área de Upload / Arrastar Arquivo */}
fileInputRef.current?.click()} className="group border-2 border-dashed border-slate-700 hover:border-indigo-500/80 bg-slate-950/40 hover:bg-slate-900/50 rounded-xl p-8 text-center cursor-pointer transition-all duration-200 flex flex-col items-center justify-center space-y-3" >

{selectedFile ? ( {selectedFile.name} ) : ( <> Clique para selecionar ou arraste a lista Advance Steel (.pdf) )}

Lê automaticamente cabeçalho (Trabalho/OF, Fase), marcas principais, subcomponentes e subtotais.

{selectedFile && ( {(selectedFile.size / 1024).toFixed(1)} KB carregado )}
{/* Cards de Métricas e Identificação da OF */} {extractedData.length > 0 && (
{labels.ofLabel} (Obra) handleOfChange(e.target.value)} className="h-7 text-xs font-bold font-mono bg-slate-900 border-slate-700 text-indigo-300 mt-1" />
{labels.faseLabel} handleFaseChange(e.target.value)} className="h-7 text-xs font-bold font-mono bg-slate-900 border-slate-700 text-indigo-300 mt-1" />
Total de {labels.pecaPlural}
{extractedData.length} itens
Compostas / Simples
SIM: {totalPecasCompostas} NÃO: {totalPecasSimples}
Peso Total
{totalPesoGeral.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}{' '} kg
)} {/* Barra de Ações */} {extractedData.length > 0 && (
{extractedData.length} Peças Prontas para Importação
)} {/* Logs Expansíveis */} {showLogs && logs.length > 0 && (
{logs.map((l, i) => (
{l}
))}
)} {/* Tabela de Pré-visualização com Visual Moderno (Igual ao Bocad) */} {extractedData.length > 0 && (
Prévia da Planilha de Destino (12 Colunas Oficiais) Pronto para uso direto no botão "Importar Peças"
{extractedData.map((p, idx) => { const pUnit = typeof p.pesoUnit === 'number' ? p.pesoUnit : parseFloat(String(p.pesoUnit)) || 0; const pTot = typeof p.pesoTotal === 'number' ? p.pesoTotal : parseFloat(String(p.pesoTotal)) || 0; return ( ); })}
{labels.ofLabel} {labels.faseLabel} {labels.pecaLabel} Descrição Com {labels.componentePlural} Quantidade Peso Unitário (kg) Peso Total (kg) Tratamento Superficial Material Perfil Principal Comprimento Ref. (mm)
{p.of} {p.fase} {p.marca} {p.descricao} {p.isComposed === 'SIM' ? ( SIM ) : ( NÃO )} {p.quantidade} {pUnit > 0 ? pUnit.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 2 }) : '-'} {pTot > 0 ? pTot.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 2 }) : '-'} {p.tratamentoSuperficial} {p.material} {p.perfilPrincipal} {p.comprimentoMax || '-'}
)}
{/* Modal de Detecção de Conflitos e Conciliação */} { setShowDiffModal(false); addLog(`✅ Importação conciliada concluída com sucesso.`); }} />
); }; export const AdvanceSteelConverterModal: React.FC = ({ open, onOpenChange }) => { if (open === undefined || onOpenChange === undefined) return null; return ( ); }; export const AdvanceSteelConverter: React.FC = ({ open, onOpenChange }) => { if (open !== undefined && onOpenChange !== undefined) { return ; } return ; }; export default AdvanceSteelConverter;