From c239dffdeeedeb3563538657bae9cf91351663f5 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Wed, 7 Oct 2026 22:48:50 -0300 Subject: [PATCH] feat: adicionar gestao de revisoes simples e criticas de pecas com integracao a apontamentos --- src/components/pecas/PecasTable.tsx | 29 +- .../pecas/revisoes/PainelRevisoesOFModal.tsx | 479 ++++++++++++++ .../pecas/revisoes/RevisaoPecaModal.tsx | 621 ++++++++++++++++++ src/components/pecas/table/TableRow.tsx | 34 +- src/hooks/usePecas.tsx | 3 + src/hooks/useRevisoes.ts | 397 +++++++++++ src/pages/CadastroPecas.tsx | 30 + src/pages/CadastroPecasFiltrado.tsx | 38 +- .../migrations/20261007_create_revisoes.sql | 88 +++ 9 files changed, 1715 insertions(+), 4 deletions(-) create mode 100644 src/components/pecas/revisoes/PainelRevisoesOFModal.tsx create mode 100644 src/components/pecas/revisoes/RevisaoPecaModal.tsx create mode 100644 src/hooks/useRevisoes.ts create mode 100644 supabase/migrations/20261007_create_revisoes.sql diff --git a/src/components/pecas/PecasTable.tsx b/src/components/pecas/PecasTable.tsx index b82bf1f..79bad23 100644 --- a/src/components/pecas/PecasTable.tsx +++ b/src/components/pecas/PecasTable.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { Table, TableBody, TableHeader } from '@/components/ui/table'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Package } from 'lucide-react'; +import { Package, FileText, AlertTriangle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { Peca } from '@/hooks/usePecas'; import { usePecasTable } from '@/hooks/usePecasTable'; import { TableFilters } from './table/TableFilters'; @@ -22,6 +23,9 @@ interface PecasTableProps { onEditPeca?: (peca: Peca) => void; onDeleteLastImport?: () => void; onBatchUpdatePecas?: (pecaIds: string[], updates: any) => Promise; + onOpenRevisaoPeca?: (peca: Peca) => void; + onOpenPainelRevisoes?: () => void; + totalRevisoesOF?: number; hasRecentImport: boolean; canEdit?: boolean; canDelete?: boolean; @@ -36,6 +40,9 @@ export function PecasTable({ onEditPeca, onDeleteLastImport, onBatchUpdatePecas, + onOpenRevisaoPeca, + onOpenPainelRevisoes, + totalRevisoesOF, hasRecentImport, canEdit = true, canDelete = true, @@ -169,6 +176,25 @@ export function PecasTable({ />
+ {onOpenPainelRevisoes && ( + + )} + {canEdit && onBatchUpdatePecas && ( handleEditPeca(peca)} onDelete={() => handleDeletePeca(peca.id)} onOpenComponentPopup={() => onOpenComponentPopup(peca.id)} + onOpenRevisao={() => onOpenRevisaoPeca?.(peca)} /> ))} diff --git a/src/components/pecas/revisoes/PainelRevisoesOFModal.tsx b/src/components/pecas/revisoes/PainelRevisoesOFModal.tsx new file mode 100644 index 0000000..06b90e8 --- /dev/null +++ b/src/components/pecas/revisoes/PainelRevisoesOFModal.tsx @@ -0,0 +1,479 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table'; +import { + FileText, + Search, + AlertTriangle, + Info, + Clock, + Printer, + ExternalLink, + Filter, + CheckCircle2, + RefreshCw, + Layers, + ArrowRight +} from 'lucide-react'; +import { useRevisoes, Revisao } from '@/hooks/useRevisoes'; +import { Peca } from '@/hooks/usePecas'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +interface PainelRevisoesOFModalProps { + isOpen: boolean; + onClose: () => void; + ofNumber: string; + pecas: Peca[]; + onOpenRevisaoPeca: (peca: Peca) => void; +} + +export function PainelRevisoesOFModal({ + isOpen, + onClose, + ofNumber, + pecas, + onOpenRevisaoPeca +}: PainelRevisoesOFModalProps) { + const { revisoes, loading, carregarRevisoesOF } = useRevisoes(ofNumber); + const [busca, setBusca] = useState(''); + const [filtroTipo, setFiltroTipo] = useState<'todos' | 'critica' | 'simples'>('todos'); + const [visualizandoRelatorio, setVisualizandoRelatorio] = useState(false); + + useEffect(() => { + if (isOpen && ofNumber) { + carregarRevisoesOF(ofNumber); + setVisualizandoRelatorio(false); + } + }, [isOpen, ofNumber, carregarRevisoesOF]); + + // Estatísticas + const stats = useMemo(() => { + const total = revisoes.length; + const criticas = revisoes.filter((r) => r.tipo_revisao === 'critica').length; + const simples = revisoes.filter((r) => r.tipo_revisao === 'simples').length; + const pecasUnicas = new Set(revisoes.map((r) => r.peca_id)).size; + return { total, criticas, simples, pecasUnicas }; + }, [revisoes]); + + // Filtro + const revisoesFiltradas = useMemo(() => { + return revisoes.filter((rev) => { + // Tipo + if (filtroTipo !== 'todos' && rev.tipo_revisao !== filtroTipo) { + return false; + } + // Busca texto + if (busca.trim()) { + const termo = busca.toLowerCase(); + const marca = (rev.peca?.marca || '').toLowerCase(); + const fase = (rev.peca?.etapa_fase || '').toLowerCase(); + const motivo = (rev.descricao_motivo || '').toLowerCase(); + const numero = (rev.numero_revisao || '').toLowerCase(); + const autor = (rev.usuario_nome || '').toLowerCase(); + return ( + marca.includes(termo) || + fase.includes(termo) || + motivo.includes(termo) || + numero.includes(termo) || + autor.includes(termo) + ); + } + return true; + }); + }, [revisoes, filtroTipo, busca]); + + const handleImprimirRelatorio = () => { + window.print(); + }; + + return ( + + + +
+
+
+ +
+
+ + Registro Geral de Revisões da OF + + {ofNumber || 'Geral'} + + + + Rastreabilidade técnica, histórico de modificações simples e críticas nas peças desta Ordem de Fabricação. + +
+
+ +
+ +
+
+
+ + {/* MODO RELATÓRIO IMPRESSÃO */} + {visualizandoRelatorio ? ( +
+
+ + Pré-visualização do Relatório Técnico para impressão ou exportação. + + +
+ + {/* Documento A4 Formato Relatório */} +
+ {/* Header do Relatório */} +
+
+

TRACKSTEEL INDUSTRIAL

+

Sistema Integrado de Gestão & Rastreabilidade de Estruturas

+

RELATÓRIO TÉCNICO DE GESTÃO DE REVISÕES

+
+
+

OF: {ofNumber}

+

Emissão: {format(new Date(), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}

+

Total de Revisões: {stats.total}

+
+
+ + {/* Tabela do Relatório */} + + + + + + + + + + + + + + + {revisoes.length === 0 ? ( + + + + ) : ( + revisoes.map((r) => ( + + + + + + + + + + + )) + )} + +
Data/HoraPeçaFaseRevisãoTipoMotivo / DescriçãoCampos AlteradosResponsável
+ Nenhum registro de revisão para esta OF. +
+ {r.data_revisao ? format(new Date(r.data_revisao), 'dd/MM/yy HH:mm') : '-'} + + {r.peca?.marca || '-'} + + {r.peca?.etapa_fase || '-'} + + {r.numero_revisao} + + + {r.tipo_revisao === 'critica' ? 'CRÍTICA' : 'SIMPLES'} + + + {r.descricao_motivo} + {r.observacoes && Obs: {r.observacoes}} + + {r.campos_alterados?.join(', ') || '-'} + + {r.usuario_nome || r.usuario_email || 'Sistema'} +
+ + {/* Rodapé e Assinaturas */} +
+
+
+

Engenharia / PCP

+

Responsável pelo Lançamento

+
+
+
+

Supervisão de Produção

+

Aceite de Modificação

+
+
+
+
+ ) : ( + /* MODO PAINEL / TABELA */ +
+ {/* CARDS DE MÉTRICAS */} +
+
+ + Total de Revisões + + + {stats.total} + + histórico completo +
+ +
+ + Revisões Críticas + + + {stats.criticas} + + alteraram marca, qtd, peso +
+ +
+ + Revisões Simples + + + {stats.simples} + + dados informativos +
+ +
+ + Peças Afetadas + + + {stats.pecasUnicas} + + peças distintas +
+
+ + {/* FILTROS E BUSCA */} +
+
+
+ + setBusca(e.target.value)} + placeholder="Filtrar por peça, fase, motivo ou autor..." + className="pl-8 bg-slate-900 border-slate-800 text-xs h-8" + /> +
+
+ +
+ + + + + + +
+
+ + {/* TABELA DE REVISÕES */} +
+ + + + Data + Peça + Fase + Revisão + Tipo + Motivo da Revisão + Campos Alterados + Autor + Ação + + + + {loading ? ( + + + Carregando revisões da OF... + + + ) : revisoesFiltradas.length === 0 ? ( + + + {revisoes.length === 0 + ? 'Nenhuma revisão cadastrada nesta OF.' + : 'Nenhuma revisão encontrada para os filtros selecionados.'} + + + ) : ( + revisoesFiltradas.map((r) => { + const pecaCorrespondente = pecas.find((p) => p.id === r.peca_id); + return ( + + + {r.data_revisao ? format(new Date(r.data_revisao), 'dd/MM/yy HH:mm') : '-'} + + + + {r.peca?.marca || '-'} + + + + {r.peca?.etapa_fase || '-'} + + + + + {r.numero_revisao} + + + + + + {r.tipo_revisao === 'critica' ? 'Crítica' : 'Simples'} + + + + + {r.descricao_motivo} + {r.observacoes && ( + + {r.observacoes} + + )} + + + +
+ {r.campos_alterados?.map((c) => ( + + {c} + + ))} +
+
+ + + {r.usuario_nome || r.usuario_email || 'Sistema'} + + + + {pecaCorrespondente ? ( + + ) : ( + - + )} + +
+ ); + }) + )} +
+
+
+
+ )} +
+
+ ); +} diff --git a/src/components/pecas/revisoes/RevisaoPecaModal.tsx b/src/components/pecas/revisoes/RevisaoPecaModal.tsx new file mode 100644 index 0000000..6aaf494 --- /dev/null +++ b/src/components/pecas/revisoes/RevisaoPecaModal.tsx @@ -0,0 +1,621 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { + AlertTriangle, + History, + CheckCircle2, + ArrowRight, + TrendingUp, + TrendingDown, + Layers, + Save, + Clock, + User, + Info, + ShieldAlert, + Edit3 +} from 'lucide-react'; +import { Peca } from '@/hooks/usePecas'; +import { useRevisoes, Revisao, TipoRevisao } from '@/hooks/useRevisoes'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +interface RevisaoPecaModalProps { + isOpen: boolean; + onClose: () => void; + peca: Peca | null; + onPecaAtualizada?: () => void; +} + +export function RevisaoPecaModal({ + isOpen, + onClose, + peca, + onPecaAtualizada +}: RevisaoPecaModalProps) { + const { carregarRevisoesPeca, detectarTipoRevisao, criarRevisao, loading } = useRevisoes(); + + const [activeTab, setActiveTab] = useState<'nova' | 'historico'>('nova'); + const [historico, setHistorico] = useState([]); + const [loadingHistorico, setLoadingHistorico] = useState(false); + + // Campos editáveis do formulário + const [formData, setFormData] = useState({ + marca: '', + quantidade: 0, + peso_unitario: 0, + peso_total: 0, + descricao: '', + etapa_fase: '', + material: '', + perfil_principal: '', + tratamento_superficial: '', + comprimento: 0 + }); + + // Campos de controle da revisão + const [numeroRevisao, setNumeroRevisao] = useState('Rev 01'); + const [motivo, setMotivo] = useState(''); + const [observacoes, setObservacoes] = useState(''); + const [tipoManual, setTipoManual] = useState(null); + + // Inicializa os dados quando o modal abre + useEffect(() => { + if (peca && isOpen) { + setFormData({ + marca: peca.marca || '', + quantidade: Number(peca.quantidade) || 0, + peso_unitario: Number(peca.peso_unitario) || 0, + peso_total: Number(peca.peso_total) || 0, + descricao: peca.descricao || '', + etapa_fase: peca.etapa_fase || '', + material: peca.material || '', + perfil_principal: peca.perfil_principal || '', + tratamento_superficial: peca.tratamento_superficial || '', + comprimento: Number(peca.comprimento) || 0 + }); + setMotivo(''); + setObservacoes(''); + setTipoManual(null); + + // Carregar histórico + setLoadingHistorico(true); + carregarRevisoesPeca(peca.id).then((revs) => { + setHistorico(revs); + setLoadingHistorico(false); + // Sugere o próximo número de revisão + const proximoNum = revs.length + 1; + setNumeroRevisao(`Rev ${String(proximoNum).padStart(2, '0')}`); + }); + } + }, [peca, isOpen, carregarRevisoesPeca]); + + if (!peca) return null; + + // Analisa alterações em tempo real + const analise = detectarTipoRevisao(peca, formData); + const tipoEfetivo = tipoManual || analise.tipo; + + const diffQtd = Number(formData.quantidade) - Number(peca.quantidade || 0); + const diffPeso = Number(formData.peso_unitario) - Number(peca.peso_unitario || 0); + + const handleInputChange = (field: string, value: any) => { + setFormData((prev) => { + const next = { ...prev, [field]: value }; + if (field === 'quantidade' || field === 'peso_unitario') { + const q = field === 'quantidade' ? Number(value) : prev.quantidade; + const p = field === 'peso_unitario' ? Number(value) : prev.peso_unitario; + next.peso_total = Number((q * p).toFixed(3)); + } + return next; + }); + }; + + const handleSalvarRevisao = async (e: React.FormEvent) => { + e.preventDefault(); + if (!motivo.trim()) { + return; + } + + const sucesso = await criarRevisao({ + peca, + numeroRevisao, + tipoRevisao: tipoEfetivo, + descricaoMotivo: motivo, + observacoes, + novosDados: formData + }); + + if (sucesso) { + if (onPecaAtualizada) { + onPecaAtualizada(); + } + onClose(); + } + }; + + return ( + + + +
+
+
+ +
+
+ + Gestão de Revisões da Peça + + {peca.marca} + + {historico.length > 0 && ( + + {historico.length} {historico.length === 1 ? 'revisão gravada' : 'revisões gravadas'} + + )} + + + OF: {peca.of_number} • Fase: {peca.etapa_fase || 'N/A'} • Qtd Atual: {peca.quantidade} un • Peso Atual: {peca.peso_total || (peca.quantidade * peca.peso_unitario)} kg + +
+
+ +
+ + {tipoEfetivo === 'critica' ? '⚠️ Revisão Crítica' : 'ℹ️ Revisão Simples'} + +
+
+
+ + setActiveTab(v as any)} className="mt-4"> + + + + Nova Revisão (Editar Peça) + + + + Histórico de Revisões ({historico.length}) + + + + {/* ABA 1: NOVA REVISÃO */} + +
+ {/* Metadados da Revisão */} +
+
+

+ + Identificação da Revisão +

+
+ Classificação: + + +
+
+ +
+
+ + setNumeroRevisao(e.target.value)} + placeholder="Ex: Rev 01, Rev 02, Rev A" + className="bg-slate-950 border-slate-700 text-slate-100 font-mono text-sm mt-1" + required + /> +
+ +
+ + setMotivo(e.target.value)} + placeholder="Ex: Ajuste de projeto, alteração de quantidade pelo cliente..." + className="bg-slate-950 border-slate-700 text-slate-100 text-sm mt-1" + required + /> +
+
+ +
+ +