From 3ec309e9f7b605d8fe108e9e8297df5e8b074742 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Wed, 9 Sep 2026 14:57:59 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20nova=20mec=C3=A2nica=20do=20conversor?= =?UTF-8?q?=20Bocad=20para=20padr=C3=A3o=20oficial=20de=20importa=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/conversores/BocadConverter.tsx | 911 ++++++++++++++---- .../conversores/exemplo-lista-bocad.xlsx | Bin 0 -> 21210 bytes 2 files changed, 730 insertions(+), 181 deletions(-) create mode 100644 src/components/conversores/exemplo-lista-bocad.xlsx 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 0000000000000000000000000000000000000000..773e5eadcb643df3673f186e5173349e59b58ae4 GIT binary patch literal 21210 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 literal 0 HcmV?d00001