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']) };