From 1e191fe56267fa58c528d123cdede024ad950232 Mon Sep 17 00:00:00 2001 From: admbrainsteel Date: Wed, 21 Oct 2026 16:05:21 -0300 Subject: [PATCH] feat(expedicao): fluxo inteligente de confirmacao de entrega de romaneio com data real e apontamento em cascata --- .../ConfirmacaoEntregaRomaneioModal.tsx | 385 ++++++++++++++++++ src/components/expedicao/RomaneioForm.tsx | 85 ++-- src/components/expedicao/RomaneioTable.tsx | 25 +- src/hooks/useRomaneios.tsx | 11 +- src/utils/validacaoRomaneio.ts | 297 +++++++++++--- 5 files changed, 692 insertions(+), 111 deletions(-) create mode 100644 src/components/expedicao/ConfirmacaoEntregaRomaneioModal.tsx diff --git a/src/components/expedicao/ConfirmacaoEntregaRomaneioModal.tsx b/src/components/expedicao/ConfirmacaoEntregaRomaneioModal.tsx new file mode 100644 index 0000000..5f07595 --- /dev/null +++ b/src/components/expedicao/ConfirmacaoEntregaRomaneioModal.tsx @@ -0,0 +1,385 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Truck, + Calendar, + AlertTriangle, + CheckCircle2, + Loader2, + Sparkles, + Info +} from 'lucide-react'; +import { toast } from 'sonner'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAuth } from '@/hooks/useAuth'; +import { + validarRomaneioParaEntrega, + executarApontamentoRomaneioEntregue, + ResultadoValidacaoEntrega +} from '@/utils/validacaoRomaneio'; + +export interface RomaneioEntregaInfo { + id: string; + numero_romaneio: string; + of_number: string; + data_prevista_entrega?: string | null; + data_romaneio?: string | null; + status?: string; +} + +interface ConfirmacaoEntregaRomaneioModalProps { + isOpen: boolean; + onClose: () => void; + romaneio: RomaneioEntregaInfo | null; + onSucesso?: () => void; +} + +export const ConfirmacaoEntregaRomaneioModal: React.FC = ({ + isOpen, + onClose, + romaneio, + onSucesso +}) => { + const { user } = useAuth(); + const queryClient = useQueryClient(); + + const [loadingAnalise, setLoadingAnalise] = useState(false); + const [analise, setAnalise] = useState(null); + const [dataRealEntrega, setDataRealEntrega] = useState(''); + const [processando, setProcessando] = useState(false); + + // Inicializar dados ao abrir + useEffect(() => { + if (isOpen && romaneio) { + const hoje = new Date().toISOString().split('T')[0]; + const dataInicial = romaneio.data_prevista_entrega || hoje; + setDataRealEntrega(dataInicial); + carregarAnalise(romaneio.id, romaneio.of_number); + } else { + setAnalise(null); + setDataRealEntrega(''); + } + }, [isOpen, romaneio]); + + const carregarAnalise = async (romaneioId: string, ofNumber: string) => { + try { + setLoadingAnalise(true); + const res = await validarRomaneioParaEntrega(romaneioId, ofNumber); + setAnalise(res); + } catch (e: any) { + console.error('Erro ao analisar romaneio:', e); + toast.error('Erro ao verificar status das peças: ' + (e?.message || 'Falha de comunicação')); + } finally { + setLoadingAnalise(false); + } + }; + + const handleExecutar = async (autorizarCascata: boolean, apontarApenasLiberadas: boolean) => { + if (!romaneio) return; + if (!dataRealEntrega) { + toast.error('Informe a data real de entrega para gravar o apontamento'); + return; + } + + try { + setProcessando(true); + const res = await executarApontamentoRomaneioEntregue({ + romaneioId: romaneio.id, + ofNumber: romaneio.of_number, + numeroRomaneio: romaneio.numero_romaneio, + dataEntregaReal: dataRealEntrega, + autorizarCascata, + apontarApenasLiberadas, + userId: user?.id + }); + + if (res.sucesso) { + toast.success(res.mensagem); + // Atualizar queries ativas para atualizar tabelas e gráficos + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['romaneios'] }), + queryClient.invalidateQueries({ queryKey: ['apontamentos'] }), + queryClient.invalidateQueries({ queryKey: ['dashboard-producao'] }), + queryClient.invalidateQueries({ queryKey: ['pecas'] }) + ]); + + onClose(); + if (onSucesso) onSucesso(); + } else { + toast.warning(res.mensagem); + } + } catch (e: any) { + console.error('Erro ao processar entrega:', e); + toast.error('Erro ao concluir apontamento: ' + (e?.message || 'Falha no banco de dados')); + } finally { + setProcessando(false); + } + }; + + if (!romaneio) return null; + + const dataPrevistaFormatada = romaneio.data_prevista_entrega + ? new Date(romaneio.data_prevista_entrega + 'T12:00:00').toLocaleDateString('pt-BR') + : null; + + const hoje = new Date().toISOString().split('T')[0]; + const dataDiferenteDePrevista = romaneio.data_prevista_entrega && dataRealEntrega !== romaneio.data_prevista_entrega; + + return ( + + + +
+
+ +
+
+ + Confirmar Entrega & Apontamento de Expedição + + + Romaneio {romaneio.numero_romaneio} • OF {romaneio.of_number} + +
+
+
+ +
+ {/* Seção 1: Data Real da Entrega */} +
+
+ +
+ {romaneio.data_prevista_entrega && ( + + )} + +
+
+ +
+ setDataRealEntrega(e.target.value)} + className="h-9 font-medium" + /> +
+ {dataDiferenteDePrevista ? ( + + + Data real difere da data prevista ({dataPrevistaFormatada}). + + ) : ( + Os apontamentos serão gravados exatamente nesta data. + )} +
+
+
+ + {/* Seção 2: Diagnóstico dos Processos de Fabricação */} + {loadingAnalise ? ( +
+ +

Analisando processos de fabricação das peças...

+
+ ) : analise ? ( +
+ {analise.totalPendentes === 0 ? ( + // Cenário A: Todas as peças liberadas +
+ +
+

+ Tudo pronto para Expedição! +

+

+ Todas as {analise.totalItens} peças deste romaneio já concluíram os processos de fabricação anteriores na fábrica. Ao confirmar, o apontamento de Expedição será registrado imediatamente para todas as unidades na data informada. +

+
+
+ ) : ( + // Cenário B: Existem peças com pendências nos processos anteriores +
+
+ +
+

+ Peças Pendentes nos Processos Anteriores ({analise.totalPendentes} de {analise.totalItens} itens) +

+

+ Algumas peças deste romaneio ainda não possuem apontamentos completos nos processos de fabricação anteriores (Corte, Solda ou Pintura/Galv). +

+
+
+ + {/* Tabela de Peças Pendentes */} +
+
+ Detalhamento das Peças com Pendência + + {analise.totalPendentes} peça(s) a regularizar + +
+ + + + + + + + + + + + {analise.pecasAnalise + ?.filter(p => !p.liberada) + .map((peca) => ( + + + + + + + ))} + +
MarcaFaseNo RomaneioProcessos que Faltam Apontar
+ {peca.marca} + + + Fase {peca.fase} + + + {peca.quantidade_expedida} un + +
+ {peca.processosFaltantes.map((f, i) => ( + + {f.nome}: faltam {f.qtdFaltante} un + + ))} +
+
+
+
+ + {/* Pergunta de Decisão com Destaque */} +
+ +
+

+ Autorizar Apontamento em Cascata? +

+

+ O sistema irá criar automaticamente os apontamentos faltantes nos processos pendentes para regularizar a produção e registrar a entrega completa de todas as peças na Expedição. +

+
+
+
+ )} +
+ ) : null} +
+ + + + +
+ {analise && analise.totalPendentes === 0 ? ( + // Botão para caso sem pendências + + ) : analise && analise.totalPendentes > 0 ? ( + <> + {analise.totalLiberadas && analise.totalLiberadas > 0 ? ( + + ) : null} + + + + ) : null} +
+
+
+
+ ); +}; diff --git a/src/components/expedicao/RomaneioForm.tsx b/src/components/expedicao/RomaneioForm.tsx index 55fa0d8..5603348 100644 --- a/src/components/expedicao/RomaneioForm.tsx +++ b/src/components/expedicao/RomaneioForm.tsx @@ -10,8 +10,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Save, X, AlertTriangle, Loader2 } from 'lucide-react'; import { RomaneioExpedicao } from '@/hooks/useRomaneios'; import { toast } from 'sonner'; -import { validarRomaneioParaEntrega } from '@/utils/validacaoRomaneio'; -import { PendenciasProducaoModal, ItemPendenciaProducao } from './PendenciasProducaoModal'; +import { ConfirmacaoEntregaRomaneioModal } from './ConfirmacaoEntregaRomaneioModal'; interface RomaneioFormProps { romaneio?: RomaneioExpedicao; @@ -118,33 +117,22 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel return Array.from(new Set(list)).filter(Boolean); }, [ofNumbers, romaneio?.of_number, selectedOF, formData.of_number]); - // Estados para controle de validação de entrega - const [validandoEntrega, setValidandoEntrega] = useState(false); - const [modalPendenciasAberto, setModalPendenciasAberto] = useState(false); - const [pendenciasEncontradas, setPendenciasEncontradas] = useState([]); - const [mensagemBloqueio, setMensagemBloqueio] = useState(undefined); +// Estados para controle de validação e confirmação de entrega + const [modalConfirmacaoAberto, setModalConfirmacaoAberto] = useState(false); - const handleStatusChange = async (novoStatus: any) => { + const handleStatusChange = (novoStatus: any) => { if (novoStatus === 'Entregue') { if (!romaneio?.id) { toast.warning('Novo romaneio deve ser criado como "Em planejamento"', { - description: 'Cadastre o romaneio primeiro, adicione as peças e confirme os apontamentos de produção antes de marcá-lo como entregue.' + description: 'Cadastre o romaneio primeiro, adicione as peças e confirme os apontamentos antes de marcá-lo como entregue.' }); setFormData(prev => ({ ...prev, status: 'Em planejamento' })); return; } - setValidandoEntrega(true); - const resultado = await validarRomaneioParaEntrega(romaneio.id, formData.of_number || romaneio.of_number); - setValidandoEntrega(false); - - if (!resultado.valido) { - setPendenciasEncontradas(resultado.pendencias || []); - setMensagemBloqueio(resultado.mensagem); - setModalPendenciasAberto(true); - // Não altera o status para Entregue, mantém o anterior - return; - } + // Abre o modal inteligente de confirmação de entrega + setModalConfirmacaoAberto(true); + return; } setFormData(prev => ({ ...prev, status: novoStatus })); @@ -153,24 +141,16 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (formData.status === 'Entregue') { + if (formData.status === 'Entregue' && romaneio?.status !== 'Entregue') { if (!romaneio?.id) { toast.error('Não é possível criar um novo romaneio diretamente como "Entregue".'); setFormData(prev => ({ ...prev, status: 'Em planejamento' })); return; } - setValidandoEntrega(true); - const resultado = await validarRomaneioParaEntrega(romaneio.id, formData.of_number || romaneio.of_number); - setValidandoEntrega(false); - - if (!resultado.valido) { - setPendenciasEncontradas(resultado.pendencias || []); - setMensagemBloqueio(resultado.mensagem); - setModalPendenciasAberto(true); - toast.error('Não é possível definir como "Entregue": existem pendências na produção.'); - return; - } + // Se tentar salvar como Entregue sem ter passado pela confirmação, abrir modal + setModalConfirmacaoAberto(true); + return; } console.log('💾 Salvando romaneio com dados:', formData); @@ -322,7 +302,6 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel