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') ); // Status de templates de fluxo export const flowTemplateStatus = v.union( v.literal('draft'), v.literal('published'), v.literal('archived') ); export type FlowTemplateStatus = Infer; // Status de instâncias de fluxo export const flowInstanceStatus = v.union( v.literal('active'), v.literal('completed'), v.literal('cancelled') ); export type FlowInstanceStatus = Infer; // Status de passos de instância de fluxo export const flowInstanceStepStatus = v.union( v.literal('pending'), v.literal('in_progress'), v.literal('completed'), v.literal('blocked') ); export type FlowInstanceStepStatus = Infer; export const situacaoContrato = v.union( v.literal('em_execucao'), v.literal('rescendido'), v.literal('aguardando_assinatura'), v.literal('finalizado') ); export default defineSchema({ // Setores da organização setores: defineTable({ nome: v.string(), sigla: v.string(), criadoPor: v.id('usuarios'), createdAt: v.number() }) .index('by_nome', ['nome']) .index('by_sigla', ['sigla']), // Relação muitos-para-muitos entre funcionários e setores funcionarioSetores: defineTable({ funcionarioId: v.id('funcionarios'), setorId: v.id('setores'), createdAt: v.number() }) .index('by_funcionarioId', ['funcionarioId']) .index('by_setorId', ['setorId']) .index('by_funcionarioId_and_setorId', ['funcionarioId', 'setorId']), // Templates de fluxo flowTemplates: defineTable({ name: v.string(), description: v.optional(v.string()), status: flowTemplateStatus, createdBy: v.id('usuarios'), createdAt: v.number() }) .index('by_status', ['status']) .index('by_createdBy', ['createdBy']), // Passos de template de fluxo flowSteps: defineTable({ flowTemplateId: v.id('flowTemplates'), name: v.string(), description: v.optional(v.string()), position: v.number(), expectedDuration: v.number(), // em dias setorId: v.id('setores'), defaultAssigneeId: v.optional(v.id('usuarios')), requiredDocuments: v.optional(v.array(v.string())) }) .index('by_flowTemplateId', ['flowTemplateId']) .index('by_flowTemplateId_and_position', ['flowTemplateId', 'position']), // Instâncias de fluxo flowInstances: defineTable({ flowTemplateId: v.id('flowTemplates'), contratoId: v.optional(v.id('contratos')), managerId: v.id('usuarios'), status: flowInstanceStatus, startedAt: v.number(), finishedAt: v.optional(v.number()), currentStepId: v.optional(v.id('flowInstanceSteps')) }) .index('by_flowTemplateId', ['flowTemplateId']) .index('by_contratoId', ['contratoId']) .index('by_managerId', ['managerId']) .index('by_status', ['status']), // Passos de instância de fluxo flowInstanceSteps: defineTable({ flowInstanceId: v.id('flowInstances'), flowStepId: v.id('flowSteps'), setorId: v.id('setores'), assignedToId: v.optional(v.id('usuarios')), status: flowInstanceStepStatus, startedAt: v.optional(v.number()), finishedAt: v.optional(v.number()), notes: v.optional(v.string()), notesUpdatedBy: v.optional(v.id('usuarios')), notesUpdatedAt: v.optional(v.number()), dueDate: v.optional(v.number()) }) .index('by_flowInstanceId', ['flowInstanceId']) .index('by_flowInstanceId_and_status', ['flowInstanceId', 'status']) .index('by_setorId', ['setorId']) .index('by_assignedToId', ['assignedToId']), // Documentos de instância de fluxo flowInstanceDocuments: defineTable({ flowInstanceStepId: v.id('flowInstanceSteps'), uploadedById: v.id('usuarios'), storageId: v.id('_storage'), name: v.string(), uploadedAt: v.number() }) .index('by_flowInstanceStepId', ['flowInstanceStepId']) .index('by_uploadedById', ['uploadedById']), // Sub-etapas de fluxo (para templates e instâncias) flowSubSteps: defineTable({ flowStepId: v.optional(v.id('flowSteps')), // Para templates flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), // Para instâncias name: v.string(), description: v.optional(v.string()), status: v.union( v.literal('pending'), v.literal('in_progress'), v.literal('completed'), v.literal('blocked') ), position: v.number(), createdBy: v.id('usuarios'), createdAt: v.number() }) .index('by_flowStepId', ['flowStepId']) .index('by_flowInstanceStepId', ['flowInstanceStepId']), // Notas de steps e sub-etapas flowStepNotes: defineTable({ flowStepId: v.optional(v.id('flowSteps')), flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), flowSubStepId: v.optional(v.id('flowSubSteps')), texto: v.string(), criadoPor: v.id('usuarios'), criadoEm: v.number(), arquivos: v.array(v.id('_storage')) }) .index('by_flowStepId', ['flowStepId']) .index('by_flowInstanceStepId', ['flowInstanceStepId']) .index('by_flowSubStepId', ['flowSubStepId']), 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 fotoPerfil: v.optional(v.id('_storage')), avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear) 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()), temaPreferido: v.optional(v.string()) // tema de aparência escolhido pelo usuário }) .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 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']), // 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 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'), v.literal('etapa_fluxo_concluida') ), 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()), 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']), // Configurações Gerais config: defineTable({ comprasSetorId: v.optional(v.id('setores')), criadoPor: v.id('usuarios'), atualizadoEm: v.number() }), // Módulo de Pedidos/Compras produtos: defineTable({ nome: v.string(), valorEstimado: v.string(), tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')), criadoPor: v.id('usuarios'), criadoEm: v.number() }) .searchIndex('search_nome', { searchField: 'nome' }) .index('by_nome', ['nome']) .index('by_tipo', ['tipo']), acoes: defineTable({ nome: v.string(), tipo: v.union(v.literal('projeto'), v.literal('lei')), criadoPor: v.id('usuarios'), criadoEm: v.number() }) .index('by_nome', ['nome']) .index('by_tipo', ['tipo']), pedidos: defineTable({ numeroSei: v.optional(v.string()), status: v.union( v.literal('em_rascunho'), v.literal('aguardando_aceite'), v.literal('em_analise'), v.literal('precisa_ajustes'), v.literal('cancelado'), v.literal('concluido') ), acaoId: v.optional(v.id('acoes')), criadoPor: v.id('usuarios'), criadoEm: v.number(), atualizadoEm: v.number() }) .index('by_numeroSei', ['numeroSei']) .index('by_status', ['status']) .index('by_criadoPor', ['criadoPor']) .index('by_acaoId', ['acaoId']), pedidoItems: defineTable({ pedidoId: v.id('pedidos'), produtoId: v.id('produtos'), valorEstimado: v.string(), valorReal: v.optional(v.string()), quantidade: v.number(), adicionadoPor: v.id('funcionarios'), criadoEm: v.number() }) .index('by_pedidoId', ['pedidoId']) .index('by_produtoId', ['produtoId']) .index('by_adicionadoPor', ['adicionadoPor']), historicoPedidos: defineTable({ pedidoId: v.id('pedidos'), usuarioId: v.id('usuarios'), acao: v.string(), // "criacao", "alteracao_status", "adicao_item", "remocao_item", "edicao_item" detalhes: v.optional(v.string()), // JSON string data: v.number() }) .index('by_pedidoId', ['pedidoId']) .index('by_usuarioId', ['usuarioId']) .index('by_data', ['data']), // 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" // Informações de Rede ipAddress: v.optional(v.string()), ipPublico: v.optional(v.string()), ipLocal: v.optional(v.string()), userAgent: v.optional(v.string()), device: v.optional(v.string()), browser: v.optional(v.string()), sistema: v.optional(v.string()), // Informações de Localização (por IP) latitude: v.optional(v.number()), longitude: v.optional(v.number()), endereco: v.optional(v.string()), cidade: v.optional(v.string()), estado: v.optional(v.string()), pais: v.optional(v.string()), // Informações de Localização (GPS do navegador) latitudeGPS: v.optional(v.number()), longitudeGPS: v.optional(v.number()), precisaoGPS: v.optional(v.number()), enderecoGPS: v.optional(v.string()), cidadeGPS: v.optional(v.string()), estadoGPS: v.optional(v.string()), paisGPS: v.optional(v.string()), timestamp: v.number() }) .index('by_usuario', ['usuarioId']) .index('by_sucesso', ['sucesso']) .index('by_timestamp', ['timestamp']) .index('by_ip', ['ipAddress']), // 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}} htmlCorpo: v.optional(v.string()), // versão HTML do corpo (com wrapper) variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.] categoria: v.optional(v.union(v.literal('email'), v.literal('chat'), v.literal('ambos'))), // categoria do template tags: v.optional(v.array(v.string())), // tags para organização criadoPor: v.optional(v.id('usuarios')), criadoEm: v.number() }) .index('by_codigo', ['codigo']) .index('by_tipo', ['tipo']) .index('by_criado_por', ['criadoPor']) .index('by_categoria', ['categoria']), // Configuração de Jitsi Meet configuracaoJitsi: defineTable({ domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com") appId: v.string(), // ID da aplicação Jitsi roomPrefix: v.string(), // Prefixo para nomes de salas useHttps: v.boolean(), // Usar HTTPS acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento) ativo: v.boolean(), // Configuração ativa testadoEm: v.optional(v.number()), // Timestamp do último teste de conexão configuradoEm: v.optional(v.number()), // Timestamp da última configuração do servidor Docker configuradoNoServidor: v.optional(v.boolean()), // Indica se a configuração foi aplicada no servidor configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor configuradoPor: v.id('usuarios'), // Usuário que configurou atualizadoEm: v.number(), // Timestamp de atualização jitsiConfigPath: v.optional(v.string()), // Caminho da configuração do Jitsi no servidor (ex: "~/.jitsi-meet-cfg") sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada) sshPort: v.optional(v.number()) // Porta SSH (padrão: 22) }).index('by_ativo', ['ativo']), // 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']) });