-
Dispensas Ativas
+
+
Dispensas
+
+
+ (filtroStatus = 'todas')}
+ >
+ Todas
+
+ (filtroStatus = 'ativas')}
+ >
+ Ativas
+
+ (filtroStatus = 'expiradas')}
+ >
+ Expiradas
+
+
+
{#if dispensas.length === 0}
- Nenhuma dispensa ativa encontrada
+
+ {#if filtroStatus === 'todas'}
+ Nenhuma dispensa encontrada
+ {:else if filtroStatus === 'ativas'}
+ Nenhuma dispensa ativa encontrada
+ {:else}
+ Nenhuma dispensa expirada encontrada
+ {/if}
+
{:else}
@@ -373,7 +416,7 @@
{#if dispensa.isento}
Isento (sem expiração)
{:else if dispensa.expirada}
- Expirada
+ Não ativo
{:else}
Ativa
{/if}
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/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 b23f8b1..f43d187 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}`);
}
}
@@ -2883,10 +2897,7 @@ export const excluirHomologacao = mutation({
};
// Se a homologação tem valores anteriores, restaurar
- if (
- homologacao.horaAnterior !== undefined &&
- homologacao.minutoAnterior !== undefined
- ) {
+ if (homologacao.horaAnterior !== undefined && homologacao.minutoAnterior !== undefined) {
patchData.hora = homologacao.horaAnterior;
patchData.minuto = homologacao.minutoAnterior;
}
@@ -3033,10 +3044,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 };
}
@@ -3117,14 +3126,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 {
@@ -3349,7 +3393,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
@@ -3361,33 +3414,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,