feat: enhance Banco de Horas management with new reporting features, including adjustments and inconsistencies tracking, advanced filters, and Excel export functionality
This commit is contained in:
@@ -4,6 +4,7 @@ import { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { internal } from './_generated/api';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
@@ -26,6 +27,38 @@ function calcularDias(dataInicio: string, dataFim: string): number {
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para 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]!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== QUERIES ==========
|
||||
|
||||
/**
|
||||
@@ -780,6 +813,9 @@ export const criarAtestadoMedico = mutation({
|
||||
atestadoId
|
||||
);
|
||||
|
||||
// Recalcular banco de horas para todas as datas do período do atestado
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
return atestadoId;
|
||||
}
|
||||
});
|
||||
@@ -825,6 +861,9 @@ export const criarDeclaracaoComparecimento = mutation({
|
||||
atestadoId
|
||||
);
|
||||
|
||||
// Recalcular banco de horas para todas as datas do período da declaração
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
return atestadoId;
|
||||
}
|
||||
});
|
||||
@@ -878,6 +917,9 @@ export const criarLicencaMaternidade = mutation({
|
||||
licencaId
|
||||
);
|
||||
|
||||
// Recalcular banco de horas para todas as datas do período da licença
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
});
|
||||
@@ -924,6 +966,9 @@ export const criarLicencaPaternidade = mutation({
|
||||
licencaId
|
||||
);
|
||||
|
||||
// Recalcular banco de horas para todas as datas do período da licença
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { api } from './_generated/api';
|
||||
import { internal } from './_generated/api';
|
||||
import { Id, Doc } from './_generated/dataModel';
|
||||
import { parseLocalDate, formatarDataBR } from './utils/datas';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
@@ -267,6 +268,36 @@ export const contarPendentesGestor = query({
|
||||
}
|
||||
});
|
||||
|
||||
// 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]!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Verificar se há sobreposição de datas
|
||||
function verificarSobreposicao(
|
||||
inicio1: string,
|
||||
@@ -641,6 +672,9 @@ export const aprovar = mutation({
|
||||
}
|
||||
}
|
||||
|
||||
// Recalcular banco de horas para todas as datas do período da ausência aprovada
|
||||
await recalcularBancoHorasPeriodo(ctx, solicitacao.funcionarioId, solicitacao.dataInicio, solicitacao.dataFim);
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -204,11 +204,26 @@ export const pontoTables = {
|
||||
horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos)
|
||||
saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit)
|
||||
registrosPontoIds: v.array(v.id('registrosPonto')), // IDs dos registros do dia
|
||||
// Novos campos para sistema avançado
|
||||
ajustesIds: v.optional(v.array(v.id('ajustesBancoHoras'))), // IDs dos ajustes aplicados no dia
|
||||
motivoAbono: v.optional(v.string()), // Motivo do abono (atestado, licença, ausência, etc.)
|
||||
tipoDia: v.optional(
|
||||
v.union(
|
||||
v.literal('normal'),
|
||||
v.literal('atestado'),
|
||||
v.literal('licenca'),
|
||||
v.literal('ausencia'),
|
||||
v.literal('abonado'),
|
||||
v.literal('descontado')
|
||||
)
|
||||
), // Tipo do dia
|
||||
inconsistenciasIds: v.optional(v.array(v.id('inconsistenciasBancoHoras'))), // IDs de inconsistências detectadas
|
||||
calculadoEm: v.number()
|
||||
})
|
||||
.index('by_funcionario_data', ['funcionarioId', 'data'])
|
||||
.index('by_funcionario', ['funcionarioId'])
|
||||
.index('by_data', ['data']),
|
||||
.index('by_data', ['data'])
|
||||
.index('by_tipo_dia', ['tipoDia']),
|
||||
|
||||
// Banco de Horas Mensal - Agregação mensal do banco de horas
|
||||
bancoHorasMensal: defineTable({
|
||||
@@ -220,6 +235,11 @@ export const pontoTables = {
|
||||
diasTrabalhados: v.number(), // Quantidade de dias com registros no mês
|
||||
horasExtras: v.number(), // Total de minutos positivos do mês
|
||||
horasDeficit: v.number(), // Total de minutos negativos do mês (valor absoluto)
|
||||
// Novos campos para sistema avançado
|
||||
totalAjustes: v.optional(v.number()), // Total de ajustes aplicados no mês (em minutos)
|
||||
totalAbonos: v.optional(v.number()), // Total de abonos no mês (em minutos)
|
||||
totalDescontos: v.optional(v.number()), // Total de descontos no mês (em minutos)
|
||||
inconsistenciasResolvidas: v.optional(v.number()), // Quantidade de inconsistências resolvidas
|
||||
calculadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
@@ -279,5 +299,119 @@ export const pontoTables = {
|
||||
.index('by_gestor', ['gestorId'])
|
||||
.index('by_ativo', ['ativo'])
|
||||
.index('by_data_inicio', ['dataInicio'])
|
||||
.index('by_data_fim', ['dataFim'])
|
||||
.index('by_data_fim', ['dataFim']),
|
||||
|
||||
// Configuração de Banco de Horas - Configurações gerais do sistema
|
||||
configuracaoBancoHoras: defineTable({
|
||||
// Limites de saldo
|
||||
limiteSaldoPositivoMinutos: v.optional(v.number()), // Limite máximo de saldo positivo (em minutos)
|
||||
limiteSaldoNegativoMinutos: v.optional(v.number()), // Limite máximo de saldo negativo (em minutos)
|
||||
// Regras de cálculo
|
||||
considerarAjustesAutomaticos: v.optional(v.boolean()), // Se deve considerar ajustes automáticos (atestados, licenças, ausências)
|
||||
// Periodicidade de verificação
|
||||
periodicidadeVerificacao: v.optional(
|
||||
v.union(v.literal('diario'), v.literal('semanal'), v.literal('mensal'))
|
||||
),
|
||||
// Metadados
|
||||
atualizadoPor: v.id('usuarios'),
|
||||
atualizadoEm: v.number()
|
||||
}).index('by_ativo', ['atualizadoEm']),
|
||||
|
||||
// Alertas de Banco de Horas - Configuração de alertas por tipo
|
||||
alertasBancoHoras: defineTable({
|
||||
tipoAlerta: v.union(
|
||||
v.literal('saldo_negativo'),
|
||||
v.literal('saldo_negativo_critico'),
|
||||
v.literal('inconsistencia_detectada'),
|
||||
v.literal('dias_sem_registro'),
|
||||
v.literal('limite_saldo_excedido')
|
||||
),
|
||||
// Periodicidade
|
||||
periodicidade: v.union(v.literal('diario'), v.literal('semanal'), v.literal('mensal')),
|
||||
// Canais de envio
|
||||
enviarEmail: v.boolean(),
|
||||
enviarChat: v.boolean(),
|
||||
// Destinatários específicos (opcional - se vazio, envia para gestor padrão)
|
||||
destinatariosEmail: v.optional(v.array(v.id('usuarios'))), // IDs de usuários que receberão email
|
||||
destinatariosChat: v.optional(v.array(v.id('usuarios'))), // IDs de usuários que receberão chat
|
||||
// Thresholds e limites
|
||||
threshold: v.optional(v.number()), // Valor limite para disparar alerta
|
||||
limiteMinutos: v.optional(v.number()), // Limite em minutos (para saldo negativo)
|
||||
// Status
|
||||
ativo: v.boolean(),
|
||||
// Metadados
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoPor: v.optional(v.id('usuarios')),
|
||||
atualizadoEm: v.optional(v.number())
|
||||
})
|
||||
.index('by_tipo', ['tipoAlerta'])
|
||||
.index('by_ativo', ['ativo'])
|
||||
.index('by_tipo_ativo', ['tipoAlerta', 'ativo']),
|
||||
|
||||
// Ajustes de Banco de Horas - Registro de ajustes manuais e automáticos
|
||||
ajustesBancoHoras: defineTable({
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
tipo: v.union(v.literal('abonar'), v.literal('descontar'), v.literal('compensar')),
|
||||
// Motivo vinculado
|
||||
motivoTipo: v.optional(
|
||||
v.union(
|
||||
v.literal('atestado'),
|
||||
v.literal('licenca'),
|
||||
v.literal('ausencia'),
|
||||
v.literal('manual')
|
||||
)
|
||||
),
|
||||
motivoId: v.optional(v.string()), // ID do atestado, licença, ausência ou null para manual
|
||||
motivoDescricao: v.optional(v.string()), // Descrição do motivo
|
||||
// Valor do ajuste
|
||||
valorMinutos: v.number(), // Valor em minutos (positivo para abonar, negativo para descontar)
|
||||
// Data de aplicação
|
||||
dataAplicacao: v.string(), // YYYY-MM-DD
|
||||
// Gestor responsável (null se automático)
|
||||
gestorId: v.optional(v.id('usuarios')),
|
||||
// Observações
|
||||
observacoes: v.optional(v.string()),
|
||||
// Status
|
||||
aplicado: v.boolean(), // Se já foi aplicado ao banco de horas
|
||||
// Metadados
|
||||
criadoEm: v.number(),
|
||||
aplicadoEm: v.optional(v.number())
|
||||
})
|
||||
.index('by_funcionario', ['funcionarioId'])
|
||||
.index('by_data_aplicacao', ['dataAplicacao'])
|
||||
.index('by_funcionario_data', ['funcionarioId', 'dataAplicacao'])
|
||||
.index('by_tipo', ['tipo'])
|
||||
.index('by_aplicado', ['aplicado'])
|
||||
.index('by_gestor', ['gestorId']),
|
||||
|
||||
// Inconsistências de Banco de Horas - Registro de inconsistências detectadas
|
||||
inconsistenciasBancoHoras: defineTable({
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
tipo: v.union(
|
||||
v.literal('ponto_com_atestado'),
|
||||
v.literal('ponto_com_licenca'),
|
||||
v.literal('ponto_com_ausencia'),
|
||||
v.literal('registro_duplicado'),
|
||||
v.literal('sequencia_invalida'),
|
||||
v.literal('saldo_inconsistente')
|
||||
),
|
||||
descricao: v.string(), // Descrição detalhada da inconsistência
|
||||
dataDetectada: v.string(), // YYYY-MM-DD
|
||||
dataInconsistencia: v.string(), // YYYY-MM-DD (data do dia com inconsistência)
|
||||
// Status
|
||||
status: v.union(v.literal('pendente'), v.literal('resolvida'), v.literal('ignorada')),
|
||||
// Resolução
|
||||
resolucao: v.optional(v.string()), // Descrição da resolução
|
||||
resolvidoPor: v.optional(v.id('usuarios')), // Usuário que resolveu
|
||||
resolvidoEm: v.optional(v.number()), // Timestamp da resolução
|
||||
// Metadados
|
||||
criadoEm: v.number()
|
||||
})
|
||||
.index('by_funcionario', ['funcionarioId'])
|
||||
.index('by_status', ['status'])
|
||||
.index('by_funcionario_status', ['funcionarioId', 'status'])
|
||||
.index('by_data_detectada', ['dataDetectada'])
|
||||
.index('by_tipo', ['tipo'])
|
||||
.index('by_data_inconsistencia', ['dataInconsistencia'])
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user