feat: implement automatic adjustment removal for deleted records in absence and atestado mutations, enhancing data integrity and recalculating work hours for specific periods

This commit is contained in:
2025-12-23 07:44:54 -03:00
parent 414ae85264
commit a731015c89
4 changed files with 191 additions and 16 deletions

View File

@@ -1254,6 +1254,7 @@ export const excluirAtestado = mutation({
const funcionarioId = atestado.funcionarioId; const funcionarioId = atestado.funcionarioId;
const dataInicio = atestado.dataInicio; // Data início do atestado const dataInicio = atestado.dataInicio; // Data início do atestado
const dataFim = atestado.dataFim; // Data fim do atestado const dataFim = atestado.dataFim; // Data fim do atestado
const atestadoId = args.id.toString(); // ID do atestado para remover ajustes
// Excluir o registro do banco de dados // Excluir o registro do banco de dados
await ctx.db.delete(args.id); await ctx.db.delete(args.id);
@@ -1267,6 +1268,19 @@ export const excluirAtestado = mutation({
args.id args.id
); );
// Remover ajustes automáticos relacionados ao atestado excluído
try {
await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
funcionarioId,
motivoTipo: 'atestado',
motivoId: atestadoId,
dataInicio,
dataFim
});
} catch (error) {
console.error('[excluirAtestado] Erro ao remover ajustes automáticos:', error);
}
// Recalcular banco de horas APENAS para o período específico do atestado excluído // Recalcular banco de horas APENAS para o período específico do atestado excluído
// Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto // Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim); await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);

View File

@@ -941,10 +941,13 @@ export const excluirSolicitacao = mutation({
throw new Error('Solicitação não encontrada'); throw new Error('Solicitação não encontrada');
} }
// Apenas solicitações ainda não processadas podem ser excluídas // IMPORTANTE: Salvar o período exato da ausência ANTES de excluir
if (solicitacao.status !== 'aguardando_aprovacao') { // para recalcular o banco de horas apenas para esse período específico
throw new Error('Apenas solicitações pendentes podem ser excluídas'); const funcionarioId = solicitacao.funcionarioId;
} const dataInicio = solicitacao.dataInicio;
const dataFim = solicitacao.dataFim;
const statusOriginal = solicitacao.status;
const ausenciaId = args.solicitacaoId.toString(); // ID da ausência para remover ajustes
// Verificar se o usuário é o criador original da solicitação // Verificar se o usuário é o criador original da solicitação
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
@@ -963,7 +966,34 @@ export const excluirSolicitacao = mutation({
throw new Error('Você não tem permissão para excluir esta solicitação'); throw new Error('Você não tem permissão para excluir esta solicitação');
} }
// Permitir exclusão de ausências aprovadas (não apenas pendentes)
// Se estiver aprovada, o gestor pode excluir para corrigir erros
if (statusOriginal === 'aprovado' && !usuarioEhGestor) {
throw new Error('Apenas o gestor pode excluir ausências aprovadas');
}
// Excluir o registro do banco de dados
await ctx.db.delete(args.solicitacaoId); await ctx.db.delete(args.solicitacaoId);
// Remover ajustes automáticos relacionados à ausência excluída (apenas se estava aprovada)
if (statusOriginal === 'aprovado') {
try {
await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
funcionarioId,
motivoTipo: 'ausencia',
motivoId: ausenciaId,
dataInicio,
dataFim
});
} catch (error) {
console.error('[excluirSolicitacao] Erro ao remover ajustes automáticos:', error);
}
// Recalcular banco de horas APENAS para o período específico da ausência excluída
// Isso garante que os dias da ausência sejam removidos corretamente dos registros de ponto
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
}
return null; return null;
} }
}); });

View File

@@ -1,12 +1,43 @@
import { v } from 'convex/values'; import { v } from 'convex/values';
import { mutation, query, internalMutation } from './_generated/server'; import { mutation, query, internalMutation } from './_generated/server';
import { internal } from './_generated/api'; import { internal } from './_generated/api';
import type { MutationCtx } from './_generated/server';
import { Id, Doc } from './_generated/dataModel'; import { Id, Doc } from './_generated/dataModel';
import { verificarLicencaAtiva } from './atestadosLicencas'; import { verificarLicencaAtiva } from './atestadosLicencas';
import { getCurrentUserFunction } from './auth'; import { getCurrentUserFunction } from './auth';
import { formatarDataBR } from './utils/datas'; import { formatarDataBR } from './utils/datas';
import { api } from './_generated/api'; import { api } from './_generated/api';
// Helper: Recalcular banco de horas em um período
async function recalcularBancoHorasPeriodo(
ctx: MutationCtx,
funcionarioId: Id<'funcionarios'>,
dataInicio: string,
dataFim: string
): Promise<void> {
// Gerar todas as datas do período
const dataInicioObj = new Date(dataInicio);
const dataFimObj = new Date(dataFim);
const datas: string[] = [];
const dataAtual = new Date(dataInicioObj);
while (dataAtual <= dataFimObj) {
const ano = dataAtual.getFullYear();
const mes = String(dataAtual.getMonth() + 1).padStart(2, '0');
const dia = String(dataAtual.getDate()).padStart(2, '0');
datas.push(`${ano}-${mes}-${dia}`);
dataAtual.setDate(dataAtual.getDate() + 1);
}
// Recalcular para cada data usando a mutation interna (agendar para execução assíncrona)
for (let i = 0; i < datas.length; i++) {
await ctx.scheduler.runAfter(i * 100, internal.pontos.recalcularBancoHorasData, {
funcionarioId,
data: datas[i]!
});
}
}
// Validador para períodos // Validador para períodos
const periodoValidator = v.object({ const periodoValidator = v.object({
dataInicio: v.string(), dataInicio: v.string(),
@@ -878,6 +909,26 @@ export const atualizarStatus = mutation({
); );
} }
// Se o status foi alterado para Cancelado_RH, recalcular banco de horas
// para garantir que os dias de férias sejam removidos do registro de ponto
if (args.novoStatus === 'Cancelado_RH') {
// IMPORTANTE: Recalcular banco de horas para o período das férias canceladas
// Isso garante que os dias de férias sejam removidos corretamente dos registros de ponto
try {
await recalcularBancoHorasPeriodo(
ctx,
registro.funcionarioId,
registro.dataInicio,
registro.dataFim
);
} catch (error) {
console.error(
'[ferias.atualizarStatus] Erro ao recalcular banco de horas após cancelamento:',
error
);
}
}
// Se o status foi alterado para Cancelado_RH, notificar o funcionário // Se o status foi alterado para Cancelado_RH, notificar o funcionário
if (args.novoStatus === 'Cancelado_RH') { if (args.novoStatus === 'Cancelado_RH') {
const funcionario = await ctx.db.get(registro.funcionarioId); const funcionario = await ctx.db.get(registro.funcionarioId);

View File

@@ -1535,6 +1535,54 @@ async function verificarAusenciaAprovada(
return { temAusencia: false }; return { temAusencia: false };
} }
/**
* Remove ajustes automáticos relacionados a um registro excluído
* Busca e remove ajustes que referenciam o motivoId fornecido
*/
async function removerAjustesAutomaticos(
ctx: MutationCtx,
funcionarioId: Id<'funcionarios'>,
motivoTipo: 'atestado' | 'licenca' | 'ausencia',
motivoId: string,
dataInicio: string,
dataFim: string
): Promise<void> {
// Gerar todas as datas do período
const dataInicioObj = new Date(dataInicio);
const dataFimObj = new Date(dataFim);
const datas: string[] = [];
const dataAtual = new Date(dataInicioObj);
while (dataAtual <= dataFimObj) {
const ano = dataAtual.getFullYear();
const mes = String(dataAtual.getMonth() + 1).padStart(2, '0');
const dia = String(dataAtual.getDate()).padStart(2, '0');
datas.push(`${ano}-${mes}-${dia}`);
dataAtual.setDate(dataAtual.getDate() + 1);
}
// Buscar todos os ajustes automáticos relacionados ao motivoId no período
for (const data of datas) {
const ajustes = await ctx.db
.query('ajustesBancoHoras')
.filter((q) =>
q.and(
q.eq(q.field('funcionarioId'), funcionarioId),
q.eq(q.field('dataAplicacao'), data),
q.eq(q.field('motivoTipo'), motivoTipo),
q.eq(q.field('motivoId'), motivoId),
q.eq(q.field('aplicado'), true)
)
)
.collect();
// Remover cada ajuste encontrado
for (const ajuste of ajustes) {
await ctx.db.delete(ajuste._id);
}
}
}
/** /**
* Verifica ajustes manuais aplicados no dia * Verifica ajustes manuais aplicados no dia
*/ */
@@ -1693,10 +1741,13 @@ async function atualizarBancoHoras(
const ajustesIds: Array<Id<'ajustesBancoHoras'>> = []; const ajustesIds: Array<Id<'ajustesBancoHoras'>> = [];
// Aplicar ajustes automáticos se houver atestado, licença ou ausência // Aplicar ajustes automáticos se houver atestado, licença ou ausência
if (atestadoInfo.temAtestado) { // IMPORTANTE: Verificar se o registro ainda existe antes de criar ajuste
tipoDia = 'atestado'; if (atestadoInfo.temAtestado && atestadoInfo.atestadoId) {
motivoAbono = atestadoInfo.motivo; // Verificar se o atestado ainda existe no banco
if (atestadoInfo.atestadoId) { const atestado = await ctx.db.get(atestadoInfo.atestadoId as Id<'atestados'>);
if (atestado) {
tipoDia = 'atestado';
motivoAbono = atestadoInfo.motivo;
const ajusteId = await aplicarAjusteAutomatico( const ajusteId = await aplicarAjusteAutomatico(
ctx, ctx,
funcionarioId, funcionarioId,
@@ -1708,10 +1759,12 @@ async function atualizarBancoHoras(
); );
ajustesIds.push(ajusteId); ajustesIds.push(ajusteId);
} }
} else if (licencaInfo.temLicenca) { } else if (licencaInfo.temLicenca && licencaInfo.licencaId) {
tipoDia = 'licenca'; // Verificar se a licença ainda existe no banco
motivoAbono = licencaInfo.motivo; const licenca = await ctx.db.get(licencaInfo.licencaId as Id<'licencas'>);
if (licencaInfo.licencaId) { if (licenca) {
tipoDia = 'licenca';
motivoAbono = licencaInfo.motivo;
const ajusteId = await aplicarAjusteAutomatico( const ajusteId = await aplicarAjusteAutomatico(
ctx, ctx,
funcionarioId, funcionarioId,
@@ -1723,10 +1776,12 @@ async function atualizarBancoHoras(
); );
ajustesIds.push(ajusteId); ajustesIds.push(ajusteId);
} }
} else if (ausenciaInfo.temAusencia) { } else if (ausenciaInfo.temAusencia && ausenciaInfo.ausenciaId) {
tipoDia = 'ausencia'; // Verificar se a ausência ainda existe no banco e está aprovada
motivoAbono = ausenciaInfo.motivo; const ausencia = await ctx.db.get(ausenciaInfo.ausenciaId as Id<'solicitacoesAusencias'>);
if (ausenciaInfo.ausenciaId) { if (ausencia && ausencia.status === 'aprovado') {
tipoDia = 'ausencia';
motivoAbono = ausenciaInfo.motivo;
const ajusteId = await aplicarAjusteAutomatico( const ajusteId = await aplicarAjusteAutomatico(
ctx, ctx,
funcionarioId, funcionarioId,
@@ -4238,6 +4293,31 @@ export const recalcularBancoHoras = mutation({
/** /**
* Mutation interna para recalcular banco de horas de uma data específica * Mutation interna para recalcular banco de horas de uma data específica
*/ */
/**
* Internal mutation para remover ajustes automáticos relacionados a um registro excluído
*/
export const removerAjustesAutomaticosInternal = internalMutation({
args: {
funcionarioId: v.id('funcionarios'),
motivoTipo: v.union(v.literal('atestado'), v.literal('licenca'), v.literal('ausencia')),
motivoId: v.string(),
dataInicio: v.string(),
dataFim: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
await removerAjustesAutomaticos(
ctx,
args.funcionarioId,
args.motivoTipo,
args.motivoId,
args.dataInicio,
args.dataFim
);
return null;
}
});
export const recalcularBancoHorasData = internalMutation({ export const recalcularBancoHorasData = internalMutation({
args: { args: {
funcionarioId: v.id('funcionarios'), funcionarioId: v.id('funcionarios'),