feat(expedicao): fluxo inteligente de confirmacao de entrega de romaneio com data real e apontamento em cascata
This commit is contained in:
+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