feat: adicionar gestao de revisoes simples e criticas de pecas com integracao a apontamentos

This commit is contained in:
Marcos Reifonas
2026-10-07 22:48:50 -03:00
parent 8755852fbd
commit c239dffdee
9 changed files with 1715 additions and 4 deletions
+397
View File
@@ -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
};
}