425 lines
17 KiB
TypeScript
425 lines
17 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
|
|
// 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_tipo_dia', ['tipoDia']),
|
|
|
|
// Banco de Horas Mensal - Agregação mensal do banco de horas
|
|
bancoHorasMensal: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
mes: v.string(), // YYYY-MM (ex: "2024-01")
|
|
saldoInicialMinutos: v.number(), // Saldo acumulado do mês anterior (pode ser negativo)
|
|
saldoFinalMinutos: v.number(), // Saldo acumulado ao final do mês
|
|
saldoMesMinutos: v.number(), // Saldo apenas do mês atual (sem acumular)
|
|
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()
|
|
})
|
|
.index('by_funcionario_mes', ['funcionarioId', 'mes'])
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_mes', ['mes']),
|
|
|
|
// 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']),
|
|
|
|
// 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
|
|
// 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
|
|
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'])
|
|
};
|