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'; 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; pesoUnitario?: number; pesoTotal: number; } 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 [selectedFile, setSelectedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isProcessing, setIsProcessing] = 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 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 handleTratamentoChange = (novoTratamento: 'pintura' | 'galvanizacao') => { setTratamentoGlobal(novoTratamento); setExtractedData((prev) => prev.map((item) => ({ ...item, tratamentoSuperficial: novoTratamento })) ); }; const handleOfChange = (newOf: string) => { setHeaderOf(newOf); setExtractedData((prev) => prev.map((item) => ({ ...item, of: newOf })) ); }; 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 = (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)) > 4.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 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}`); 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('DetailDwgExtract') || lineText.includes('Cliente:') || lineText.includes('Desenhado') || lineText.includes('Superfície') || lineText.includes('Trabalho:') || lineText.includes('Kg/piece') || lineText.includes('( mm)') || (lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome')) ) { continue; } // 1. Linha Mestre de Peça Principal (ex: "B133-1-20 Pl 6x160x154" ou "B133-1-20 2 Pl 6x160x154") const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/); if (masterMatch) { const fullMark = masterMatch[1]; const quant = 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)); if (!hasMaterial) { const ofCode = parts.length >= 2 ? parts[0] : defaultOf; const phaseCode = parts.length >= 3 ? parts[1] : '0'; currentAssembly = { rawMark: fullMark, of: ofCode || defaultOf, fase: phaseCode, marca: pieceNumber, numeroPeca: pieceNumber, descricao: descCandidate || 'ESTRUTURA', quantidade: quant > 0 ? quant : 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: "81 3,07" ou "3,5 0,155" ou "2,6 0,25") 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) { const isSubComp = itemMark === '-' || itemMark.includes('-100') || itemMark.includes('-10') || (itemMark.match(/-\d+$/) && parseInt(itemMark.match(/-(\d+)$/)![1], 10) >= 1000); if (isSubComp) { currentAssembly.isComposed = 'SIM'; currentAssembly.components.push({ quantidade: itemQuant, perfil: itemDesc || currentAssembly.descricao, material: detectedMaterial || 'A36', comprimento: itemComp, pesoUnitario: itemPUn, pesoTotal: itemPesoTot }); } else { // Monopeça (peça simples sem componentes subordinados) 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 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 > 0) { sumUnitComponentsWeight += c.pesoUnitario * c.quantidade; } }); if (bestComp) { perfilPrincipal = bestComp.perfil || asm.descricao; materialPrincipal = bestComp.material || 'A36'; maxComp = bestComp.comprimento; } // Se não obteve subtotal na linha isolada, usar a somatória dos componentes if (!totalPeso || totalPeso === 0) { totalPeso = sumComponentsWeight; } // Detecção de quantidade de conjunto caso tenha vindo 1: if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) { const ratio = Math.round(totalPeso / sumUnitComponentsWeight); 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 : 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: 'pintura' }; }); 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}`); 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; 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 = `${headerOf || currentFileName}_Fase_${headerFase || '1'}_Lista_Pecas_AdvanceSteel.xlsx`; XLSX.writeFile(wb, outFileName); addLog(`Planilha Excel baixada: ${outFileName}`); toast.success(`Planilha padrão baixada com sucesso: ${outFileName}`); }; 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.
{/* Configuração de Tratamento Superficial */}
Tratamento Superficial: Configuração Global
handleTratamentoChange(val as 'pintura' | 'galvanizacao')} className="grid grid-cols-2 gap-2" >
{/* Á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 && (
OF (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" />
Fase 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 Peças
{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 ( ); })}
OF Fase Marca Descrição Composto por Componentes 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 || '-'}
)}
); }; 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;