import { defineSchema, defineTable } from "convex/server"; import { Infer, v } from "convex/values"; export const simboloTipo = v.union( v.literal("cargo_comissionado"), v.literal("funcao_gratificada") ); export type SimboloTipo = Infer; export const ataqueCiberneticoTipo = v.union( v.literal("phishing"), v.literal("malware"), v.literal("ransomware"), v.literal("brute_force"), v.literal("credential_stuffing"), v.literal("sql_injection"), v.literal("xss"), v.literal("path_traversal"), v.literal("command_injection"), v.literal("nosql_injection"), v.literal("xxe"), v.literal("man_in_the_middle"), v.literal("ddos"), v.literal("engenharia_social"), v.literal("cve_exploit"), v.literal("apt"), v.literal("zero_day"), v.literal("supply_chain"), v.literal("fileless_malware"), v.literal("polymorphic_malware"), v.literal("ransomware_lateral"), v.literal("deepfake_phishing"), v.literal("adversarial_ai"), v.literal("side_channel"), v.literal("firmware_bootloader"), v.literal("bec"), v.literal("botnet"), v.literal("ot_ics"), v.literal("quantum_attack") ); export type AtaqueCiberneticoTipo = Infer; export const severidadeSeguranca = v.union( v.literal("informativo"), v.literal("baixo"), v.literal("moderado"), v.literal("alto"), v.literal("critico") ); export type SeveridadeSeguranca = Infer; export const statusEventoSeguranca = v.union( v.literal("detectado"), v.literal("investigando"), v.literal("contido"), v.literal("falso_positivo"), v.literal("escalado"), v.literal("resolvido") ); export type StatusEventoSeguranca = Infer; export const sensorSegurancaTipo = v.union( v.literal("network"), v.literal("endpoint"), v.literal("application"), v.literal("gateway"), v.literal("ot"), v.literal("honeypot") ); export type SensorSegurancaTipo = Infer; export const sensorSegurancaStatus = v.union( v.literal("ativo"), v.literal("inativo"), v.literal("degradado"), v.literal("manutencao") ); export type SensorSegurancaStatus = Infer; export const threatIntelTipo = v.union( v.literal("open_source"), v.literal("commercial"), v.literal("internal"), v.literal("gov"), v.literal("research") ); export const threatIntelFormato = v.union( v.literal("json"), v.literal("stix"), v.literal("csv"), v.literal("text"), v.literal("custom") ); export const acaoIncidenteTipo = v.union( v.literal("block_ip"), v.literal("unblock_ip"), v.literal("block_port"), v.literal("liberar_porta"), v.literal("notificar"), v.literal("isolar_host"), v.literal("gerar_relatorio"), v.literal("criar_ticket"), v.literal("ajuste_regra"), v.literal("custom") ); export const acaoIncidenteStatus = v.union( v.literal("pendente"), v.literal("executando"), v.literal("concluido"), v.literal("falhou") ); export const reportStatus = v.union( v.literal("pendente"), v.literal("processando"), v.literal("concluido"), v.literal("falhou") ); export const situacaoContrato = v.union( v.literal("em_execucao"), v.literal("rescendido"), v.literal("aguardando_assinatura"), v.literal("finalizado") ); export default defineSchema({ contratos: defineTable({ contratadaId: v.id("empresas"), objeto: v.string(), numeroNotaEmpenho: v.string(), responsavelId: v.id("funcionarios"), departamento: v.string(), situacao: situacaoContrato, numeroProcessoLicitatorio: v.string(), modalidade: v.string(), numeroContrato: v.string(), anoContrato: v.number(), dataInicioVigencia: v.string(), dataFimVigencia: v.string(), nomeFiscal: v.string(), valorTotal: v.string(), dataAditivoPrazo: v.optional(v.string()), diasAvisoVencimento: v.number(), criadoPor: v.id("usuarios"), criadoEm: v.number(), atualizadoEm: v.optional(v.number()), }) .index("by_responsavel", ["responsavelId"]) .index("by_situacao", ["situacao"]) .index("by_vigencia_inicio", ["dataInicioVigencia"]) .index("by_vigencia_fim", ["dataFimVigencia"]), todos: defineTable({ text: v.string(), completed: v.boolean(), }), enderecos: defineTable({ cep: v.string(), logradouro: v.string(), numero: v.string(), complemento: v.optional(v.string()), bairro: v.string(), cidade: v.string(), uf: v.string(), criadoPor: v.optional(v.id("usuarios")), atualizadoPor: v.optional(v.id("usuarios")), }).index("by_cep", ["cep"]), empresas: defineTable({ razao_social: v.string(), nome_fantasia: v.optional(v.string()), cnpj: v.string(), telefone: v.string(), email: v.string(), descricao: v.optional(v.string()), enderecoId: v.optional(v.id("enderecos")), criadoPor: v.optional(v.id("usuarios")), }) .index("by_razao_social", ["razao_social"]) .index("by_cnpj", ["cnpj"]), contatosEmpresa: defineTable({ empresaId: v.id("empresas"), nome: v.string(), funcao: v.string(), email: v.string(), telefone: v.string(), adicionadoPor: v.optional(v.id("usuarios")), descricao: v.optional(v.string()), }) .index("by_empresa", ["empresaId"]) .index("by_email", ["email"]), funcionarios: defineTable({ // Campos obrigatórios existentes nome: v.string(), nascimento: v.string(), rg: v.string(), cpf: v.string(), endereco: v.string(), cep: v.string(), cidade: v.string(), uf: v.string(), telefone: v.string(), email: v.string(), matricula: v.optional(v.string()), admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()), simboloId: v.id("simbolos"), simboloTipo: simboloTipo, gestorId: v.optional(v.id("usuarios")), statusFerias: v.optional( v.union(v.literal("ativo"), v.literal("em_ferias")) ), // Regime de trabalho (para cálculo correto de férias) regimeTrabalho: v.optional( v.union( v.literal("clt"), // CLT - Consolidação das Leis do Trabalho v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco v.literal("estatutario_federal"), // Servidor Público Federal v.literal("estatutario_municipal") // Servidor Público Municipal ) ), // Dados Pessoais Adicionais (opcionais) nomePai: v.optional(v.string()), nomeMae: v.optional(v.string()), naturalidade: v.optional(v.string()), naturalidadeUF: v.optional(v.string()), sexo: v.optional( v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")) ), estadoCivil: v.optional( v.union( v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel") ) ), nacionalidade: v.optional(v.string()), // Documentos Pessoais rgOrgaoExpedidor: v.optional(v.string()), rgDataEmissao: v.optional(v.string()), carteiraProfissionalNumero: v.optional(v.string()), carteiraProfissionalSerie: v.optional(v.string()), carteiraProfissionalDataEmissao: v.optional(v.string()), reservistaNumero: v.optional(v.string()), reservistaSerie: v.optional(v.string()), tituloEleitorNumero: v.optional(v.string()), tituloEleitorZona: v.optional(v.string()), tituloEleitorSecao: v.optional(v.string()), pisNumero: v.optional(v.string()), // Formação e Saúde grauInstrucao: v.optional( v.union( v.literal("fundamental"), v.literal("medio"), v.literal("superior"), v.literal("pos_graduacao"), v.literal("mestrado"), v.literal("doutorado") ) ), formacao: v.optional(v.string()), formacaoRegistro: v.optional(v.string()), grupoSanguineo: v.optional( v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")) ), fatorRH: v.optional(v.union(v.literal("positivo"), v.literal("negativo"))), // Cargo e Vínculo descricaoCargo: v.optional(v.string()), nomeacaoPortaria: v.optional(v.string()), nomeacaoData: v.optional(v.string()), nomeacaoDOE: v.optional(v.string()), pertenceOrgaoPublico: v.optional(v.boolean()), orgaoOrigem: v.optional(v.string()), aposentado: v.optional( v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")) ), // Dados Bancários contaBradescoNumero: v.optional(v.string()), contaBradescoDV: v.optional(v.string()), contaBradescoAgencia: v.optional(v.string()), // Documentos Anexos (Storage IDs) certidaoAntecedentesPF: v.optional(v.id("_storage")), certidaoAntecedentesJFPE: v.optional(v.id("_storage")), certidaoAntecedentesSDS: v.optional(v.id("_storage")), certidaoAntecedentesTJPE: v.optional(v.id("_storage")), certidaoImprobidade: v.optional(v.id("_storage")), rgFrente: v.optional(v.id("_storage")), rgVerso: v.optional(v.id("_storage")), cpfFrente: v.optional(v.id("_storage")), cpfVerso: v.optional(v.id("_storage")), situacaoCadastralCPF: v.optional(v.id("_storage")), tituloEleitorFrente: v.optional(v.id("_storage")), tituloEleitorVerso: v.optional(v.id("_storage")), comprovanteVotacao: v.optional(v.id("_storage")), carteiraProfissionalFrente: v.optional(v.id("_storage")), carteiraProfissionalVerso: v.optional(v.id("_storage")), comprovantePIS: v.optional(v.id("_storage")), certidaoRegistroCivil: v.optional(v.id("_storage")), certidaoNascimentoDependentes: v.optional(v.id("_storage")), cpfDependentes: v.optional(v.id("_storage")), reservistaDoc: v.optional(v.id("_storage")), comprovanteEscolaridade: v.optional(v.id("_storage")), comprovanteResidencia: v.optional(v.id("_storage")), comprovanteContaBradesco: v.optional(v.id("_storage")), // Dependentes do funcionário (uploads opcionais) dependentes: v.optional( v.array( v.object({ parentesco: v.union( v.literal("filho"), v.literal("filha"), v.literal("conjuge"), v.literal("outro") ), nome: v.string(), cpf: v.string(), nascimento: v.string(), documentoId: v.optional(v.id("_storage")), // Benefícios/declarações por dependente salarioFamilia: v.optional(v.boolean()), impostoRenda: v.optional(v.boolean()), }) ) ), // Declarações (Storage IDs) declaracaoAcumulacaoCargo: v.optional(v.id("_storage")), declaracaoDependentesIR: v.optional(v.id("_storage")), declaracaoIdoneidade: v.optional(v.id("_storage")), termoNepotismo: v.optional(v.id("_storage")), termoOpcaoRemuneracao: v.optional(v.id("_storage")), }) .index("by_matricula", ["matricula"]) .index("by_nome", ["nome"]) .index("by_simboloId", ["simboloId"]) .index("by_simboloTipo", ["simboloTipo"]) .index("by_cpf", ["cpf"]) .index("by_rg", ["rg"]) .index("by_gestor", ["gestorId"]), atestados: defineTable({ funcionarioId: v.id("funcionarios"), tipo: v.union( v.literal("atestado_medico"), v.literal("declaracao_comparecimento") ), dataInicio: v.string(), dataFim: v.string(), cid: v.optional(v.string()), // Apenas para atestado médico observacoes: v.optional(v.string()), documentoId: v.optional(v.id("_storage")), criadoPor: v.id("usuarios"), criadoEm: v.number(), }) .index("by_funcionario", ["funcionarioId"]) .index("by_tipo", ["tipo"]) .index("by_data_inicio", ["dataInicio"]) .index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]), licencas: defineTable({ funcionarioId: v.id("funcionarios"), tipo: v.union(v.literal("maternidade"), v.literal("paternidade")), dataInicio: v.string(), dataFim: v.string(), documentoId: v.optional(v.id("_storage")), observacoes: v.optional(v.string()), licencaOriginalId: v.optional(v.id("licencas")), // Para prorrogações ehProrrogacao: v.boolean(), criadoPor: v.id("usuarios"), criadoEm: v.number(), }) .index("by_funcionario", ["funcionarioId"]) .index("by_tipo", ["tipo"]) .index("by_data_inicio", ["dataInicio"]) .index("by_licenca_original", ["licencaOriginalId"]) .index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]), ferias: defineTable({ funcionarioId: v.id("funcionarios"), anoReferencia: v.number(), dataInicio: v.string(), dataFim: v.string(), diasFerias: v.number(), status: v.union( v.literal("aguardando_aprovacao"), v.literal("aprovado"), v.literal("reprovado"), v.literal("data_ajustada_aprovada"), v.literal("EmFérias"), v.literal("Cancelado_RH") ), gestorId: v.optional(v.id("usuarios")), observacao: v.optional(v.string()), motivoReprovacao: v.optional(v.string()), dataAprovacao: v.optional(v.number()), dataReprovacao: v.optional(v.number()), diasAbono: v.number(), historicoAlteracoes: v.optional( v.array( v.object({ data: v.number(), usuarioId: v.id("usuarios"), acao: v.string(), }) ) ), }) .index("by_funcionario", ["funcionarioId"]) .index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"]) .index("by_funcionario_and_status", ["funcionarioId", "status"]) .index("by_status", ["status"]) .index("by_ano", ["anoReferencia"]), notificacoesFerias: defineTable({ destinatarioId: v.id("usuarios"), feriasId: v.id("ferias"), tipo: v.union( v.literal("nova_solicitacao"), v.literal("aprovado"), v.literal("reprovado"), v.literal("data_ajustada") ), lida: v.boolean(), mensagem: v.string(), }) .index("by_destinatario", ["destinatarioId"]) .index("by_destinatario_and_lida", ["destinatarioId", "lida"]), // Solicitações de Ausências solicitacoesAusencias: defineTable({ funcionarioId: v.id("funcionarios"), dataInicio: v.string(), dataFim: v.string(), motivo: v.string(), status: v.union( v.literal("aguardando_aprovacao"), v.literal("aprovado"), v.literal("reprovado") ), gestorId: v.optional(v.id("usuarios")), dataAprovacao: v.optional(v.number()), dataReprovacao: v.optional(v.number()), motivoReprovacao: v.optional(v.string()), observacao: v.optional(v.string()), criadoEm: v.number(), }) .index("by_funcionario", ["funcionarioId"]) .index("by_status", ["status"]) .index("by_funcionario_and_status", ["funcionarioId", "status"]), notificacoesAusencias: defineTable({ destinatarioId: v.id("usuarios"), solicitacaoAusenciaId: v.id("solicitacoesAusencias"), tipo: v.union( v.literal("nova_solicitacao"), v.literal("aprovado"), v.literal("reprovado") ), lida: v.boolean(), mensagem: v.string(), }) .index("by_destinatario", ["destinatarioId"]) .index("by_destinatario_and_lida", ["destinatarioId", "lida"]), times: defineTable({ nome: v.string(), descricao: v.optional(v.string()), gestorId: v.id("usuarios"), gestorSuperiorId: v.optional(v.id("usuarios")), ativo: v.boolean(), cor: v.optional(v.string()), // Cor para identificação visual }) .index("by_gestor", ["gestorId"]) .index("by_gestor_superior", ["gestorSuperiorId"]), timesMembros: defineTable({ timeId: v.id("times"), funcionarioId: v.id("funcionarios"), dataEntrada: v.number(), dataSaida: v.optional(v.number()), ativo: v.boolean(), }) .index("by_time", ["timeId"]) .index("by_funcionario", ["funcionarioId"]) .index("by_time_and_ativo", ["timeId", "ativo"]), cursos: defineTable({ funcionarioId: v.id("funcionarios"), descricao: v.string(), data: v.string(), certificadoId: v.optional(v.id("_storage")), }).index("by_funcionario", ["funcionarioId"]), simbolos: defineTable({ nome: v.string(), tipo: simboloTipo, descricao: v.string(), vencValor: v.string(), repValor: v.string(), valor: v.string(), }), // Sistema de Autenticação e Controle de Acesso usuarios: defineTable({ authId: v.string(), nome: v.string(), email: v.string(), funcionarioId: v.optional(v.id("funcionarios")), roleId: v.id("roles"), ativo: v.boolean(), primeiroAcesso: v.boolean(), ultimoAcesso: v.optional(v.number()), criadoEm: v.number(), atualizadoEm: v.number(), // Controle de Bloqueio e Segurança bloqueado: v.optional(v.boolean()), motivoBloqueio: v.optional(v.string()), dataBloqueio: v.optional(v.number()), tentativasLogin: v.optional(v.number()), // contador de tentativas falhas ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa // Campos de Chat e Perfil avatar: v.optional(v.string()), // "avatar-1" até "avatar-15" ou storageId fotoPerfil: v.optional(v.id("_storage")), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), // max 100 chars statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), ultimaAtividade: v.optional(v.number()), // timestamp notificacoesAtivadas: v.optional(v.boolean()), somNotificacao: v.optional(v.boolean()), }) .index("by_email", ["email"]) .index("by_role", ["roleId"]) .index("by_ativo", ["ativo"]) .index("by_status_presenca", ["statusPresenca"]) .index("by_bloqueado", ["bloqueado"]) .index("by_funcionarioId", ["funcionarioId"]) .index("authId", ["authId"]), roles: defineTable({ nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario" descricao: v.string(), nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc. customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER criadoPor: v.optional(v.id("usuarios")), // usuário TI_MASTER que criou este perfil editavel: v.optional(v.boolean()), // se pode ser editado (false para roles fixas) }) .index("by_nome", ["nome"]) .index("by_nivel", ["nivel"]) .index("by_setor", ["setor"]) .index("by_customizado", ["customizado"]), permissoes: defineTable({ nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc. descricao: v.string(), recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc. acao: v.string(), // "criar", "ler", "editar", "excluir" }) .index("by_recurso", ["recurso"]) .index("by_recurso_e_acao", ["recurso", "acao"]) .index("by_nome", ["nome"]), rolePermissoes: defineTable({ roleId: v.id("roles"), permissaoId: v.id("permissoes"), }) .index("by_role", ["roleId"]) .index("by_permissao", ["permissaoId"]), sessoes: defineTable({ usuarioId: v.id("usuarios"), token: v.string(), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), criadoEm: v.number(), expiraEm: v.number(), ativo: v.boolean(), }) .index("by_usuario", ["usuarioId"]) .index("by_token", ["token"]) .index("by_ativo", ["ativo"]) .index("by_expiracao", ["expiraEm"]), logsAcesso: defineTable({ usuarioId: v.id("usuarios"), tipo: v.union( v.literal("login"), v.literal("logout"), v.literal("acesso_negado"), v.literal("senha_alterada"), v.literal("sessao_expirada") ), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), detalhes: v.optional(v.string()), timestamp: v.number(), }) .index("by_usuario", ["usuarioId"]) .index("by_tipo", ["tipo"]) .index("by_timestamp", ["timestamp"]), // Logs de Login Detalhados logsLogin: defineTable({ usuarioId: v.optional(v.id("usuarios")), // pode ser null se falha antes de identificar usuário matriculaOuEmail: v.string(), // tentativa de login sucesso: v.boolean(), motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente" ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), device: v.optional(v.string()), browser: v.optional(v.string()), sistema: v.optional(v.string()), timestamp: v.number(), }) .index("by_usuario", ["usuarioId"]) .index("by_sucesso", ["sucesso"]) .index("by_timestamp", ["timestamp"]) .index("by_ip", ["ipAddress"]), // Logs de Atividades logsAtividades: defineTable({ usuarioId: v.id("usuarios"), acao: v.string(), // "criar", "editar", "excluir", "bloquear", "desbloquear", etc. recurso: v.string(), // "funcionarios", "simbolos", "usuarios", "perfis", etc. recursoId: v.optional(v.string()), // ID do recurso afetado detalhes: v.optional(v.string()), // JSON com detalhes da ação timestamp: v.number(), }) .index("by_usuario", ["usuarioId"]) .index("by_acao", ["acao"]) .index("by_recurso", ["recurso"]) .index("by_timestamp", ["timestamp"]) .index("by_recurso_id", ["recurso", "recursoId"]), // Histórico de Bloqueios bloqueiosUsuarios: defineTable({ usuarioId: v.id("usuarios"), motivo: v.string(), bloqueadoPor: v.id("usuarios"), // ID do TI_MASTER que bloqueou dataInicio: v.number(), dataFim: v.optional(v.number()), // quando foi desbloqueado desbloqueadoPor: v.optional(v.id("usuarios")), ativo: v.boolean(), // se é o bloqueio atual ativo }) .index("by_usuario", ["usuarioId"]) .index("by_bloqueado_por", ["bloqueadoPor"]) .index("by_ativo", ["ativo"]) .index("by_data_inicio", ["dataInicio"]), // Perfis Customizados // Templates de Mensagens templatesMensagens: defineTable({ codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc. nome: v.string(), tipo: v.union( v.literal("sistema"), // predefinido, não editável v.literal("customizado") // criado por TI_MASTER ), titulo: v.string(), corpo: v.string(), // pode ter variáveis {{variavel}} variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.] criadoPor: v.optional(v.id("usuarios")), criadoEm: v.number(), }) .index("by_codigo", ["codigo"]) .index("by_tipo", ["tipo"]) .index("by_criado_por", ["criadoPor"]), // Configuração de Email/SMTP configuracaoEmail: defineTable({ servidor: v.string(), // smtp.gmail.com porta: v.number(), // 587, 465, etc. usuario: v.string(), senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP emailRemetente: v.string(), nomeRemetente: v.string(), usarSSL: v.boolean(), usarTLS: v.boolean(), ativo: v.boolean(), testadoEm: v.optional(v.number()), configuradoPor: v.id("usuarios"), atualizadoEm: v.number(), }).index("by_ativo", ["ativo"]), // Fila de Emails notificacoesEmail: defineTable({ destinatario: v.string(), // email destinatarioId: v.optional(v.id("usuarios")), assunto: v.string(), corpo: v.string(), // HTML ou texto templateId: v.optional(v.id("templatesMensagens")), status: v.union( v.literal("pendente"), v.literal("enviando"), v.literal("enviado"), v.literal("falha") ), tentativas: v.number(), ultimaTentativa: v.optional(v.number()), erroDetalhes: v.optional(v.string()), enviadoPor: v.id("usuarios"), criadoEm: v.number(), enviadoEm: v.optional(v.number()), agendadaPara: v.optional(v.number()), // timestamp para agendamento }) .index("by_status", ["status"]) .index("by_destinatario", ["destinatarioId"]) .index("by_enviado_por", ["enviadoPor"]) .index("by_criado_em", ["criadoEm"]) .index("by_agendamento", ["agendadaPara"]), configuracaoAcesso: defineTable({ chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc. valor: v.string(), descricao: v.string(), }).index("by_chave", ["chave"]), // Rate Limiting de Emails rateLimitEmails: defineTable({ remetenteId: v.id("usuarios"), timestamp: v.number(), contador: v.number(), // quantidade de emails enviados neste período periodo: v.union( v.literal("minuto"), // último minuto v.literal("hora") // última hora ), }) .index("by_remetente_periodo", ["remetenteId", "periodo", "timestamp"]) .index("by_timestamp", ["timestamp"]), // Sistema de Chat conversas: defineTable({ tipo: v.union( v.literal("individual"), v.literal("grupo"), v.literal("sala_reuniao") ), nome: v.optional(v.string()), // nome do grupo/sala avatar: v.optional(v.string()), // avatar do grupo/sala participantes: v.array(v.id("usuarios")), // IDs dos participantes administradores: v.optional(v.array(v.id("usuarios"))), // IDs dos administradores (apenas para sala_reuniao) ultimaMensagem: v.optional(v.string()), ultimaMensagemTimestamp: v.optional(v.number()), ultimaMensagemRemetenteId: v.optional(v.id("usuarios")), // ID do remetente da última mensagem criadoPor: v.id("usuarios"), criadoEm: v.number(), }) .index("by_criado_por", ["criadoPor"]) .index("by_tipo", ["tipo"]) .index("by_ultima_mensagem", ["ultimaMensagemTimestamp"]), mensagens: defineTable({ conversaId: v.id("conversas"), remetenteId: v.id("usuarios"), tipo: v.union( v.literal("texto"), v.literal("arquivo"), v.literal("imagem") ), conteudo: v.string(), // texto ou nome do arquivo conteudoBusca: v.optional(v.string()), // versão normalizada para busca arquivoId: v.optional(v.id("_storage")), arquivoNome: v.optional(v.string()), arquivoTamanho: v.optional(v.number()), arquivoTipo: v.optional(v.string()), linkPreview: v.optional( v.object({ url: v.string(), titulo: v.optional(v.string()), descricao: v.optional(v.string()), imagem: v.optional(v.string()), site: v.optional(v.string()), }) ), reagiuPor: v.optional( v.array( v.object({ usuarioId: v.id("usuarios"), emoji: v.string(), }) ) ), mencoes: v.optional(v.array(v.id("usuarios"))), respostaPara: v.optional(v.id("mensagens")), // ID da mensagem que está respondendo agendadaPara: v.optional(v.number()), // timestamp enviadaEm: v.number(), editadaEm: v.optional(v.number()), deletada: v.optional(v.boolean()), lidaPor: v.optional(v.array(v.id("usuarios"))), // IDs dos usuários que leram a mensagem }) .index("by_conversa", ["conversaId", "enviadaEm"]) .index("by_remetente", ["remetenteId"]) .index("by_agendamento", ["agendadaPara"]) .index("by_resposta", ["respostaPara"]), leituras: defineTable({ conversaId: v.id("conversas"), usuarioId: v.id("usuarios"), ultimaMensagemLida: v.id("mensagens"), lidaEm: v.number(), }) .index("by_conversa_usuario", ["conversaId", "usuarioId"]) .index("by_usuario", ["usuarioId"]), // Sistema de Chamadas de Áudio/Vídeo chamadas: defineTable({ conversaId: v.id("conversas"), tipo: v.union(v.literal("audio"), v.literal("video")), roomName: v.string(), // Nome único da sala Jitsi criadoPor: v.id("usuarios"), // Anfitrião/criador participantes: v.array(v.id("usuarios")), status: v.union( v.literal("aguardando"), v.literal("em_andamento"), v.literal("finalizada"), v.literal("cancelada") ), iniciadaEm: v.optional(v.number()), finalizadaEm: v.optional(v.number()), duracaoSegundos: v.optional(v.number()), gravando: v.boolean(), gravacaoIniciadaPor: v.optional(v.id("usuarios")), gravacaoIniciadaEm: v.optional(v.number()), gravacaoFinalizadaEm: v.optional(v.number()), configuracoes: v.optional(v.object({ audioHabilitado: v.boolean(), videoHabilitado: v.boolean(), participantesConfig: v.optional(v.array(v.object({ usuarioId: v.id("usuarios"), audioHabilitado: v.boolean(), videoHabilitado: v.boolean(), forcadoPeloAnfitriao: v.optional(v.boolean()), // Se foi forçado pelo anfitrião }))) })), criadoEm: v.number(), }) .index("by_conversa", ["conversaId", "status"]) .index("by_criado_por", ["criadoPor"]) .index("by_status", ["status"]) .index("by_room_name", ["roomName"]), notificacoes: defineTable({ usuarioId: v.id("usuarios"), tipo: v.union( v.literal("nova_mensagem"), v.literal("mencao"), v.literal("grupo_criado"), v.literal("adicionado_grupo"), v.literal("alerta_seguranca") ), conversaId: v.optional(v.id("conversas")), mensagemId: v.optional(v.id("mensagens")), remetenteId: v.optional(v.id("usuarios")), titulo: v.string(), descricao: v.string(), lida: v.boolean(), criadaEm: v.number(), }) .index("by_usuario", ["usuarioId", "lida", "criadaEm"]) .index("by_usuario_lida", ["usuarioId", "lida"]), digitando: defineTable({ conversaId: v.id("conversas"), usuarioId: v.id("usuarios"), iniciouEm: v.number(), }) .index("by_conversa", ["conversaId", "iniciouEm"]) .index("by_usuario", ["usuarioId"]), // Push Notifications pushSubscriptions: defineTable({ usuarioId: v.id("usuarios"), endpoint: v.string(), // URL do serviço de push keys: v.object({ p256dh: v.string(), // Chave pública auth: v.string(), // Chave de autenticação }), userAgent: v.optional(v.string()), criadoEm: v.number(), ultimaAtividade: v.number(), ativo: v.boolean(), }) .index("by_usuario", ["usuarioId", "ativo"]) .index("by_endpoint", ["endpoint"]), // Preferências de Notificação por Conversa preferenciasNotificacaoConversa: defineTable({ usuarioId: v.id("usuarios"), conversaId: v.id("conversas"), pushAtivado: v.boolean(), // Receber push notifications emailAtivado: v.boolean(), // Receber emails quando offline somAtivado: v.boolean(), // Tocar som silenciado: v.boolean(), // Silenciar completamente apenasMencoes: v.boolean(), // Notificar apenas quando mencionado criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_usuario_conversa", ["usuarioId", "conversaId"]) .index("by_conversa", ["conversaId"]), // Tabelas de Monitoramento do Sistema systemMetrics: defineTable({ timestamp: v.number(), // Métricas de Sistema cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), // Métricas de Aplicação usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }).index("by_timestamp", ["timestamp"]), alertConfigurations: defineTable({ metricName: v.string(), threshold: v.number(), operator: v.union( v.literal(">"), v.literal("<"), v.literal(">="), v.literal("<="), v.literal("==") ), enabled: v.boolean(), notifyByEmail: v.boolean(), notifyByChat: v.boolean(), createdBy: v.id("usuarios"), lastModified: v.number(), }).index("by_enabled", ["enabled"]), alertHistory: defineTable({ configId: v.id("alertConfigurations"), metricName: v.string(), metricValue: v.number(), threshold: v.number(), timestamp: v.number(), status: v.union(v.literal("triggered"), v.literal("resolved")), notificationsSent: v.object({ email: v.boolean(), chat: v.boolean(), }), }) .index("by_timestamp", ["timestamp"]) .index("by_status", ["status"]) .index("by_config", ["configId", "timestamp"]), tickets: defineTable({ numero: v.string(), titulo: v.string(), descricao: v.string(), tipo: v.union( v.literal("reclamacao"), v.literal("elogio"), v.literal("sugestao"), v.literal("chamado") ), categoria: v.optional(v.string()), status: v.union( v.literal("aberto"), v.literal("em_andamento"), v.literal("aguardando_usuario"), v.literal("resolvido"), v.literal("encerrado"), v.literal("cancelado") ), prioridade: v.union( v.literal("baixa"), v.literal("media"), v.literal("alta"), v.literal("critica") ), solicitanteId: v.id("usuarios"), solicitanteNome: v.string(), solicitanteEmail: v.string(), responsavelId: v.optional(v.id("usuarios")), setorResponsavel: v.optional(v.string()), slaConfigId: v.optional(v.id("slaConfigs")), conversaId: v.optional(v.id("conversas")), prazoResposta: v.optional(v.number()), prazoConclusao: v.optional(v.number()), prazoEncerramento: v.optional(v.number()), timeline: v.optional( v.array( v.object({ etapa: v.string(), status: v.union( v.literal("pendente"), v.literal("em_andamento"), v.literal("concluido"), v.literal("vencido") ), prazo: v.optional(v.number()), concluidoEm: v.optional(v.number()), observacao: v.optional(v.string()), }) ) ), alertasEmitidos: v.optional( v.array( v.object({ tipo: v.union( v.literal("resposta"), v.literal("conclusao"), v.literal("encerramento") ), emitidoEm: v.number(), }) ) ), anexos: v.optional( v.array( v.object({ arquivoId: v.id("_storage"), nome: v.optional(v.string()), tipo: v.optional(v.string()), tamanho: v.optional(v.number()), }) ) ), tags: v.optional(v.array(v.string())), canalOrigem: v.optional(v.string()), ultimaInteracaoEm: v.number(), criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_numero", ["numero"]) .index("by_status", ["status"]) .index("by_solicitante", ["solicitanteId", "status"]) .index("by_responsavel", ["responsavelId", "status"]) .index("by_setor", ["setorResponsavel", "status"]), ticketInteractions: defineTable({ ticketId: v.id("tickets"), autorId: v.optional(v.id("usuarios")), origem: v.union( v.literal("usuario"), v.literal("ti"), v.literal("sistema") ), tipo: v.union( v.literal("mensagem"), v.literal("status"), v.literal("anexo"), v.literal("alerta") ), conteudo: v.string(), anexos: v.optional( v.array( v.object({ arquivoId: v.id("_storage"), nome: v.optional(v.string()), tipo: v.optional(v.string()), tamanho: v.optional(v.number()), }) ) ), statusAnterior: v.optional( v.union( v.literal("aberto"), v.literal("em_andamento"), v.literal("aguardando_usuario"), v.literal("resolvido"), v.literal("encerrado"), v.literal("cancelado") ) ), statusNovo: v.optional( v.union( v.literal("aberto"), v.literal("em_andamento"), v.literal("aguardando_usuario"), v.literal("resolvido"), v.literal("encerrado"), v.literal("cancelado") ) ), visibilidade: v.union( v.literal("publico"), v.literal("interno") ), criadoEm: v.number(), }) .index("by_ticket", ["ticketId"]) .index("by_ticket_type", ["ticketId", "tipo"]) .index("by_autor", ["autorId"]), slaConfigs: defineTable({ nome: v.string(), descricao: v.optional(v.string()), prioridade: v.optional( v.union( v.literal("baixa"), v.literal("media"), v.literal("alta"), v.literal("critica") ) ), tempoRespostaHoras: v.number(), tempoConclusaoHoras: v.number(), tempoEncerramentoHoras: v.optional(v.number()), alertaAntecedenciaHoras: v.number(), ativo: v.boolean(), criadoPor: v.id("usuarios"), atualizadoPor: v.optional(v.id("usuarios")), criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_ativo", ["ativo"]) .index("by_prioridade", ["prioridade", "ativo"]) .index("by_nome", ["nome"]), ticketAssignments: defineTable({ ticketId: v.id("tickets"), responsavelId: v.id("usuarios"), atribuidoPor: v.id("usuarios"), motivo: v.optional(v.string()), ativo: v.boolean(), criadoEm: v.number(), encerradoEm: v.optional(v.number()), }) .index("by_ticket", ["ticketId", "ativo"]) .index("by_responsavel", ["responsavelId", "ativo"]), // Sistema de Segurança Cibernética networkSensors: defineTable({ nome: v.string(), tipo: sensorSegurancaTipo, status: sensorSegurancaStatus, escopo: v.optional(v.string()), ipMonitorado: v.optional(v.string()), hostname: v.optional(v.string()), regioes: v.optional(v.array(v.string())), portasMonitoradas: v.optional(v.array(v.number())), protocolos: v.optional(v.array(v.string())), capacidades: v.optional(v.array(v.string())), ultimaSincronizacao: v.number(), ultimoHeartbeat: v.optional(v.number()), latenciaMs: v.optional(v.number()), errosConsecutivos: v.optional(v.number()), agenteVersao: v.optional(v.string()), notas: v.optional(v.string()), }) .index("by_tipo", ["tipo"]) .index("by_status", ["status"]) .index("by_hostname", ["hostname"]), ipReputation: defineTable({ indicador: v.string(), categoria: v.union( v.literal("ip"), v.literal("dominio"), v.literal("hash"), v.literal("email") ), reputacao: v.number(), // -100 (malicioso) até 100 (confiável) severidadeMax: severidadeSeguranca, whitelist: v.boolean(), blacklist: v.boolean(), ocorrencias: v.number(), primeiroRegistro: v.number(), ultimoRegistro: v.number(), bloqueadoAte: v.optional(v.number()), origem: v.optional(v.string()), comentarios: v.optional(v.string()), classificacoes: v.optional(v.array(v.string())), ultimaAcaoId: v.optional(v.id("incidentActions")), }) .index("by_indicador", ["indicador"]) .index("by_reputacao", ["reputacao"]) .index("by_blacklist", ["blacklist"]) .index("by_whitelist", ["whitelist"]), portRules: defineTable({ porta: v.number(), protocolo: v.union( v.literal("tcp"), v.literal("udp"), v.literal("icmp"), v.literal("quic"), v.literal("any") ), acao: v.union( v.literal("permitir"), v.literal("bloquear"), v.literal("monitorar"), v.literal("rate_limit") ), temporario: v.boolean(), severidadeMin: severidadeSeguranca, duracaoSegundos: v.optional(v.number()), expiraEm: v.optional(v.number()), criadoPor: v.id("usuarios"), atualizadoPor: v.optional(v.id("usuarios")), criadoEm: v.number(), atualizadoEm: v.number(), notas: v.optional(v.string()), tags: v.optional(v.array(v.string())), listaReferencia: v.optional(v.id("ipReputation")), }) .index("by_porta_protocolo", ["porta", "protocolo"]) .index("by_acao", ["acao"]) .index("by_expiracao", ["expiraEm"]), threatIntelFeeds: defineTable({ nomeFonte: v.string(), tipo: threatIntelTipo, formato: threatIntelFormato, url: v.optional(v.string()), ativo: v.boolean(), prioridade: v.union( v.literal("baixa"), v.literal("media"), v.literal("alta"), v.literal("critica") ), ultimaSincronizacao: v.optional(v.number()), entradasProcessadas: v.optional(v.number()), errosConsecutivos: v.optional(v.number()), autenticacaoNecessaria: v.optional(v.boolean()), configuracao: v.optional( v.object({ tokenId: v.optional(v.id("_storage")), escopo: v.optional(v.string()), }) ), criadoPor: v.id("usuarios"), atualizadoPor: v.optional(v.id("usuarios")), criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_tipo", ["tipo"]) .index("by_ativo", ["ativo"]) .index("by_prioridade", ["prioridade"]), securityEvents: defineTable({ referencia: v.string(), timestamp: v.number(), tipoAtaque: ataqueCiberneticoTipo, severidade: severidadeSeguranca, status: statusEventoSeguranca, descricao: v.string(), origemIp: v.optional(v.string()), origemRegiao: v.optional(v.string()), origemAsn: v.optional(v.string()), destinoIp: v.optional(v.string()), destinoPorta: v.optional(v.number()), protocolo: v.optional(v.string()), transporte: v.optional(v.string()), sensorId: v.optional(v.id("networkSensors")), detectadoPor: v.optional(v.string()), mitreTechnique: v.optional(v.string()), geolocalizacao: v.optional( v.object({ pais: v.optional(v.string()), regiao: v.optional(v.string()), cidade: v.optional(v.string()), latitude: v.optional(v.number()), longitude: v.optional(v.number()), }) ), fingerprint: v.optional( v.object({ userAgent: v.optional(v.string()), deviceId: v.optional(v.string()), ja3: v.optional(v.string()), tlsVersion: v.optional(v.string()), }) ), indicadores: v.optional( v.array( v.object({ tipo: v.string(), valor: v.string(), confianca: v.optional(v.number()), }) ) ), metricas: v.optional( v.object({ pps: v.optional(v.number()), bps: v.optional(v.number()), rpm: v.optional(v.number()), errosPorSegundo: v.optional(v.number()), hostsAfetados: v.optional(v.number()), }) ), correlacoes: v.optional(v.array(v.id("securityEvents"))), referenciasExternas: v.optional(v.array(v.string())), tags: v.optional(v.array(v.string())), criadoPor: v.optional(v.id("usuarios")), atualizadoEm: v.number(), }) .index("by_referencia", ["referencia"]) .index("by_timestamp", ["timestamp"]) .index("by_tipo", ["tipoAtaque", "timestamp"]) .index("by_severidade", ["severidade", "timestamp"]) .index("by_status", ["status", "timestamp"]), incidentActions: defineTable({ eventoId: v.id("securityEvents"), tipo: acaoIncidenteTipo, origem: v.union(v.literal("automatico"), v.literal("manual")), status: acaoIncidenteStatus, executadoPor: v.optional(v.id("usuarios")), detalhes: v.optional(v.string()), resultado: v.optional(v.string()), relacionadoA: v.optional(v.id("ipReputation")), criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_evento", ["eventoId", "status"]) .index("by_tipo", ["tipo", "status"]), reportRequests: defineTable({ solicitanteId: v.id("usuarios"), filtros: v.object({ dataInicio: v.number(), dataFim: v.number(), severidades: v.optional(v.array(severidadeSeguranca)), tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), incluirIndicadores: v.optional(v.boolean()), incluirMetricas: v.optional(v.boolean()), incluirAcoes: v.optional(v.boolean()), }), status: reportStatus, resultadoId: v.optional(v.id("_storage")), observacoes: v.optional(v.string()), criadoEm: v.number(), atualizadoEm: v.number(), concluidoEm: v.optional(v.number()), erro: v.optional(v.string()), }) .index("by_status", ["status"]) .index("by_solicitante", ["solicitanteId", "status"]) .index("by_criado_em", ["criadoEm"]), rateLimitConfig: defineTable({ nome: v.string(), tipo: v.union( v.literal("ip"), v.literal("usuario"), v.literal("endpoint"), v.literal("global") ), identificador: v.optional(v.string()), limite: v.number(), janelaSegundos: v.number(), estrategia: v.union( v.literal("fixed_window"), v.literal("sliding_window"), v.literal("token_bucket") ), acaoExcedido: v.union( v.literal("bloquear"), v.literal("throttle"), v.literal("alertar") ), bloqueioTemporarioSegundos: v.optional(v.number()), ativo: v.boolean(), prioridade: v.number(), criadoPor: v.id("usuarios"), atualizadoPor: v.optional(v.id("usuarios")), criadoEm: v.number(), atualizadoEm: v.number(), notas: v.optional(v.string()), tags: v.optional(v.array(v.string())) }) .index("by_tipo_identificador", ["tipo", "identificador"]) .index("by_ativo", ["ativo"]) .index("by_prioridade", ["prioridade"]) , alertConfigs: defineTable({ nome: v.string(), canais: v.object({ email: v.boolean(), chat: v.boolean(), }), emails: v.array(v.string()), chatUsers: v.array(v.string()), severidadeMin: severidadeSeguranca, tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), reenvioMin: v.number(), criadoPor: v.id("usuarios"), criadoEm: v.number(), atualizadoEm: v.number(), }) .index("by_criadoEm", ["criadoEm"]), // 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()), 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()), // 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"]), 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) 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"]), });