diff --git a/.vscode/settings.json b/.vscode/settings.json index 45d220c..1f3dad7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,8 +12,12 @@ // }, "eslint.useFlatConfig": true, "eslint.workingDirectories": [ - { "pattern": "apps/*" }, - { "pattern": "packages/*" } + { + "pattern": "apps/*" + }, + { + "pattern": "packages/*" + } ], "eslint.validate": [ "javascript", @@ -25,5 +29,16 @@ "eslint.options": { "cache": true, "cacheLocation": ".eslintcache" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.addMissingImports": "always", + "source.removeUnusedImports": "always", + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" } -} +} \ No newline at end of file diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 796db0a..ba50a17 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1,1970 +1,1835 @@ -import { defineSchema, defineTable } from "convex/server"; -import { Infer, v } from "convex/values"; +import { defineSchema, defineTable } from 'convex/server'; +import { Infer, v } from 'convex/values'; +import { lgpdTables } from './tables/lgpdTables'; export const simboloTipo = v.union( - v.literal("cargo_comissionado"), - v.literal("funcao_gratificada") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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") + 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 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"]), - - // 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}} - 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 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"]), - - // 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"]), - - // 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"]), - - // 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"]), - - // ========== LGPD - Lei Geral de Proteção de Dados ========== - - // Solicitações de direitos LGPD - solicitacoesLGPD: defineTable({ - tipo: v.union( - v.literal("acesso"), - v.literal("correcao"), - v.literal("exclusao"), - v.literal("portabilidade"), - v.literal("revogacao_consentimento"), - v.literal("informacao_compartilhamento") - ), - usuarioId: v.id("usuarios"), - funcionarioId: v.optional(v.id("funcionarios")), - status: v.union( - v.literal("pendente"), - v.literal("em_analise"), - v.literal("concluida"), - v.literal("rejeitada") - ), - dadosSolicitados: v.optional(v.string()), // JSON com detalhes da solicitação - resposta: v.optional(v.string()), // Resposta da solicitação - arquivoResposta: v.optional(v.id("_storage")), // Arquivo gerado (ex: exportação de dados) - respondidoPor: v.optional(v.id("usuarios")), - respondidoEm: v.optional(v.number()), - criadoEm: v.number(), - prazoResposta: v.number(), // Prazo legal (15 dias) - timestamp - observacoes: v.optional(v.string()), - }) - .index("by_usuario", ["usuarioId"]) - .index("by_status", ["status"]) - .index("by_tipo", ["tipo"]) - .index("by_prazo", ["prazoResposta"]) - .index("by_funcionario", ["funcionarioId"]), - - // Consentimentos dos usuários - consentimentos: defineTable({ - usuarioId: v.id("usuarios"), - tipo: v.union( - v.literal("termo_uso"), - v.literal("politica_privacidade"), - v.literal("comunicacoes"), - v.literal("compartilhamento_dados") - ), - aceito: v.boolean(), - versao: v.string(), // Versão do documento aceito (ex: "1.0") - ipAddress: v.optional(v.string()), - userAgent: v.optional(v.string()), - aceitoEm: v.number(), - revogadoEm: v.optional(v.number()), - revogadoPor: v.optional(v.id("usuarios")), // Se revogado pelo próprio usuário ou por TI - }) - .index("by_usuario", ["usuarioId"]) - .index("by_tipo", ["tipo"]) - .index("by_usuario_tipo", ["usuarioId", "tipo"]) - .index("by_versao", ["versao"]), - - // Registro de Operações de Tratamento (ROT) - registrosTratamento: defineTable({ - finalidade: v.string(), // Finalidade do tratamento - baseLegal: v.string(), // Base legal (ex: "Art. 7º, II - Execução de políticas públicas") - categoriasDados: v.array(v.string()), // ["dados_identificacao", "dados_contato", "dados_profissionais"] - categoriasTitulares: v.array(v.string()), // ["funcionarios", "servidores", "colaboradores"] - medidasSeguranca: v.array(v.string()), // ["criptografia", "controle_acesso", "logs_auditoria"] - prazoRetencao: v.number(), // em dias - compartilhamentoTerceiros: v.boolean(), - terceiros: v.optional(v.array(v.string())), // Lista de terceiros com quem compartilha - responsavel: v.id("usuarios"), // Responsável pelo tratamento - criadoEm: v.number(), - atualizadoEm: v.number(), - ativo: v.boolean(), - descricao: v.optional(v.string()), // Descrição detalhada - }) - .index("by_finalidade", ["finalidade"]) - .index("by_ativo", ["ativo"]) - .index("by_responsavel", ["responsavel"]), - - // Configurações LGPD - configuracaoLGPD: defineTable({ - encarregadoNome: v.optional(v.string()), - encarregadoEmail: v.optional(v.string()), - encarregadoTelefone: v.optional(v.string()), - encarregadoHorarioAtendimento: v.optional(v.string()), // Ex: "Segunda a Sexta, das 8h às 17h" - prazoRespostaPadrao: v.number(), // em dias (padrão: 15) - diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3) - termoObrigatorio: v.boolean(), // Se o termo de consentimento é obrigatório - versaoTermoAtual: v.string(), // Versão atual do termo (ex: "1.0") - politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado - ativo: v.boolean(), - atualizadoPor: v.id("usuarios"), - atualizadoEm: v.number(), - }) - .index("by_ativo", ["ativo"]), + // 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 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']), + + // 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}} + 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 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']), + + // 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']), + + // 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']), + + // 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']), + + ...lgpdTables }); diff --git a/packages/backend/convex/tables/lgpdTables.ts b/packages/backend/convex/tables/lgpdTables.ts new file mode 100644 index 0000000..75822cc --- /dev/null +++ b/packages/backend/convex/tables/lgpdTables.ts @@ -0,0 +1,97 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const lgpdTables = { + // ========== LGPD - Lei Geral de Proteção de Dados ========== + + // Solicitações de direitos LGPD + solicitacoesLGPD: defineTable({ + tipo: v.union( + v.literal('acesso'), + v.literal('correcao'), + v.literal('exclusao'), + v.literal('portabilidade'), + v.literal('revogacao_consentimento'), + v.literal('informacao_compartilhamento') + ), + usuarioId: v.id('usuarios'), + funcionarioId: v.optional(v.id('funcionarios')), + status: v.union( + v.literal('pendente'), + v.literal('em_analise'), + v.literal('concluida'), + v.literal('rejeitada') + ), + dadosSolicitados: v.optional(v.string()), // JSON com detalhes da solicitação + resposta: v.optional(v.string()), // Resposta da solicitação + arquivoResposta: v.optional(v.id('_storage')), // Arquivo gerado (ex: exportação de dados) + respondidoPor: v.optional(v.id('usuarios')), + respondidoEm: v.optional(v.number()), + criadoEm: v.number(), + prazoResposta: v.number(), // Prazo legal (15 dias) - timestamp + observacoes: v.optional(v.string()) + }) + .index('by_usuario', ['usuarioId']) + .index('by_status', ['status']) + .index('by_tipo', ['tipo']) + .index('by_prazo', ['prazoResposta']) + .index('by_funcionario', ['funcionarioId']), + + // Consentimentos dos usuários + consentimentos: defineTable({ + usuarioId: v.id('usuarios'), + tipo: v.union( + v.literal('termo_uso'), + v.literal('politica_privacidade'), + v.literal('comunicacoes'), + v.literal('compartilhamento_dados') + ), + aceito: v.boolean(), + versao: v.string(), // Versão do documento aceito (ex: "1.0") + ipAddress: v.optional(v.string()), + userAgent: v.optional(v.string()), + aceitoEm: v.number(), + revogadoEm: v.optional(v.number()), + revogadoPor: v.optional(v.id('usuarios')) // Se revogado pelo próprio usuário ou por TI + }) + .index('by_usuario', ['usuarioId']) + .index('by_tipo', ['tipo']) + .index('by_usuario_tipo', ['usuarioId', 'tipo']) + .index('by_versao', ['versao']), + + // Registro de Operações de Tratamento (ROT) + registrosTratamento: defineTable({ + finalidade: v.string(), // Finalidade do tratamento + baseLegal: v.string(), // Base legal (ex: "Art. 7º, II - Execução de políticas públicas") + categoriasDados: v.array(v.string()), // ["dados_identificacao", "dados_contato", "dados_profissionais"] + categoriasTitulares: v.array(v.string()), // ["funcionarios", "servidores", "colaboradores"] + medidasSeguranca: v.array(v.string()), // ["criptografia", "controle_acesso", "logs_auditoria"] + prazoRetencao: v.number(), // em dias + compartilhamentoTerceiros: v.boolean(), + terceiros: v.optional(v.array(v.string())), // Lista de terceiros com quem compartilha + responsavel: v.id('usuarios'), // Responsável pelo tratamento + criadoEm: v.number(), + atualizadoEm: v.number(), + ativo: v.boolean(), + descricao: v.optional(v.string()) // Descrição detalhada + }) + .index('by_finalidade', ['finalidade']) + .index('by_ativo', ['ativo']) + .index('by_responsavel', ['responsavel']), + + // Configurações LGPD + configuracaoLGPD: defineTable({ + encarregadoNome: v.optional(v.string()), + encarregadoEmail: v.optional(v.string()), + encarregadoTelefone: v.optional(v.string()), + encarregadoHorarioAtendimento: v.optional(v.string()), // Ex: "Segunda a Sexta, das 8h às 17h" + prazoRespostaPadrao: v.number(), // em dias (padrão: 15) + diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3) + termoObrigatorio: v.boolean(), // Se o termo de consentimento é obrigatório + versaoTermoAtual: v.string(), // Versão atual do termo (ex: "1.0") + politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado + ativo: v.boolean(), + atualizadoPor: v.id('usuarios'), + atualizadoEm: v.number() + }).index('by_ativo', ['ativo']) +};