fix: edicao de romaneio com dados preenchidos, remocao de X e validacao de processos para status entregue

This commit is contained in:
2026-09-09 21:27:21 +00:00
parent 6d47f7eac9
commit 173a0e18a9
5 changed files with 722 additions and 265 deletions
@@ -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<PendenciasProducaoModalProps> = ({
isOpen,
onClose,
pendencias,
numeroRomaneio,
mensagemSemItens
}) => {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent hideClose className="max-w-2xl max-h-[85vh] flex flex-col p-6">
<DialogHeader className="space-y-3">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-red-100 dark:bg-red-950/40 text-red-600 dark:text-red-400">
<AlertTriangle className="h-6 w-6" />
</div>
<div>
<DialogTitle className="text-xl font-bold text-red-700 dark:text-red-400">
Bloqueio de Expedição: Peças Pendentes
</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground mt-1">
{numeroRomaneio ? `Romaneio ${numeroRomaneio} • ` : ''}
Não é permitido definir o status como "Entregue" com pendências de produção.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="py-3 flex-1 overflow-hidden">
{mensagemSemItens ? (
<div className="p-4 rounded-lg bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 text-red-800 dark:text-red-300 text-sm flex items-start gap-3">
<AlertCircle className="h-5 w-5 flex-shrink-0 text-red-600 mt-0.5" />
<span>{mensagemSemItens}</span>
</div>
) : (
<div className="space-y-3">
<div className="p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 rounded-md text-xs text-amber-900 dark:text-amber-200">
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.
</div>
<ScrollArea className="max-h-[320px] rounded-md border">
<table className="w-full text-sm text-left">
<thead className="bg-muted text-muted-foreground font-semibold text-xs uppercase border-b sticky top-0">
<tr>
<th className="py-2.5 px-3">Marca / Peça</th>
<th className="py-2.5 px-3">Processo Pendente</th>
<th className="py-2.5 px-3 text-center">No Romaneio</th>
<th className="py-2.5 px-3 text-center">Apontado</th>
<th className="py-2.5 px-3 text-center text-red-600">Faltam</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{pendencias.map((p, idx) => (
<tr key={`${p.peca_id}-${p.processo}-${idx}`} className="hover:bg-muted/40">
<td className="py-2.5 px-3 font-semibold text-foreground">
{p.marca}
</td>
<td className="py-2.5 px-3">
<Badge variant="outline" className="text-xs bg-muted/60">
{p.processo}
</Badge>
</td>
<td className="py-2.5 px-3 text-center font-medium">
{p.qtdNecessaria}
</td>
<td className="py-2.5 px-3 text-center text-muted-foreground">
{p.qtdApontada}
</td>
<td className="py-2.5 px-3 text-center font-bold text-red-600 dark:text-red-400">
{p.qtdFaltante}
</td>
</tr>
))}
</tbody>
</table>
</ScrollArea>
</div>
)}
</div>
<DialogFooter className="pt-2 border-t flex justify-end">
<Button onClick={onClose} variant="default" className="w-full sm:w-auto">
Entendido, manter em planejamento
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
+400 -257
View File
@@ -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<ItemPendenciaProducao[]>([]);
const [mensagemBloqueio, setMensagemBloqueio] = useState<string | undefined>(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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle>
{romaneio ? 'Editar Romaneio' : 'Novo Romaneio'}
</DialogTitle>
<Button variant="outline" onClick={handleCancel} size="sm">
<X className="h-4 w-4 mr-2" />
Fechar
</Button>
</div>
</DialogHeader>
<ScrollArea className="max-h-[calc(90vh-120px)] pr-4">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Seção: Dados Básicos */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados Básicos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="of_number">Ordem de Fabricação *</Label>
<Select
value={formData.of_number}
onValueChange={(value) => setFormData(prev => ({ ...prev, of_number: value }))}
disabled={!!romaneio || !!selectedOF}
>
<SelectTrigger>
<SelectValue placeholder="Selecionar OF" />
</SelectTrigger>
<SelectContent>
{ofNumbers.length === 0 ? (
<SelectItem value="" disabled>
Nenhuma OF disponível
</SelectItem>
) : (
ofNumbers.map((ofNumber) => (
<SelectItem key={ofNumber} value={ofNumber}>
{ofNumber}
</SelectItem>
))
<>
<Dialog open={isOpen} onOpenChange={onClose}>
{/* hideClose suprime o botão 'X' redundante no canto superior direito */}
<DialogContent hideClose className="max-w-4xl max-h-[90vh] overflow-hidden [&>button.absolute]:hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="text-xl font-bold">
{romaneio ? 'Editar Romaneio' : 'Novo Romaneio'}
</DialogTitle>
<Button variant="outline" onClick={handleCancel} size="sm">
<X className="h-4 w-4 mr-2" />
Fechar
</Button>
</div>
</DialogHeader>
<ScrollArea className="max-h-[calc(90vh-120px)] pr-4">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Seção: Dados Básicos */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados Básicos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="of_number">Ordem de Fabricação *</Label>
<Select
value={formData.of_number}
onValueChange={(value) => setFormData(prev => ({ ...prev, of_number: value }))}
disabled={!!romaneio || !!selectedOF}
>
<SelectTrigger>
<SelectValue placeholder="Selecionar OF" />
</SelectTrigger>
<SelectContent>
{availableOFs.length === 0 ? (
<SelectItem value="" disabled>
Nenhuma OF disponível
</SelectItem>
) : (
availableOFs.map((ofNumber) => (
<SelectItem key={ofNumber} value={ofNumber}>
{ofNumber}
</SelectItem>
))
)}
</SelectContent>
</Select>
{availableOFs.length === 0 && (
<p className="text-xs text-muted-foreground">
Nenhuma OF encontrada. Verifique suas permissões ou contate o administrador.
</p>
)}
</SelectContent>
</Select>
{ofNumbers.length === 0 && (
<p className="text-xs text-muted-foreground">
Nenhuma OF encontrada. Verifique suas permissões ou contate o administrador.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="data_criacao">Data de Criação *</Label>
<Input
id="data_criacao"
type="date"
value={formData.data_criacao}
onChange={(e) => setFormData(prev => ({ ...prev, data_criacao: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="data_romaneio">Data do Romaneio</Label>
<Input
id="data_romaneio"
type="date"
value={formData.data_romaneio}
onChange={(e) => setFormData(prev => ({ ...prev, data_romaneio: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="revisao">Revisão</Label>
<Input
id="revisao"
type="number"
min="0"
value={formData.revisao}
onChange={(e) => setFormData(prev => ({ ...prev, revisao: parseInt(e.target.value) || 0 }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="data_prevista_entrega">Data Prevista de Entrega</Label>
<Input
id="data_prevista_entrega"
type="date"
value={formData.data_prevista_entrega}
onChange={(e) => setFormData(prev => ({ ...prev, data_prevista_entrega: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="prioridade">Prioridade</Label>
<Select
value={formData.prioridade}
onValueChange={(value: 'Normal' | 'Urgente') => setFormData(prev => ({ ...prev, prioridade: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Normal">Normal</SelectItem>
<SelectItem value="Urgente">Urgente</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={formData.status}
onValueChange={handleStatusChange}
disabled={validandoEntrega}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{statusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Badge de Alerta */}
<Alert className="border-orange-500 bg-orange-50 dark:bg-orange-950/20 dark:border-orange-400 mt-4">
<AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
<AlertDescription className="text-orange-800 dark:text-orange-200 font-medium">
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)
</AlertDescription>
</Alert>
</div>
<div className="space-y-2">
<Label htmlFor="data_criacao">Data de Criação *</Label>
<Input
id="data_criacao"
type="date"
value={formData.data_criacao}
onChange={(e) => setFormData(prev => ({ ...prev, data_criacao: e.target.value }))}
/>
{/* Seção: Dados de Transporte */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados de Transporte</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="previsao_kg">Previsão de Kg</Label>
<Input
id="previsao_kg"
type="number"
min="0"
step="0.01"
value={formData.previsao_kg}
onChange={(e) => setFormData(prev => ({ ...prev, previsao_kg: parseFloat(e.target.value) || 0 }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maior_dimensao">Maior Dimensão</Label>
<Input
id="maior_dimensao"
value={formData.maior_dimensao}
onChange={(e) => setFormData(prev => ({ ...prev, maior_dimensao: e.target.value }))}
placeholder="Ex: 12m x 2m x 3m"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tipo_transporte">Tipo de Transporte</Label>
<Select
value={formData.tipo_transporte}
onValueChange={(value: any) => setFormData(prev => ({ ...prev, tipo_transporte: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{transporteOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="frete_tipo">Tipo de Frete</Label>
<Select
value={formData.frete_tipo}
onValueChange={(value: 'terceiros' | 'proprio') => setFormData(prev => ({ ...prev, frete_tipo: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="terceiros">Terceiros</SelectItem>
<SelectItem value="proprio">Próprio</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="nome_motorista">Nome do Motorista</Label>
<Input
id="nome_motorista"
value={formData.nome_motorista}
onChange={(e) => setFormData(prev => ({ ...prev, nome_motorista: e.target.value }))}
placeholder="Nome completo do motorista (opcional)"
/>
<p className="text-xs text-muted-foreground">
Se não preenchido, será definido automaticamente como "sem_definição"
</p>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="data_romaneio">Data do Romaneio</Label>
<Input
id="data_romaneio"
type="date"
value={formData.data_romaneio}
onChange={(e) => setFormData(prev => ({ ...prev, data_romaneio: e.target.value }))}
/>
{/* Seção: Observações */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Observações</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="motivo_revisao">Motivo da Revisão</Label>
<Textarea
id="motivo_revisao"
value={formData.motivo_revisao}
onChange={(e) => setFormData(prev => ({ ...prev, motivo_revisao: e.target.value }))}
placeholder="Descreva o motivo da revisão..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="observacoes">Observações Gerais</Label>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => setFormData(prev => ({ ...prev, observacoes: e.target.value }))}
placeholder="Observações do romaneio..."
rows={3}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="revisao">Revisão</Label>
<Input
id="revisao"
type="number"
min="0"
value={formData.revisao}
onChange={(e) => setFormData(prev => ({ ...prev, revisao: parseInt(e.target.value) || 0 }))}
/>
<div className="flex gap-2 pt-4">
<Button type="submit" disabled={loading || validandoEntrega || !formData.of_number}>
{validandoEntrega ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Validando processos...
</>
) : loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Salvando...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Salvar
</>
)}
</Button>
</div>
</form>
</ScrollArea>
</DialogContent>
</Dialog>
<div className="space-y-2">
<Label htmlFor="data_prevista_entrega">Data Prevista de Entrega</Label>
<Input
id="data_prevista_entrega"
type="date"
value={formData.data_prevista_entrega}
onChange={(e) => setFormData(prev => ({ ...prev, data_prevista_entrega: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="prioridade">Prioridade</Label>
<Select
value={formData.prioridade}
onValueChange={(value: 'Normal' | 'Urgente') => setFormData(prev => ({ ...prev, prioridade: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Normal">Normal</SelectItem>
<SelectItem value="Urgente">Urgente</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={formData.status}
onValueChange={(value: any) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{statusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Badge de Alerta */}
<Alert className="border-orange-500 bg-orange-50 dark:bg-orange-950/20 dark:border-orange-400 mt-4">
<AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
<AlertDescription className="text-orange-800 dark:text-orange-200 font-medium">
QUANDO O STATUS PASSAR PARA "ENTREGUE" AS PEÇAS SERÃO APONTADAS AUTOMATICAMENTE NO PROCESSO DE EXPEDIÇÃO
</AlertDescription>
</Alert>
</div>
{/* Seção: Dados de Transporte */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Dados de Transporte</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="previsao_kg">Previsão de Kg</Label>
<Input
id="previsao_kg"
type="number"
min="0"
step="0.01"
value={formData.previsao_kg}
onChange={(e) => setFormData(prev => ({ ...prev, previsao_kg: parseFloat(e.target.value) || 0 }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maior_dimensao">Maior Dimensão</Label>
<Input
id="maior_dimensao"
value={formData.maior_dimensao}
onChange={(e) => setFormData(prev => ({ ...prev, maior_dimensao: e.target.value }))}
placeholder="Ex: 12m x 2m x 3m"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tipo_transporte">Tipo de Transporte</Label>
<Select
value={formData.tipo_transporte}
onValueChange={(value: any) => setFormData(prev => ({ ...prev, tipo_transporte: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{transporteOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="frete_tipo">Tipo de Frete</Label>
<Select
value={formData.frete_tipo}
onValueChange={(value: 'terceiros' | 'proprio') => setFormData(prev => ({ ...prev, frete_tipo: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="terceiros">Terceiros</SelectItem>
<SelectItem value="proprio">Próprio</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="nome_motorista">Nome do Motorista</Label>
<Input
id="nome_motorista"
value={formData.nome_motorista}
onChange={(e) => setFormData(prev => ({ ...prev, nome_motorista: e.target.value }))}
placeholder="Nome completo do motorista (opcional)"
/>
<p className="text-xs text-muted-foreground">
Se não preenchido, será definido automaticamente como "sem_definição"
</p>
</div>
</div>
</div>
{/* Seção: Observações */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground border-b pb-2">Observações</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="motivo_revisao">Motivo da Revisão</Label>
<Textarea
id="motivo_revisao"
value={formData.motivo_revisao}
onChange={(e) => setFormData(prev => ({ ...prev, motivo_revisao: e.target.value }))}
placeholder="Descreva o motivo da revisão..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="observacoes">Observações Gerais</Label>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => setFormData(prev => ({ ...prev, observacoes: e.target.value }))}
placeholder="Observações do romaneio..."
rows={3}
/>
</div>
</div>
</div>
<div className="flex gap-2 pt-4">
<Button type="submit" disabled={loading || !formData.of_number}>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Salvando...' : 'Salvar'}
</Button>
</div>
</form>
</ScrollArea>
</DialogContent>
</Dialog>
{/* Modal de bloqueio/alerta caso existam peças pendentes */}
<PendenciasProducaoModal
isOpen={modalPendenciasAberto}
onClose={() => {
setModalPendenciasAberto(false);
setFormData(prev => ({
...prev,
status: (romaneio?.status as any) || 'Em planejamento'
}));
}}
pendencias={pendenciasEncontradas}
numeroRomaneio={romaneio?.numero_romaneio}
mensagemSemItens={mensagemBloqueio}
/>
</>
);
}
+13 -6
View File
@@ -27,10 +27,15 @@ const DialogOverlay = React.forwardRef<
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
hideClose?: boolean;
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
DialogContentProps
>(({ className, children, hideClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@@ -42,10 +47,12 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
{!hideClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
))
+16 -2
View File
@@ -2,6 +2,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { validarRomaneioParaEntrega } from '@/utils/validacaoRomaneio';
export interface RomaneioExpedicao {
id: string;
@@ -275,6 +276,19 @@ export const useAtualizarRomaneio = () => {
console.log('📋 Status atual do romaneio:', romaneioAtual?.status);
console.log('📋 Novo status a ser definido:', romaneio.status);
// Validação rigorosa: Não permitir passar para Entregue se houver pendências nos processos anteriores
if (romaneio.status === 'Entregue') {
const validacao = await validarRomaneioParaEntrega(id, romaneio.of_number || romaneioAtual?.of_number);
if (!validacao.valido) {
const msg = validacao.mensagem || (
validacao.pendencias && validacao.pendencias.length > 0
? `Não é possível marcar como "Entregue": existem ${validacao.pendencias.length} peça(s) com apontamentos pendentes nos processos anteriores.`
: 'Não é possível marcar como "Entregue": pendências encontradas.'
);
throw new Error(msg);
}
}
const { data, error } = await supabase
.from('romaneios_expedicao')
.update(romaneio)
@@ -352,9 +366,9 @@ export const useAtualizarRomaneio = () => {
queryClient.invalidateQueries({ queryKey: ['romaneios'] });
toast.success('Romaneio atualizado com sucesso!');
},
onError: (error) => {
onError: (error: any) => {
console.error('Erro ao atualizar romaneio:', error);
toast.error('Erro ao atualizar romaneio');
toast.error(error?.message || 'Erro ao atualizar romaneio');
},
});
};
+173
View File
@@ -0,0 +1,173 @@
import { supabase } from '@/integrations/supabase/client';
import { ItemPendenciaProducao } from '@/components/expedicao/PendenciasProducaoModal';
export interface ResultadoValidacaoEntrega {
valido: boolean;
mensagem?: string;
pendencias?: ItemPendenciaProducao[];
}
/**
* Valida se um romaneio pode ser marcado com o status "Entregue".
* Verifica se todas as peças cadastradas no romaneio foram devidamente apontadas
* em todos os processos de fabricação anteriores à Expedição (Detalhamento, Corte, Solda - quando aplicável -, Pintura/Galv).
*/
export async function validarRomaneioParaEntrega(
romaneioId?: string,
ofNumber?: string
): Promise<ResultadoValidacaoEntrega> {
// Se não tem ID do romaneio (está criando um novo romaneio no momento)
if (!romaneioId) {
return {
valido: false,
mensagem: 'Não é possível criar um novo romaneio diretamente com o status "Entregue". Crie o romaneio primeiro em planejamento, adicione as peças e garanta que todos os processos anteriores foram apontados na produção.'
};
}
try {
// 1. Buscar as peças cadastradas no romaneio
const { data: itens, error: itensError } = await supabase
.from('itens_romaneio_pecas')
.select('id, peca_id, marca, quantidade_expedida, fase')
.eq('romaneio_id', romaneioId);
if (itensError) {
console.error('❌ Erro ao buscar peças do romaneio:', itensError);
return {
valido: false,
mensagem: 'Erro ao consultar peças do romaneio: ' + itensError.message
};
}
if (!itens || itens.length === 0) {
return {
valido: false,
mensagem: 'O romaneio não possui nenhuma peça cadastrada. Adicione as peças antes de alterar o status para "Entregue".'
};
}
// 2. Buscar processos de fabricação ativos ordenados por ordem
const { data: processos, error: procError } = await supabase
.from('processos_fabricacao')
.select('id, nome, ordem')
.eq('ativo', true)
.order('ordem');
if (procError || !processos) {
console.error('❌ Erro ao buscar processos de fabricação:', procError);
return {
valido: false,
mensagem: 'Erro ao consultar processos de fabricação.'
};
}
// Encontrar o processo de Expedição (ordem 5 ou nome contendo 'expedi')
const processoExpedicao = processos.find(p =>
p.nome?.toLowerCase().includes('expedi') || p.ordem === 5
) || { ordem: 5 };
// Processos anteriores que precisam estar concluídos
const processosAnteriores = processos.filter(p => p.ordem < processoExpedicao.ordem);
// 3. Buscar dados das peças (para saber se tem_componentes)
const pecaIds = Array.from(new Set(itens.map(i => i.peca_id).filter(Boolean)));
const { data: pecasInfo, error: pecasError } = await supabase
.from('pecas')
.select('id, marca, tem_componentes')
.in('id', pecaIds);
if (pecasError) {
console.error('❌ Erro ao buscar especificações das peças:', pecasError);
}
const pecasMap = new Map((pecasInfo || []).map(p => [p.id, p]));
// 4. Buscar apontamentos de produção existentes para essas peças
let queryApontamentos = supabase
.from('apontamentos_producao')
.select('peca_id, processo_id, quantidade_produzida')
.in('peca_id', pecaIds);
if (ofNumber) {
queryApontamentos = queryApontamentos.eq('of_number', ofNumber);
}
const { data: apontamentos, error: aptError } = await queryApontamentos;
if (aptError) {
console.error('❌ Erro ao buscar apontamentos:', aptError);
}
// Mapear quantidades apontadas: peca_id -> processo_id -> soma(quantidade_produzida)
const apontamentosMap = new Map<string, Map<string, number>>();
(apontamentos || []).forEach(apt => {
if (!apt.peca_id || !apt.processo_id) return;
if (!apontamentosMap.has(apt.peca_id)) {
apontamentosMap.set(apt.peca_id, new Map<string, number>());
}
const procMap = apontamentosMap.get(apt.peca_id)!;
const atual = procMap.get(apt.processo_id) || 0;
procMap.set(apt.processo_id, atual + Number(apt.quantidade_produzida || 0));
});
// 5. Consolidar quantidade total exigida por peça no romaneio
const somaQtdRomaneioPorPeca = new Map<string, { marca: string; totalQtd: number }>();
itens.forEach(item => {
const atual = somaQtdRomaneioPorPeca.get(item.peca_id) || {
marca: item.marca,
totalQtd: 0
};
atual.totalQtd += Number(item.quantidade_expedida || 0);
somaQtdRomaneioPorPeca.set(item.peca_id, atual);
});
// 6. Verificar conformidade em todos os processos anteriores
const pendencias: ItemPendenciaProducao[] = [];
for (const [pecaId, { marca, totalQtd }] of somaQtdRomaneioPorPeca.entries()) {
const peca = pecasMap.get(pecaId);
const temComponentes = peca?.tem_componentes ?? false;
const procMap = apontamentosMap.get(pecaId) || new Map<string, number>();
for (const processo of processosAnteriores) {
// Se for Solda e a peça NÃO tem componentes (peça solta), ela não passa por solda
const isSolda = processo.ordem === 3 || processo.nome.toLowerCase().includes('solda');
if (isSolda && !temComponentes) {
continue;
}
const qtdApontada = procMap.get(processo.id) || 0;
if (qtdApontada < totalQtd) {
pendencias.push({
peca_id: pecaId,
marca: marca || peca?.marca || 'Peça',
processo: processo.nome,
ordem: processo.ordem,
qtdNecessaria: totalQtd,
qtdApontada,
qtdFaltante: Math.max(0, totalQtd - qtdApontada)
});
}
}
}
if (pendencias.length > 0) {
console.warn(`⚠️ Romaneio ${romaneioId} possui ${pendencias.length} pendência(s) de processos anteriores:`, pendencias);
return {
valido: false,
pendencias
};
}
return {
valido: true
};
} catch (err: any) {
console.error('❌ Exceção ao validar romaneio para entrega:', err);
return {
valido: false,
mensagem: 'Erro inesperado ao validar romaneio: ' + (err?.message || 'Falha de comunicação')
};
}
}