Data:
- {new Date(homologacaoSelecionada.criadoEm).toLocaleDateString('pt-BR', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })}
+ {#if homologacaoSelecionada.dataAplicacaoAjuste}
+ {new Date(homologacaoSelecionada.dataAplicacaoAjuste + 'T00:00:00').toLocaleDateString('pt-BR')}
+ (Aplicado em)
+ {:else}
+ {new Date(homologacaoSelecionada.criadoEm).toLocaleDateString('pt-BR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+ {/if}
Funcionário:
@@ -1124,6 +1151,16 @@
{homologacaoSelecionada.periodoHoras || 0}h
{homologacaoSelecionada.periodoMinutos || 0}min
+ {#if homologacaoSelecionada.periodoAjuste?.dataInicio && homologacaoSelecionada.periodoAjuste?.horaInicio !== undefined && homologacaoSelecionada.periodoAjuste?.minutoInicio !== undefined && homologacaoSelecionada.periodoAjuste?.dataFim && homologacaoSelecionada.periodoAjuste?.horaFim !== undefined && homologacaoSelecionada.periodoAjuste?.minutoFim !== undefined}
+
+ Data/Hora Início:
+ {new Date(homologacaoSelecionada.periodoAjuste.dataInicio + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacaoSelecionada.periodoAjuste.horaInicio, homologacaoSelecionada.periodoAjuste.minutoInicio)}
+
+
+ Data/Hora Fim:
+ {new Date(homologacaoSelecionada.periodoAjuste.dataFim + 'T00:00:00').toLocaleDateString('pt-BR')} {formatarHoraPonto(homologacaoSelecionada.periodoAjuste.horaFim, homologacaoSelecionada.periodoAjuste.minutoFim)}
+
+ {/if}
{#if homologacaoSelecionada.ajusteMinutos}
Ajuste Total:
diff --git a/packages/backend/convex/atestadosLicencas.ts b/packages/backend/convex/atestadosLicencas.ts
index a96042f..469f0bd 100644
--- a/packages/backend/convex/atestadosLicencas.ts
+++ b/packages/backend/convex/atestadosLicencas.ts
@@ -182,7 +182,7 @@ export const listarTodos = query({
fotoPerfilUrl,
criadoPorNome: criadoPor?.nome || 'Sistema',
dias: calcularDias(a.dataInicio, a.dataFim),
- status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado'
+ status: new Date() > new Date(a.dataFim) ? 'finalizado' : 'ativo'
};
} catch (error) {
console.error('Erro ao buscar detalhes do atestado:', error);
@@ -192,7 +192,7 @@ export const listarTodos = query({
fotoPerfilUrl: null,
criadoPorNome: 'Sistema',
dias: calcularDias(a.dataInicio, a.dataFim),
- status: new Date(a.dataFim) >= new Date() ? 'ativo' : 'finalizado'
+ status: new Date() > new Date(a.dataFim) ? 'finalizado' : 'ativo'
};
}
})
@@ -226,7 +226,7 @@ export const listarTodos = query({
criadoPorNome: criadoPor?.nome || 'Sistema',
licencaOriginal,
dias: calcularDias(l.dataInicio, l.dataFim),
- status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado'
+ status: new Date() > new Date(l.dataFim) ? 'finalizado' : 'ativo'
};
} catch (error) {
console.error('Erro ao buscar detalhes da licença:', error);
@@ -237,7 +237,7 @@ export const listarTodos = query({
criadoPorNome: 'Sistema',
licencaOriginal: null,
dias: calcularDias(l.dataInicio, l.dataFim),
- status: new Date(l.dataFim) >= new Date() ? 'ativo' : 'finalizado'
+ status: new Date() > new Date(l.dataFim) ? 'finalizado' : 'ativo'
};
}
})
@@ -1254,6 +1254,33 @@ 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
+ const documentoId = atestado.documentoId; // ID do documento para remover do storage
+
+ // Remover logs de atividades relacionados ao atestado
+ try {
+ const logs = await ctx.db
+ .query('logsAtividades')
+ .withIndex('by_recurso_id', (q) =>
+ q.eq('recurso', 'atestados').eq('recursoId', atestadoId)
+ )
+ .collect();
+ for (const log of logs) {
+ await ctx.db.delete(log._id);
+ }
+ } catch (error) {
+ console.error('[excluirAtestado] Erro ao remover logs de atividades:', error);
+ }
+
+ // Remover documento do storage se existir
+ if (documentoId) {
+ try {
+ await ctx.storage.delete(documentoId);
+ } catch (error) {
+ console.error('[excluirAtestado] Erro ao remover documento do storage:', error);
+ // Não falhar a exclusão se o documento não existir mais
+ }
+ }
// Excluir o registro do banco de dados
await ctx.db.delete(args.id);
@@ -1267,6 +1294,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);
@@ -1305,6 +1345,33 @@ export const excluirLicenca = mutation({
const funcionarioId = licenca.funcionarioId;
const dataInicio = licenca.dataInicio; // Data início da licença
const dataFim = licenca.dataFim; // Data fim da licença
+ const licencaId = args.id.toString(); // ID da licença para remover logs
+ const documentoId = licenca.documentoId; // ID do documento para remover do storage
+
+ // Remover logs de atividades relacionados à licença
+ try {
+ const logs = await ctx.db
+ .query('logsAtividades')
+ .withIndex('by_recurso_id', (q) =>
+ q.eq('recurso', 'licencas').eq('recursoId', licencaId)
+ )
+ .collect();
+ for (const log of logs) {
+ await ctx.db.delete(log._id);
+ }
+ } catch (error) {
+ console.error('[excluirLicenca] Erro ao remover logs de atividades:', error);
+ }
+
+ // Remover documento do storage se existir
+ if (documentoId) {
+ try {
+ await ctx.storage.delete(documentoId);
+ } catch (error) {
+ console.error('[excluirLicenca] Erro ao remover documento do storage:', error);
+ // Não falhar a exclusão se o documento não existir mais
+ }
+ }
// Excluir o registro do banco de dados
await ctx.db.delete(args.id);
@@ -1318,6 +1385,19 @@ export const excluirLicenca = mutation({
args.id
);
+ // Remover ajustes automáticos relacionados à licença excluída
+ try {
+ await ctx.runMutation(internal.pontos.removerAjustesAutomaticosInternal, {
+ funcionarioId,
+ motivoTipo: 'licenca',
+ motivoId: licencaId,
+ dataInicio,
+ dataFim
+ });
+ } catch (error) {
+ console.error('[excluirLicenca] Erro ao remover ajustes automáticos:', error);
+ }
+
// Recalcular banco de horas APENAS para o período específico da licença excluída
// Isso garante que os dias da licença 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/dashboard.ts b/packages/backend/convex/dashboard.ts
index 16aba68..6159206 100644
--- a/packages/backend/convex/dashboard.ts
+++ b/packages/backend/convex/dashboard.ts
@@ -7,10 +7,12 @@ export const getStats = query({
returns: v.object({
totalFuncionarios: v.number(),
totalSimbolos: v.number(),
+ totalUsuarios: v.number(),
funcionariosAtivos: v.number(),
funcionariosDesligados: v.number(),
cargoComissionado: v.number(),
- funcaoGratificada: v.number()
+ funcaoGratificada: v.number(),
+ totalCadastros: v.number()
}),
handler: async (ctx) => {
// Contar funcionários
@@ -36,41 +38,22 @@ export const getStats = query({
const simbolos = await ctx.db.query('simbolos').collect();
const totalSimbolos = simbolos.length;
+ // Contar usuários cadastrados
+ const usuarios = await ctx.db.query('usuarios').collect();
+ const totalUsuarios = usuarios.length;
+
+ // Calcular total de cadastros (funcionários + símbolos + usuários)
+ const totalCadastros = totalFuncionarios + totalSimbolos + totalUsuarios;
+
return {
totalFuncionarios,
totalSimbolos,
+ totalUsuarios,
funcionariosAtivos,
funcionariosDesligados,
cargoComissionado,
- funcaoGratificada
- };
- }
-});
-
-// Obter atividades recentes (últimas 24 horas)
-export const getRecentActivity = query({
- args: {},
- returns: v.object({
- funcionariosCadastrados24h: v.number(),
- simbolosCadastrados24h: v.number()
- }),
- handler: async (ctx) => {
- const now = Date.now();
- const last24h = now - 24 * 60 * 60 * 1000;
-
- // Funcionários cadastrados nas últimas 24h
- const funcionarios = await ctx.db.query('funcionarios').collect();
- const funcionariosCadastrados24h = funcionarios.filter(
- (f) => f._creationTime >= last24h
- ).length;
-
- // Símbolos cadastrados nas últimas 24h
- const simbolos = await ctx.db.query('simbolos').collect();
- const simbolosCadastrados24h = simbolos.filter((s) => s._creationTime >= last24h).length;
-
- return {
- funcionariosCadastrados24h,
- simbolosCadastrados24h
+ funcaoGratificada,
+ totalCadastros
};
}
});
diff --git a/packages/backend/convex/ferias.ts b/packages/backend/convex/ferias.ts
index 862fa1a..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(),
@@ -821,8 +852,13 @@ export const atualizarStatus = mutation({
throw new Error('Período de férias não encontrado');
}
- // Atualizar status e histórico
- const acao = `Status alterado para ${args.novoStatus}`;
+ // Buscar usuário que está alterando o status para incluir na mensagem quando for Cancelado_RH
+ let acao = `Status alterado para ${args.novoStatus}`;
+ if (args.novoStatus === 'Cancelado_RH') {
+ const usuarioRH = await ctx.db.get(args.usuarioId);
+ const nomeUsuario = usuarioRH?.nome || 'Usuário Desconhecido';
+ acao = `Status alterado para Cancelado_RH por ${nomeUsuario}`;
+ }
const updateData: {
status: typeof args.novoStatus;
@@ -873,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/monitoramento.ts b/packages/backend/convex/monitoramento.ts
index dd6fefb..a76ea1b 100644
--- a/packages/backend/convex/monitoramento.ts
+++ b/packages/backend/convex/monitoramento.ts
@@ -847,224 +847,3 @@ export const obterHistoricoAlertas = query({
}
});
-/**
- * Status consolidado do sistema para o dashboard
- */
-export const getStatusSistema = query({
- args: {},
- returns: v.object({
- usuariosOnline: v.number(),
- totalRegistros: v.number(),
- tempoMedioResposta: v.number(),
- cpuUsada: v.number(),
- memoriaUsada: v.number(),
- ultimaAtualizacao: v.number()
- }),
- handler: async (ctx) => {
- try {
- // Última métrica, se existir
- const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
-
- // Usuários online: usar métrica se disponível, senão derivar de usuários
- let usuariosOnline = 0;
- if (ultimaMetrica?.usuariosOnline !== undefined) {
- usuariosOnline = ultimaMetrica.usuariosOnline;
- } else {
- const usuarios = await ctx.db.query('usuarios').collect();
- usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length;
- }
-
- // Total de registros (estimativa baseada em tabelas principais)
- const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
- ctx.db.query('usuarios').collect(),
- ctx.db.query('funcionarios').collect(),
- ctx.db.query('simbolos').collect(),
- ctx.db.query('alertConfigurations').collect(),
- ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
- ]);
- const totalRegistros =
- usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
-
- // Métricas de performance com fallbacks seguros
- const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
- const cpuUsada = Math.max(
- 0,
- Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100)
- );
- const memoriaUsada = Math.max(
- 0,
- Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100)
- );
- const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now();
-
- return {
- usuariosOnline,
- totalRegistros,
- tempoMedioResposta,
- cpuUsada,
- memoriaUsada,
- ultimaAtualizacao
- };
- } catch (error) {
- console.error('Erro em getStatusSistema:', error);
- // Retornar valores padrão em caso de erro
- return {
- usuariosOnline: 0,
- totalRegistros: 0,
- tempoMedioResposta: 0,
- cpuUsada: 0,
- memoriaUsada: 0,
- ultimaAtualizacao: Date.now()
- };
- }
- }
-});
-
-/**
- * Atividade do banco no último minuto (agregada em buckets)
- * Usa logsAtividades e systemMetrics para calcular atividade real.
- */
-export const getAtividadeBancoDados = query({
- args: {},
- returns: v.object({
- historico: v.array(
- v.object({
- entradas: v.number(),
- saidas: v.number()
- })
- )
- }),
- handler: async (ctx) => {
- try {
- const agora = Date.now();
- const haUmMinuto = agora - 60 * 1000;
-
- // Buscar atividades reais do sistema
- const atividadesRecentes = await ctx.db
- .query('logsAtividades')
- .withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
- .order('asc')
- .collect();
-
- // Buscar métricas também (para mensagens se houver)
- const metricasRecentes = await ctx.db
- .query('systemMetrics')
- .withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
- .order('asc')
- .collect();
-
- // Bucketizar em 30 pontos (~2s cada) para visualização
- const numBuckets = 30;
- const bucketSizeMs = Math.ceil(60_000 / numBuckets);
- const historico: Array<{ entradas: number; saidas: number }> = [];
-
- for (let i = 0; i < numBuckets; i++) {
- const inicio = haUmMinuto + i * bucketSizeMs;
- const fim = inicio + bucketSizeMs;
-
- // Contar atividades de criação/inserção (entradas)
- const atividadesBucket = atividadesRecentes.filter(
- (a) => a.timestamp >= inicio && a.timestamp < fim
- );
- const entradasAtividades = atividadesBucket.filter(
- (a) => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
- ).length;
-
- // Contar atividades de exclusão/remoção (saídas)
- const saidasAtividades = atividadesBucket.filter(
- (a) => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
- ).length;
-
- // Usar mensagensPorMinuto como adicional se disponível
- const bucketMetricas = metricasRecentes.filter(
- (m) => m.timestamp >= inicio && m.timestamp < fim
- );
- const somaMensagens =
- bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
-
- // Combinar atividades reais com métricas de mensagens
- const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
- const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
-
- historico.push({ entradas, saidas });
- }
-
- return { historico };
- } catch (error) {
- console.error('Erro em getAtividadeBancoDados:', error);
- // Retornar histórico vazio em caso de erro
- return { historico: Array(30).fill({ entradas: 0, saidas: 0 }) };
- }
- }
-});
-
-/**
- * Distribuição de operações (calculada a partir de logsAtividades e métricas)
- */
-export const getDistribuicaoRequisicoes = query({
- args: {},
- returns: v.object({
- queries: v.number(),
- mutations: v.number(),
- leituras: v.number(),
- escritas: v.number()
- }),
- handler: async (ctx) => {
- try {
- const umaHoraAtras = Date.now() - 60 * 60 * 1000;
-
- // Buscar atividades reais do sistema
- const atividades = await ctx.db
- .query('logsAtividades')
- .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
- .collect();
-
- // Buscar métricas também
- const metricas = await ctx.db
- .query('systemMetrics')
- .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
- .order('desc')
- .take(100);
-
- // Contar operações de leitura (consultas, visualizações)
- const leituras = atividades.filter(
- (a) =>
- a.acao === 'consultar' ||
- a.acao === 'visualizar' ||
- a.acao === 'listar' ||
- a.acao === 'buscar'
- ).length;
-
- // Contar operações de escrita (criar, editar, excluir)
- const escritas = atividades.filter(
- (a) =>
- a.acao === 'criar' ||
- a.acao === 'editar' ||
- a.acao === 'excluir' ||
- a.acao === 'inserir' ||
- a.acao === 'atualizar' ||
- a.acao === 'deletar' ||
- a.acao === 'cadastrar' ||
- a.acao === 'remover'
- ).length;
-
- // Adicionar estimativa baseada em mensagens se disponível
- const totalMensagens = Math.max(
- 0,
- Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
- );
-
- // Queries são leituras + parte das mensagens (como consultas de chat)
- const queries = leituras + Math.round(totalMensagens * 0.5);
-
- // Mutations são escritas + parte das mensagens (como envio de mensagens)
- const mutations = escritas + Math.round(totalMensagens * 0.3);
-
- return { queries, mutations, leituras, escritas };
- } catch (error) {
- console.error('Erro em getDistribuicaoRequisicoes:', error);
- // Retornar valores padrão em caso de erro
- return { queries: 0, mutations: 0, leituras: 0, escritas: 0 };
- }
- }
-});
diff --git a/packages/backend/convex/pontos.ts b/packages/backend/convex/pontos.ts
index 332a0b0..9c3e8b3 100644
--- a/packages/backend/convex/pontos.ts
+++ b/packages/backend/convex/pontos.ts
@@ -636,45 +636,59 @@ export const registrarPonto = mutation({
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
- const dataConsulta = new Date(data);
+ // Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
+ // A hora informada está em GMT-3, então precisamos adicionar 3 horas para obter UTC
+ const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
+ function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
+ const [ano, mes, dia] = data.split('-').map(Number);
+ return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
+ }
+
+ // Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto que já estão em UTC
+ function criarTimestampUTC(data: string, horaUTC: number, minutoUTC: number): number {
+ const [ano, mes, dia] = data.split('-').map(Number);
+ return Date.UTC(ano, mes - 1, dia, horaUTC, minutoUTC, 0, 0);
+ }
+
+ // Obter timestamp atual em UTC
+ const agoraUTC = new Date();
+ const agoraTimestampUTC = agoraUTC.getTime();
+
+ // Timestamp da consulta (registro sendo feito) em UTC
+ // hora/minuto já estão em UTC (extraídos com getUTCHours/getUTCMinutes)
+ const timestampConsultaUTC = criarTimestampUTC(data, hora, minuto);
+
for (const dispensa of dispensas) {
// Se for isento, sempre está dispensado
if (dispensa.isento) {
throw new Error('Registro dispensado pelo gestor: Isento de registro (caso excepcional)');
}
- // Verificar se está no período
- const dataInicio = new Date(dispensa.dataInicio);
- const dataFim = new Date(dispensa.dataFim);
+ // Calcular timestamps de início e fim da dispensa em UTC
+ const timestampInicioUTC = criarTimestampUTCDeGMT3(
+ dispensa.dataInicio,
+ dispensa.horaInicio,
+ dispensa.minutoInicio
+ );
+ const timestampFimUTC = criarTimestampUTCDeGMT3(
+ dispensa.dataFim,
+ dispensa.horaFim,
+ dispensa.minutoFim
+ );
- if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
- // Verificar hora e minuto se necessário
- const timestampConsulta = new Date(
- `${data}T${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}:00`
- ).getTime();
- const timestampInicio = new Date(
- `${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
- ).getTime();
- const timestampFim = new Date(
- `${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
- ).getTime();
-
- if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
- throw new Error(`Registro dispensado pelo gestor: ${dispensa.motivo}`);
- }
- }
-
- // Verificar se expirou (desativar na mutation de registro)
- const agora = new Date();
- const dataFimTimestamp = new Date(
- `${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
- ).getTime();
-
- if (agora.getTime() > dataFimTimestamp && !dispensa.isento) {
- // Desativar dispensa expirada (mutation pode fazer isso)
+ // Desativar dispensa expirada ANTES de verificar bloqueio (após o fim)
+ // Verificar se AGORA já passou do horário de fim da dispensa
+ if (agoraTimestampUTC > timestampFimUTC) {
await ctx.db.patch(dispensa._id, {
ativo: false
});
+ continue; // Pular verificação de bloqueio se já expirou
+ }
+
+ // Verificar se AGORA está dentro do período da dispensa (não o horário do registro)
+ // Se o momento atual está dentro do período, bloqueia qualquer tentativa de registro
+ if (agoraTimestampUTC >= timestampInicioUTC && agoraTimestampUTC <= timestampFimUTC) {
+ throw new Error(`Registro dispensado pelo gestor: ${dispensa.motivo}`);
}
}
@@ -1400,78 +1414,45 @@ function calcularHorasTrabalhadas(
return minutosA - minutosB;
});
- // Procurar registros principais
- const entrada = registrosOrdenados.find((r) => r.tipo === 'entrada');
- const saidaAlmoco = registrosOrdenados.find((r) => r.tipo === 'saida_almoco');
- const retornoAlmoco = registrosOrdenados.find((r) => r.tipo === 'retorno_almoco');
- const saida = registrosOrdenados.find((r) => r.tipo === 'saida');
+ let totalMinutos = 0;
+ let entradaPendente: { hora: number; minuto: number } | null = null;
- // Caso 1: Tem entrada e saída completas
- if (entrada && saida) {
- const minutosEntrada = entrada.hora * 60 + entrada.minuto;
- const minutosSaida = saida.hora * 60 + saida.minuto;
+ // Processar registros sequencialmente para capturar todos os períodos de trabalho
+ // Isso permite calcular múltiplas entradas/saídas no mesmo dia
+ for (const registro of registrosOrdenados) {
+ const minutosRegistro = registro.hora * 60 + registro.minuto;
- // Caso 1.1: Tem intervalo de almoço completo (saída almoço + retorno almoço)
- if (saidaAlmoco && retornoAlmoco) {
- const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
- const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
-
- // Validar ordem lógica
- if (
- minutosSaidaAlmoco > minutosEntrada &&
- minutosRetornoAlmoco > minutosSaidaAlmoco &&
- minutosSaida > minutosRetornoAlmoco
- ) {
- const horasManha = minutosSaidaAlmoco - minutosEntrada;
- const horasTarde = minutosSaida - minutosRetornoAlmoco;
- return horasManha + horasTarde;
+ if (registro.tipo === 'entrada') {
+ // Se já havia uma entrada pendente sem saída, ignorar a anterior (inconsistência)
+ // e usar a nova entrada
+ entradaPendente = { hora: registro.hora, minuto: registro.minuto };
+ } else if (registro.tipo === 'saida_almoco') {
+ // Se há entrada pendente, calcular período da manhã
+ if (entradaPendente) {
+ const minutosEntrada = entradaPendente.hora * 60 + entradaPendente.minuto;
+ if (minutosRegistro > minutosEntrada) {
+ totalMinutos += minutosRegistro - minutosEntrada;
+ }
+ // Limpar entrada pendente após saída almoço (aguardar retorno)
+ entradaPendente = null;
}
- }
-
- // Caso 1.2: Tem apenas saída almoço (sem retorno) - considerar apenas manhã
- if (saidaAlmoco && !retornoAlmoco) {
- const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
- if (minutosSaidaAlmoco > minutosEntrada) {
- return minutosSaidaAlmoco - minutosEntrada;
- }
- }
-
- // Caso 1.3: Tem apenas retorno almoço (sem saída) - considerar apenas tarde
- if (!saidaAlmoco && retornoAlmoco) {
- const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
- if (minutosSaida > minutosRetornoAlmoco) {
- return minutosSaida - minutosRetornoAlmoco;
- }
- }
-
- // Caso 1.4: Sem intervalo de almoço registrado - calcular direto
- if (!saidaAlmoco && !retornoAlmoco) {
- if (minutosSaida > minutosEntrada) {
- return minutosSaida - minutosEntrada;
+ } else if (registro.tipo === 'retorno_almoco') {
+ // Marcar como nova entrada para período da tarde
+ entradaPendente = { hora: registro.hora, minuto: registro.minuto };
+ } else if (registro.tipo === 'saida') {
+ // Se há entrada pendente (pode ser entrada inicial ou retorno almoço), calcular período
+ if (entradaPendente) {
+ const minutosEntrada = entradaPendente.hora * 60 + entradaPendente.minuto;
+ if (minutosRegistro > minutosEntrada) {
+ totalMinutos += minutosRegistro - minutosEntrada;
+ }
+ // Limpar entrada pendente após saída
+ entradaPendente = null;
}
}
}
- // Caso 2: Tem apenas entrada (sem saída) - retornar 0 (dia incompleto)
- if (entrada && !saida) {
- // Se tiver saída almoço, considerar até a saída almoço
- if (saidaAlmoco) {
- const minutosEntrada = entrada.hora * 60 + entrada.minuto;
- const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
- if (minutosSaidaAlmoco > minutosEntrada) {
- return minutosSaidaAlmoco - minutosEntrada;
- }
- }
- return 0;
- }
-
- // Caso 3: Tem apenas saída (sem entrada) - inconsistência, retornar 0
- if (saida && !entrada) {
- return 0;
- }
-
- // Caso 4: Não tem entrada nem saída - retornar 0
- return 0;
+ return totalMinutos;
}
/**
@@ -1568,6 +1549,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
*/
@@ -1726,10 +1755,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,
@@ -1741,10 +1773,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,
@@ -1756,10 +1790,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,
@@ -2582,6 +2618,14 @@ export const ajustarBancoHoras = mutation({
periodoDias: v.number(),
periodoHoras: v.number(),
periodoMinutos: v.number(),
+ dataAplicacao: v.string(), // YYYY-MM-DD - Data em que o ajuste deve ser aplicado
+ // Período do ajuste (data/hora início e fim)
+ dataInicio: v.optional(v.string()), // YYYY-MM-DD
+ horaInicio: v.optional(v.number()), // 0-23
+ minutoInicio: v.optional(v.number()), // 0-59
+ dataFim: v.optional(v.string()), // YYYY-MM-DD
+ horaFim: v.optional(v.number()), // 0-23
+ minutoFim: v.optional(v.number()), // 0-59
motivoId: v.optional(v.string()),
motivoTipo: v.optional(v.string()),
motivoDescricao: v.optional(v.string()),
@@ -2619,8 +2663,8 @@ export const ajustarBancoHoras = mutation({
ajusteFinal = -ajusteMinutos;
}
- // Buscar banco de horas mais recente ou criar um registro de ajuste
- const hoje = new Date().toISOString().split('T')[0]!;
+ // Usar a data de aplicação fornecida pelo usuário
+ const dataAplicacao = args.dataAplicacao;
// Criar registro de ajuste na nova tabela
const ajusteId = await ctx.db.insert('ajustesBancoHoras', {
@@ -2630,7 +2674,13 @@ export const ajustarBancoHoras = mutation({
motivoId: args.motivoId,
motivoDescricao: args.motivoDescricao || `Ajuste ${args.tipoAjuste}`,
valorMinutos: ajusteFinal,
- dataAplicacao: hoje,
+ dataAplicacao: dataAplicacao,
+ dataInicio: args.dataInicio,
+ horaInicio: args.horaInicio,
+ minutoInicio: args.minutoInicio,
+ dataFim: args.dataFim,
+ horaFim: args.horaFim,
+ minutoFim: args.minutoFim,
gestorId: usuario._id,
observacoes: args.observacoes,
aplicado: false,
@@ -2640,7 +2690,7 @@ export const ajustarBancoHoras = mutation({
const bancoHorasAtual = await ctx.db
.query('bancoHoras')
.withIndex('by_funcionario_data', (q) =>
- q.eq('funcionarioId', args.funcionarioId).eq('data', hoje)
+ q.eq('funcionarioId', args.funcionarioId).eq('data', dataAplicacao)
)
.first();
@@ -2672,7 +2722,7 @@ export const ajustarBancoHoras = mutation({
await ctx.db.insert('bancoHoras', {
funcionarioId: args.funcionarioId,
- data: hoje,
+ data: dataAplicacao,
cargaHorariaDiaria,
horasTrabalhadas: 0,
saldoMinutos: ajusteFinal,
@@ -2691,7 +2741,7 @@ export const ajustarBancoHoras = mutation({
});
// Recalcular banco de horas mensal após ajuste
- const mes = hoje.substring(0, 7); // YYYY-MM
+ const mes = dataAplicacao.substring(0, 7); // YYYY-MM
// Verificar se estamos ajustando um mês passado
const hojeDate = new Date();
@@ -2702,6 +2752,7 @@ export const ajustarBancoHoras = mutation({
await calcularBancoHorasMensal(ctx, args.funcionarioId, mes, estaAjustandoMesPassado);
// Criar registro de homologação (mantido para compatibilidade)
+ // Armazenar o ajusteId para facilitar a busca posterior
const homologacaoId = await ctx.db.insert('homologacoesPonto', {
funcionarioId: args.funcionarioId,
gestorId: usuario._id,
@@ -2717,6 +2768,9 @@ export const ajustarBancoHoras = mutation({
criadoEm: Date.now()
});
+ // Armazenar ajusteId na homologação usando um campo customizado ou buscar depois
+ // Por enquanto, vamos buscar o ajuste no listarHomologacoes usando os critérios
+
return { success: true, homologacaoId, ajusteId, ajusteMinutos: ajusteFinal };
}
});
@@ -2784,6 +2838,62 @@ export const listarHomologacoes = query({
}
}
+ // Buscar dataAplicacao e período do ajuste se for um ajuste de banco de horas
+ let dataAplicacaoAjuste: string | null = null;
+ let periodoAjuste: {
+ dataInicio?: string;
+ horaInicio?: number;
+ minutoInicio?: number;
+ dataFim?: string;
+ horaFim?: number;
+ minutoFim?: number;
+ } | null = null;
+ if (h.tipoAjuste && h.ajusteMinutos !== undefined) {
+ // Buscar ajustes criados próximo ao tempo da homologação (dentro de 5 minutos antes)
+ // O ajuste é criado logo antes da homologação em ajustarBancoHoras
+ const tempoLimite = h.criadoEm - 5 * 60 * 1000; // 5 minutos antes
+
+ // Buscar todos os ajustes do funcionário com os mesmos critérios
+ // e criados próximo ao tempo da homologação
+ const ajustes = await ctx.db
+ .query('ajustesBancoHoras')
+ .withIndex('by_funcionario', (q) => q.eq('funcionarioId', h.funcionarioId))
+ .filter((q) =>
+ q.and(
+ q.eq(q.field('motivoTipo'), 'manual'),
+ q.eq(q.field('tipo'), h.tipoAjuste),
+ q.eq(q.field('valorMinutos'), h.ajusteMinutos),
+ q.eq(q.field('gestorId'), h.gestorId),
+ q.gte(q.field('criadoEm'), tempoLimite),
+ q.lte(q.field('criadoEm'), h.criadoEm)
+ )
+ )
+ .collect();
+
+ // Se encontrou ajuste(s), encontrar o mais próximo em tempo à homologação
+ if (ajustes.length > 0) {
+ // Encontrar o ajuste com timestamp mais próximo ao da homologação
+ let ajusteMaisProximo = ajustes[0]!;
+ let menorDiferenca = Math.abs(ajustes[0]!.criadoEm - h.criadoEm);
+ for (const ajusteCandidato of ajustes) {
+ const diferenca = Math.abs(ajusteCandidato.criadoEm - h.criadoEm);
+ if (diferenca < menorDiferenca) {
+ menorDiferenca = diferenca;
+ ajusteMaisProximo = ajusteCandidato;
+ }
+ }
+ dataAplicacaoAjuste = ajusteMaisProximo.dataAplicacao;
+ periodoAjuste = {
+ dataInicio: ajusteMaisProximo.dataInicio,
+ horaInicio: ajusteMaisProximo.horaInicio,
+ minutoInicio: ajusteMaisProximo.minutoInicio,
+ dataFim: ajusteMaisProximo.dataFim,
+ horaFim: ajusteMaisProximo.horaFim,
+ minutoFim: ajusteMaisProximo.minutoFim
+ };
+ }
+ }
+
return {
...h,
funcionario: funcionario
@@ -2803,7 +2913,9 @@ export const listarHomologacoes = query({
data: registro.data,
tipo: registro.tipo
}
- : null
+ : null,
+ dataAplicacaoAjuste,
+ periodoAjuste
};
})
);
@@ -2845,14 +2957,157 @@ export const excluirHomologacao = mutation({
throw new Error('Você não tem permissão para excluir esta homologação');
}
- // Se a homologação estiver vinculada a um registro, remover a referência
+ // Se a homologação estiver vinculada a um registro, restaurar valores originais
if (homologacao.registroId) {
const registro = await ctx.db.get(homologacao.registroId);
if (registro && registro.homologacaoId === args.homologacaoId) {
- await ctx.db.patch(homologacao.registroId, {
+ // Restaurar valores originais se existirem
+ const patchData: {
+ homologacaoId: undefined;
+ editadoPorGestor: boolean;
+ hora?: number;
+ minuto?: number;
+ } = {
homologacaoId: undefined,
editadoPorGestor: false
- });
+ };
+
+ // Se a homologação tem valores anteriores, restaurar
+ if (homologacao.horaAnterior !== undefined && homologacao.minutoAnterior !== undefined) {
+ patchData.hora = homologacao.horaAnterior;
+ patchData.minuto = homologacao.minutoAnterior;
+ }
+
+ await ctx.db.patch(homologacao.registroId, patchData);
+
+ // Recalcular banco de horas após restaurar valores
+ const config = await ctx.db
+ .query('configuracaoPonto')
+ .withIndex('by_ativo', (q) => q.eq('ativo', true))
+ .first();
+
+ if (config) {
+ await atualizarBancoHoras(ctx, registro.funcionarioId, registro.data, config);
+ }
+ }
+ }
+
+ // Se for um ajuste de banco de horas, remover completamente do banco de dados
+ if (homologacao.tipoAjuste && homologacao.ajusteMinutos !== undefined) {
+ // Buscar ajustes criados próximo ao tempo da homologação (dentro de 5 minutos antes)
+ // O ajuste é criado logo antes da homologação em ajustarBancoHoras
+ const tempoLimite = homologacao.criadoEm - 5 * 60 * 1000; // 5 minutos antes
+
+ // Buscar todos os ajustes do funcionário com os mesmos critérios
+ // e criados próximo ao tempo da homologação
+ const ajustes = await ctx.db
+ .query('ajustesBancoHoras')
+ .withIndex('by_funcionario', (q) => q.eq('funcionarioId', homologacao.funcionarioId))
+ .filter((q) =>
+ q.and(
+ q.eq(q.field('motivoTipo'), 'manual'),
+ q.eq(q.field('tipo'), homologacao.tipoAjuste),
+ q.eq(q.field('valorMinutos'), homologacao.ajusteMinutos),
+ q.eq(q.field('gestorId'), homologacao.gestorId),
+ q.gte(q.field('criadoEm'), tempoLimite),
+ q.lte(q.field('criadoEm'), homologacao.criadoEm)
+ )
+ )
+ .collect();
+
+ // Se encontrou ajuste(s), encontrar o mais próximo em tempo à homologação
+ if (ajustes.length > 0) {
+ // Encontrar o ajuste com timestamp mais próximo ao da homologação
+ // (o ajuste geralmente é criado um pouco antes da homologação)
+ let ajusteMaisProximo = ajustes[0]!;
+ let menorDiferenca = Math.abs(ajustes[0]!.criadoEm - homologacao.criadoEm);
+ for (const ajusteCandidato of ajustes) {
+ const diferenca = Math.abs(ajusteCandidato.criadoEm - homologacao.criadoEm);
+ if (diferenca < menorDiferenca) {
+ menorDiferenca = diferenca;
+ ajusteMaisProximo = ajusteCandidato;
+ }
+ }
+ const ajuste = ajusteMaisProximo;
+
+ // Buscar o banco de horas do dia onde o ajuste foi aplicado
+ const bancoHoras = await ctx.db
+ .query('bancoHoras')
+ .withIndex('by_funcionario_data', (q) =>
+ q.eq('funcionarioId', homologacao.funcionarioId).eq('data', ajuste.dataAplicacao)
+ )
+ .first();
+
+ if (bancoHoras) {
+ // Remover o ajuste do array ajustesIds
+ const novosAjustesIds = (bancoHoras.ajustesIds || []).filter(
+ (id) => id !== ajuste._id
+ );
+
+ // Reverter o ajuste do saldo (subtrair o valor que foi adicionado)
+ const novoSaldoMinutos = bancoHoras.saldoMinutos - ajuste.valorMinutos;
+
+ // Verificar se ainda há outros ajustes ou se precisa resetar tipoDia
+ let novoTipoDia = bancoHoras.tipoDia;
+ if (novosAjustesIds.length > 0) {
+ // Se ainda há outros ajustes, verificar qual tipoDia deve ser mantido
+ const outrosAjustes = await Promise.all(
+ novosAjustesIds.map((id) => ctx.db.get(id))
+ );
+ const temAjusteAbonar = outrosAjustes.some((a) => a?.tipo === 'abonar');
+ const temAjusteDescontar = outrosAjustes.some((a) => a?.tipo === 'descontar');
+
+ // Se há ajuste de abonar, manter ou definir como 'abonado'
+ if (temAjusteAbonar) {
+ novoTipoDia = 'abonado';
+ } else if (temAjusteDescontar) {
+ // Se há ajuste de descontar, manter ou definir como 'descontado'
+ novoTipoDia = 'descontado';
+ } else {
+ // Se não há ajustes que determinem tipoDia, resetar
+ novoTipoDia = undefined;
+ }
+ } else {
+ // Se não há mais ajustes, verificar se deve resetar tipoDia
+ // Se o tipoDia estava relacionado ao ajuste removido, resetar
+ if (
+ (bancoHoras.tipoDia === 'abonado' && ajuste.tipo === 'abonar') ||
+ (bancoHoras.tipoDia === 'descontado' && ajuste.tipo === 'descontar')
+ ) {
+ novoTipoDia = undefined;
+ }
+ }
+
+ // Atualizar banco de horas
+ await ctx.db.patch(bancoHoras._id, {
+ saldoMinutos: novoSaldoMinutos,
+ ajustesIds: novosAjustesIds.length > 0 ? novosAjustesIds : undefined,
+ tipoDia: novoTipoDia
+ });
+
+ // Recalcular banco de horas do dia específico para garantir consistência
+ const config = await ctx.db
+ .query('configuracaoPonto')
+ .withIndex('by_ativo', (q) => q.eq('ativo', true))
+ .first();
+
+ if (config) {
+ // Recalcular banco de horas do dia para atualizar valores baseados nos registros
+ await atualizarBancoHoras(ctx, homologacao.funcionarioId, ajuste.dataAplicacao, config);
+ }
+
+ // Recalcular banco de horas mensal após remover ajuste
+ const mes = ajuste.dataAplicacao.substring(0, 7); // YYYY-MM
+ const hojeDate = new Date();
+ const mesAtual = `${hojeDate.getFullYear()}-${String(hojeDate.getMonth() + 1).padStart(2, '0')}`;
+ const estaRemovendoMesPassado = mes < mesAtual;
+
+ // Recalcular em cascata se for mês passado
+ await calcularBancoHorasMensal(ctx, homologacao.funcionarioId, mes, estaRemovendoMesPassado);
+ }
+
+ // Excluir o registro de ajuste do banco de dados
+ await ctx.db.delete(ajuste._id);
}
}
@@ -2929,11 +3184,9 @@ export const criarDispensaRegistro = mutation({
throw new Error('Você não tem permissão para criar dispensa para este funcionário');
}
- // Validar datas
- const dataInicioObj = new Date(args.dataInicio);
- const dataFimObj = new Date(args.dataFim);
-
- if (dataFimObj < dataInicioObj) {
+ // Validar datas (comparar strings diretamente para evitar problemas de timezone)
+ // Formato YYYY-MM-DD permite comparação lexicográfica
+ if (args.dataFim < args.dataInicio) {
throw new Error('Data fim deve ser maior ou igual à data início');
}
@@ -2986,10 +3239,8 @@ export const removerDispensaRegistro = mutation({
throw new Error('Você não tem permissão para remover esta dispensa');
}
- // Desativar dispensa
- await ctx.db.patch(args.dispensaId, {
- ativo: false
- });
+ // Deletar dispensa do banco de dados
+ await ctx.db.delete(args.dispensaId);
return { success: true };
}
@@ -3070,14 +3321,49 @@ export const listarDispensas = query({
}
}
- // Verificar se expirou (se não for isento)
+ // Verificar se está ativa ou expirada (considerando data, hora e minuto em GMT-3)
let expirada = false;
+
+ // GMT-3 está 3 horas ATRÁS do UTC
+ // Offset: +3 horas para converter GMT-3 para UTC
+ const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
+
+ // Obter data/hora atual em UTC
+ const agoraUTC = new Date();
+ const agoraTimestampUTC = agoraUTC.getTime();
+
+ // Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
+ // A hora informada está em GMT-3, então precisamos adicionar 3 horas para obter UTC
+ // Exemplo: 08:00 GMT-3 = 11:00 UTC
+ function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
+ const [ano, mes, dia] = data.split('-').map(Number);
+ // Date.UTC cria timestamp UTC
+ // Se a hora está em GMT-3, adicionamos 3 horas para obter o equivalente UTC
+ return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
+ }
+
if (!d.isento) {
- const agora = new Date();
- const dataFimTimestamp = new Date(
- `${d.dataFim}T${d.horaFim.toString().padStart(2, '0')}:${d.minutoFim.toString().padStart(2, '0')}:00`
- ).getTime();
- expirada = agora.getTime() > dataFimTimestamp;
+ // Para dispensas não isentas, verificar se está dentro do período
+ const dataInicioTimestamp = criarTimestampUTCDeGMT3(
+ d.dataInicio,
+ d.horaInicio,
+ d.minutoInicio
+ );
+ const dataFimTimestamp = criarTimestampUTCDeGMT3(d.dataFim, d.horaFim, d.minutoFim);
+
+ // Está expirada se estiver antes do início OU depois do fim
+ // Está ativa se: dataInicioTimestamp <= agoraTimestampUTC <= dataFimTimestamp
+ expirada =
+ agoraTimestampUTC < dataInicioTimestamp || agoraTimestampUTC > dataFimTimestamp;
+ } else {
+ // Se for isento, verificar apenas se já passou do início
+ const dataInicioTimestamp = criarTimestampUTCDeGMT3(
+ d.dataInicio,
+ d.horaInicio,
+ d.minutoInicio
+ );
+ // Se ainda não começou, está expirada (não ativa ainda)
+ expirada = agoraTimestampUTC < dataInicioTimestamp;
}
return {
@@ -3302,7 +3588,16 @@ export const verificarDispensaAtiva = query({
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
- const dataConsulta = new Date(args.data);
+ // Helper para criar timestamp UTC a partir de data (YYYY-MM-DD), hora e minuto em GMT-3
+ const offsetGMT3ParaUTC = 3 * 60 * 60 * 1000; // 3 horas em milissegundos
+ function criarTimestampUTCDeGMT3(data: string, hora: number, minuto: number): number {
+ const [ano, mes, dia] = data.split('-').map(Number);
+ return Date.UTC(ano, mes - 1, dia, hora, minuto, 0, 0) + offsetGMT3ParaUTC;
+ }
+
+ // Obter timestamp atual em UTC
+ const agoraUTC = new Date();
+ const agoraTimestampUTC = agoraUTC.getTime();
for (const dispensa of dispensas) {
// Se for isento, sempre está dispensado
@@ -3314,33 +3609,39 @@ export const verificarDispensaAtiva = query({
};
}
- // Verificar se está no período
- const dataInicio = new Date(dispensa.dataInicio);
- const dataFim = new Date(dispensa.dataFim);
+ // Calcular timestamps de início e fim da dispensa em UTC
+ const timestampInicioUTC = criarTimestampUTCDeGMT3(
+ dispensa.dataInicio,
+ dispensa.horaInicio,
+ dispensa.minutoInicio
+ );
+ const timestampFimUTC = criarTimestampUTCDeGMT3(
+ dispensa.dataFim,
+ dispensa.horaFim,
+ dispensa.minutoFim
+ );
- // Se a data está dentro do período
- if (dataConsulta >= dataInicio && dataConsulta <= dataFim) {
- // Se hora e minuto foram fornecidos, verificar também
- if (args.hora !== undefined && args.minuto !== undefined) {
- const timestampConsulta = new Date(
- `${args.data}T${args.hora.toString().padStart(2, '0')}:${args.minuto.toString().padStart(2, '0')}:00`
- ).getTime();
- const timestampInicio = new Date(
- `${dispensa.dataInicio}T${dispensa.horaInicio.toString().padStart(2, '0')}:${dispensa.minutoInicio.toString().padStart(2, '0')}:00`
- ).getTime();
- const timestampFim = new Date(
- `${dispensa.dataFim}T${dispensa.horaFim.toString().padStart(2, '0')}:${dispensa.minutoFim.toString().padStart(2, '0')}:00`
- ).getTime();
+ // Verificar se AGORA já passou do horário de fim da dispensa
+ // Se já expirou, não está mais dispensado
+ if (agoraTimestampUTC > timestampFimUTC) {
+ // Dispensa expirada, continuar para próxima
+ continue;
+ }
- if (timestampConsulta >= timestampInicio && timestampConsulta <= timestampFim) {
- return {
- dispensado: true,
- dispensa,
- motivo: dispensa.motivo
- };
- }
- } else {
- // Apenas verificar data
+ // Se hora e minuto foram fornecidos, verificar timestamp completo
+ if (args.hora !== undefined && args.minuto !== undefined) {
+ const timestampConsultaUTC = criarTimestampUTCDeGMT3(args.data, args.hora, args.minuto);
+ if (timestampConsultaUTC >= timestampInicioUTC && timestampConsultaUTC <= timestampFimUTC) {
+ return {
+ dispensado: true,
+ dispensa,
+ motivo: dispensa.motivo
+ };
+ }
+ } else {
+ // Se apenas data foi fornecida, verificar se AGORA está dentro do período
+ // (não apenas a data, mas também o horário)
+ if (agoraTimestampUTC >= timestampInicioUTC && agoraTimestampUTC <= timestampFimUTC) {
return {
dispensado: true,
dispensa,
@@ -3476,7 +3777,13 @@ export const obterBancoHorasCompleto = query({
tipo: a.tipo,
valorMinutos: a.valorMinutos,
motivoDescricao: a.motivoDescricao,
- motivoTipo: a.motivoTipo
+ motivoTipo: a.motivoTipo,
+ dataInicio: a.dataInicio,
+ horaInicio: a.horaInicio,
+ minutoInicio: a.minutoInicio,
+ dataFim: a.dataFim,
+ horaFim: a.horaFim,
+ minutoFim: a.minutoFim
})),
inconsistencias: inconsistenciasFiltradas.map((i) => ({
_id: i._id,
@@ -3513,6 +3820,12 @@ export const listarAjustesBancoHoras = query({
),
motivoDescricao: v.optional(v.string()),
dataAplicacao: v.string(),
+ dataInicio: v.optional(v.string()),
+ horaInicio: v.optional(v.number()),
+ minutoInicio: v.optional(v.number()),
+ dataFim: v.optional(v.string()),
+ horaFim: v.optional(v.number()),
+ minutoFim: v.optional(v.number()),
aplicado: v.boolean(),
gestor: v.union(
v.object({
@@ -3567,6 +3880,12 @@ export const listarAjustesBancoHoras = query({
motivoTipo: ajuste.motivoTipo,
motivoDescricao: ajuste.motivoDescricao,
dataAplicacao: ajuste.dataAplicacao,
+ dataInicio: ajuste.dataInicio,
+ horaInicio: ajuste.horaInicio,
+ minutoInicio: ajuste.minutoInicio,
+ dataFim: ajuste.dataFim,
+ horaFim: ajuste.horaFim,
+ minutoFim: ajuste.minutoFim,
aplicado: ajuste.aplicado,
gestor: gestor ? { nome: gestor.nome } : null
};
@@ -4271,6 +4590,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'),
diff --git a/packages/backend/convex/tables/ponto.ts b/packages/backend/convex/tables/ponto.ts
index d206f32..99f6e1d 100644
--- a/packages/backend/convex/tables/ponto.ts
+++ b/packages/backend/convex/tables/ponto.ts
@@ -368,6 +368,13 @@ export const pontoTables = {
valorMinutos: v.number(), // Valor em minutos (positivo para abonar, negativo para descontar)
// Data de aplicação
dataAplicacao: v.string(), // YYYY-MM-DD
+ // Período do ajuste (data/hora início e fim)
+ dataInicio: v.optional(v.string()), // YYYY-MM-DD
+ horaInicio: v.optional(v.number()), // 0-23
+ minutoInicio: v.optional(v.number()), // 0-59
+ dataFim: v.optional(v.string()), // YYYY-MM-DD
+ horaFim: v.optional(v.number()), // 0-23
+ minutoFim: v.optional(v.number()), // 0-59
// Gestor responsável (null se automático)
gestorId: v.optional(v.id('usuarios')),
// Observações