398 lines
13 KiB
TypeScript
398 lines
13 KiB
TypeScript
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
|
|
};
|
|
}
|