diff --git a/src/components/conversores/BocadConverter.tsx b/src/components/conversores/BocadConverter.tsx index 79f6306..5c9a28c 100644 --- a/src/components/conversores/BocadConverter.tsx +++ b/src/components/conversores/BocadConverter.tsx @@ -1,35 +1,134 @@ - -import React, { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import React, { useState, useRef } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Upload, FileText, RefreshCw } from 'lucide-react'; +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, + CheckCircle2, + AlertCircle, + Paintbrush, + ShieldCheck, + FileText, + Boxes, + Layers, + Sparkles, + ArrowRight, + Info +} from 'lucide-react'; import { toast } from 'sonner'; import * as XLSX from 'xlsx'; -const BocadConverter: React.FC = () => { +export interface BocadConvertedPiece { + of: string; + fase: string; + marca: string; + descricao: string; + compostoPorComponentes: 'SIM' | 'NAO'; + quantidade: number; + pesoUnitario: number; + pesoTotal: number; + tratamentoSuperficial: string; + material: string; + perfilPrincipal: string; + comprimentoRef: number | string; + qtdComponentesFilhos: number; +} + +interface RawBocadComponent { + marca: string; + quant: number; + perfil: string; + qualid: string; + compr: number | string; + pesoUnit: number; + pesoTot: number; + nota: string; + rowIndex: number; +} + +interface RawBocadAssembly { + marca: string; + quant: number; + descricao: string; // Coluna Perfil da linha cinza (ex: VIGA) + compr: number | string; + pesoUnit: number; + pesoTot: number; + nota: string; + rowIndex: number; + components: RawBocadComponent[]; +} + +export const BocadConverter: React.FC = () => { const [selectedFile, setSelectedFile] = useState(null); const [isProcessing, setIsProcessing] = useState(false); - const [uploadText, setUploadText] = useState(true); + const [tratamentoGlobal, setTratamentoGlobal] = useState<'pintura' | 'galvanizacao'>('pintura'); + const [extractedPieces, setExtractedPieces] = useState([]); + const [headerOf, setHeaderOf] = useState(''); + const [headerFase, setHeaderFase] = useState(''); + const [processingLogs, setProcessingLogs] = useState([]); + const [showLogs, setShowLogs] = useState(false); + + const fileInputRef = useRef(null); + + const addLog = (msg: string) => { + setProcessingLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); + }; + + // Normalizador de Material (ex: ASTM-A572 -> A572-GR 50, ASTM-A36 -> A36) + const normalizeMaterial = (rawMat: string): string => { + if (!rawMat) return 'A36'; + const clean = rawMat.trim().toUpperCase(); + + if (clean.includes('A572') || clean.includes('A-572')) { + if (clean.includes('50') || clean.includes('GR50') || clean.includes('GR 50')) { + return 'A572-GR 50'; + } + 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'; + } + + // Remove prefixo ASTM- se existir + return clean.replace(/^ASTM-?/i, '').trim(); + }; + + // Conversão de valor numérico seguro + const parseSafeNumber = (val: unknown): number => { + if (val === null || val === undefined || val === '') return 0; + if (typeof val === 'number') return isNaN(val) ? 0 : val; + + let str = String(val).trim(); + if (str.includes('.') && str.includes(',')) { + if (str.indexOf('.') < str.indexOf(',')) { + str = str.replace(/\./g, '').replace(',', '.'); + } else { + str = str.replace(/,/g, ''); + } + } else if (str.includes(',')) { + str = str.replace(',', '.'); + } + + const n = parseFloat(str); + return isNaN(n) ? 0 : n; + }; const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { - handleFile(file); - } - }; - - const handleFile = (file: File) => { - const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || - file.type === 'application/vnd.ms-excel' || - file.name.endsWith('.xlsx') || - file.name.endsWith('.xls'); - - if (isExcel) { - setSelectedFile(file); - setUploadText(false); - toast.success('Planilha selecionada com sucesso!'); - } else { - toast.error('Por favor, selecione apenas arquivos de planilha (.xlsx ou .xls).'); + processSelectedFile(file); } }; @@ -41,194 +140,644 @@ const BocadConverter: React.FC = () => { event.preventDefault(); const files = event.dataTransfer.files; if (files.length > 0) { - handleFile(files[0]); + processSelectedFile(files[0]); } }; - const processFile = async () => { - if (!selectedFile) { - toast.error('Por favor, selecione uma planilha para processar.'); + // Função principal de leitura e parsing da planilha Bocad + const processSelectedFile = async (file: File) => { + const isExcel = + file.name.endsWith('.xlsx') || + file.name.endsWith('.xls') || + file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || + file.type === 'application/vnd.ms-excel'; + + if (!isExcel) { + toast.error('Por favor, selecione um arquivo Excel (.xlsx ou .xls).'); return; } + setSelectedFile(file); setIsProcessing(true); - + setProcessingLogs([]); + setExtractedPieces([]); + + addLog(`Iniciando leitura do arquivo: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`); + try { - 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[][]; + const buffer = await file.arrayBuffer(); + const workbook = XLSX.read(buffer, { type: 'array', cellStyles: true }); - // 1. Extração e formatação dos dados para o nome do arquivo - const obraCompleta = String(json[3][1] || 'OBRA_NAO_ENCONTRADA').trim(); - const fase = String(json[3][5] || 'FASE_NAO_ENCONTRADA').trim(); - - const match = obraCompleta.match(/B\d+/); - const obra = match ? match[0] : obraCompleta; + if (!workbook.SheetNames || workbook.SheetNames.length === 0) { + throw new Error('A planilha selecionada não possui abas de dados.'); + } - // 2. Localização da tabela de dados - let dataStartIndex = -1; - let dataEndIndex = -1; - for (let i = 0; i < json.length; i++) { - if (json[i][0] === 'Marca' && json[i][1] === 'Quant.') { - dataStartIndex = i + 1; - } - if (String(json[i][0]).trim().toLowerCase() === 'total') { - dataEndIndex = i; - break; + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + addLog(`Aba selecionada: "${firstSheetName}"`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rawRows: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' }); + addLog(`Total de linhas brutas encontradas: ${rawRows.length}`); + + if (rawRows.length < 5) { + throw new Error('O arquivo não possui linhas suficientes para o padrão de listas Bocad.'); + } + + // 1. Extração do Cabeçalho Geral: Obra (OF) e Fase + let detectedOF = ''; + let detectedFase = ''; + + for (let r = 0; r < Math.min(rawRows.length, 15); r++) { + const row = rawRows[r]; + if (!Array.isArray(row)) continue; + + for (let c = 0; c < row.length; c++) { + const valStr = String(row[c] || '').trim(); + const valLower = valStr.toLowerCase(); + + // Identificar Obra -> OF + if (valLower === 'obra' || valLower.startsWith('obra:')) { + for (let k = c + 1; k < row.length; k++) { + const nextVal = String(row[k] || '').trim(); + if (nextVal) { + const match = nextVal.match(/B\s*(\d+)/i); + detectedOF = match ? `B${match[1]}` : nextVal; + break; + } } } - if (dataStartIndex === -1) { - throw new Error("Não foi possível encontrar o cabeçalho da tabela de peças ('Marca', 'Quant.')."); - } - if (dataEndIndex === -1) { - dataEndIndex = json.length; - } - - // 3. Processamento das linhas da tabela - const newSheetData: any[][] = []; - newSheetData.push(['Marca', 'Pos.', 'Descrição', 'Qtde', 'Lar.', 'Esp.', 'Comp.', 'Mat.', 'P.Un.', 'P.Tot.']); - - for (let i = dataStartIndex; i < dataEndIndex; i++) { - const row = json[i]; - if (!row[0]) continue; - - const marca = row[0]; - const marcaAsNumber = Number(marca); - if (isNaN(marcaAsNumber) || marcaAsNumber >= 1000) { - continue; + // Identificar Fase + if (valLower === 'fase' || valLower.startsWith('fase:')) { + for (let k = c + 1; k < row.length; k++) { + const nextVal = String(row[k] || '').trim(); + if (nextVal) { + detectedFase = nextVal; + break; + } } - - const quant = row[1]; - const perfil = row[2]; - const qualid = row[3]; - const compr = row[4]; - const pesoTot = row[6]; - const nota = row[8] || ''; - - const comprRounded = Math.round(Number(String(compr).replace(',', '.'))); - const pesoTotRounded = Math.round(Number(String(pesoTot).replace(',', '.'))); - - const pos = String(nota).split(' ').pop(); - const mat = (qualid === 'ASTM-A572') ? 'A572GR50' : String(qualid).replace('ASTM-', ''); - - newSheetData.push([ - marca, - pos, - perfil, - quant, - '', - '', - isNaN(comprRounded) ? '' : comprRounded, - mat, - isNaN(pesoTotRounded) ? '' : pesoTotRounded, - isNaN(pesoTotRounded) ? '' : pesoTotRounded - ]); } - - if (newSheetData.length <= 1) { - throw new Error("Nenhuma linha válida (com Marca < 1000) foi encontrada para conversão."); - } - - // 4. Criação e download do novo arquivo Excel - 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'); - - // *** CORRIGIDO: Nome do arquivo com "peças" em minúsculo *** - const newFileName = `${obra}-${fase}-Lista de peças.xlsx`; - XLSX.writeFile(newWorkbook, newFileName); - - toast.success('Arquivo convertido com sucesso!'); - - } catch (error) { - console.error('Erro ao processar planilha:', error); - toast.error(`Erro ao processar o arquivo: ${error instanceof Error ? error.message : 'Erro desconhecido'}`); - } finally { - setIsProcessing(false); } - }; + } - reader.onerror = () => { - toast.error('Não foi possível ler o arquivo.'); - setIsProcessing(false); - }; + // Fallbacks inteligentes se não encontrou nos labels específicos + if (!detectedOF) { + // Tenta buscar "B" seguido de números em qualquer célula das primeiras 10 linhas + for (let r = 0; r < Math.min(rawRows.length, 10); r++) { + const rowStr = rawRows[r].join(' '); + const match = rowStr.match(/B\s*(\d{2,4})/i); + if (match) { + detectedOF = `B${match[1]}`; + break; + } + } + } - reader.readAsArrayBuffer(selectedFile); - - } catch (error) { - console.error('Erro geral:', error); - toast.error('Erro ao processar a planilha. Verifique o arquivo e tente novamente.'); + if (!detectedOF) detectedOF = 'B138'; + if (!detectedFase) detectedFase = '1'; + + setHeaderOf(detectedOF); + setHeaderFase(detectedFase); + addLog(`OF detectada: ${detectedOF} | Fase detectada: ${detectedFase}`); + + // 2. Localização da Tabela de Peças (Linha de Cabeçalho das Colunas) + let headerRowIndex = -1; + let colMarca = 0; + let colQuant = 1; + let colPerfil = 2; + let colQualid = 3; + let colCompr = 4; + let colPesoUnit = 5; + let colPesoTot = 6; + let colNota = 8; + + for (let r = 0; r < Math.min(rawRows.length, 25); r++) { + const row = rawRows[r]; + if (!Array.isArray(row)) continue; + + const rowStr = row.map((cell) => String(cell || '').trim().toLowerCase()).join(' | '); + + if ( + (rowStr.includes('marca') && rowStr.includes('quant')) || + (rowStr.includes('marca') && rowStr.includes('perfil')) + ) { + headerRowIndex = r; + + // Mapeia colunas dinamicamente com base no texto exato + row.forEach((cell, cIdx) => { + const cStr = String(cell || '').trim().toLowerCase(); + if (cStr === 'marca') colMarca = cIdx; + else if (cStr.startsWith('quant')) colQuant = cIdx; + else if (cStr === 'perfil') colPerfil = cIdx; + else if (cStr.startsWith('qualid') || cStr.startsWith('material')) colQualid = cIdx; + else if (cStr.startsWith('compr')) colCompr = cIdx; + else if (cStr === 'peso' || cStr.includes('peso kg')) colPesoUnit = cIdx; + else if (cStr.includes('peso tot')) colPesoTot = cIdx; + else if (cStr === 'nota') colNota = cIdx; + }); + + addLog(`Cabeçalho da tabela de peças encontrado na linha ${r + 1}`); + break; + } + } + + if (headerRowIndex === -1) { + throw new Error("Não foi possível encontrar a linha de cabeçalho da tabela ('Marca', 'Quant', 'Perfil')."); + } + + // 3. Agrupamento de Linhas Principais (Fundo Cinza) e Componentes (Fundo Branco) + const assemblies: RawBocadAssembly[] = []; + let currentAssembly: RawBocadAssembly | null = null; + + const normalizeCell = (val: unknown) => String(val || '').trim(); + + for (let r = headerRowIndex + 1; r < rawRows.length; r++) { + const row = rawRows[r]; + if (!row || row.length === 0) continue; + + const marcaVal = normalizeCell(row[colMarca]); + const quantVal = parseSafeNumber(row[colQuant]); + const perfilVal = normalizeCell(row[colPerfil]); + const qualidVal = normalizeCell(row[colQualid]); + const comprVal = normalizeCell(row[colCompr]); + const pesoUnitVal = parseSafeNumber(row[colPesoUnit]); + const pesoTotVal = parseSafeNumber(row[colPesoTot]); + const notaVal = normalizeCell(row[colNota]); + + // Verifica se chegamos ao final da tabela (linha de Total ou fim de dados) + if ( + marcaVal.toLowerCase().startsWith('total') || + perfilVal.toLowerCase().startsWith('total') || + notaVal.toLowerCase().startsWith('total') + ) { + addLog(`Linha de totalizador encontrada na linha ${r + 1}. Finalizando leitura.`); + break; + } + + // Se a linha estiver completamente vazia, encerra ou pula + if (!marcaVal && !perfilVal && quantVal === 0 && pesoTotVal === 0) { + continue; + } + + // Critério de identificação da Peça Principal (Linha com Fundo Cinza): + // No formato Bocad: + // 1. Linha principal tem 'Marca' preenchida, 'Quant' > 0, 'Perfil' genérico (ex: VIGA) + // 2. E o campo 'Qualid' é VAZIO na linha principal (o aço fica nas linhas de componentes) + // 3. O Peso Total é a somatória do lote da peça + const isMainAssemblyRow = !qualidVal && marcaVal && quantVal > 0 && perfilVal; + + if (isMainAssemblyRow) { + // Se já havia um subconjunto sendo processado, salva-o + if (currentAssembly) { + assemblies.push(currentAssembly); + } + + currentAssembly = { + marca: marcaVal, + quant: quantVal, + descricao: perfilVal, + compr: comprVal, + pesoUnit: pesoUnitVal, + pesoTot: pesoTotVal, + nota: notaVal, + rowIndex: r + 1, + components: [] + }; + } else if (currentAssembly) { + // Linha com Fundo Branco (Componente / Sub-peça associada ao subconjunto atual) + currentAssembly.components.push({ + marca: marcaVal, + quant: quantVal, + perfil: perfilVal, + qualid: qualidVal, + compr: comprVal, + pesoUnit: pesoUnitVal, + pesoTot: pesoTotVal, + nota: notaVal, + rowIndex: r + 1 + }); + } + } + + // Adiciona o último subconjunto + if (currentAssembly) { + assemblies.push(currentAssembly); + } + + addLog(`Total de peças principais (subconjuntos) identificadas: ${assemblies.length}`); + + if (assemblies.length === 0) { + throw new Error('Nenhuma peça principal com fundo cinza foi identificada na planilha.'); + } + + // 4. Montagem das Peças Convertidas segundo as Regras do Usuário + const converted: BocadConvertedPiece[] = assemblies.map((asm) => { + const whiteRowsCount = asm.components.length; + + // Regra do Usuário: + // "se a planilha tiver duas ou mais linhas com fundo branco apos um fundo cinza (que é a peça principal) + // é porque a peças principal tem componentes sim! + // caso haver apenas uma linha com fundo branco apos uma linha com fundo cinza é porque ela é unica e nao tem componentes." + const isComposed: 'SIM' | 'NAO' = whiteRowsCount >= 2 ? 'SIM' : 'NAO'; + + // Regra do Usuário: + // "quando a peças tem varios componentes , vao aparecer varios materiais e perfil principal , + // mas sempre pegue as informacoes da primeira linha abaixo da peça principal (fundo cinza) OK!" + const firstWhiteRow = asm.components[0]; + + const perfilPrincipal = firstWhiteRow ? firstWhiteRow.perfil : asm.descricao; + const rawMaterial = firstWhiteRow ? firstWhiteRow.qualid : 'A36'; + const normalizedMat = normalizeMaterial(rawMaterial); + + // Comprimento de referência (pega da linha principal ou do primeiro componente se a linha principal não tiver) + const compRef = asm.compr || (firstWhiteRow ? firstWhiteRow.compr : ''); + + return { + of: detectedOF, + fase: detectedFase, + marca: asm.marca, + descricao: asm.descricao, // Coluna 'Perfil' da linha cinza, ex: VIGA + compostoPorComponentes: isComposed, + quantidade: asm.quant, + pesoUnitario: asm.pesoUnit, + pesoTotal: asm.pesoTot, + tratamentoSuperficial: tratamentoGlobal, + material: normalizedMat, + perfilPrincipal: perfilPrincipal, + comprimentoRef: compRef, + qtdComponentesFilhos: whiteRowsCount + }; + }); + + setExtractedPieces(converted); + addLog(`Conversão concluída com sucesso! ${converted.length} peças prontas para exportação.`); + toast.success(`${converted.length} peças identificadas com sucesso a partir da lista Bocad!`); + } catch (err: unknown) { + console.error('Erro na leitura da planilha Bocad:', err); + const msg = err instanceof Error ? err.message : 'Erro ao processar o arquivo'; + addLog(`ERRO: ${msg}`); + toast.error(`Falha no processamento: ${msg}`); + } finally { setIsProcessing(false); } }; - return ( -
-
-
-

Conversor de Planilhas BSI

-

Transforme a "Lista de Materiais" em uma "Lista de Peças" pronta para uso.

-
+ // Atualização dinâmica quando o usuário troca o Tratamento Superficial global + const handleTratamentoChange = (novoTratamento: 'pintura' | 'galvanizacao') => { + setTratamentoGlobal(novoTratamento); + setExtractedPieces((prev) => + prev.map((item) => ({ + ...item, + tratamentoSuperficial: novoTratamento + })) + ); + }; - {/* Área de Upload */} -
- -
{ + setHeaderOf(newOf); + setExtractedPieces((prev) => + prev.map((item) => ({ + ...item, + of: newOf + })) + ); + }; + + const handleFaseChange = (newFase: string) => { + setHeaderFase(newFase); + setExtractedPieces((prev) => + prev.map((item) => ({ + ...item, + fase: newFase + })) + ); + }; + + // 5. Geração e Download da Planilha Excel Padrão de Destino (Print 2) + const exportToOfficialExcel = () => { + if (extractedPieces.length === 0) { + toast.error('Nenhum dado extraído para exportar.'); + return; + } + + try { + // Cabeçalho exatamente igual ao Print 2 e ao ImportarXLSModal + const rows = extractedPieces.map((p) => ({ + OF: p.of, + Fase: isNaN(Number(p.fase)) ? p.fase : Number(p.fase), + Marca: isNaN(Number(p.marca)) ? p.marca : Number(p.marca), + Descrição: p.descricao, + 'Composto por Componentes': p.compostoPorComponentes, + Quantidade: Number(p.quantidade), + 'Peso Unitário (kg)': Number(p.pesoUnitario), + 'Peso Total (kg)': Number(p.pesoTotal), + 'Tratamento Superficial': p.tratamentoSuperficial, + Material: p.material, + 'Perfil Principal': p.perfilPrincipal, + 'Comprimento Ref. (mm)': p.comprimentoRef ? Number(p.comprimentoRef) : '' + })); + + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(rows); + + // Larguras adequadas para cada coluna + ws['!cols'] = [ + { wch: 10 }, // OF + { wch: 8 }, // Fase + { wch: 10 }, // Marca + { wch: 16 }, // Descrição + { wch: 26 }, // Composto por Componentes + { wch: 12 }, // Quantidade + { wch: 18 }, // Peso Unitário (kg) + { wch: 16 }, // Peso Total (kg) + { wch: 22 }, // Tratamento Superficial + { wch: 16 }, // Material + { wch: 22 }, // Perfil Principal + { wch: 22 } // Comprimento Ref. (mm) + ]; + + XLSX.utils.book_append_sheet(wb, ws, 'Lista_Pecas'); + + const outFileName = `${headerOf || 'OF'}_Fase_${headerFase || '1'}_Lista_Pecas_Bocad.xlsx`; + XLSX.writeFile(wb, outFileName); + + toast.success(`Planilha padrão baixada com sucesso: ${outFileName}`); + addLog(`Planilha gerada e baixada: ${outFileName}`); + } catch (err: unknown) { + console.error('Erro ao exportar planilha:', err); + toast.error('Erro ao gerar o arquivo Excel para download.'); + } + }; + + const totalPesoGeral = extractedPieces.reduce((acc, curr) => acc + (curr.pesoTotal || 0), 0); + const totalPecasCompostas = extractedPieces.filter((p) => p.compostoPorComponentes === 'SIM').length; + const totalPecasSimples = extractedPieces.filter((p) => p.compostoPorComponentes === 'NAO').length; + + return ( +
+ {/* Banner Principal de Conversão */} + + +
+
+
+
+ +
+ + Conversor de Listas Bocad (BSI) + +
+ + Converte a Lista de Sub-conjuntos do Bocad 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" > - - {uploadText ? ( -
-

- Clique para carregar ou arraste e solte a planilha. -

-

Apenas arquivos .xlsx e .xls

-
- ) : ( -
-

{selectedFile?.name}

-

Planilha selecionada

-
+ + +
+ +
+ +
+

+ {selectedFile ? ( + {selectedFile.name} + ) : ( + <> + Clique para selecionar ou arraste a lista Bocad (.xlsx / .xls) + + )} +

+

+ Lê automaticamente cabeçalho (Obra, Fase), peças com fundo cinza e componentes filhos com fundo branco. +

+
+ + {selectedFile && ( + + {(selectedFile.size / 1024).toFixed(1)} KB carregado + )}
-
- {/* Botão de Conversão */} -
- -
-
+ {/* Cards de Métricas e Identificação da OF */} + {extractedPieces.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 +
+ {extractedPieces.length} itens +
+
+ +
+ Compostas / Simples +
+ + SIM: {totalPecasCompostas} + + + NÃO: {totalPecasSimples} + +
+
+ +
+ Peso Total +
+ {totalPesoGeral.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}{' '} + kg +
+
+
+ )} + + {/* Barra de Ações */} + {extractedPieces.length > 0 && ( +
+
+ + + {extractedPieces.length} Peças Prontas para Importação + + + +
+ +
+ +
+
+ )} + + {/* Logs Expansíveis */} + {showLogs && processingLogs.length > 0 && ( +
+ {processingLogs.map((l, i) => ( +
{l}
+ ))} +
+ )} + + {/* Tabela de Pré-visualização com Visual Moderno (Igual ao Print 2) */} + {extractedPieces.length > 0 && ( +
+
+ + + Prévia da Planilha de Destino (12 Colunas Oficiais) + + + Pronto para uso direto no botão "Importar Peças" + +
+ + + + + + + + + + + + + + + + + + + + + {extractedPieces.map((p, idx) => ( + + + + + + + + + + + + + + + ))} + +
OFFaseMarcaDescriçãoComposto por ComponentesQuantidadePeso Unitário (kg)Peso Total (kg)Tratamento SuperficialMaterialPerfil PrincipalComprimento Ref. (mm)
{p.of}{p.fase}{p.marca}{p.descricao} + {p.compostoPorComponentes === 'SIM' ? ( + + SIM ({p.qtdComponentesFilhos}) + + ) : ( + + NAO (1) + + )} + {p.quantidade} + {p.pesoUnitario.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} + + {p.pesoTotal.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} + + + {p.tratamentoSuperficial} + + {p.material}{p.perfilPrincipal} + {p.comprimentoRef || '-'} +
+
+
+ )} + +
); }; diff --git a/src/components/conversores/exemplo-lista-bocad.xlsx b/src/components/conversores/exemplo-lista-bocad.xlsx new file mode 100644 index 0000000..773e5ea Binary files /dev/null and b/src/components/conversores/exemplo-lista-bocad.xlsx differ