Files
sgse-app/packages/backend/convex/tables/ponto.ts

267 lines
10 KiB
TypeScript

import { defineTable } from 'convex/server';
import { v } from 'convex/values';
export const pontoTables = {
// Sistema de Controle de Ponto
registrosPonto: defineTable({
funcionarioId: v.id('funcionarios'),
tipo: v.union(
v.literal('entrada'),
v.literal('saida_almoco'),
v.literal('retorno_almoco'),
v.literal('saida')
),
data: v.string(), // YYYY-MM-DD
hora: v.number(),
minuto: v.number(),
segundo: v.number(),
timestamp: v.number(), // Timestamp completo para ordenação
imagemId: v.optional(v.id('_storage')),
sincronizadoComServidor: v.boolean(),
toleranciaMinutos: v.number(),
dentroDoPrazo: v.boolean(),
// Informações de Rede
ipAddress: v.optional(v.string()),
ipPublico: v.optional(v.string()),
ipLocal: v.optional(v.string()),
// Informações do Navegador
userAgent: v.optional(v.string()),
browser: v.optional(v.string()),
browserVersion: v.optional(v.string()),
engine: v.optional(v.string()),
// Informações do Sistema
sistemaOperacional: v.optional(v.string()),
osVersion: v.optional(v.string()),
arquitetura: v.optional(v.string()),
plataforma: v.optional(v.string()),
// Informações de Localização
latitude: v.optional(v.number()),
longitude: v.optional(v.number()),
precisao: v.optional(v.number()),
altitude: v.optional(v.union(v.number(), v.null())),
altitudeAccuracy: v.optional(v.union(v.number(), v.null())),
heading: v.optional(v.union(v.number(), v.null())),
speed: v.optional(v.union(v.number(), v.null())),
confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend)
scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend)
suspeitaSpoofing: v.optional(v.boolean()),
motivoSuspeita: v.optional(v.string()),
avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação
distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS
velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro
distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro
tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro
// Informações de Geofencing
enderecoMarcacaoEsperado: v.optional(v.id('enderecosMarcacao')), // Endereço mais próximo esperado
distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado
dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido
enderecoMarcacaoUsado: v.optional(v.id('enderecosMarcacao')), // Qual endereço foi usado na validação
raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros
endereco: v.optional(v.string()),
cidade: v.optional(v.string()),
estado: v.optional(v.string()),
pais: v.optional(v.string()),
timezone: v.optional(v.string()),
// Informações do Dispositivo
deviceType: v.optional(v.string()),
deviceModel: v.optional(v.string()),
screenResolution: v.optional(v.string()),
coresTela: v.optional(v.number()),
idioma: v.optional(v.string()),
// Informações Adicionais
isMobile: v.optional(v.boolean()),
isTablet: v.optional(v.boolean()),
isDesktop: v.optional(v.boolean()),
connectionType: v.optional(v.string()),
memoryInfo: v.optional(v.string()),
// Informações de Sensores (Acelerômetro e Giroscópio)
acelerometroX: v.optional(v.number()),
acelerometroY: v.optional(v.number()),
acelerometroZ: v.optional(v.number()),
movimentoDetectado: v.optional(v.boolean()),
magnitudeMovimento: v.optional(v.number()),
variacaoAcelerometro: v.optional(v.number()),
giroscopioAlpha: v.optional(v.number()),
giroscopioBeta: v.optional(v.number()),
giroscopioGamma: v.optional(v.number()),
sensorDisponivel: v.optional(v.boolean()),
permissaoSensorNegada: v.optional(v.boolean()),
// Justificativa opcional para o registro
justificativa: v.optional(v.string()),
// Campos para homologação
editadoPorGestor: v.optional(v.boolean()),
homologacaoId: v.optional(v.id('homologacoesPonto')),
criadoEm: v.number()
})
.index('by_funcionario_data', ['funcionarioId', 'data'])
.index('by_data', ['data'])
.index('by_dentro_prazo', ['dentroDoPrazo', 'data'])
.index('by_funcionario_timestamp', ['funcionarioId', 'timestamp']),
// Endereços de Marcação - Locais permitidos para registro de ponto
enderecosMarcacao: defineTable({
nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC"
descricao: v.optional(v.string()), // Descrição opcional
// Coordenadas (obrigatórias)
latitude: v.number(),
longitude: v.number(),
// Endereço físico (para exibição)
endereco: v.string(), // Ex: "Rua Exemplo, 123"
bairro: v.optional(v.string()), // Bairro do endereço
cep: v.optional(v.string()),
cidade: v.string(),
estado: v.string(),
pais: v.optional(v.string()), // Padrão: "Brasil"
// Configurações
raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m)
ativo: v.boolean(),
// Tipos de uso
tipo: v.union(
v.literal('sede'), // Sede principal (para todos)
v.literal('home_office'), // Home office específico
v.literal('deslocamento'), // Deslocamento temporário
v.literal('cliente') // Local de cliente
),
// Metadados
criadoPor: v.id('usuarios'),
criadoEm: v.number(),
atualizadoPor: v.optional(v.id('usuarios')),
atualizadoEm: v.optional(v.number())
})
.index('by_ativo', ['ativo'])
.index('by_tipo', ['tipo'])
.index('by_cidade', ['cidade']),
// Associação Funcionário ↔ Endereço de Marcação
funcionarioEnderecosMarcacao: defineTable({
funcionarioId: v.id('funcionarios'),
enderecoMarcacaoId: v.id('enderecosMarcacao'),
// Configurações específicas do funcionário
raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão
// Período de validade (para deslocamentos temporários)
dataInicio: v.optional(v.string()), // YYYY-MM-DD
dataFim: v.optional(v.string()), // YYYY-MM-DD
// Status
ativo: v.boolean(),
// Metadados
criadoPor: v.id('usuarios'),
criadoEm: v.number()
})
.index('by_funcionario', ['funcionarioId'])
.index('by_endereco', ['enderecoMarcacaoId'])
.index('by_funcionario_ativo', ['funcionarioId', 'ativo'])
.index('by_endereco_ativo', ['enderecoMarcacaoId', 'ativo']),
configuracaoPonto: defineTable({
horarioEntrada: v.string(), // HH:mm
horarioSaidaAlmoco: v.string(), // HH:mm
horarioRetornoAlmoco: v.string(), // HH:mm
horarioSaida: v.string(), // HH:mm
toleranciaMinutos: v.number(),
// Nomes personalizados dos tipos de registro
nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1"
nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1"
nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2"
nomeSaida: v.optional(v.string()), // Padrão: "Saída 2"
// Ajuste de fuso horário (GMT offset em horas)
gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
// Configurações de geofencing
validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização
toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros
ativo: v.boolean(),
atualizadoPor: v.id('usuarios'),
atualizadoEm: v.number()
}).index('by_ativo', ['ativo']),
configuracaoRelogio: defineTable({
servidorNTP: v.optional(v.string()),
portaNTP: v.optional(v.number()),
usarServidorExterno: v.boolean(),
fallbackParaPC: v.boolean(),
ultimaSincronizacao: v.optional(v.number()),
offsetSegundos: v.optional(v.number()),
// Ajuste de fuso horário (GMT offset em horas)
gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
atualizadoPor: v.id('usuarios'),
atualizadoEm: v.number()
}).index('by_ativo', ['usarServidorExterno']),
// Banco de Horas - Saldo diário de horas trabalhadas
bancoHoras: defineTable({
funcionarioId: v.id('funcionarios'),
data: v.string(), // YYYY-MM-DD
cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos)
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
calculadoEm: v.number()
})
.index('by_funcionario_data', ['funcionarioId', 'data'])
.index('by_funcionario', ['funcionarioId'])
.index('by_data', ['data']),
// Homologações de Ponto - Edições e ajustes realizados pelo gestor
homologacoesPonto: defineTable({
registroId: v.optional(v.id('registrosPonto')), // ID do registro editado (se for edição)
funcionarioId: v.id('funcionarios'),
gestorId: v.id('usuarios'),
// Dados do registro original (se for edição)
horaAnterior: v.optional(v.number()),
minutoAnterior: v.optional(v.number()),
// Dados do registro novo (se for edição)
horaNova: v.optional(v.number()),
minutoNova: v.optional(v.number()),
// Motivo e observações
motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações)
motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc)
motivoDescricao: v.optional(v.string()), // Descrição do motivo
observacoes: v.optional(v.string()),
// Tipo de ajuste (se for ajuste de banco de horas)
tipoAjuste: v.optional(
v.union(v.literal('compensar'), v.literal('abonar'), v.literal('descontar'))
),
// Período do ajuste (se for ajuste de banco de horas)
periodoDias: v.optional(v.number()),
periodoHoras: v.optional(v.number()),
periodoMinutos: v.optional(v.number()),
// Ajuste em minutos (calculado)
ajusteMinutos: v.optional(v.number()),
criadoEm: v.number()
})
.index('by_funcionario', ['funcionarioId'])
.index('by_gestor', ['gestorId'])
.index('by_registro', ['registroId'])
.index('by_data', ['criadoEm']),
// Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto
dispensasRegistro: defineTable({
funcionarioId: v.id('funcionarios'),
gestorId: v.id('usuarios'),
dataInicio: v.string(), // YYYY-MM-DD
horaInicio: v.number(),
minutoInicio: v.number(),
dataFim: v.string(), // YYYY-MM-DD
horaFim: v.number(),
minutoFim: v.number(),
motivo: v.string(),
isento: v.boolean(), // Se true, não expira (casos excepcionais)
ativo: v.boolean(),
criadoEm: v.number()
})
.index('by_funcionario', ['funcionarioId'])
.index('by_gestor', ['gestorId'])
.index('by_ativo', ['ativo'])
.index('by_data_inicio', ['dataInicio'])
.index('by_data_fim', ['dataFim'])
};