From a731015c893fee17a36db81cc77ed349483d45cc Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 23 Dec 2025 07:44:54 -0300 Subject: [PATCH] feat: implement automatic adjustment removal for deleted records in absence and atestado mutations, enhancing data integrity and recalculating work hours for specific periods --- packages/backend/convex/atestadosLicencas.ts | 14 +++ packages/backend/convex/ausencias.ts | 38 ++++++- packages/backend/convex/ferias.ts | 51 +++++++++ packages/backend/convex/pontos.ts | 104 ++++++++++++++++--- 4 files changed, 191 insertions(+), 16 deletions(-) diff --git a/packages/backend/convex/atestadosLicencas.ts b/packages/backend/convex/atestadosLicencas.ts index a96042f..767dda3 100644 --- a/packages/backend/convex/atestadosLicencas.ts +++ b/packages/backend/convex/atestadosLicencas.ts @@ -1254,6 +1254,7 @@ export const excluirAtestado = mutation({ const funcionarioId = atestado.funcionarioId; const dataInicio = atestado.dataInicio; // Data início 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 await ctx.db.delete(args.id); @@ -1267,6 +1268,19 @@ export const excluirAtestado = mutation({ 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 // Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim); diff --git a/packages/backend/convex/ausencias.ts b/packages/backend/convex/ausencias.ts index bb3f6a8..b424b47 100644 --- a/packages/backend/convex/ausencias.ts +++ b/packages/backend/convex/ausencias.ts @@ -941,10 +941,13 @@ export const excluirSolicitacao = mutation({ throw new Error('Solicitação não encontrada'); } - // Apenas solicitações ainda não processadas podem ser excluídas - if (solicitacao.status !== 'aguardando_aprovacao') { - throw new Error('Apenas solicitações pendentes podem ser excluídas'); - } + // IMPORTANTE: Salvar o período exato da ausência ANTES de excluir + // para recalcular o banco de horas apenas para esse período específico + 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 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'); } + // 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); + + // 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; } }); diff --git a/packages/backend/convex/ferias.ts b/packages/backend/convex/ferias.ts index 78d5887..3909c4b 100644 --- a/packages/backend/convex/ferias.ts +++ b/packages/backend/convex/ferias.ts @@ -1,12 +1,43 @@ import { v } from 'convex/values'; import { mutation, query, internalMutation } from './_generated/server'; import { internal } from './_generated/api'; +import type { MutationCtx } from './_generated/server'; import { Id, Doc } from './_generated/dataModel'; import { verificarLicencaAtiva } from './atestadosLicencas'; import { getCurrentUserFunction } from './auth'; import { formatarDataBR } from './utils/datas'; 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 { + // 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 const periodoValidator = v.object({ 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 if (args.novoStatus === 'Cancelado_RH') { const funcionario = await ctx.db.get(registro.funcionarioId); diff --git a/packages/backend/convex/pontos.ts b/packages/backend/convex/pontos.ts index 63638b2..59ac91a 100644 --- a/packages/backend/convex/pontos.ts +++ b/packages/backend/convex/pontos.ts @@ -1535,6 +1535,54 @@ async function verificarAusenciaAprovada( 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 { + // 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 */ @@ -1693,10 +1741,13 @@ async function atualizarBancoHoras( const ajustesIds: Array> = []; // Aplicar ajustes automáticos se houver atestado, licença ou ausência - if (atestadoInfo.temAtestado) { - tipoDia = 'atestado'; - motivoAbono = atestadoInfo.motivo; - if (atestadoInfo.atestadoId) { + // IMPORTANTE: Verificar se o registro ainda existe antes de criar ajuste + if (atestadoInfo.temAtestado && atestadoInfo.atestadoId) { + // Verificar se o atestado ainda existe no banco + const atestado = await ctx.db.get(atestadoInfo.atestadoId as Id<'atestados'>); + if (atestado) { + tipoDia = 'atestado'; + motivoAbono = atestadoInfo.motivo; const ajusteId = await aplicarAjusteAutomatico( ctx, funcionarioId, @@ -1708,10 +1759,12 @@ async function atualizarBancoHoras( ); ajustesIds.push(ajusteId); } - } else if (licencaInfo.temLicenca) { - tipoDia = 'licenca'; - motivoAbono = licencaInfo.motivo; - if (licencaInfo.licencaId) { + } else if (licencaInfo.temLicenca && licencaInfo.licencaId) { + // Verificar se a licença ainda existe no banco + const licenca = await ctx.db.get(licencaInfo.licencaId as Id<'licencas'>); + if (licenca) { + tipoDia = 'licenca'; + motivoAbono = licencaInfo.motivo; const ajusteId = await aplicarAjusteAutomatico( ctx, funcionarioId, @@ -1723,10 +1776,12 @@ async function atualizarBancoHoras( ); ajustesIds.push(ajusteId); } - } else if (ausenciaInfo.temAusencia) { - tipoDia = 'ausencia'; - motivoAbono = ausenciaInfo.motivo; - if (ausenciaInfo.ausenciaId) { + } else if (ausenciaInfo.temAusencia && ausenciaInfo.ausenciaId) { + // Verificar se a ausência ainda existe no banco e está aprovada + const ausencia = await ctx.db.get(ausenciaInfo.ausenciaId as Id<'solicitacoesAusencias'>); + if (ausencia && ausencia.status === 'aprovado') { + tipoDia = 'ausencia'; + motivoAbono = ausenciaInfo.motivo; const ajusteId = await aplicarAjusteAutomatico( ctx, funcionarioId, @@ -4238,6 +4293,31 @@ export const recalcularBancoHoras = mutation({ /** * 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({ args: { funcionarioId: v.id('funcionarios'),