diff --git a/src/components/expedicao/PendenciasProducaoModal.tsx b/src/components/expedicao/PendenciasProducaoModal.tsx new file mode 100644 index 0000000..954d855 --- /dev/null +++ b/src/components/expedicao/PendenciasProducaoModal.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle, AlertCircle, CheckCircle2 } from 'lucide-react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Badge } from '@/components/ui/badge'; + +export interface ItemPendenciaProducao { + peca_id: string; + marca: string; + processo: string; + ordem: number; + qtdNecessaria: number; + qtdApontada: number; + qtdFaltante: number; +} + +interface PendenciasProducaoModalProps { + isOpen: boolean; + onClose: () => void; + pendencias: ItemPendenciaProducao[]; + numeroRomaneio?: string; + mensagemSemItens?: string; +} + +export const PendenciasProducaoModal: React.FC = ({ + isOpen, + onClose, + pendencias, + numeroRomaneio, + mensagemSemItens +}) => { + return ( + + + +
+
+ +
+
+ + Bloqueio de Expedição: Peças Pendentes + + + {numeroRomaneio ? `Romaneio ${numeroRomaneio} • ` : ''} + Não é permitido definir o status como "Entregue" com pendências de produção. + +
+
+
+ +
+ {mensagemSemItens ? ( +
+ + {mensagemSemItens} +
+ ) : ( +
+
+ As peças abaixo ainda não foram totalmente apontadas nos processos de fabricação anteriores (Corte, Solda, Pintura, etc.). É necessário concluir os apontamentos na produção antes de marcar a entrega. +
+ + + + + + + + + + + + + + {pendencias.map((p, idx) => ( + + + + + + + + ))} + +
Marca / PeçaProcesso PendenteNo RomaneioApontadoFaltam
+ {p.marca} + + + {p.processo} + + + {p.qtdNecessaria} + + {p.qtdApontada} + + {p.qtdFaltante} +
+
+
+ )} +
+ + + + +
+
+ ); +}; diff --git a/src/components/expedicao/RomaneioForm.tsx b/src/components/expedicao/RomaneioForm.tsx index 96f78e2..c46e032 100644 --- a/src/components/expedicao/RomaneioForm.tsx +++ b/src/components/expedicao/RomaneioForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -7,8 +7,11 @@ import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Save, X, AlertTriangle } from 'lucide-react'; +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'; interface RomaneioFormProps { romaneio?: RomaneioExpedicao; @@ -22,12 +25,11 @@ interface RomaneioFormProps { } export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel, loading, isOpen, onClose }: RomaneioFormProps) { - console.log('📋 RomaneioForm renderizado com OFs:', ofNumbers); - // Helper function to format date for input (YYYY-MM-DD) - const formatDateForInput = (date: string | Date) => { + const formatDateForInput = (date: string | Date | undefined | null) => { if (!date) return ''; const d = new Date(date); + if (isNaN(d.getTime())) return ''; return d.toISOString().split('T')[0]; }; @@ -41,34 +43,141 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel of_number: romaneio?.of_number || selectedOF || '', data_romaneio: romaneio?.data_romaneio ? formatDateForInput(romaneio.data_romaneio) : getCurrentLocalDate(), data_criacao: romaneio?.data_criacao ? formatDateForInput(romaneio.data_criacao) : getCurrentLocalDate(), - revisao: romaneio?.revisao || 0, + revisao: romaneio?.revisao ?? 0, motivo_revisao: romaneio?.motivo_revisao || '', data_prevista_entrega: romaneio?.data_prevista_entrega ? formatDateForInput(romaneio.data_prevista_entrega) : '', - prioridade: romaneio?.prioridade || 'Normal' as 'Normal' | 'Urgente', - status: romaneio?.status || 'Em planejamento' as 'Em planejamento' | 'Entregue' | 'Conferido em Obra', + prioridade: (romaneio?.prioridade as 'Normal' | 'Urgente') || 'Normal', + status: (romaneio?.status as 'Em planejamento' | 'Entregue' | 'Conferido em Obra') || 'Em planejamento', observacoes: romaneio?.observacoes || '', peso_total_romaneio: romaneio?.peso_total_romaneio || 0, previsao_kg: romaneio?.previsao_kg || 0, maior_dimensao: romaneio?.maior_dimensao || '', tipo_transporte: romaneio?.tipo_transporte || 'carro' as 'carro' | 'utilitario' | 'caminho_pequeno' | 'caminhao_trucado' | 'caminhao_munck' | 'carreta_12m' | 'carreta_15m' | 'especial', - frete_tipo: romaneio?.frete_tipo || 'terceiros' as 'terceiros' | 'proprio', + frete_tipo: (romaneio?.frete_tipo as 'terceiros' | 'proprio') || 'terceiros', nome_motorista: romaneio?.nome_motorista || '' }); - const handleSubmit = (e: React.FormEvent) => { + // Sincronizar os dados do formulário sempre que o modal abrir ou o romaneio selecionado mudar + useEffect(() => { + if (isOpen) { + if (romaneio) { + setFormData({ + of_number: romaneio.of_number || '', + data_romaneio: romaneio.data_romaneio ? formatDateForInput(romaneio.data_romaneio) : getCurrentLocalDate(), + data_criacao: romaneio.data_criacao ? formatDateForInput(romaneio.data_criacao) : getCurrentLocalDate(), + revisao: romaneio.revisao ?? 0, + motivo_revisao: romaneio.motivo_revisao || '', + data_prevista_entrega: romaneio.data_prevista_entrega ? formatDateForInput(romaneio.data_prevista_entrega) : '', + prioridade: (romaneio.prioridade as 'Normal' | 'Urgente') || 'Normal', + status: (romaneio.status as 'Em planejamento' | 'Entregue' | 'Conferido em Obra') || 'Em planejamento', + observacoes: romaneio.observacoes || '', + peso_total_romaneio: romaneio.peso_total_romaneio || 0, + previsao_kg: romaneio.previsao_kg || 0, + maior_dimensao: romaneio.maior_dimensao || '', + tipo_transporte: romaneio.tipo_transporte || 'carro', + frete_tipo: (romaneio.frete_tipo as 'terceiros' | 'proprio') || 'terceiros', + nome_motorista: romaneio.nome_motorista || '' + }); + } else { + setFormData({ + of_number: selectedOF || '', + data_romaneio: getCurrentLocalDate(), + data_criacao: getCurrentLocalDate(), + revisao: 0, + motivo_revisao: '', + data_prevista_entrega: '', + prioridade: 'Normal', + status: 'Em planejamento', + observacoes: '', + peso_total_romaneio: 0, + previsao_kg: 0, + maior_dimensao: '', + tipo_transporte: 'carro', + frete_tipo: 'terceiros', + nome_motorista: '' + }); + } + } + }, [isOpen, romaneio, selectedOF]); + + // Lista consolidada de OFs garantindo que a OF do romaneio atual sempre apareça no Select + const availableOFs = useMemo(() => { + const list = [...(ofNumbers || [])]; + if (romaneio?.of_number && !list.includes(romaneio.of_number)) { + list.unshift(romaneio.of_number); + } + if (selectedOF && !list.includes(selectedOF)) { + list.unshift(selectedOF); + } + if (formData.of_number && !list.includes(formData.of_number)) { + list.unshift(formData.of_number); + } + 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); + + const handleStatusChange = async (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.' + }); + 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; + } + } + + setFormData(prev => ({ ...prev, status: novoStatus })); + }; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + + if (formData.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; + } + } + console.log('💾 Salvando romaneio com dados:', formData); - - // Preparar dados para envio + const dataToSave = { ...formData, - // Garantir que nome_motorista não fique vazio nome_motorista: formData.nome_motorista.trim() || 'sem_definição', - // Garantir que a revisão seja um número revisao: parseInt(formData.revisao.toString()) || 0 }; - + onSave(dataToSave); }; @@ -95,255 +204,289 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel ]; return ( - - - -
- - {romaneio ? 'Editar Romaneio' : 'Novo Romaneio'} - - -
-
- -
- {/* Seção: Dados Básicos */} -
-

Dados Básicos

-
-
- - setFormData(prev => ({ ...prev, of_number: value }))} + disabled={!!romaneio || !!selectedOF} + > + + + + + {availableOFs.length === 0 ? ( + + Nenhuma OF disponível + + ) : ( + availableOFs.map((ofNumber) => ( + + {ofNumber} + + )) + )} + + + {availableOFs.length === 0 && ( +

+ Nenhuma OF encontrada. Verifique suas permissões ou contate o administrador. +

)} - - - {ofNumbers.length === 0 && ( -

- Nenhuma OF encontrada. Verifique suas permissões ou contate o administrador. -

- )} +
+ +
+ + setFormData(prev => ({ ...prev, data_criacao: e.target.value }))} + /> +
+ +
+ + setFormData(prev => ({ ...prev, data_romaneio: e.target.value }))} + /> +
+ +
+ + setFormData(prev => ({ ...prev, revisao: parseInt(e.target.value) || 0 }))} + /> +
+ +
+ + setFormData(prev => ({ ...prev, data_prevista_entrega: e.target.value }))} + /> +
+ +
+ + +
+ +
+ + +
+
+ + {/* Badge de Alerta */} + + + + QUANDO O STATUS PASSAR PARA "ENTREGUE" AS PEÇAS SERÃO APONTADAS AUTOMATICAMENTE NO PROCESSO DE EXPEDIÇÃO (DESDE QUE TODOS OS PROCESSOS ANTERIORES ESTEJAM CONCLUÍDOS) + +
-
- - setFormData(prev => ({ ...prev, data_criacao: e.target.value }))} - /> + {/* Seção: Dados de Transporte */} +
+

Dados de Transporte

+
+
+ + setFormData(prev => ({ ...prev, previsao_kg: parseFloat(e.target.value) || 0 }))} + /> +
+ +
+ + setFormData(prev => ({ ...prev, maior_dimensao: e.target.value }))} + placeholder="Ex: 12m x 2m x 3m" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + setFormData(prev => ({ ...prev, nome_motorista: e.target.value }))} + placeholder="Nome completo do motorista (opcional)" + /> +

+ Se não preenchido, será definido automaticamente como "sem_definição" +

+
+
-
- - setFormData(prev => ({ ...prev, data_romaneio: e.target.value }))} - /> + {/* Seção: Observações */} +
+

Observações

+
+
+ +