feat(expedicao): fluxo inteligente de confirmacao de entrega de romaneio com data real e apontamento em cascata
This commit is contained in:
@@ -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<ConfirmacaoEntregaRomaneioModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
romaneio,
|
||||
onSucesso
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loadingAnalise, setLoadingAnalise] = useState(false);
|
||||
const [analise, setAnalise] = useState<ResultadoValidacaoEntrega | null>(null);
|
||||
const [dataRealEntrega, setDataRealEntrega] = useState<string>('');
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col p-6">
|
||||
<DialogHeader className="border-b pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20">
|
||||
<Truck className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl font-bold flex items-center gap-2">
|
||||
<span>Confirmar Entrega & Apontamento de Expedição</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm text-muted-foreground mt-0.5">
|
||||
Romaneio <strong>{romaneio.numero_romaneio}</strong> • OF <strong>{romaneio.of_number}</strong>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 space-y-4 overflow-y-auto flex-1 pr-1">
|
||||
{/* Seção 1: Data Real da Entrega */}
|
||||
<div className="bg-card border border-border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<Label htmlFor="data-real-entrega" className="font-semibold text-sm flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-primary" />
|
||||
Data Real da Entrega *
|
||||
</Label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{romaneio.data_prevista_entrega && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={() => setDataRealEntrega(romaneio.data_prevista_entrega!)}
|
||||
>
|
||||
Usar Data Prevista ({dataPrevistaFormatada})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={() => setDataRealEntrega(hoje)}
|
||||
>
|
||||
Usar Hoje
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 items-center">
|
||||
<Input
|
||||
id="data-real-entrega"
|
||||
type="date"
|
||||
value={dataRealEntrega}
|
||||
onChange={(e) => setDataRealEntrega(e.target.value)}
|
||||
className="h-9 font-medium"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{dataDiferenteDePrevista ? (
|
||||
<span className="text-amber-600 dark:text-amber-400 font-medium flex items-center gap-1">
|
||||
<Info className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
Data real difere da data prevista ({dataPrevistaFormatada}).
|
||||
</span>
|
||||
) : (
|
||||
<span>Os apontamentos serão gravados exatamente nesta data.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seção 2: Diagnóstico dos Processos de Fabricação */}
|
||||
{loadingAnalise ? (
|
||||
<div className="p-8 text-center space-y-2">
|
||||
<Loader2 className="h-7 w-7 animate-spin mx-auto text-primary" />
|
||||
<p className="text-xs text-muted-foreground">Analisando processos de fabricação das peças...</p>
|
||||
</div>
|
||||
) : analise ? (
|
||||
<div className="space-y-3">
|
||||
{analise.totalPendentes === 0 ? (
|
||||
// Cenário A: Todas as peças liberadas
|
||||
<div className="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-lg flex items-start gap-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-emerald-700 dark:text-emerald-300 text-sm">
|
||||
Tudo pronto para Expedição!
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Todas as <strong>{analise.totalItens} peças</strong> 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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Cenário B: Existem peças com pendências nos processos anteriores
|
||||
<div className="space-y-3">
|
||||
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-amber-700 dark:text-amber-300 text-sm">
|
||||
Peças Pendentes nos Processos Anteriores ({analise.totalPendentes} de {analise.totalItens} itens)
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Algumas peças deste romaneio ainda não possuem apontamentos completos nos processos de fabricação anteriores (Corte, Solda ou Pintura/Galv).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabela de Peças Pendentes */}
|
||||
<div className="border rounded-md overflow-hidden bg-card">
|
||||
<div className="bg-muted/50 px-3 py-2 text-xs font-semibold text-foreground border-b flex justify-between items-center">
|
||||
<span>Detalhamento das Peças com Pendência</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{analise.totalPendentes} peça(s) a regularizar
|
||||
</Badge>
|
||||
</div>
|
||||
<ScrollArea className="max-h-[220px]">
|
||||
<table className="w-full text-xs text-left">
|
||||
<thead className="bg-muted/30 text-muted-foreground uppercase border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="py-2 px-3">Marca</th>
|
||||
<th className="py-2 px-3 text-center">Fase</th>
|
||||
<th className="py-2 px-3 text-center">No Romaneio</th>
|
||||
<th className="py-2 px-3">Processos que Faltam Apontar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{analise.pecasAnalise
|
||||
?.filter(p => !p.liberada)
|
||||
.map((peca) => (
|
||||
<tr key={peca.peca_id} className="hover:bg-accent/40">
|
||||
<td className="py-2 px-3 font-semibold text-foreground">
|
||||
{peca.marca}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-center">
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
Fase {peca.fase}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-center font-bold">
|
||||
{peca.quantidade_expedida} un
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{peca.processosFaltantes.map((f, i) => (
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="text-[10px] bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20"
|
||||
>
|
||||
{f.nome}: faltam {f.qtdFaltante} un
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Pergunta de Decisão com Destaque */}
|
||||
<div className="p-3.5 bg-blue-500/10 border border-blue-500/30 rounded-lg flex items-center gap-3">
|
||||
<Sparkles className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<div className="text-xs">
|
||||
<p className="font-semibold text-blue-900 dark:text-blue-200">
|
||||
Autorizar Apontamento em Cascata?
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-0.5">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t pt-3 flex flex-wrap gap-2 justify-between items-center sm:justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={processando}
|
||||
size="sm"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-wrap gap-2 ml-auto">
|
||||
{analise && analise.totalPendentes === 0 ? (
|
||||
// Botão para caso sem pendências
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleExecutar(false, false)}
|
||||
disabled={processando || !dataRealEntrega}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-xs shadow-sm"
|
||||
size="sm"
|
||||
>
|
||||
{processando ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
)}
|
||||
Confirmar Entrega & Apontar Expedição
|
||||
</Button>
|
||||
) : analise && analise.totalPendentes > 0 ? (
|
||||
<>
|
||||
{analise.totalLiberadas && analise.totalLiberadas > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => handleExecutar(false, true)}
|
||||
disabled={processando || !dataRealEntrega}
|
||||
className="text-xs"
|
||||
size="sm"
|
||||
title="Aponta somente as peças que já concluíram Pintura"
|
||||
>
|
||||
Apontar Apenas Liberadas ({analise.totalLiberadas})
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleExecutar(true, false)}
|
||||
disabled={processando || !dataRealEntrega}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-bold text-xs shadow-sm flex items-center gap-1.5"
|
||||
size="sm"
|
||||
>
|
||||
{processando ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
)}
|
||||
Autorizar Cascata e Apontar Todas ({analise.totalItens} peças)
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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<ItemPendenciaProducao[]>([]);
|
||||
const [mensagemBloqueio, setMensagemBloqueio] = useState<string | undefined>(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
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={handleStatusChange}
|
||||
disabled={validandoEntrega}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -339,10 +318,10 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel
|
||||
</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)
|
||||
<Alert className="border-blue-500 bg-blue-50 dark:bg-blue-950/20 dark:border-blue-400 mt-4">
|
||||
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<AlertDescription className="text-blue-900 dark:text-blue-200 font-medium">
|
||||
AO PASSAR O STATUS PARA "ENTREGUE", VOCÊ ESCOLHE A DATA REAL DA ENTREGA E PODE AUTORIZAR O APONTAMENTO AUTOMÁTICO (INCLUSIVE EM CASCATA CASO HAJA PROCESSOS ANTERIORES PENDENTES).
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -491,13 +470,8 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel
|
||||
</div>
|
||||
|
||||
<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 ? (
|
||||
<Button type="submit" disabled={loading || !formData.of_number}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Salvando...
|
||||
@@ -515,19 +489,28 @@ export function RomaneioForm({ romaneio, ofNumbers, selectedOF, onSave, onCancel
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modal de bloqueio/alerta caso existam peças pendentes */}
|
||||
<PendenciasProducaoModal
|
||||
isOpen={modalPendenciasAberto}
|
||||
{/* Modal de confirmação inteligente com Data Real e Cascata */}
|
||||
<ConfirmacaoEntregaRomaneioModal
|
||||
isOpen={modalConfirmacaoAberto}
|
||||
onClose={() => {
|
||||
setModalPendenciasAberto(false);
|
||||
setModalConfirmacaoAberto(false);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
status: (romaneio?.status as any) || 'Em planejamento'
|
||||
}));
|
||||
}}
|
||||
pendencias={pendenciasEncontradas}
|
||||
numeroRomaneio={romaneio?.numero_romaneio}
|
||||
mensagemSemItens={mensagemBloqueio}
|
||||
romaneio={romaneio ? {
|
||||
id: romaneio.id,
|
||||
numero_romaneio: romaneio.numero_romaneio,
|
||||
of_number: formData.of_number || romaneio.of_number,
|
||||
data_prevista_entrega: formData.data_prevista_entrega || romaneio.data_prevista_entrega,
|
||||
data_romaneio: formData.data_romaneio || romaneio.data_romaneio,
|
||||
status: formData.status
|
||||
} : null}
|
||||
onSucesso={() => {
|
||||
setModalConfirmacaoAberto(false);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,10 +3,11 @@ import React, { useState } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Edit, Eye, Trash2, Printer } from 'lucide-react';
|
||||
import { Edit, Eye, Trash2, Printer, Truck } from 'lucide-react';
|
||||
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
|
||||
import { formatBrazilianDateFromString } from '@/utils/dateTimeUtils';
|
||||
import { RomaneioReportModal } from './RomaneioReportModal';
|
||||
import { ConfirmacaoEntregaRomaneioModal } from './ConfirmacaoEntregaRomaneioModal';
|
||||
|
||||
|
||||
interface RomaneioTableProps {
|
||||
@@ -28,6 +29,7 @@ export const RomaneioTable: React.FC<RomaneioTableProps> = ({
|
||||
}) => {
|
||||
const [selectedRomaneioForReport, setSelectedRomaneioForReport] = useState<RomaneioExpedicao | null>(null);
|
||||
const [showReportModal, setShowReportModal] = useState(false);
|
||||
const [romaneioParaConfirmacao, setRomaneioParaConfirmacao] = useState<RomaneioExpedicao | null>(null);
|
||||
|
||||
const handlePrintReport = (romaneio: RomaneioExpedicao) => {
|
||||
setSelectedRomaneioForReport(romaneio);
|
||||
@@ -126,7 +128,16 @@ export const RomaneioTable: React.FC<RomaneioTableProps> = ({
|
||||
{romaneio.peso_total_romaneio?.toFixed(2) || '0.00'} kg
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setRomaneioParaConfirmacao(romaneio)}
|
||||
title="Confirmar Entrega / Apontamentos"
|
||||
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/40"
|
||||
>
|
||||
<Truck className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -175,6 +186,16 @@ export const RomaneioTable: React.FC<RomaneioTableProps> = ({
|
||||
romaneio={selectedRomaneioForReport}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modal de Confirmação de Entrega com Data Real e Cascata */}
|
||||
{romaneioParaConfirmacao && (
|
||||
<ConfirmacaoEntregaRomaneioModal
|
||||
isOpen={!!romaneioParaConfirmacao}
|
||||
onClose={() => setRomaneioParaConfirmacao(null)}
|
||||
romaneio={romaneioParaConfirmacao}
|
||||
onSucesso={() => setRomaneioParaConfirmacao(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -280,16 +280,11 @@ 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') {
|
||||
// Se status for Entregue e não passou pelo modal de confirmação, validar amigavelmente
|
||||
if (romaneio.status === 'Entregue' && romaneioAtual?.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);
|
||||
console.warn('⚠️ Romaneio possui pendências fabris ao passar para Entregue:', validacao.pendencias?.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+247
-50
@@ -1,31 +1,54 @@
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { ItemPendenciaProducao } from '@/components/expedicao/PendenciasProducaoModal';
|
||||
|
||||
export interface ProcessoFaltanteItem {
|
||||
processo_id: string;
|
||||
nome: string;
|
||||
ordem: number;
|
||||
qtdFaltante: number;
|
||||
qtdFeita: number;
|
||||
qtdNecessaria: number;
|
||||
}
|
||||
|
||||
export interface PecaAnaliseRomaneio {
|
||||
peca_id: string;
|
||||
marca: string;
|
||||
fase?: string;
|
||||
quantidade_expedida: number;
|
||||
tem_componentes: boolean;
|
||||
processosFaltantes: ProcessoFaltanteItem[];
|
||||
liberada: boolean;
|
||||
qtdJaExpedida: number;
|
||||
}
|
||||
|
||||
export interface ResultadoValidacaoEntrega {
|
||||
valido: boolean;
|
||||
mensagem?: string;
|
||||
pendencias?: ItemPendenciaProducao[];
|
||||
pecasAnalise?: PecaAnaliseRomaneio[];
|
||||
totalItens?: number;
|
||||
totalLiberadas?: number;
|
||||
totalPendentes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Valida e analisa se um romaneio pode ser marcado com o status "Entregue".
|
||||
* Identifica para cada peça do romaneio se todos os processos anteriores foram concluídos,
|
||||
* e detalha com precisão cirúrgica quais processos estão faltantes.
|
||||
*/
|
||||
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.'
|
||||
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 confirme a entrega.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Buscar as peças cadastradas no romaneio
|
||||
// 1. Buscar peças cadastradas no romaneio
|
||||
const { data: itens, error: itensError } = await supabase
|
||||
.from('itens_romaneio_pecas')
|
||||
.select('id, peca_id, marca, quantidade_expedida, fase')
|
||||
@@ -61,10 +84,10 @@ export async function validarRomaneioParaEntrega(
|
||||
};
|
||||
}
|
||||
|
||||
// Encontrar o processo de Expedição (ordem 5 ou nome contendo 'expedi')
|
||||
// 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 };
|
||||
) || { id: '', nome: 'Expedicao', ordem: 5 };
|
||||
|
||||
// Processos anteriores que precisam estar concluídos
|
||||
const processosAnteriores = processos.filter(p => p.ordem < processoExpedicao.ordem);
|
||||
@@ -73,7 +96,7 @@ export async function validarRomaneioParaEntrega(
|
||||
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')
|
||||
.select('id, marca, tem_componentes, fase')
|
||||
.in('id', pecaIds);
|
||||
|
||||
if (pecasError) {
|
||||
@@ -82,10 +105,10 @@ export async function validarRomaneioParaEntrega(
|
||||
|
||||
const pecasMap = new Map((pecasInfo || []).map(p => [p.id, p]));
|
||||
|
||||
// 4. Buscar apontamentos de produção existentes para essas peças
|
||||
// 4. Buscar apontamentos existentes para essas peças
|
||||
let queryApontamentos = supabase
|
||||
.from('apontamentos_producao')
|
||||
.select('peca_id, processo_id, quantidade_produzida')
|
||||
.select('peca_id, processo_id, quantidade_produzida, tipo_apontamento')
|
||||
.in('peca_id', pecaIds);
|
||||
|
||||
if (ofNumber) {
|
||||
@@ -98,10 +121,13 @@ export async function validarRomaneioParaEntrega(
|
||||
console.error('❌ Erro ao buscar apontamentos:', aptError);
|
||||
}
|
||||
|
||||
// Mapear quantidades apontadas: peca_id -> processo_id -> soma(quantidade_produzida)
|
||||
// Mapear quantidades apontadas por peça mestre: peca_id -> processo_id -> soma(quantidade_produzida)
|
||||
const apontamentosMap = new Map<string, Map<string, number>>();
|
||||
(apontamentos || []).forEach(apt => {
|
||||
// Ignorar apontamento de subcomponente para não inflar a contagem da peça mestre
|
||||
if (apt.tipo_apontamento && apt.tipo_apontamento !== 'peca') return;
|
||||
if (!apt.peca_id || !apt.processo_id) return;
|
||||
|
||||
if (!apontamentosMap.has(apt.peca_id)) {
|
||||
apontamentosMap.set(apt.peca_id, new Map<string, number>());
|
||||
}
|
||||
@@ -110,58 +136,73 @@ export async function validarRomaneioParaEntrega(
|
||||
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
|
||||
// 5. Analisar cada item do romaneio
|
||||
const pendencias: ItemPendenciaProducao[] = [];
|
||||
const pecasAnalise: PecaAnaliseRomaneio[] = [];
|
||||
|
||||
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>();
|
||||
itens.forEach(item => {
|
||||
const peca = pecasMap.get(item.peca_id);
|
||||
const temComp = peca?.tem_componentes ?? false;
|
||||
const procMap = apontamentosMap.get(item.peca_id) || new Map<string, number>();
|
||||
const qtdRomaneio = Number(item.quantidade_expedida || 0);
|
||||
|
||||
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) {
|
||||
const processosFaltantes: ProcessoFaltanteItem[] = [];
|
||||
|
||||
for (const proc of processosAnteriores) {
|
||||
// Se for Solda e a peça NÃO tem componentes (peça simples sem montagem), não passa por solda
|
||||
const isSolda = proc.ordem === 3 || proc.nome.toLowerCase().includes('solda');
|
||||
if (isSolda && !temComp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const qtdApontada = procMap.get(processo.id) || 0;
|
||||
const qtdFeita = procMap.get(proc.id) || 0;
|
||||
if (qtdFeita < qtdRomaneio) {
|
||||
const qtdFaltante = Math.max(0, qtdRomaneio - qtdFeita);
|
||||
processosFaltantes.push({
|
||||
processo_id: proc.id,
|
||||
nome: proc.nome,
|
||||
ordem: proc.ordem,
|
||||
qtdNecessaria: qtdRomaneio,
|
||||
qtdFeita,
|
||||
qtdFaltante
|
||||
});
|
||||
|
||||
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)
|
||||
peca_id: item.peca_id,
|
||||
marca: item.marca || peca?.marca || 'Peça',
|
||||
processo: proc.nome,
|
||||
ordem: proc.ordem,
|
||||
qtdNecessaria: qtdRomaneio,
|
||||
qtdApontada: qtdFeita,
|
||||
qtdFaltante
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendencias.length > 0) {
|
||||
console.warn(`⚠️ Romaneio ${romaneioId} possui ${pendencias.length} pendência(s) de processos anteriores:`, pendencias);
|
||||
return {
|
||||
valido: false,
|
||||
pendencias
|
||||
};
|
||||
}
|
||||
const qtdJaExpedida = procMap.get(processoExpedicao.id) || 0;
|
||||
|
||||
pecasAnalise.push({
|
||||
peca_id: item.peca_id,
|
||||
marca: item.marca || peca?.marca || 'Peça',
|
||||
fase: item.fase || peca?.fase || '-',
|
||||
quantidade_expedida: qtdRomaneio,
|
||||
tem_componentes: temComp,
|
||||
processosFaltantes,
|
||||
liberada: processosFaltantes.length === 0,
|
||||
qtdJaExpedida
|
||||
});
|
||||
});
|
||||
|
||||
const totalLiberadas = pecasAnalise.filter(p => p.liberada).length;
|
||||
const totalPendentes = pecasAnalise.filter(p => !p.liberada).length;
|
||||
|
||||
return {
|
||||
valido: true
|
||||
valido: pendencias.length === 0,
|
||||
pendencias,
|
||||
pecasAnalise,
|
||||
totalItens: pecasAnalise.length,
|
||||
totalLiberadas,
|
||||
totalPendentes
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('❌ Exceção ao validar romaneio para entrega:', err);
|
||||
@@ -171,3 +212,159 @@ export async function validarRomaneioParaEntrega(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa o apontamento automático para o romaneio entregue:
|
||||
* - Grava na Data Real informada pelo usuário.
|
||||
* - Suporta apontamento em cascata nos processos anteriores pendentes (se autorizado pelo usuário).
|
||||
* - Registra apontamento oficial no processo de Expedição.
|
||||
* - Atualiza o status do romaneio para "Entregue".
|
||||
*/
|
||||
export async function executarApontamentoRomaneioEntregue({
|
||||
romaneioId,
|
||||
ofNumber,
|
||||
numeroRomaneio,
|
||||
dataEntregaReal,
|
||||
autorizarCascata = false,
|
||||
apontarApenasLiberadas = false,
|
||||
userId
|
||||
}: {
|
||||
romaneioId: string;
|
||||
ofNumber: string;
|
||||
numeroRomaneio: string;
|
||||
dataEntregaReal: string;
|
||||
autorizarCascata?: boolean;
|
||||
apontarApenasLiberadas?: boolean;
|
||||
userId?: string;
|
||||
}): Promise<{ sucesso: boolean; mensagem: string; totalApontamentos: number; pecasProcessadas: string[] }> {
|
||||
try {
|
||||
const analise = await validarRomaneioParaEntrega(romaneioId, ofNumber);
|
||||
if (!analise.pecasAnalise || analise.pecasAnalise.length === 0) {
|
||||
throw new Error(analise.mensagem || 'Nenhuma peça encontrada no romaneio');
|
||||
}
|
||||
|
||||
// Buscar processo de Expedição
|
||||
const { data: processos } = await supabase
|
||||
.from('processos_fabricacao')
|
||||
.select('id, nome, ordem')
|
||||
.eq('ativo', true)
|
||||
.order('ordem');
|
||||
|
||||
const processoExpedicao = processos?.find(p =>
|
||||
p.nome?.toLowerCase().includes('expedi') || p.ordem === 5
|
||||
);
|
||||
|
||||
if (!processoExpedicao) {
|
||||
throw new Error('Processo Expedição não encontrado');
|
||||
}
|
||||
|
||||
const apontamentosParaInserir: any[] = [];
|
||||
const pecasProcessadas: string[] = [];
|
||||
const obsOrigem = `Apontamento via Romaneio ${numeroRomaneio} (ID: ${romaneioId})`;
|
||||
|
||||
for (const peca of analise.pecasAnalise) {
|
||||
// Cenário 1: Peça com processos anteriores pendentes
|
||||
if (!peca.liberada) {
|
||||
if (autorizarCascata) {
|
||||
// Criar apontamento para cada processo faltante
|
||||
for (const faltante of peca.processosFaltantes) {
|
||||
apontamentosParaInserir.push({
|
||||
of_number: ofNumber,
|
||||
peca_id: peca.peca_id,
|
||||
processo_id: faltante.processo_id,
|
||||
quantidade_produzida: faltante.qtdFaltante,
|
||||
data_apontamento: dataEntregaReal,
|
||||
observacoes: `Apontamento em cascata (${faltante.nome}) - ${obsOrigem}`,
|
||||
tipo_apontamento: 'peca',
|
||||
created_by: userId
|
||||
});
|
||||
}
|
||||
|
||||
// Criar apontamento de Expedição
|
||||
apontamentosParaInserir.push({
|
||||
of_number: ofNumber,
|
||||
peca_id: peca.peca_id,
|
||||
processo_id: processoExpedicao.id,
|
||||
quantidade_produzida: peca.quantidade_expedida,
|
||||
data_apontamento: dataEntregaReal,
|
||||
observacoes: `Apontamento de expedição - ${obsOrigem}`,
|
||||
tipo_apontamento: 'peca',
|
||||
created_by: userId
|
||||
});
|
||||
|
||||
pecasProcessadas.push(`${peca.marca} (${peca.quantidade_expedida} un - com cascata)`);
|
||||
} else if (apontarApenasLiberadas) {
|
||||
// Pular peça pendente
|
||||
continue;
|
||||
} else {
|
||||
// Não autorizado e não pediu para apontar parciais
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Cenário 2: Peça 100% liberada nos processos anteriores
|
||||
apontamentosParaInserir.push({
|
||||
of_number: ofNumber,
|
||||
peca_id: peca.peca_id,
|
||||
processo_id: processoExpedicao.id,
|
||||
quantidade_produzida: peca.quantidade_expedida,
|
||||
data_apontamento: dataEntregaReal,
|
||||
observacoes: `Apontamento de expedição - ${obsOrigem}`,
|
||||
tipo_apontamento: 'peca',
|
||||
created_by: userId
|
||||
});
|
||||
|
||||
pecasProcessadas.push(`${peca.marca} (${peca.quantidade_expedida} un)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (apontamentosParaInserir.length === 0) {
|
||||
return {
|
||||
sucesso: false,
|
||||
mensagem: 'Nenhum apontamento foi inserido pois não houve peças autorizadas para expedição.',
|
||||
totalApontamentos: 0,
|
||||
pecasProcessadas: []
|
||||
};
|
||||
}
|
||||
|
||||
// Inserir os apontamentos na tabela oficial
|
||||
const { error: insertError } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.insert(apontamentosParaInserir);
|
||||
|
||||
if (insertError) {
|
||||
console.error('❌ Erro ao inserir apontamentos:', insertError);
|
||||
throw new Error(`Falha ao registrar apontamentos: ${insertError.message}`);
|
||||
}
|
||||
|
||||
// Atualizar o status do romaneio para "Entregue"
|
||||
await supabase
|
||||
.from('romaneios_expedicao')
|
||||
.update({
|
||||
status: 'Entregue',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', romaneioId);
|
||||
|
||||
// Registrar log em logs_apontamentos_automaticos
|
||||
await supabase
|
||||
.from('logs_apontamentos_automaticos')
|
||||
.insert({
|
||||
romaneio_id: romaneioId,
|
||||
numero_romaneio: numeroRomaneio,
|
||||
of_number: ofNumber,
|
||||
total_pecas_processadas: pecasProcessadas.length,
|
||||
status_operacao: 'sucesso',
|
||||
usuario_executou: userId || null
|
||||
});
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
mensagem: `${apontamentosParaInserir.length} apontamento(s) realizado(s) com sucesso na data ${dataEntregaReal}!`,
|
||||
totalApontamentos: apontamentosParaInserir.length,
|
||||
pecasProcessadas
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('❌ Erro na execução de apontamentos do romaneio:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user