diff --git a/apps/web/src/routes/(dashboard)/+page.svelte b/apps/web/src/routes/(dashboard)/+page.svelte index 1117734..dae1a87 100644 --- a/apps/web/src/routes/(dashboard)/+page.svelte +++ b/apps/web/src/routes/(dashboard)/+page.svelte @@ -1,20 +1,14 @@ @@ -176,17 +198,23 @@ {/if} - -
+ +
-
-

+
+

{getSaudacao()}! 👋

-

- Bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria +

Bem-vindo ao SGSE

+

+ Simplificando a Gestão Pública

-

+

Sistema de Gerenciamento de Secretaria

+

{currentTime.toLocaleDateString('pt-BR', { weekday: 'long', year: 'numeric', @@ -197,518 +225,259 @@ {currentTime.toLocaleTimeString('pt-BR')}

-
-
Sistema Online
+
+
Sistema Online
Atualizado
+
Disponível 24h

- + {#if statsQuery.isLoading}
{:else if statsQuery.data} -
- +
+
-
-

Total de Funcionários

-

- {formatNumber(statsQuery.data.totalFuncionarios)} -

-

- {statsQuery.data.funcionariosAtivos} ativos +

+

+ Usuários Cadastrados

+

+ {#if statsQuery.data} + 0 + {:else} + 0 + {/if} +

+

no sistema

-
- {calcPercentage( - statsQuery.data.funcionariosAtivos, - statsQuery.data.totalFuncionarios - )}% +
+
- +
-
-

Solicitações Pendentes

-

4

-

de 5 total

+
+

+ Funcionários Ativos +

+

+ {#if statsQuery.data} + 0 + {:else} + 0 + {/if} +

+

em atividade

-
+
+ +
+
+
+
+ + +
+
+
+
+

+ Cadastros Realizados +

+

+ {#if statsQuery.data} + 0 + {:else} + 0 + {/if} +

+

total de registros

+
+
+ +
+
+
+
+ + +
+
+
+
+

+ Disponibilidade +

+

24h

+

funcionando continuamente

+
+
- - -
-
-
-
-

Símbolos Cadastrados

-

- {formatNumber(statsQuery.data.totalSimbolos)} -

-

- {statsQuery.data.cargoComissionado} CC / {statsQuery.data.funcaoGratificada} FG -

-
-
- -
-
-
-
- - - {#if activityQuery.data} -
-
-
-
-

Atividade (24h)

-

- {activityQuery.data.funcionariosCadastrados24h} cadastros -

-
-
- -
-
-
-
- {/if} -
- - - {#if statusSistemaQuery?.data} - {@const status = statusSistemaQuery.data} - {@const atividade = atividadeBDQuery?.data || { - historico: Array.from({ length: 30 }, () => ({ entradas: 0, saidas: 0 })) - }} - {@const distribuicao = distribuicaoQuery?.data || { - queries: 0, - mutations: 0, - leituras: 0, - escritas: 0 - }} - {@const maxAtividade = - atividade.historico && atividade.historico.length > 0 - ? Math.max( - 1, - ...atividade.historico.map((p) => Math.max(p.entradas || 0, p.saidas || 0)) - ) - : 1} - -
-
-
- -
-
-

Monitoramento em Tempo Real

-

- Atualizado a cada segundo • {new Date(status.ultimaAtualizacao).toLocaleTimeString( - 'pt-BR' - )} -

-
-
- - - LIVE -
-
- - -
- -
-
-
-
-

- Usuários Online -

-

- {status.usuariosOnline} -

-

sessões ativas

-
-
- -
-
-
-
- - -
-
-
-
-

- Total Registros -

-

- {status.totalRegistros.toLocaleString('pt-BR')} -

-

no banco de dados

-
-
- -
-
-
-
- - -
-
-
-
-

- Tempo Resposta -

-

- {status.tempoMedioResposta}ms -

-

média atual

-
-
- - - -
-
-
-
- - -
-
-
-

- Uso do Sistema -

-
-
-
- CPU - {status.cpuUsada}% -
- -
-
-
- Memória - {status.memoriaUsada}% -
- -
-
-
-
-
-
- - -
-
-
-
-

Atividade do Banco de Dados

-

- Entradas e saídas em tempo real (último minuto) -

-
-
- - Atualizando -
-
- -
- -
- {#each [10, 8, 6, 4, 2, 0] as val (val)} - {val} - {/each} -
- - -
- - {#each [0, 1, 2, 3, 4, 5] as i (i)} -
- {/each} - - -
- {#each atividade.historico || [] as ponto, idx (idx)} - {@const entradas = ponto?.entradas || 0} - {@const saidas = ponto?.saidas || 0} -
- -
- -
- - -
-
↑ {entradas} entradas
-
↓ {saidas} saídas
-
-
- {/each} -
-
- - -
- - -
- -60s - -30s - agora -
-
- - -
-
-
- Entradas no BD -
-
-
- Saídas do BD -
-
-
-
- - -
-
-
-

Tipos de Operações

-
-
-
- Queries (Leituras) - {distribuicao?.queries ?? 0} -
- -
-
-
- Mutations (Escritas) - {distribuicao?.mutations ?? 0} -
- -
-
-
-
- -
-
-

Operações no Banco

-
-
-
- Leituras - {distribuicao?.leituras ?? 0} -
- -
-
-
- Escritas - {distribuicao?.escritas ?? 0} -
- -
-
-
-
-
-
- {/if} - - -
-
-
-

Status do Sistema

-
-
- Banco de Dados - Online -
-
- API - Operacional -
-
- Backup - Atualizado -
-
-
-
- - - -
-
-

Informações

-
-

- Versão: 1.0.0 -

-

- Última Atualização: - {new Date().toLocaleDateString('pt-BR')} -

-

- Suporte: TI SGSE -

-
-
-
-
- {:else} - -
- Não foi possível carregar os dados do dashboard.
{/if} + + +
+
+

Sobre o SGSE

+

+ Simplificando a Gestão Pública +

+

+ O Sistema de Gerenciamento de Secretaria (SGSE) é uma solução completa e moderna + desenvolvida para otimizar e simplificar os processos administrativos da gestão pública. + Com tecnologia de ponta e interface intuitiva, oferecemos rapidez, comodidade e + disponibilidade 24 horas por dia para atender às necessidades dos nossos usuários. +

+

+ Nossa plataforma integra todas as funcionalidades essenciais em um único ambiente, + permitindo gestão eficiente de funcionários, controle de ponto, férias, licenças, símbolos + e muito mais. Trabalhamos continuamente para garantir que você tenha acesso rápido e + seguro a todas as informações e ferramentas necessárias para uma gestão pública de + excelência. +

+
+
+ + + @@ -724,7 +493,37 @@ } } - .card { - animation: fadeIn 0.5s ease-out; + .fade-in { + animation: fadeIn 0.6s ease-out; + } + + .fade-in-delay { + animation: fadeIn 0.8s ease-out 0.2s both; + } + + .fade-in-delay-2 { + animation: fadeIn 1s ease-out 0.4s both; + } + + .stats-card { + animation: fadeIn 0.6s ease-out; + } + + .feature-card { + animation: fadeIn 0.8s ease-out; + } + + @keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } + + .animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/dispensa/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/dispensa/+page.svelte index 0286283..77e66b8 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/dispensa/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/dispensa/+page.svelte @@ -13,6 +13,7 @@ let modoCriacao = $state(false); let mostrandoModalExcluir = $state(false); let dispensaParaExcluir = $state | null>(null); + let filtroStatus = $state<'todas' | 'ativas' | 'expiradas'>('todas'); // Formulário let dataInicio = $state(new Date().toISOString().split('T')[0]!); @@ -25,22 +26,33 @@ // Computed para converter time string para hora/minuto let horaInicio = $derived.by(() => { const [hora, minuto] = horaInicioTime.split(':').map(Number); - return { hora: hora || 8, minuto: minuto || 0 }; + return { hora: isNaN(hora) ? 8 : hora, minuto: isNaN(minuto) ? 0 : minuto }; }); let horaFim = $derived.by(() => { const [hora, minuto] = horaFimTime.split(':').map(Number); - return { hora: hora || 18, minuto: minuto || 0 }; + return { hora: isNaN(hora) ? 18 : hora, minuto: isNaN(minuto) ? 0 : minuto }; }); // Queries const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {}); - const dispensasQuery = useQuery(api.pontos.listarDispensas, { - apenasAtivas: true // Mostrar apenas dispensas ativas - }); + const dispensasQuery = useQuery(api.pontos.listarDispensas, {}); let subordinados = $derived(subordinadosQuery?.data || []); - let dispensas = $derived(dispensasQuery?.data || []); + let todasDispensas = $derived(dispensasQuery?.data || []); + + // Filtrar dispensas baseado no filtro selecionado + let dispensas = $derived.by(() => { + if (filtroStatus === 'todas') { + return todasDispensas; + } else if (filtroStatus === 'ativas') { + // Ativas: não expiradas (inclui isentos que já começaram) + return todasDispensas.filter((d) => !d.expirada); + } else { + // Expiradas: apenas dispensas não isentas que expiraram + return todasDispensas.filter((d) => d.expirada && !d.isento); + } + }); // Lista de funcionários do time let funcionarios = $derived.by(() => { @@ -313,11 +325,42 @@
-

Dispensas Ativas

+
+

Dispensas

+ +
+ + + +
+
{#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,