feat: adicionar gestao de revisoes simples e criticas de pecas com integracao a apontamentos
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { Peca } from '@/hooks/usePecas';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export type TipoRevisao = 'simples' | 'critica';
|
||||
|
||||
export interface Revisao {
|
||||
id: string;
|
||||
of_number: string;
|
||||
peca_id: string;
|
||||
numero_revisao: string;
|
||||
tipo_revisao: TipoRevisao;
|
||||
descricao_motivo: string;
|
||||
observacoes: string | null;
|
||||
usuario_id: string | null;
|
||||
usuario_nome: string | null;
|
||||
usuario_email: string | null;
|
||||
data_revisao: string;
|
||||
dados_anteriores: Record<string, any>;
|
||||
dados_novos: Record<string, any>;
|
||||
campos_alterados: string[];
|
||||
afeta_apontamentos: boolean;
|
||||
impacto_processos: Record<string, any> | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
peca?: {
|
||||
marca: string;
|
||||
etapa_fase: string;
|
||||
descricao: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CriarRevisaoDTO {
|
||||
peca: Peca;
|
||||
numeroRevisao: string;
|
||||
tipoRevisao?: TipoRevisao;
|
||||
descricaoMotivo: string;
|
||||
observacoes?: string;
|
||||
novosDados: Partial<Peca>;
|
||||
}
|
||||
|
||||
export function useRevisoes(ofNumber?: string) {
|
||||
const { user } = useAuth();
|
||||
const [revisoes, setRevisoes] = useState<Revisao[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Carrega todas as revisões de uma OF
|
||||
const carregarRevisoesOF = useCallback(async (of?: string) => {
|
||||
const ofAlvo = of || ofNumber;
|
||||
if (!ofAlvo) return [];
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('revisoes' as any)
|
||||
.select(`
|
||||
*,
|
||||
peca:pecas(marca, etapa_fase, descricao)
|
||||
`)
|
||||
.eq('of_number', ofAlvo)
|
||||
.order('data_revisao', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao buscar revisões da OF:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lista = (data || []) as unknown as Revisao[];
|
||||
setRevisoes(lista);
|
||||
return lista;
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao carregar revisões:', err);
|
||||
toast.error('Erro ao carregar registros de revisões');
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [ofNumber]);
|
||||
|
||||
// Carrega histórico de revisões de uma peça específica
|
||||
const carregarRevisoesPeca = useCallback(async (pecaId: string): Promise<Revisao[]> => {
|
||||
if (!pecaId) return [];
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('revisoes' as any)
|
||||
.select(`
|
||||
*,
|
||||
peca:pecas(marca, etapa_fase, descricao)
|
||||
`)
|
||||
.eq('peca_id', pecaId)
|
||||
.order('data_revisao', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao buscar revisões da peça:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (data || []) as unknown as Revisao[];
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao carregar revisões da peça:', err);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Analisa automaticamente se uma alteração é Crítica ou Simples
|
||||
const detectarTipoRevisao = (pecaAtual: Peca, novosDados: Partial<Peca>): {
|
||||
tipo: TipoRevisao;
|
||||
camposAlterados: string[];
|
||||
afetaApontamentos: boolean;
|
||||
detalhes: string[];
|
||||
} => {
|
||||
const camposCriticos = ['marca', 'quantidade', 'peso_unitario', 'peso_total', 'tem_componentes'];
|
||||
const camposAlterados: string[] = [];
|
||||
const detalhes: string[] = [];
|
||||
let isCritica = false;
|
||||
let afetaApontamentos = false;
|
||||
|
||||
// Verificar cada campo relevante
|
||||
const chavesParaChecar = [
|
||||
'marca',
|
||||
'quantidade',
|
||||
'peso_unitario',
|
||||
'peso_total',
|
||||
'descricao',
|
||||
'etapa_fase',
|
||||
'material',
|
||||
'perfil_principal',
|
||||
'tratamento_superficial',
|
||||
'comprimento',
|
||||
'tem_componentes'
|
||||
];
|
||||
|
||||
for (const chave of chavesParaChecar) {
|
||||
const valorAntigo = (pecaAtual as any)[chave];
|
||||
const valorNovo = (novosDados as any)[chave];
|
||||
|
||||
if (valorNovo !== undefined && valorNovo !== null && valorNovo !== valorAntigo) {
|
||||
// Para números, checar diferença de valor
|
||||
if (typeof valorAntigo === 'number' && typeof valorNovo === 'number') {
|
||||
if (Math.abs(valorAntigo - valorNovo) > 0.0001) {
|
||||
camposAlterados.push(chave);
|
||||
if (camposCriticos.includes(chave)) {
|
||||
isCritica = true;
|
||||
if (chave === 'marca' || chave === 'quantidade') {
|
||||
afetaApontamentos = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
camposAlterados.push(chave);
|
||||
if (camposCriticos.includes(chave)) {
|
||||
isCritica = true;
|
||||
if (chave === 'marca') {
|
||||
afetaApontamentos = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (camposAlterados.includes('marca')) {
|
||||
detalhes.push(`Alteração no código/marca da peça (${pecaAtual.marca} ➔ ${novosDados.marca})`);
|
||||
}
|
||||
if (camposAlterados.includes('quantidade')) {
|
||||
const diff = Number(novosDados.quantidade || 0) - Number(pecaAtual.quantidade || 0);
|
||||
detalhes.push(`Alteração na quantidade (${pecaAtual.quantidade} ➔ ${novosDados.quantidade}, dif: ${diff > 0 ? '+' : ''}${diff})`);
|
||||
}
|
||||
if (camposAlterados.includes('peso_unitario') || camposAlterados.includes('peso_total')) {
|
||||
detalhes.push(`Alteração dimensional/peso`);
|
||||
}
|
||||
|
||||
return {
|
||||
tipo: isCritica ? 'critica' : 'simples',
|
||||
camposAlterados,
|
||||
afetaApontamentos,
|
||||
detalhes
|
||||
};
|
||||
};
|
||||
|
||||
// Cria e aplica a revisão
|
||||
const criarRevisao = async (dto: CriarRevisaoDTO): Promise<boolean> => {
|
||||
const { peca, numeroRevisao, descricaoMotivo, observacoes, novosDados } = dto;
|
||||
|
||||
if (!dto.descricaoMotivo?.trim()) {
|
||||
toast.error('Informe o motivo ou observações da revisão');
|
||||
return false;
|
||||
}
|
||||
|
||||
const analise = detectarTipoRevisao(peca, novosDados);
|
||||
const tipoFinal = dto.tipoRevisao || analise.tipo;
|
||||
const camposAlterados = analise.camposAlterados;
|
||||
|
||||
if (camposAlterados.length === 0) {
|
||||
toast.error('Nenhum campo foi alterado nesta peça.');
|
||||
return false;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dadosAnteriores: Record<string, any> = {};
|
||||
const dadosNovos: Record<string, any> = {};
|
||||
|
||||
camposAlterados.forEach(c => {
|
||||
dadosAnteriores[c] = (peca as any)[c];
|
||||
dadosNovos[c] = (novosDados as any)[c];
|
||||
});
|
||||
|
||||
const impactoProcessos: Record<string, any> = {
|
||||
resumo: [],
|
||||
ajustes_apontamentos: []
|
||||
};
|
||||
|
||||
// REGRA DE NEGÓCIO DE APONTAMENTOS / PROCESSOS:
|
||||
// Caso 1: A quantidade foi reduzida (pra menos)
|
||||
const novaQtd = Number(novosDados.quantidade !== undefined ? novosDados.quantidade : peca.quantidade);
|
||||
const qtdAntiga = Number(peca.quantidade || 0);
|
||||
|
||||
if (camposAlterados.includes('quantidade') && novaQtd < qtdAntiga) {
|
||||
// Buscar todos os apontamentos da peça agrupados por processo
|
||||
const { data: apontamentos, error: apErr } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select(`
|
||||
id,
|
||||
processo_id,
|
||||
quantidade_produzida,
|
||||
data_apontamento,
|
||||
created_at,
|
||||
processo:processos_fabricacao(id, nome, ordem)
|
||||
`)
|
||||
.eq('peca_id', peca.id)
|
||||
.eq('tipo_apontamento', 'peca')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!apErr && apontamentos && apontamentos.length > 0) {
|
||||
// Agrupar apontamentos por processo
|
||||
const porProcesso: Record<string, typeof apontamentos> = {};
|
||||
apontamentos.forEach(a => {
|
||||
if (!porProcesso[a.processo_id]) porProcesso[a.processo_id] = [];
|
||||
porProcesso[a.processo_id].push(a);
|
||||
});
|
||||
|
||||
for (const [procId, listaAps] of Object.entries(porProcesso)) {
|
||||
const totalProduzido = listaAps.reduce((sum, a) => sum + Number(a.quantidade_produzida || 0), 0);
|
||||
const nomeProc = (listaAps[0].processo as any)?.nome || procId;
|
||||
|
||||
if (totalProduzido > novaQtd) {
|
||||
let excesso = totalProduzido - novaQtd;
|
||||
impactoProcessos.resumo.push(
|
||||
`Processo "${nomeProc}": Produção de ${totalProduzido} un reduzida para o novo teto de ${novaQtd} un (corte de ${excesso} un).`
|
||||
);
|
||||
|
||||
// Reduzir dos apontamentos mais recentes para os mais antigos
|
||||
for (const ap of listaAps) {
|
||||
if (excesso <= 0) break;
|
||||
const apQtd = Number(ap.quantidade_produzida || 0);
|
||||
|
||||
if (apQtd <= excesso) {
|
||||
// Apontamento inteiro é absorvido/removido
|
||||
await supabase
|
||||
.from('apontamentos_producao')
|
||||
.delete()
|
||||
.eq('id', ap.id);
|
||||
|
||||
excesso -= apQtd;
|
||||
impactoProcessos.ajustes_apontamentos.push({
|
||||
apontamento_id: ap.id,
|
||||
processo: nomeProc,
|
||||
acao: 'removido',
|
||||
quantidade_anterior: apQtd,
|
||||
quantidade_nova: 0
|
||||
});
|
||||
} else {
|
||||
// Apontamento parcial
|
||||
const novaQtdAp = apQtd - excesso;
|
||||
await supabase
|
||||
.from('apontamentos_producao')
|
||||
.update({
|
||||
quantidade_produzida: novaQtdAp,
|
||||
observacoes: `${ap.data_apontamento || ''} [Revisado por ${numeroRevisao} - Qtd ajustada de ${apQtd} para ${novaQtdAp}]`
|
||||
})
|
||||
.eq('id', ap.id);
|
||||
|
||||
impactoProcessos.ajustes_apontamentos.push({
|
||||
apontamento_id: ap.id,
|
||||
processo: nomeProc,
|
||||
acao: 'reduzido',
|
||||
quantidade_anterior: apQtd,
|
||||
quantidade_nova: novaQtdAp
|
||||
});
|
||||
excesso = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
impactoProcessos.resumo.push(
|
||||
`Processo "${nomeProc}": Mantido (${totalProduzido} un já apontadas estão dentro do novo limite de ${novaQtd} un).`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (camposAlterados.includes('quantidade') && novaQtd > qtdAntiga) {
|
||||
// Caso 2: A quantidade foi aumentada (pra mais)
|
||||
const diferenca = novaQtd - qtdAntiga;
|
||||
impactoProcessos.resumo.push(
|
||||
`Aumento de quantidade (+${diferenca} un): O saldo excedente nasce automaticamente no 1º processo da linha de fabricação para novos apontamentos.`
|
||||
);
|
||||
}
|
||||
|
||||
// Caso 3: Marca alterada
|
||||
if (camposAlterados.includes('marca')) {
|
||||
const novaMarca = String(novosDados.marca || '');
|
||||
impactoProcessos.resumo.push(
|
||||
`Código da peça alterado de "${peca.marca}" para "${novaMarca}". Vínculos sincronizados.`
|
||||
);
|
||||
|
||||
// Atualizar romaneios que desnormalizam a marca
|
||||
await supabase
|
||||
.from('itens_romaneio_pecas')
|
||||
.update({ marca: novaMarca })
|
||||
.eq('peca_id', peca.id);
|
||||
}
|
||||
|
||||
// 1. Atualizar a peça no banco de dados
|
||||
const payloadUpdate: any = {
|
||||
...novosDados,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Recalcular peso total se peso_unitario ou quantidade mudaram
|
||||
if (novosDados.quantidade !== undefined || novosDados.peso_unitario !== undefined) {
|
||||
const qtdFinal = Number(novosDados.quantidade !== undefined ? novosDados.quantidade : peca.quantidade);
|
||||
const pesoUnitFinal = Number(novosDados.peso_unitario !== undefined ? novosDados.peso_unitario : peca.peso_unitario);
|
||||
payloadUpdate.peso_total = Number((qtdFinal * pesoUnitFinal).toFixed(3));
|
||||
}
|
||||
|
||||
const { error: updateErr } = await supabase
|
||||
.from('pecas')
|
||||
.update(payloadUpdate)
|
||||
.eq('id', peca.id);
|
||||
|
||||
if (updateErr) {
|
||||
console.error('Erro ao atualizar peça na revisão:', updateErr);
|
||||
throw updateErr;
|
||||
}
|
||||
|
||||
// 2. Inserir na tabela revisoes
|
||||
const { error: revErr } = await supabase
|
||||
.from('revisoes' as any)
|
||||
.insert({
|
||||
of_number: peca.of_number,
|
||||
peca_id: peca.id,
|
||||
numero_revisao: numeroRevisao.trim(),
|
||||
tipo_revisao: tipoFinal,
|
||||
descricao_motivo: descricaoMotivo.trim(),
|
||||
observacoes: observacoes?.trim() || null,
|
||||
usuario_id: user?.id || null,
|
||||
usuario_nome: user?.name || user?.email || 'Administrador',
|
||||
usuario_email: user?.email || null,
|
||||
dados_anteriores: dadosAnteriores,
|
||||
dados_novos: dadosNovos,
|
||||
campos_alterados: camposAlterados,
|
||||
afeta_apontamentos: analise.afetaApontamentos,
|
||||
impacto_processos: impactoProcessos,
|
||||
data_revisao: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (revErr) {
|
||||
console.error('Erro ao registrar revisão:', revErr);
|
||||
throw revErr;
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`Revisão ${numeroRevisao} (${tipoFinal === 'critica' ? 'Crítica' : 'Simples'}) registrada com sucesso!`
|
||||
);
|
||||
|
||||
// Recarregar revisões da OF
|
||||
await carregarRevisoesOF(peca.of_number);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('Falha ao processar revisão:', err);
|
||||
toast.error('Erro ao processar revisão: ' + (err.message || 'Erro desconhecido'));
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
revisoes,
|
||||
loading,
|
||||
carregarRevisoesOF,
|
||||
carregarRevisoesPeca,
|
||||
detectarTipoRevisao,
|
||||
criarRevisao
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user