diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 477d9db..2ff4a8b 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -24,6 +24,17 @@ import { import { toast } from 'sonner'; import * as XLSX from 'xlsx'; import { useAppLabels } from '@/hooks/useAppLabels'; +import { usePecas } from '@/hooks/usePecas'; + +export interface ComponentItem { + marca: string; + quantidade: number; + perfil: string; + material: string; + comprimento: number; + pesoUnitario?: number; + pesoTotal: number; +} export interface ExtractedPiece { of: string; @@ -38,15 +49,7 @@ export interface ExtractedPiece { pesoUnit: number | string; pesoTotal: number | string; tratamentoSuperficial: string; -} - -interface ComponentItem { - quantidade: number; - perfil: string; - material: string; - comprimento: number; - pesoUnitario?: number; - pesoTotal: number; + components?: ComponentItem[]; } interface AssemblyItem { @@ -127,9 +130,11 @@ const ensurePdfJs = async (): Promise => { 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'); @@ -488,13 +493,15 @@ const AdvanceSteelConverterContent: React.FC = () => { if (currentAssembly) { const isSubComp = itemMark === '-' || + isComponente(itemMark) || itemMark.includes('-100') || itemMark.includes('-10') || - (itemMark.match(/-\d+$/) && parseInt(itemMark.match(/-(\d+)$/)![1], 10) >= 1000); + (itemMark.match(/-\d+$/) && parseInt(itemMark.match(/-(\d+)$/)![1], 10) >= (labels.faixaComponenteMin || 1000)); if (isSubComp) { currentAssembly.isComposed = 'SIM'; currentAssembly.components.push({ + marca: itemMark, quantidade: itemQuant, perfil: itemDesc || currentAssembly.descricao, material: detectedMaterial || 'A36', @@ -548,17 +555,15 @@ const AdvanceSteelConverterContent: React.FC = () => { }); if (bestComp) { - perfilPrincipal = bestComp.perfil || asm.descricao; - materialPrincipal = bestComp.material || 'A36'; - maxComp = bestComp.comprimento; + perfilPrincipal = bestComp.perfil; + materialPrincipal = bestComp.material; } - // Se não obteve subtotal na linha isolada, usar a somatória dos componentes - if (!totalPeso || totalPeso === 0) { + if (sumComponentsWeight > 0) { totalPeso = sumComponentsWeight; } - // Fallback de segurança para conjunto caso a quantidade na linha 1 não tenha sido lida + // 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) { @@ -566,7 +571,7 @@ const AdvanceSteelConverterContent: React.FC = () => { } } } else { - // Monopeça simples + // Monopeça: se totalPeso não veio, calcula if (!totalPeso || totalPeso === 0) { totalPeso = (asm.pesoUnitario || 0) * asm.quantidade; } @@ -595,7 +600,8 @@ const AdvanceSteelConverterContent: React.FC = () => { comprimentoMax: maxComp > 0 ? maxComp : '-', pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(2) : '-', pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : '-', - tratamentoSuperficial: 'pintura' + tratamentoSuperficial: 'pintura', + components: isComposed ? asm.components : [] }; }); @@ -663,7 +669,8 @@ const AdvanceSteelConverterContent: React.FC = () => { const exportToExcel = () => { if (extractedData.length === 0) return; - const excelRows = extractedData.map((item) => ({ + // 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, @@ -672,35 +679,167 @@ const AdvanceSteelConverterContent: React.FC = () => { 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, + '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 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 } + 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'); - 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}`); + 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 + }); + } + }); + + 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) => { @@ -880,10 +1019,29 @@ const AdvanceSteelConverterContent: React.FC = () => {
+ +
diff --git a/src/components/pecas/table/TableHeaderSortable.tsx b/src/components/pecas/table/TableHeaderSortable.tsx index 2c0e362..78117cb 100644 --- a/src/components/pecas/table/TableHeaderSortable.tsx +++ b/src/components/pecas/table/TableHeaderSortable.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { ChevronDown, ChevronUp } from 'lucide-react'; import { SortField, SortOrder } from '@/hooks/usePecasTable'; +import { useAppLabels } from '@/hooks/useAppLabels'; interface TableHeaderSortableProps { selectAll: boolean; @@ -21,6 +22,8 @@ export function TableHeaderSortable({ sortOrder, onSort }: TableHeaderSortableProps) { + const { labels } = useAppLabels(); + const SortButton = ({ field, children }: { field: SortField; children: React.ReactNode }) => ( - - {onEdit && ( + <> + + +
+ onSelect(checked as boolean)} + aria-label={`Selecionar peça ${peca.id}`} + /> + {hasSubComponents && ( + + )} +
+
+ + + {peca.of_number} + + + + {peca.etapa_fase} + + + +
+ {peca.marca} + {hasSubComponents && ( + + Comp + + )} +
+
+ + + {peca.descricao} + + + + + + + +
+ {!peca.tem_componentes ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ + + {peca.quantidade} + + + + {peca.peso_unitario} kg + + + + {(peca.peso_total || (peca.quantidade * peca.peso_unitario)).toFixed(2)} kg + + + + {peca.material || '-'} + + + + {peca.perfil_principal || '-'} + + + +
- )} - - {onDelete && ( - - )} -
-
-
+ + {onEdit && ( + + )} + + {onDelete && ( + + )} + + + + + {/* Sub-linha Expansível de Componentes (Acordeão / Master-Detail Inline) */} + {isExpanded && ( + + +
+
+
+ + + {labels.componentePlural} da {labels.pecaLabel} {peca.marca} + + + {loading ? 'Carregando...' : `${componentes.length} subitens cadastrados`} + + + • Lote total da peça: {peca.quantidade} un + +
+ + +
+ + {loading ? ( +
+ + Buscando {labels.componentePlural.toLowerCase()} no banco... +
+ ) : componentes.length === 0 ? ( +
+ + Nenhum {labels.componenteLabel.toLowerCase()} individual foi vinculado a esta peça no banco ainda. + + +
+ ) : ( +
+ + + + + + + + + + + + + {componentes.map((comp) => { + const qtdPorPeca = comp.quantidade_por_peca || 1; + const qtdTotalLote = qtdPorPeca * (peca.quantidade || 1); + const pesoUnit = comp.peso_unitario || 0; + const pesoTotalLote = qtdTotalLote * pesoUnit; + + return ( + + + + + + + + + ); + })} + +
Marca {labels.componenteLabel}Descrição / PerfilQtd / {labels.pecaLabel}Qtd Total no LotePeso Unit. (kg)Peso Total (kg)
+ + {comp.marca_componente} + + + {comp.perfil || comp.descricao || '-'} + + {qtdPorPeca} un + + {qtdTotalLote} un + + {pesoUnit > 0 ? `${pesoUnit.toFixed(2)} kg` : '-'} + + {pesoTotalLote > 0 ? `${pesoTotalLote.toFixed(2)} kg` : '-'} +
+
+ )} +
+
+
+ )} + ); } diff --git a/src/pages/CadastroPecas.tsx b/src/pages/CadastroPecas.tsx index d5657e6..9836e2f 100644 --- a/src/pages/CadastroPecas.tsx +++ b/src/pages/CadastroPecas.tsx @@ -8,6 +8,7 @@ import { usePecas, Peca } from '@/hooks/usePecas'; import { usePecasP4Listener } from '@/hooks/usePecasP4Listener'; import { PecaForm } from '@/components/pecas/PecaForm'; import { PecasTable } from '@/components/pecas/PecasTable'; +import { ComponentesPopup } from '@/components/pecas/ComponentesPopup'; import { Package, Plus, RefreshCw } from 'lucide-react'; import { usePermissionControl } from '@/hooks/usePermissionControl'; @@ -36,6 +37,7 @@ export default function CadastroPecas() { const [syncing, setSyncing] = useState(false); const [editingPeca, setEditingPeca] = useState(null); const [showForm, setShowForm] = useState(false); + const [selectedPecaForPopup, setSelectedPecaForPopup] = useState(null); const handleSave = async (formData: any) => { if (!canCreate()) return false; @@ -92,8 +94,12 @@ export default function CadastroPecas() { }; const handleOpenComponentPopup = (pecaId: string) => { - console.log('Abrir popup de componentes para peça:', pecaId); - toast.info('Funcionalidade de componentes será implementada em breve'); + const found = pecas.find(p => p.id === pecaId); + if (found) { + setSelectedPecaForPopup(found); + } else { + toast.error('Peça não encontrada'); + } }; const handleImportPecas = async (pecasData: any[]) => { @@ -235,6 +241,18 @@ export default function CadastroPecas() { /> + + {/* Modal de Gerenciamento de Componentes */} + {selectedPecaForPopup && ( + { + setSelectedPecaForPopup(null); + loadPecas(); + }} + peca={selectedPecaForPopup} + /> + )} ); diff --git a/src/pages/CadastroPecasFiltrado.tsx b/src/pages/CadastroPecasFiltrado.tsx index e72a1c5..d98ccc1 100644 --- a/src/pages/CadastroPecasFiltrado.tsx +++ b/src/pages/CadastroPecasFiltrado.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ArrowLeft, Plus, Package } from 'lucide-react'; import { PecaForm } from '@/components/pecas/PecaForm'; import { PecasTable } from '@/components/pecas/PecasTable'; +import { ComponentesPopup } from '@/components/pecas/ComponentesPopup'; import { usePecas, Peca } from '@/hooks/usePecas'; import { toast } from 'sonner'; import { usePermissionControl } from '@/hooks/usePermissionControl'; @@ -35,6 +36,7 @@ export default function CadastroPecasFiltrado() { const [saving, setSaving] = useState(false); const [editingPeca, setEditingPeca] = useState(null); const [showForm, setShowForm] = useState(false); + const [selectedPecaForPopup, setSelectedPecaForPopup] = useState(null); // Filtrar peças pela OF selecionada const pecasFiltradas = pecas.filter(peca => peca.of_number === ofSelecionada); @@ -106,8 +108,12 @@ export default function CadastroPecasFiltrado() { }; const handleOpenComponentPopup = (pecaId: string) => { - console.log('Abrir popup de componentes para peça:', pecaId); - toast.info('Funcionalidade de componentes será implementada em breve'); + const found = pecas.find(p => p.id === pecaId); + if (found) { + setSelectedPecaForPopup(found); + } else { + toast.error('Peça não encontrada'); + } }; const handleImportPecas = async (pecasData: any[]) => { @@ -282,6 +288,18 @@ export default function CadastroPecasFiltrado() { /> + + {/* Modal de Gerenciamento de Componentes */} + {selectedPecaForPopup && ( + { + setSelectedPecaForPopup(null); + loadPecas(); + }} + peca={selectedPecaForPopup} + /> + )} );