From 05e7f1181d7321a4f354207aa7473328e9692780 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Tue, 2 Dec 2025 09:55:07 -0300 Subject: [PATCH] feat: Introduce structured table definitions in `convex/tables` for various entities and remove the `todos` example table. --- packages/backend/convex/_generated/api.d.ts | 42 +- packages/backend/convex/contratos.ts | 318 ++- packages/backend/convex/flows.ts | 57 +- packages/backend/convex/funcionarios.ts | 4 +- packages/backend/convex/logsAtividades.ts | 286 +-- packages/backend/convex/schema.ts | 1940 +---------------- packages/backend/convex/security.ts | 222 +- packages/backend/convex/seed.ts | 14 - packages/backend/convex/simbolos.ts | 332 +-- packages/backend/convex/tables/atestados.ts | 20 + packages/backend/convex/tables/ausencias.ts | 36 + packages/backend/convex/tables/auth.ts | 172 ++ packages/backend/convex/tables/chat.ts | 173 ++ packages/backend/convex/tables/contratos.ts | 37 + packages/backend/convex/tables/cursos.ts | 11 + packages/backend/convex/tables/empresas.ts | 29 + packages/backend/convex/tables/enderecos.ts | 16 + packages/backend/convex/tables/ferias.ts | 55 + packages/backend/convex/tables/flows.ts | 132 ++ .../backend/convex/tables/funcionarios.ts | 172 ++ packages/backend/convex/tables/licencas.ts | 22 + packages/backend/convex/tables/pedidos.ts | 48 + packages/backend/convex/tables/ponto.ts | 266 +++ packages/backend/convex/tables/produtos.ts | 24 + packages/backend/convex/tables/security.ts | 330 +++ packages/backend/convex/tables/setores.ts | 24 + packages/backend/convex/tables/system.ts | 220 ++ packages/backend/convex/tables/tickets.ts | 165 ++ packages/backend/convex/tables/times.ts | 26 + packages/backend/convex/todos.ts | 42 - 30 files changed, 2700 insertions(+), 2535 deletions(-) create mode 100644 packages/backend/convex/tables/atestados.ts create mode 100644 packages/backend/convex/tables/ausencias.ts create mode 100644 packages/backend/convex/tables/auth.ts create mode 100644 packages/backend/convex/tables/chat.ts create mode 100644 packages/backend/convex/tables/contratos.ts create mode 100644 packages/backend/convex/tables/cursos.ts create mode 100644 packages/backend/convex/tables/empresas.ts create mode 100644 packages/backend/convex/tables/enderecos.ts create mode 100644 packages/backend/convex/tables/ferias.ts create mode 100644 packages/backend/convex/tables/flows.ts create mode 100644 packages/backend/convex/tables/funcionarios.ts create mode 100644 packages/backend/convex/tables/licencas.ts create mode 100644 packages/backend/convex/tables/pedidos.ts create mode 100644 packages/backend/convex/tables/ponto.ts create mode 100644 packages/backend/convex/tables/produtos.ts create mode 100644 packages/backend/convex/tables/security.ts create mode 100644 packages/backend/convex/tables/setores.ts create mode 100644 packages/backend/convex/tables/system.ts create mode 100644 packages/backend/convex/tables/tickets.ts create mode 100644 packages/backend/convex/tables/times.ts delete mode 100644 packages/backend/convex/todos.ts diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 6ea5997..0e016aa 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -57,9 +57,28 @@ import type * as security from "../security.js"; import type * as seed from "../seed.js"; import type * as setores from "../setores.js"; import type * as simbolos from "../simbolos.js"; +import type * as tables_atestados from "../tables/atestados.js"; +import type * as tables_ausencias from "../tables/ausencias.js"; +import type * as tables_auth from "../tables/auth.js"; +import type * as tables_chat from "../tables/chat.js"; +import type * as tables_contratos from "../tables/contratos.js"; +import type * as tables_cursos from "../tables/cursos.js"; +import type * as tables_empresas from "../tables/empresas.js"; +import type * as tables_enderecos from "../tables/enderecos.js"; +import type * as tables_ferias from "../tables/ferias.js"; +import type * as tables_flows from "../tables/flows.js"; +import type * as tables_funcionarios from "../tables/funcionarios.js"; +import type * as tables_licencas from "../tables/licencas.js"; +import type * as tables_pedidos from "../tables/pedidos.js"; +import type * as tables_ponto from "../tables/ponto.js"; +import type * as tables_produtos from "../tables/produtos.js"; +import type * as tables_security from "../tables/security.js"; +import type * as tables_setores from "../tables/setores.js"; +import type * as tables_system from "../tables/system.js"; +import type * as tables_tickets from "../tables/tickets.js"; +import type * as tables_times from "../tables/times.js"; import type * as templatesMensagens from "../templatesMensagens.js"; import type * as times from "../times.js"; -import type * as todos from "../todos.js"; import type * as usuarios from "../usuarios.js"; import type * as utils_chatTemplateWrapper from "../utils/chatTemplateWrapper.js"; import type * as utils_emailTemplateWrapper from "../utils/emailTemplateWrapper.js"; @@ -123,9 +142,28 @@ declare const fullApi: ApiFromModules<{ seed: typeof seed; setores: typeof setores; simbolos: typeof simbolos; + "tables/atestados": typeof tables_atestados; + "tables/ausencias": typeof tables_ausencias; + "tables/auth": typeof tables_auth; + "tables/chat": typeof tables_chat; + "tables/contratos": typeof tables_contratos; + "tables/cursos": typeof tables_cursos; + "tables/empresas": typeof tables_empresas; + "tables/enderecos": typeof tables_enderecos; + "tables/ferias": typeof tables_ferias; + "tables/flows": typeof tables_flows; + "tables/funcionarios": typeof tables_funcionarios; + "tables/licencas": typeof tables_licencas; + "tables/pedidos": typeof tables_pedidos; + "tables/ponto": typeof tables_ponto; + "tables/produtos": typeof tables_produtos; + "tables/security": typeof tables_security; + "tables/setores": typeof tables_setores; + "tables/system": typeof tables_system; + "tables/tickets": typeof tables_tickets; + "tables/times": typeof tables_times; templatesMensagens: typeof templatesMensagens; times: typeof times; - todos: typeof todos; usuarios: typeof usuarios; "utils/chatTemplateWrapper": typeof utils_chatTemplateWrapper; "utils/emailTemplateWrapper": typeof utils_emailTemplateWrapper; diff --git a/packages/backend/convex/contratos.ts b/packages/backend/convex/contratos.ts index 059101a..acfe6e1 100644 --- a/packages/backend/convex/contratos.ts +++ b/packages/backend/convex/contratos.ts @@ -1,200 +1,198 @@ -import { mutation, query } from "./_generated/server"; -import { v } from "convex/values"; -import { situacaoContrato } from "./schema"; -import { getCurrentUserFunction } from "./auth"; -import { internal } from "./_generated/api"; +import { mutation, query } from './_generated/server'; +import { v } from 'convex/values'; +import { situacaoContrato } from './tables/contratos'; +import { getCurrentUserFunction } from './auth'; +import { internal } from './_generated/api'; export const listar = query({ - args: { - responsavelId: v.optional(v.id("funcionarios")), - dataInicio: v.optional(v.string()), - dataFim: v.optional(v.string()), - }, - handler: async (ctx, args) => { - await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { - recurso: "contratos", - acao: "listar", - }); + args: { + responsavelId: v.optional(v.id('funcionarios')), + dataInicio: v.optional(v.string()), + dataFim: v.optional(v.string()) + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'contratos', + acao: 'listar' + }); - let q = ctx.db.query("contratos"); + let q = ctx.db.query('contratos'); - if (args.responsavelId) { - q = q.withIndex("by_responsavel", (q) => - q.eq("responsavelId", args.responsavelId!) - ) as typeof q; - } + if (args.responsavelId) { + q = q.withIndex('by_responsavel', (q) => + q.eq('responsavelId', args.responsavelId!) + ) as typeof q; + } - const contratos = await q.collect(); + const contratos = await q.collect(); - // Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo) - // Se o volume for muito grande, ideal seria criar índices específicos ou usar search. - let resultado = contratos; + // Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo) + // Se o volume for muito grande, ideal seria criar índices específicos ou usar search. + let resultado = contratos; - if (args.dataInicio) { - resultado = resultado.filter( - (c) => c.dataInicioVigencia >= args.dataInicio! - ); - } + if (args.dataInicio) { + resultado = resultado.filter((c) => c.dataInicioVigencia >= args.dataInicio!); + } - if (args.dataFim) { - resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!); - } + if (args.dataFim) { + resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!); + } - // Enriquecer com dados relacionados - const contratosEnriquecidos = await Promise.all( - resultado.map(async (c) => { - const contratada = await ctx.db.get(c.contratadaId); - const responsavel = await ctx.db.get(c.responsavelId); - return { - ...c, - contratada, - responsavel, - }; - }) - ); + // Enriquecer com dados relacionados + const contratosEnriquecidos = await Promise.all( + resultado.map(async (c) => { + const contratada = await ctx.db.get(c.contratadaId); + const responsavel = await ctx.db.get(c.responsavelId); + return { + ...c, + contratada, + responsavel + }; + }) + ); - return contratosEnriquecidos; - }, + return contratosEnriquecidos; + } }); export const obter = query({ - args: { id: v.id("contratos") }, - handler: async (ctx, args) => { - await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { - recurso: "contratos", - acao: "ver", - }); - const contrato = await ctx.db.get(args.id); - if (!contrato) return null; + args: { id: v.id('contratos') }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'contratos', + acao: 'ver' + }); + const contrato = await ctx.db.get(args.id); + if (!contrato) return null; - const contratada = await ctx.db.get(contrato.contratadaId); - const responsavel = await ctx.db.get(contrato.responsavelId); + const contratada = await ctx.db.get(contrato.contratadaId); + const responsavel = await ctx.db.get(contrato.responsavelId); - return { - ...contrato, - contratada, - responsavel, - }; - }, + return { + ...contrato, + contratada, + responsavel + }; + } }); export const criar = mutation({ - args: { - 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(), - }, - handler: async (ctx, args) => { - await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { - recurso: "contratos", - acao: "criar", - }); + args: { + 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() + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'contratos', + acao: 'criar' + }); - const usuario = await getCurrentUserFunction(ctx); - if (!usuario) throw new Error("Não autenticado"); + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) throw new Error('Não autenticado'); - const id = await ctx.db.insert("contratos", { - ...args, - criadoPor: usuario._id, - criadoEm: Date.now(), - }); + const id = await ctx.db.insert('contratos', { + ...args, + criadoPor: usuario._id, + criadoEm: Date.now() + }); - return id; - }, + return id; + } }); export const editar = mutation({ - args: { - id: v.id("contratos"), - contratadaId: v.optional(v.id("empresas")), - objeto: v.optional(v.string()), - numeroNotaEmpenho: v.optional(v.string()), - responsavelId: v.optional(v.id("funcionarios")), - departamento: v.optional(v.string()), - situacao: v.optional(situacaoContrato), - numeroProcessoLicitatorio: v.optional(v.string()), - modalidade: v.optional(v.string()), - numeroContrato: v.optional(v.string()), - anoContrato: v.optional(v.number()), - dataInicioVigencia: v.optional(v.string()), - dataFimVigencia: v.optional(v.string()), - nomeFiscal: v.optional(v.string()), - valorTotal: v.optional(v.string()), - dataAditivoPrazo: v.optional(v.string()), - diasAvisoVencimento: v.optional(v.number()), - }, - handler: async (ctx, args) => { - await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { - recurso: "contratos", - acao: "editar", - }); + args: { + id: v.id('contratos'), + contratadaId: v.optional(v.id('empresas')), + objeto: v.optional(v.string()), + numeroNotaEmpenho: v.optional(v.string()), + responsavelId: v.optional(v.id('funcionarios')), + departamento: v.optional(v.string()), + situacao: v.optional(situacaoContrato), + numeroProcessoLicitatorio: v.optional(v.string()), + modalidade: v.optional(v.string()), + numeroContrato: v.optional(v.string()), + anoContrato: v.optional(v.number()), + dataInicioVigencia: v.optional(v.string()), + dataFimVigencia: v.optional(v.string()), + nomeFiscal: v.optional(v.string()), + valorTotal: v.optional(v.string()), + dataAditivoPrazo: v.optional(v.string()), + diasAvisoVencimento: v.optional(v.number()) + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'contratos', + acao: 'editar' + }); - const { id, ...campos } = args; + const { id, ...campos } = args; - await ctx.db.patch(id, { - ...campos, - atualizadoEm: Date.now(), - }); - }, + await ctx.db.patch(id, { + ...campos, + atualizadoEm: Date.now() + }); + } }); export const excluir = mutation({ - args: { id: v.id("contratos") }, - handler: async (ctx, args) => { - await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { - recurso: "contratos", - acao: "excluir", - }); - await ctx.db.delete(args.id); - }, + args: { id: v.id('contratos') }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'contratos', + acao: 'excluir' + }); + await ctx.db.delete(args.id); + } }); export const verificarVencimentos = query({ - args: {}, - handler: async (ctx) => { - // Esta query pode ser usada por um componente de notificação ou cron job - // Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento + args: {}, + handler: async (ctx) => { + // Esta query pode ser usada por um componente de notificação ou cron job + // Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento - const hoje = new Date(); - const hojeStr = hoje.toISOString().split("T")[0]; + const hoje = new Date(); + const hojeStr = hoje.toISOString().split('T')[0]; - // Buscar contratos ativos (em execução ou aguardando assinatura) - const contratos = await ctx.db - .query("contratos") - .filter((q) => - q.or( - q.eq(q.field("situacao"), "em_execucao"), - q.eq(q.field("situacao"), "aguardando_assinatura") - ) - ) - .collect(); + // Buscar contratos ativos (em execução ou aguardando assinatura) + const contratos = await ctx.db + .query('contratos') + .filter((q) => + q.or( + q.eq(q.field('situacao'), 'em_execucao'), + q.eq(q.field('situacao'), 'aguardando_assinatura') + ) + ) + .collect(); - const proximosVencimento = contratos.filter((c) => { - if (!c.dataFimVigencia) return false; + const proximosVencimento = contratos.filter((c) => { + if (!c.dataFimVigencia) return false; - const dataFim = new Date(c.dataFimVigencia); - const dataAviso = new Date(dataFim); - dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento); + const dataFim = new Date(c.dataFimVigencia); + const dataAviso = new Date(dataFim); + dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento); - const dataAvisoStr = dataAviso.toISOString().split("T")[0]; + const dataAvisoStr = dataAviso.toISOString().split('T')[0]; - // Se hoje for maior ou igual a data de aviso e menor que a data fim - return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia; - }); + // Se hoje for maior ou igual a data de aviso e menor que a data fim + return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia; + }); - return proximosVencimento; - }, + return proximosVencimento; + } }); diff --git a/packages/backend/convex/flows.ts b/packages/backend/convex/flows.ts index 0749140..1292765 100644 --- a/packages/backend/convex/flows.ts +++ b/packages/backend/convex/flows.ts @@ -4,7 +4,7 @@ import { v } from 'convex/values'; import { getCurrentUserFunction } from './auth'; import type { Id, Doc } from './_generated/dataModel'; import type { MutationCtx, QueryCtx } from './_generated/server'; -import { flowTemplateStatus, flowInstanceStatus, flowInstanceStepStatus } from './schema'; +import { flowInstanceStatus, flowInstanceStepStatus, flowTemplateStatus } from './tables/flows'; // ============================================ // HELPER FUNCTIONS @@ -852,7 +852,7 @@ export const getInstanceWithSteps = query({ // Verificar permissão de visualização const temPermissaoVerTodas = await verificarPermissaoVerTodasFluxos(ctx); - + if (!temPermissaoVerTodas) { // Verificar se usuário pertence a algum setor do fluxo ou é o manager const pertenceAoSetor = await usuarioPertenceAAlgumSetorDoFluxo( @@ -860,7 +860,7 @@ export const getInstanceWithSteps = query({ usuario._id, instance._id ); - + if (!pertenceAoSetor && instance.managerId !== usuario._id) { return null; // Usuário não tem acesso } @@ -1066,7 +1066,8 @@ export const instantiateFlow = mutation({ for (let i = 0; i < templateSteps.length; i++) { const step = templateSteps[i]; - const dueDate = now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000; + const dueDate = + now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000; cumulativeDays += step.expectedDuration; const instanceStepId = await ctx.db.insert('flowInstanceSteps', { @@ -1202,7 +1203,12 @@ export const completeStep = mutation({ if (nextSetor && nextFlowStep) { const tituloProximoSetor = 'Nova Etapa de Fluxo Disponível'; const descricaoProximoSetor = `A etapa "${nextFlowStep.name}" do fluxo "${template?.name ?? 'Fluxo'}" está pronta para ser iniciada.`; - await criarNotificacaoParaSetor(ctx, nextStepData.setorId, tituloProximoSetor, descricaoProximoSetor); + await criarNotificacaoParaSetor( + ctx, + nextStepData.setorId, + tituloProximoSetor, + descricaoProximoSetor + ); } } } else { @@ -1303,7 +1309,9 @@ export const alterarGestorFluxo = mutation({ const eCriador = template?.createdBy === usuario._id; if (!eGestor && !temPermissao && !eCriador) { - throw new Error('Somente o gestor atual, criador do template ou usuário com permissão pode alterar o gestor'); + throw new Error( + 'Somente o gestor atual, criador do template ou usuário com permissão pode alterar o gestor' + ); } // Verificar se novo gestor existe @@ -1371,7 +1379,7 @@ export const reassignStep = mutation({ if (!eCriador) { // Se não for criador, verificar regra normal const etapaAnterior = await obterEtapaAnterior(ctx, args.instanceStepId); - + if (etapaAnterior) { // Se há etapa anterior, verificar se o usuário atual é a pessoa atribuída if (etapaAnterior.assignedToId) { @@ -1386,7 +1394,9 @@ export const reassignStep = mutation({ if (instance.managerId !== usuario._id) { const temPermissao = await verificarPermissaoVerTodasFluxos(ctx); if (!temPermissao) { - throw new Error('Somente o gerente do fluxo ou pessoa da etapa anterior pode atribuir esta etapa'); + throw new Error( + 'Somente o gerente do fluxo ou pessoa da etapa anterior pode atribuir esta etapa' + ); } } } @@ -1408,9 +1418,7 @@ export const reassignStep = mutation({ } // Verificar se o usuário atribuído corresponde a um funcionário do setor - const funcionarioDoUsuario = funcionariosDoSetor.find( - (f) => f.email === assignee.email - ); + const funcionarioDoUsuario = funcionariosDoSetor.find((f) => f.email === assignee.email); if (!funcionarioDoUsuario) { throw new Error('O funcionário atribuído não pertence ao setor deste passo'); @@ -1441,7 +1449,7 @@ export const updateStepNotes = mutation({ throw new Error('Passo não encontrado'); } - await ctx.db.patch(args.instanceStepId, { + await ctx.db.patch(args.instanceStepId, { notes: args.notes, notesUpdatedBy: usuario._id, notesUpdatedAt: Date.now() @@ -1526,7 +1534,9 @@ export const listarSubEtapas = query({ } else if (args.flowInstanceStepId) { subEtapas = await ctx.db .query('flowSubSteps') - .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) + .withIndex('by_flowInstanceStepId', (q) => + q.eq('flowInstanceStepId', args.flowInstanceStepId) + ) .collect(); } else { return []; @@ -1607,7 +1617,9 @@ export const criarSubEtapa = mutation({ } else if (args.flowInstanceStepId) { const existingSubEtapas = await ctx.db .query('flowSubSteps') - .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) + .withIndex('by_flowInstanceStepId', (q) => + q.eq('flowInstanceStepId', args.flowInstanceStepId) + ) .collect(); if (existingSubEtapas.length > 0) { maxPosition = Math.max(...existingSubEtapas.map((s) => s.position)); @@ -1766,7 +1778,9 @@ export const listarNotas = query({ } else if (args.flowInstanceStepId) { notas = await ctx.db .query('flowStepNotes') - .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) + .withIndex('by_flowInstanceStepId', (q) => + q.eq('flowInstanceStepId', args.flowInstanceStepId) + ) .collect(); } else if (args.flowSubStepId) { notas = await ctx.db @@ -1784,17 +1798,15 @@ export const listarNotas = query({ const notasComDetalhes = await Promise.all( notas.map(async (nota) => { const criador = await ctx.db.get(nota.criadoPor); - + // Obter informações dos arquivos const arquivosComNome = await Promise.all( nota.arquivos.map(async (storageId) => { // Buscar documento que referencia este storageId // Como não temos uma tabela direta, vamos buscar nos flowInstanceDocuments - const documentos = await ctx.db - .query('flowInstanceDocuments') - .collect(); + const documentos = await ctx.db.query('flowInstanceDocuments').collect(); const documento = documentos.find((d) => d.storageId === storageId); - + return { storageId, name: documento?.name ?? 'Arquivo' @@ -2003,7 +2015,9 @@ export const listDocumentsByStep = query({ handler: async (ctx, args) => { const documents = await ctx.db .query('flowInstanceDocuments') - .withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId)) + .withIndex('by_flowInstanceStepId', (q) => + q.eq('flowInstanceStepId', args.flowInstanceStepId) + ) .collect(); const result: Array<{ @@ -2158,4 +2172,3 @@ export const getUsuariosBySetorForAssignment = query({ return usuarios; } }); - diff --git a/packages/backend/convex/funcionarios.ts b/packages/backend/convex/funcionarios.ts index e84ed71..9e4400d 100644 --- a/packages/backend/convex/funcionarios.ts +++ b/packages/backend/convex/funcionarios.ts @@ -1,8 +1,8 @@ import { Infer, v } from 'convex/values'; import { query, mutation } from './_generated/server'; import { internal } from './_generated/api'; -import { simboloTipo } from './schema'; import { getCurrentUserFunction } from './auth'; +import { simboloTipo } from './tables/funcionarios'; // Validadores para campos opcionais const sexoValidator = v.optional( @@ -60,7 +60,7 @@ export const getAll = query({ recurso: 'funcionarios', acao: 'listar' }); - } catch (error) { + } catch { // Se não tiver permissão, retornar array vazio return []; } diff --git a/packages/backend/convex/logsAtividades.ts b/packages/backend/convex/logsAtividades.ts index 090957a..fc70a83 100644 --- a/packages/backend/convex/logsAtividades.ts +++ b/packages/backend/convex/logsAtividades.ts @@ -1,180 +1,180 @@ -import { v } from "convex/values"; -import { mutation, query, QueryCtx, MutationCtx } from "./_generated/server"; -import { Doc, Id } from "./_generated/dataModel"; +import { v } from 'convex/values'; +import { MutationCtx, query } from './_generated/server'; +import { Id } from './_generated/dataModel'; /** * Helper function para registrar atividades no sistema * Use em todas as mutations que modificam dados */ export async function registrarAtividade( - ctx: MutationCtx, - usuarioId: Id<"usuarios">, - acao: string, - recurso: string, - detalhes?: string, - recursoId?: string + ctx: MutationCtx, + usuarioId: Id<'usuarios'>, + acao: string, + recurso: string, + detalhes?: string, + recursoId?: string ) { - await ctx.db.insert("logsAtividades", { - usuarioId, - acao, - recurso, - recursoId, - detalhes, - timestamp: Date.now(), - }); + await ctx.db.insert('logsAtividades', { + usuarioId, + acao, + recurso, + recursoId, + detalhes, + timestamp: Date.now() + }); } /** * Lista atividades com filtros */ export const listarAtividades = query({ - args: { - usuarioId: v.optional(v.id("usuarios")), - acao: v.optional(v.string()), - recurso: v.optional(v.string()), - dataInicio: v.optional(v.number()), - dataFim: v.optional(v.number()), - limite: v.optional(v.number()), - }, - handler: async (ctx, args) => { - let atividades; + args: { + usuarioId: v.optional(v.id('usuarios')), + acao: v.optional(v.string()), + recurso: v.optional(v.string()), + dataInicio: v.optional(v.number()), + dataFim: v.optional(v.number()), + limite: v.optional(v.number()) + }, + handler: async (ctx, args) => { + let atividades; - if (args.usuarioId) { - atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId!)) - .order("desc") - .take(args.limite || 100); - } else if (args.acao) { - atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_acao", (q) => q.eq("acao", args.acao!)) - .order("desc") - .take(args.limite || 100); - } else if (args.recurso) { - atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_recurso", (q) => q.eq("recurso", args.recurso!)) - .order("desc") - .take(args.limite || 100); - } else { - atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_timestamp") - .order("desc") - .take(args.limite || 100); - } + if (args.usuarioId) { + atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId!)) + .order('desc') + .take(args.limite || 100); + } else if (args.acao) { + atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_acao', (q) => q.eq('acao', args.acao!)) + .order('desc') + .take(args.limite || 100); + } else if (args.recurso) { + atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_recurso', (q) => q.eq('recurso', args.recurso!)) + .order('desc') + .take(args.limite || 100); + } else { + atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_timestamp') + .order('desc') + .take(args.limite || 100); + } - // Filtrar por range de datas se fornecido - if (args.dataInicio || args.dataFim) { - atividades = atividades.filter((log) => { - if (args.dataInicio && log.timestamp < args.dataInicio) return false; - if (args.dataFim && log.timestamp > args.dataFim) return false; - return true; - }); - } + // Filtrar por range de datas se fornecido + if (args.dataInicio || args.dataFim) { + atividades = atividades.filter((log) => { + if (args.dataInicio && log.timestamp < args.dataInicio) return false; + if (args.dataFim && log.timestamp > args.dataFim) return false; + return true; + }); + } - // Buscar informações dos usuários - const atividadesComUsuarios = await Promise.all( - atividades.map(async (atividade) => { - const usuario = await ctx.db.get(atividade.usuarioId); - let matricula = "N/A"; - if (usuario?.funcionarioId) { - const funcionario = await ctx.db.get(usuario.funcionarioId); - matricula = funcionario?.matricula || "N/A"; - } - return { - ...atividade, - usuarioNome: usuario?.nome || "Usuário Desconhecido", - usuarioMatricula: matricula, - }; - }) - ); + // Buscar informações dos usuários + const atividadesComUsuarios = await Promise.all( + atividades.map(async (atividade) => { + const usuario = await ctx.db.get(atividade.usuarioId); + let matricula = 'N/A'; + if (usuario?.funcionarioId) { + const funcionario = await ctx.db.get(usuario.funcionarioId); + matricula = funcionario?.matricula || 'N/A'; + } + return { + ...atividade, + usuarioNome: usuario?.nome || 'Usuário Desconhecido', + usuarioMatricula: matricula + }; + }) + ); - return atividadesComUsuarios; - }, + return atividadesComUsuarios; + } }); /** * Obtém estatísticas de atividades */ export const obterEstatisticasAtividades = query({ - args: { - periodo: v.optional(v.number()), // dias (ex: 7, 30) - }, - handler: async (ctx, args) => { - const periodo = args.periodo || 30; - const dataInicio = Date.now() - periodo * 24 * 60 * 60 * 1000; + args: { + periodo: v.optional(v.number()) // dias (ex: 7, 30) + }, + handler: async (ctx, args) => { + const periodo = args.periodo || 30; + const dataInicio = Date.now() - periodo * 24 * 60 * 60 * 1000; - const atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_timestamp") - .filter((q) => q.gte(q.field("timestamp"), dataInicio)) - .collect(); + const atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_timestamp') + .filter((q) => q.gte(q.field('timestamp'), dataInicio)) + .collect(); - // Agrupar por ação - const porAcao: Record = {}; - atividades.forEach((ativ) => { - porAcao[ativ.acao] = (porAcao[ativ.acao] || 0) + 1; - }); + // Agrupar por ação + const porAcao: Record = {}; + atividades.forEach((ativ) => { + porAcao[ativ.acao] = (porAcao[ativ.acao] || 0) + 1; + }); - // Agrupar por recurso - const porRecurso: Record = {}; - atividades.forEach((ativ) => { - porRecurso[ativ.recurso] = (porRecurso[ativ.recurso] || 0) + 1; - }); + // Agrupar por recurso + const porRecurso: Record = {}; + atividades.forEach((ativ) => { + porRecurso[ativ.recurso] = (porRecurso[ativ.recurso] || 0) + 1; + }); - // Agrupar por dia - const porDia: Record = {}; - atividades.forEach((ativ) => { - const data = new Date(ativ.timestamp); - const dia = data.toISOString().split("T")[0]; - porDia[dia] = (porDia[dia] || 0) + 1; - }); + // Agrupar por dia + const porDia: Record = {}; + atividades.forEach((ativ) => { + const data = new Date(ativ.timestamp); + const dia = data.toISOString().split('T')[0]; + porDia[dia] = (porDia[dia] || 0) + 1; + }); - return { - total: atividades.length, - porAcao, - porRecurso, - porDia, - }; - }, + return { + total: atividades.length, + porAcao, + porRecurso, + porDia + }; + } }); /** * Obtém histórico de atividades de um recurso específico */ export const obterHistoricoRecurso = query({ - args: { - recurso: v.string(), - recursoId: v.string(), - }, - handler: async (ctx, args) => { - const atividades = await ctx.db - .query("logsAtividades") - .withIndex("by_recurso_id", (q) => - q.eq("recurso", args.recurso).eq("recursoId", args.recursoId) - ) - .order("desc") - .collect(); + args: { + recurso: v.string(), + recursoId: v.string() + }, + handler: async (ctx, args) => { + const atividades = await ctx.db + .query('logsAtividades') + .withIndex('by_recurso_id', (q) => + q.eq('recurso', args.recurso).eq('recursoId', args.recursoId) + ) + .order('desc') + .collect(); - // Buscar informações dos usuários - const atividadesComUsuarios = await Promise.all( - atividades.map(async (atividade) => { - const usuario = await ctx.db.get(atividade.usuarioId); - let matricula = "N/A"; - if (usuario?.funcionarioId) { - const funcionario = await ctx.db.get(usuario.funcionarioId); - matricula = funcionario?.matricula || "N/A"; - } - return { - ...atividade, - usuarioNome: usuario?.nome || "Usuário Desconhecido", - usuarioMatricula: matricula, - }; - }) - ); + // Buscar informações dos usuários + const atividadesComUsuarios = await Promise.all( + atividades.map(async (atividade) => { + const usuario = await ctx.db.get(atividade.usuarioId); + let matricula = 'N/A'; + if (usuario?.funcionarioId) { + const funcionario = await ctx.db.get(usuario.funcionarioId); + matricula = funcionario?.matricula || 'N/A'; + } + return { + ...atividade, + usuarioNome: usuario?.nome || 'Usuário Desconhecido', + usuarioMatricula: matricula + }; + }) + ); - return atividadesComUsuarios; - }, + return atividadesComUsuarios; + } }); diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 66cb6c8..387401f 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1,1902 +1,44 @@ -import { defineSchema, defineTable } from 'convex/server'; -import { Infer, v } from 'convex/values'; - -export const simboloTipo = v.union( - v.literal('cargo_comissionado'), - v.literal('funcao_gratificada') -); -export type SimboloTipo = Infer; - -export const ataqueCiberneticoTipo = v.union( - v.literal('phishing'), - v.literal('malware'), - v.literal('ransomware'), - v.literal('brute_force'), - v.literal('credential_stuffing'), - v.literal('sql_injection'), - v.literal('xss'), - v.literal('path_traversal'), - v.literal('command_injection'), - v.literal('nosql_injection'), - v.literal('xxe'), - v.literal('man_in_the_middle'), - v.literal('ddos'), - v.literal('engenharia_social'), - v.literal('cve_exploit'), - v.literal('apt'), - v.literal('zero_day'), - v.literal('supply_chain'), - v.literal('fileless_malware'), - v.literal('polymorphic_malware'), - v.literal('ransomware_lateral'), - v.literal('deepfake_phishing'), - v.literal('adversarial_ai'), - v.literal('side_channel'), - v.literal('firmware_bootloader'), - v.literal('bec'), - v.literal('botnet'), - v.literal('ot_ics'), - v.literal('quantum_attack') -); -export type AtaqueCiberneticoTipo = Infer; - -export const severidadeSeguranca = v.union( - v.literal('informativo'), - v.literal('baixo'), - v.literal('moderado'), - v.literal('alto'), - v.literal('critico') -); -export type SeveridadeSeguranca = Infer; - -export const statusEventoSeguranca = v.union( - v.literal('detectado'), - v.literal('investigando'), - v.literal('contido'), - v.literal('falso_positivo'), - v.literal('escalado'), - v.literal('resolvido') -); -export type StatusEventoSeguranca = Infer; - -export const sensorSegurancaTipo = v.union( - v.literal('network'), - v.literal('endpoint'), - v.literal('application'), - v.literal('gateway'), - v.literal('ot'), - v.literal('honeypot') -); -export type SensorSegurancaTipo = Infer; - -export const sensorSegurancaStatus = v.union( - v.literal('ativo'), - v.literal('inativo'), - v.literal('degradado'), - v.literal('manutencao') -); -export type SensorSegurancaStatus = Infer; - -export const threatIntelTipo = v.union( - v.literal('open_source'), - v.literal('commercial'), - v.literal('internal'), - v.literal('gov'), - v.literal('research') -); - -export const threatIntelFormato = v.union( - v.literal('json'), - v.literal('stix'), - v.literal('csv'), - v.literal('text'), - v.literal('custom') -); - -export const acaoIncidenteTipo = v.union( - v.literal('block_ip'), - v.literal('unblock_ip'), - v.literal('block_port'), - v.literal('liberar_porta'), - v.literal('notificar'), - v.literal('isolar_host'), - v.literal('gerar_relatorio'), - v.literal('criar_ticket'), - v.literal('ajuste_regra'), - v.literal('custom') -); - -export const acaoIncidenteStatus = v.union( - v.literal('pendente'), - v.literal('executando'), - v.literal('concluido'), - v.literal('falhou') -); - -export const reportStatus = v.union( - v.literal('pendente'), - v.literal('processando'), - v.literal('concluido'), - v.literal('falhou') -); - -// Status de templates de fluxo -export const flowTemplateStatus = v.union( - v.literal('draft'), - v.literal('published'), - v.literal('archived') -); -export type FlowTemplateStatus = Infer; - -// Status de instâncias de fluxo -export const flowInstanceStatus = v.union( - v.literal('active'), - v.literal('completed'), - v.literal('cancelled') -); -export type FlowInstanceStatus = Infer; - -// Status de passos de instância de fluxo -export const flowInstanceStepStatus = v.union( - v.literal('pending'), - v.literal('in_progress'), - v.literal('completed'), - v.literal('blocked') -); -export type FlowInstanceStepStatus = Infer; - -export const situacaoContrato = v.union( - v.literal('em_execucao'), - v.literal('rescendido'), - v.literal('aguardando_assinatura'), - v.literal('finalizado') -); +import { defineSchema } from 'convex/server'; +import { setoresTables } from './tables/setores'; +import { contratosTables } from './tables/contratos'; +import { enderecosTables } from './tables/enderecos'; +import { empresasTable } from './tables/empresas'; +import { funcionariosTables } from './tables/funcionarios'; +import { flowsTables } from './tables/flows'; +import { atestadosTables } from './tables/atestados'; +import { licencasTables } from './tables/licencas'; +import { feriasTables } from './tables/ferias'; +import { ausenciasTables } from './tables/ausencias'; +import { timesTables } from './tables/times'; +import { cursosTables } from './tables/cursos'; +import { authTables } from './tables/auth'; +import { systemTables } from './tables/system'; +import { chatTables } from './tables/chat'; +import { ticketsTables } from './tables/tickets'; +import { securityTables } from './tables/security'; +import { pontoTables } from './tables/ponto'; +import { pedidosTables } from './tables/pedidos'; +import { produtosTables } from './tables/produtos'; export default defineSchema({ - // Setores da organização - setores: defineTable({ - nome: v.string(), - sigla: v.string(), - criadoPor: v.id('usuarios'), - createdAt: v.number() - }) - .index('by_nome', ['nome']) - .index('by_sigla', ['sigla']), - - // Relação muitos-para-muitos entre funcionários e setores - funcionarioSetores: defineTable({ - funcionarioId: v.id('funcionarios'), - setorId: v.id('setores'), - createdAt: v.number() - }) - .index('by_funcionarioId', ['funcionarioId']) - .index('by_setorId', ['setorId']) - .index('by_funcionarioId_and_setorId', ['funcionarioId', 'setorId']), - - // Templates de fluxo - flowTemplates: defineTable({ - name: v.string(), - description: v.optional(v.string()), - status: flowTemplateStatus, - createdBy: v.id('usuarios'), - createdAt: v.number() - }) - .index('by_status', ['status']) - .index('by_createdBy', ['createdBy']), - - // Passos de template de fluxo - flowSteps: defineTable({ - flowTemplateId: v.id('flowTemplates'), - name: v.string(), - description: v.optional(v.string()), - position: v.number(), - expectedDuration: v.number(), // em dias - setorId: v.id('setores'), - defaultAssigneeId: v.optional(v.id('usuarios')), - requiredDocuments: v.optional(v.array(v.string())) - }) - .index('by_flowTemplateId', ['flowTemplateId']) - .index('by_flowTemplateId_and_position', ['flowTemplateId', 'position']), - - // Instâncias de fluxo - flowInstances: defineTable({ - flowTemplateId: v.id('flowTemplates'), - contratoId: v.optional(v.id('contratos')), - managerId: v.id('usuarios'), - status: flowInstanceStatus, - startedAt: v.number(), - finishedAt: v.optional(v.number()), - currentStepId: v.optional(v.id('flowInstanceSteps')) - }) - .index('by_flowTemplateId', ['flowTemplateId']) - .index('by_contratoId', ['contratoId']) - .index('by_managerId', ['managerId']) - .index('by_status', ['status']), - - // Passos de instância de fluxo - flowInstanceSteps: defineTable({ - flowInstanceId: v.id('flowInstances'), - flowStepId: v.id('flowSteps'), - setorId: v.id('setores'), - assignedToId: v.optional(v.id('usuarios')), - status: flowInstanceStepStatus, - startedAt: v.optional(v.number()), - finishedAt: v.optional(v.number()), - notes: v.optional(v.string()), - notesUpdatedBy: v.optional(v.id('usuarios')), - notesUpdatedAt: v.optional(v.number()), - dueDate: v.optional(v.number()) - }) - .index('by_flowInstanceId', ['flowInstanceId']) - .index('by_flowInstanceId_and_status', ['flowInstanceId', 'status']) - .index('by_setorId', ['setorId']) - .index('by_assignedToId', ['assignedToId']), - - // Documentos de instância de fluxo - flowInstanceDocuments: defineTable({ - flowInstanceStepId: v.id('flowInstanceSteps'), - uploadedById: v.id('usuarios'), - storageId: v.id('_storage'), - name: v.string(), - uploadedAt: v.number() - }) - .index('by_flowInstanceStepId', ['flowInstanceStepId']) - .index('by_uploadedById', ['uploadedById']), - - // Sub-etapas de fluxo (para templates e instâncias) - flowSubSteps: defineTable({ - flowStepId: v.optional(v.id('flowSteps')), // Para templates - flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), // Para instâncias - name: v.string(), - description: v.optional(v.string()), - status: v.union( - v.literal('pending'), - v.literal('in_progress'), - v.literal('completed'), - v.literal('blocked') - ), - position: v.number(), - createdBy: v.id('usuarios'), - createdAt: v.number() - }) - .index('by_flowStepId', ['flowStepId']) - .index('by_flowInstanceStepId', ['flowInstanceStepId']), - - // Notas de steps e sub-etapas - flowStepNotes: defineTable({ - flowStepId: v.optional(v.id('flowSteps')), - flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), - flowSubStepId: v.optional(v.id('flowSubSteps')), - texto: v.string(), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - arquivos: v.array(v.id('_storage')) - }) - .index('by_flowStepId', ['flowStepId']) - .index('by_flowInstanceStepId', ['flowInstanceStepId']) - .index('by_flowSubStepId', ['flowSubStepId']), - - contratos: defineTable({ - contratadaId: v.id('empresas'), - objeto: v.string(), - numeroNotaEmpenho: v.string(), - responsavelId: v.id('funcionarios'), - departamento: v.string(), - situacao: situacaoContrato, - numeroProcessoLicitatorio: v.string(), - modalidade: v.string(), - numeroContrato: v.string(), - anoContrato: v.number(), - dataInicioVigencia: v.string(), - dataFimVigencia: v.string(), - nomeFiscal: v.string(), - valorTotal: v.string(), - dataAditivoPrazo: v.optional(v.string()), - diasAvisoVencimento: v.number(), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoEm: v.optional(v.number()) - }) - .index('by_responsavel', ['responsavelId']) - .index('by_situacao', ['situacao']) - .index('by_vigencia_inicio', ['dataInicioVigencia']) - .index('by_vigencia_fim', ['dataFimVigencia']), - - todos: defineTable({ - text: v.string(), - completed: v.boolean() - }), - enderecos: defineTable({ - cep: v.string(), - logradouro: v.string(), - numero: v.string(), - complemento: v.optional(v.string()), - bairro: v.string(), - cidade: v.string(), - uf: v.string(), - criadoPor: v.optional(v.id('usuarios')), - atualizadoPor: v.optional(v.id('usuarios')) - }).index('by_cep', ['cep']), - empresas: defineTable({ - razao_social: v.string(), - nome_fantasia: v.optional(v.string()), - cnpj: v.string(), - telefone: v.string(), - email: v.string(), - descricao: v.optional(v.string()), - enderecoId: v.optional(v.id('enderecos')), - criadoPor: v.optional(v.id('usuarios')) - }) - .index('by_razao_social', ['razao_social']) - .index('by_cnpj', ['cnpj']), - contatosEmpresa: defineTable({ - empresaId: v.id('empresas'), - nome: v.string(), - funcao: v.string(), - email: v.string(), - telefone: v.string(), - adicionadoPor: v.optional(v.id('usuarios')), - descricao: v.optional(v.string()) - }) - .index('by_empresa', ['empresaId']) - .index('by_email', ['email']), - funcionarios: defineTable({ - // Campos obrigatórios existentes - nome: v.string(), - nascimento: v.string(), - rg: v.string(), - cpf: v.string(), - endereco: v.string(), - cep: v.string(), - cidade: v.string(), - uf: v.string(), - telefone: v.string(), - email: v.string(), - matricula: v.optional(v.string()), - admissaoData: v.optional(v.string()), - desligamentoData: v.optional(v.string()), - simboloId: v.id('simbolos'), - simboloTipo: simboloTipo, - gestorId: v.optional(v.id('usuarios')), - statusFerias: v.optional(v.union(v.literal('ativo'), v.literal('em_ferias'))), - - // Regime de trabalho (para cálculo correto de férias) - regimeTrabalho: v.optional( - v.union( - v.literal('clt'), // CLT - Consolidação das Leis do Trabalho - v.literal('estatutario_pe'), // Servidor Público Estadual de Pernambuco - v.literal('estatutario_federal'), // Servidor Público Federal - v.literal('estatutario_municipal') // Servidor Público Municipal - ) - ), - - // Dados Pessoais Adicionais (opcionais) - nomePai: v.optional(v.string()), - nomeMae: v.optional(v.string()), - naturalidade: v.optional(v.string()), - naturalidadeUF: v.optional(v.string()), - sexo: v.optional(v.union(v.literal('masculino'), v.literal('feminino'), v.literal('outro'))), - estadoCivil: v.optional( - v.union( - v.literal('solteiro'), - v.literal('casado'), - v.literal('divorciado'), - v.literal('viuvo'), - v.literal('uniao_estavel') - ) - ), - nacionalidade: v.optional(v.string()), - - // Documentos Pessoais - rgOrgaoExpedidor: v.optional(v.string()), - rgDataEmissao: v.optional(v.string()), - carteiraProfissionalNumero: v.optional(v.string()), - carteiraProfissionalSerie: v.optional(v.string()), - carteiraProfissionalDataEmissao: v.optional(v.string()), - reservistaNumero: v.optional(v.string()), - reservistaSerie: v.optional(v.string()), - tituloEleitorNumero: v.optional(v.string()), - tituloEleitorZona: v.optional(v.string()), - tituloEleitorSecao: v.optional(v.string()), - pisNumero: v.optional(v.string()), - - // Formação e Saúde - grauInstrucao: v.optional( - v.union( - v.literal('fundamental'), - v.literal('medio'), - v.literal('superior'), - v.literal('pos_graduacao'), - v.literal('mestrado'), - v.literal('doutorado') - ) - ), - formacao: v.optional(v.string()), - formacaoRegistro: v.optional(v.string()), - grupoSanguineo: v.optional( - v.union(v.literal('A'), v.literal('B'), v.literal('AB'), v.literal('O')) - ), - fatorRH: v.optional(v.union(v.literal('positivo'), v.literal('negativo'))), - - // Cargo e Vínculo - descricaoCargo: v.optional(v.string()), - nomeacaoPortaria: v.optional(v.string()), - nomeacaoData: v.optional(v.string()), - nomeacaoDOE: v.optional(v.string()), - pertenceOrgaoPublico: v.optional(v.boolean()), - orgaoOrigem: v.optional(v.string()), - aposentado: v.optional(v.union(v.literal('nao'), v.literal('funape_ipsep'), v.literal('inss'))), - - // Dados Bancários - contaBradescoNumero: v.optional(v.string()), - contaBradescoDV: v.optional(v.string()), - contaBradescoAgencia: v.optional(v.string()), - - // Documentos Anexos (Storage IDs) - certidaoAntecedentesPF: v.optional(v.id('_storage')), - certidaoAntecedentesJFPE: v.optional(v.id('_storage')), - certidaoAntecedentesSDS: v.optional(v.id('_storage')), - certidaoAntecedentesTJPE: v.optional(v.id('_storage')), - certidaoImprobidade: v.optional(v.id('_storage')), - rgFrente: v.optional(v.id('_storage')), - rgVerso: v.optional(v.id('_storage')), - cpfFrente: v.optional(v.id('_storage')), - cpfVerso: v.optional(v.id('_storage')), - situacaoCadastralCPF: v.optional(v.id('_storage')), - tituloEleitorFrente: v.optional(v.id('_storage')), - tituloEleitorVerso: v.optional(v.id('_storage')), - comprovanteVotacao: v.optional(v.id('_storage')), - carteiraProfissionalFrente: v.optional(v.id('_storage')), - carteiraProfissionalVerso: v.optional(v.id('_storage')), - comprovantePIS: v.optional(v.id('_storage')), - certidaoRegistroCivil: v.optional(v.id('_storage')), - certidaoNascimentoDependentes: v.optional(v.id('_storage')), - cpfDependentes: v.optional(v.id('_storage')), - reservistaDoc: v.optional(v.id('_storage')), - comprovanteEscolaridade: v.optional(v.id('_storage')), - comprovanteResidencia: v.optional(v.id('_storage')), - comprovanteContaBradesco: v.optional(v.id('_storage')), - - // Dependentes do funcionário (uploads opcionais) - dependentes: v.optional( - v.array( - v.object({ - parentesco: v.union( - v.literal('filho'), - v.literal('filha'), - v.literal('conjuge'), - v.literal('outro') - ), - nome: v.string(), - cpf: v.string(), - nascimento: v.string(), - documentoId: v.optional(v.id('_storage')), - // Benefícios/declarações por dependente - salarioFamilia: v.optional(v.boolean()), - impostoRenda: v.optional(v.boolean()) - }) - ) - ), - - // Declarações (Storage IDs) - declaracaoAcumulacaoCargo: v.optional(v.id('_storage')), - declaracaoDependentesIR: v.optional(v.id('_storage')), - declaracaoIdoneidade: v.optional(v.id('_storage')), - termoNepotismo: v.optional(v.id('_storage')), - termoOpcaoRemuneracao: v.optional(v.id('_storage')) - }) - .index('by_matricula', ['matricula']) - .index('by_nome', ['nome']) - .index('by_simboloId', ['simboloId']) - .index('by_simboloTipo', ['simboloTipo']) - .index('by_cpf', ['cpf']) - .index('by_rg', ['rg']) - .index('by_gestor', ['gestorId']), - - atestados: defineTable({ - funcionarioId: v.id('funcionarios'), - tipo: v.union(v.literal('atestado_medico'), v.literal('declaracao_comparecimento')), - dataInicio: v.string(), - dataFim: v.string(), - cid: v.optional(v.string()), // Apenas para atestado médico - observacoes: v.optional(v.string()), - documentoId: v.optional(v.id('_storage')), - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_tipo', ['tipo']) - .index('by_data_inicio', ['dataInicio']) - .index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']), - - licencas: defineTable({ - funcionarioId: v.id('funcionarios'), - tipo: v.union(v.literal('maternidade'), v.literal('paternidade')), - dataInicio: v.string(), - dataFim: v.string(), - documentoId: v.optional(v.id('_storage')), - observacoes: v.optional(v.string()), - licencaOriginalId: v.optional(v.id('licencas')), // Para prorrogações - ehProrrogacao: v.boolean(), - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_tipo', ['tipo']) - .index('by_data_inicio', ['dataInicio']) - .index('by_licenca_original', ['licencaOriginalId']) - .index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']), - - ferias: defineTable({ - funcionarioId: v.id('funcionarios'), - anoReferencia: v.number(), - dataInicio: v.string(), - dataFim: v.string(), - diasFerias: v.number(), - status: v.union( - v.literal('aguardando_aprovacao'), - v.literal('aprovado'), - v.literal('reprovado'), - v.literal('data_ajustada_aprovada'), - v.literal('EmFérias'), - v.literal('Cancelado_RH') - ), - gestorId: v.optional(v.id('usuarios')), - observacao: v.optional(v.string()), - motivoReprovacao: v.optional(v.string()), - dataAprovacao: v.optional(v.number()), - dataReprovacao: v.optional(v.number()), - diasAbono: v.number(), - historicoAlteracoes: v.optional( - v.array( - v.object({ - data: v.number(), - usuarioId: v.id('usuarios'), - acao: v.string() - }) - ) - ) - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_funcionario_and_ano', ['funcionarioId', 'anoReferencia']) - .index('by_funcionario_and_status', ['funcionarioId', 'status']) - .index('by_status', ['status']) - .index('by_ano', ['anoReferencia']), - - notificacoesFerias: defineTable({ - destinatarioId: v.id('usuarios'), - feriasId: v.id('ferias'), - tipo: v.union( - v.literal('nova_solicitacao'), - v.literal('aprovado'), - v.literal('reprovado'), - v.literal('data_ajustada') - ), - lida: v.boolean(), - mensagem: v.string() - }) - .index('by_destinatario', ['destinatarioId']) - .index('by_destinatario_and_lida', ['destinatarioId', 'lida']), - - // Solicitações de Ausências - solicitacoesAusencias: defineTable({ - funcionarioId: v.id('funcionarios'), - dataInicio: v.string(), - dataFim: v.string(), - motivo: v.string(), - status: v.union( - v.literal('aguardando_aprovacao'), - v.literal('aprovado'), - v.literal('reprovado') - ), - gestorId: v.optional(v.id('usuarios')), - dataAprovacao: v.optional(v.number()), - dataReprovacao: v.optional(v.number()), - motivoReprovacao: v.optional(v.string()), - observacao: v.optional(v.string()), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_status', ['status']) - .index('by_funcionario_and_status', ['funcionarioId', 'status']), - - notificacoesAusencias: defineTable({ - destinatarioId: v.id('usuarios'), - solicitacaoAusenciaId: v.id('solicitacoesAusencias'), - tipo: v.union(v.literal('nova_solicitacao'), v.literal('aprovado'), v.literal('reprovado')), - lida: v.boolean(), - mensagem: v.string() - }) - .index('by_destinatario', ['destinatarioId']) - .index('by_destinatario_and_lida', ['destinatarioId', 'lida']), - - times: defineTable({ - nome: v.string(), - descricao: v.optional(v.string()), - gestorId: v.id('usuarios'), - gestorSuperiorId: v.optional(v.id('usuarios')), - ativo: v.boolean(), - cor: v.optional(v.string()) // Cor para identificação visual - }) - .index('by_gestor', ['gestorId']) - .index('by_gestor_superior', ['gestorSuperiorId']), - - timesMembros: defineTable({ - timeId: v.id('times'), - funcionarioId: v.id('funcionarios'), - dataEntrada: v.number(), - dataSaida: v.optional(v.number()), - ativo: v.boolean() - }) - .index('by_time', ['timeId']) - .index('by_funcionario', ['funcionarioId']) - .index('by_time_and_ativo', ['timeId', 'ativo']), - - cursos: defineTable({ - funcionarioId: v.id('funcionarios'), - descricao: v.string(), - data: v.string(), - certificadoId: v.optional(v.id('_storage')) - }).index('by_funcionario', ['funcionarioId']), - - simbolos: defineTable({ - nome: v.string(), - tipo: simboloTipo, - descricao: v.string(), - vencValor: v.string(), - repValor: v.string(), - valor: v.string() - }), - - // Sistema de Autenticação e Controle de Acesso - usuarios: defineTable({ - authId: v.string(), - nome: v.string(), - email: v.string(), - funcionarioId: v.optional(v.id('funcionarios')), - roleId: v.id('roles'), - ativo: v.boolean(), - primeiroAcesso: v.boolean(), - ultimoAcesso: v.optional(v.number()), - criadoEm: v.number(), - atualizadoEm: v.number(), - - // Controle de Bloqueio e Segurança - bloqueado: v.optional(v.boolean()), - motivoBloqueio: v.optional(v.string()), - dataBloqueio: v.optional(v.number()), - tentativasLogin: v.optional(v.number()), // contador de tentativas falhas - ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa - - // Campos de Chat e Perfil - - fotoPerfil: v.optional(v.id('_storage')), - avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear) - setor: v.optional(v.string()), - statusMensagem: v.optional(v.string()), // max 100 chars - statusPresenca: v.optional( - v.union( - v.literal('online'), - v.literal('offline'), - v.literal('ausente'), - v.literal('externo'), - v.literal('em_reuniao') - ) - ), - ultimaAtividade: v.optional(v.number()), // timestamp - notificacoesAtivadas: v.optional(v.boolean()), - somNotificacao: v.optional(v.boolean()), - temaPreferido: v.optional(v.string()) // tema de aparência escolhido pelo usuário - }) - .index('by_email', ['email']) - .index('by_role', ['roleId']) - .index('by_ativo', ['ativo']) - .index('by_status_presenca', ['statusPresenca']) - .index('by_bloqueado', ['bloqueado']) - .index('by_funcionarioId', ['funcionarioId']) - .index('authId', ['authId']), - - roles: defineTable({ - nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario" - descricao: v.string(), - nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado - setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc. - customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER - criadoPor: v.optional(v.id('usuarios')), // usuário TI_MASTER que criou este perfil - editavel: v.optional(v.boolean()) // se pode ser editado (false para roles fixas) - }) - .index('by_nome', ['nome']) - .index('by_nivel', ['nivel']) - .index('by_setor', ['setor']) - .index('by_customizado', ['customizado']), - - permissoes: defineTable({ - nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc. - descricao: v.string(), - recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc. - acao: v.string() // "criar", "ler", "editar", "excluir" - }) - .index('by_recurso', ['recurso']) - .index('by_recurso_e_acao', ['recurso', 'acao']) - .index('by_nome', ['nome']), - - rolePermissoes: defineTable({ - roleId: v.id('roles'), - permissaoId: v.id('permissoes') - }) - .index('by_role', ['roleId']) - .index('by_permissao', ['permissaoId']), - - sessoes: defineTable({ - usuarioId: v.id('usuarios'), - token: v.string(), - ipAddress: v.optional(v.string()), - userAgent: v.optional(v.string()), - criadoEm: v.number(), - expiraEm: v.number(), - ativo: v.boolean() - }) - .index('by_usuario', ['usuarioId']) - .index('by_token', ['token']) - .index('by_ativo', ['ativo']) - .index('by_expiracao', ['expiraEm']), - - logsAcesso: defineTable({ - usuarioId: v.id('usuarios'), - tipo: v.union( - v.literal('login'), - v.literal('logout'), - v.literal('acesso_negado'), - v.literal('senha_alterada'), - v.literal('sessao_expirada') - ), - ipAddress: v.optional(v.string()), - userAgent: v.optional(v.string()), - detalhes: v.optional(v.string()), - timestamp: v.number() - }) - .index('by_usuario', ['usuarioId']) - .index('by_tipo', ['tipo']) - .index('by_timestamp', ['timestamp']), - - // Logs de Atividades - logsAtividades: defineTable({ - usuarioId: v.id('usuarios'), - acao: v.string(), // "criar", "editar", "excluir", "bloquear", "desbloquear", etc. - recurso: v.string(), // "funcionarios", "simbolos", "usuarios", "perfis", etc. - recursoId: v.optional(v.string()), // ID do recurso afetado - detalhes: v.optional(v.string()), // JSON com detalhes da ação - timestamp: v.number() - }) - .index('by_usuario', ['usuarioId']) - .index('by_acao', ['acao']) - .index('by_recurso', ['recurso']) - .index('by_timestamp', ['timestamp']) - .index('by_recurso_id', ['recurso', 'recursoId']), - - // Histórico de Bloqueios - bloqueiosUsuarios: defineTable({ - usuarioId: v.id('usuarios'), - motivo: v.string(), - bloqueadoPor: v.id('usuarios'), // ID do TI_MASTER que bloqueou - dataInicio: v.number(), - dataFim: v.optional(v.number()), // quando foi desbloqueado - desbloqueadoPor: v.optional(v.id('usuarios')), - ativo: v.boolean() // se é o bloqueio atual ativo - }) - .index('by_usuario', ['usuarioId']) - .index('by_bloqueado_por', ['bloqueadoPor']) - .index('by_ativo', ['ativo']) - .index('by_data_inicio', ['dataInicio']), - - // Configuração de Email/SMTP - configuracaoEmail: defineTable({ - servidor: v.string(), // smtp.gmail.com - porta: v.number(), // 587, 465, etc. - usuario: v.string(), - senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP - emailRemetente: v.string(), - nomeRemetente: v.string(), - usarSSL: v.boolean(), - usarTLS: v.boolean(), - ativo: v.boolean(), - testadoEm: v.optional(v.number()), - configuradoPor: v.id('usuarios'), - atualizadoEm: v.number() - }).index('by_ativo', ['ativo']), - - // Fila de Emails - notificacoesEmail: defineTable({ - destinatario: v.string(), // email - destinatarioId: v.optional(v.id('usuarios')), - assunto: v.string(), - corpo: v.string(), // HTML ou texto - templateId: v.optional(v.id('templatesMensagens')), - status: v.union( - v.literal('pendente'), - v.literal('enviando'), - v.literal('enviado'), - v.literal('falha') - ), - tentativas: v.number(), - ultimaTentativa: v.optional(v.number()), - erroDetalhes: v.optional(v.string()), - enviadoPor: v.id('usuarios'), - criadoEm: v.number(), - enviadoEm: v.optional(v.number()), - agendadaPara: v.optional(v.number()) // timestamp para agendamento - }) - .index('by_status', ['status']) - .index('by_destinatario', ['destinatarioId']) - .index('by_enviado_por', ['enviadoPor']) - .index('by_criado_em', ['criadoEm']) - .index('by_agendamento', ['agendadaPara']), - - configuracaoAcesso: defineTable({ - chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc. - valor: v.string(), - descricao: v.string() - }).index('by_chave', ['chave']), - - // Rate Limiting de Emails - rateLimitEmails: defineTable({ - remetenteId: v.id('usuarios'), - timestamp: v.number(), - contador: v.number(), // quantidade de emails enviados neste período - periodo: v.union( - v.literal('minuto'), // último minuto - v.literal('hora') // última hora - ) - }) - .index('by_remetente_periodo', ['remetenteId', 'periodo', 'timestamp']) - .index('by_timestamp', ['timestamp']), - - // Sistema de Chat - conversas: defineTable({ - tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')), - nome: v.optional(v.string()), // nome do grupo/sala - - participantes: v.array(v.id('usuarios')), // IDs dos participantes - administradores: v.optional(v.array(v.id('usuarios'))), // IDs dos administradores (apenas para sala_reuniao) - ultimaMensagem: v.optional(v.string()), - ultimaMensagemTimestamp: v.optional(v.number()), - ultimaMensagemRemetenteId: v.optional(v.id('usuarios')), // ID do remetente da última mensagem - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_criado_por', ['criadoPor']) - .index('by_tipo', ['tipo']) - .index('by_ultima_mensagem', ['ultimaMensagemTimestamp']), - - mensagens: defineTable({ - conversaId: v.id('conversas'), - remetenteId: v.id('usuarios'), - tipo: v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem')), - conteudo: v.string(), // texto ou nome do arquivo - conteudoBusca: v.optional(v.string()), // versão normalizada para busca - arquivoId: v.optional(v.id('_storage')), - arquivoNome: v.optional(v.string()), - arquivoTamanho: v.optional(v.number()), - arquivoTipo: v.optional(v.string()), - linkPreview: v.optional( - v.object({ - url: v.string(), - titulo: v.optional(v.string()), - descricao: v.optional(v.string()), - imagem: v.optional(v.string()), - site: v.optional(v.string()) - }) - ), - reagiuPor: v.optional( - v.array( - v.object({ - usuarioId: v.id('usuarios'), - emoji: v.string() - }) - ) - ), - mencoes: v.optional(v.array(v.id('usuarios'))), - respostaPara: v.optional(v.id('mensagens')), // ID da mensagem que está respondendo - agendadaPara: v.optional(v.number()), // timestamp - enviadaEm: v.number(), - editadaEm: v.optional(v.number()), - deletada: v.optional(v.boolean()), - lidaPor: v.optional(v.array(v.id('usuarios'))) // IDs dos usuários que leram a mensagem - }) - .index('by_conversa', ['conversaId', 'enviadaEm']) - .index('by_remetente', ['remetenteId']) - .index('by_agendamento', ['agendadaPara']) - .index('by_resposta', ['respostaPara']), - - leituras: defineTable({ - conversaId: v.id('conversas'), - usuarioId: v.id('usuarios'), - ultimaMensagemLida: v.id('mensagens'), - lidaEm: v.number() - }) - .index('by_conversa_usuario', ['conversaId', 'usuarioId']) - .index('by_usuario', ['usuarioId']), - - // Sistema de Chamadas de Áudio/Vídeo - chamadas: defineTable({ - conversaId: v.id('conversas'), - tipo: v.union(v.literal('audio'), v.literal('video')), - roomName: v.string(), // Nome único da sala Jitsi - criadoPor: v.id('usuarios'), // Anfitrião/criador - participantes: v.array(v.id('usuarios')), - status: v.union( - v.literal('aguardando'), - v.literal('em_andamento'), - v.literal('finalizada'), - v.literal('cancelada') - ), - iniciadaEm: v.optional(v.number()), - finalizadaEm: v.optional(v.number()), - duracaoSegundos: v.optional(v.number()), - gravando: v.boolean(), - gravacaoIniciadaPor: v.optional(v.id('usuarios')), - gravacaoIniciadaEm: v.optional(v.number()), - gravacaoFinalizadaEm: v.optional(v.number()), - configuracoes: v.optional( - v.object({ - audioHabilitado: v.boolean(), - videoHabilitado: v.boolean(), - participantesConfig: v.optional( - v.array( - v.object({ - usuarioId: v.id('usuarios'), - audioHabilitado: v.boolean(), - videoHabilitado: v.boolean(), - forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião - }) - ) - ) - }) - ), - criadoEm: v.number() - }) - .index('by_conversa', ['conversaId', 'status']) - .index('by_criado_por', ['criadoPor']) - .index('by_status', ['status']) - .index('by_room_name', ['roomName']), - - notificacoes: defineTable({ - usuarioId: v.id('usuarios'), - tipo: v.union( - v.literal('nova_mensagem'), - v.literal('mencao'), - v.literal('grupo_criado'), - v.literal('adicionado_grupo'), - v.literal('alerta_seguranca'), - v.literal('etapa_fluxo_concluida') - ), - conversaId: v.optional(v.id('conversas')), - mensagemId: v.optional(v.id('mensagens')), - remetenteId: v.optional(v.id('usuarios')), - titulo: v.string(), - descricao: v.string(), - lida: v.boolean(), - criadaEm: v.number() - }) - .index('by_usuario', ['usuarioId', 'lida', 'criadaEm']) - .index('by_usuario_lida', ['usuarioId', 'lida']), - - digitando: defineTable({ - conversaId: v.id('conversas'), - usuarioId: v.id('usuarios'), - iniciouEm: v.number() - }) - .index('by_conversa', ['conversaId', 'iniciouEm']) - .index('by_usuario', ['usuarioId']), - - // Push Notifications - pushSubscriptions: defineTable({ - usuarioId: v.id('usuarios'), - endpoint: v.string(), // URL do serviço de push - keys: v.object({ - p256dh: v.string(), // Chave pública - auth: v.string() // Chave de autenticação - }), - userAgent: v.optional(v.string()), - criadoEm: v.number(), - ultimaAtividade: v.number(), - ativo: v.boolean() - }) - .index('by_usuario', ['usuarioId', 'ativo']) - .index('by_endpoint', ['endpoint']), - - // Preferências de Notificação por Conversa - preferenciasNotificacaoConversa: defineTable({ - usuarioId: v.id('usuarios'), - conversaId: v.id('conversas'), - pushAtivado: v.boolean(), // Receber push notifications - emailAtivado: v.boolean(), // Receber emails quando offline - somAtivado: v.boolean(), // Tocar som - silenciado: v.boolean(), // Silenciar completamente - apenasMencoes: v.boolean(), // Notificar apenas quando mencionado - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_usuario_conversa', ['usuarioId', 'conversaId']) - .index('by_conversa', ['conversaId']), - - // Tabelas de Monitoramento do Sistema - systemMetrics: defineTable({ - timestamp: v.number(), - // Métricas de Sistema - cpuUsage: v.optional(v.number()), - memoryUsage: v.optional(v.number()), - networkLatency: v.optional(v.number()), - storageUsed: v.optional(v.number()), - // Métricas de Aplicação - usuariosOnline: v.optional(v.number()), - mensagensPorMinuto: v.optional(v.number()), - tempoRespostaMedio: v.optional(v.number()), - errosCount: v.optional(v.number()) - }).index('by_timestamp', ['timestamp']), - - alertConfigurations: defineTable({ - metricName: v.string(), - threshold: v.number(), - operator: v.union( - v.literal('>'), - v.literal('<'), - v.literal('>='), - v.literal('<='), - v.literal('==') - ), - enabled: v.boolean(), - notifyByEmail: v.boolean(), - notifyByChat: v.boolean(), - createdBy: v.id('usuarios'), - lastModified: v.number() - }).index('by_enabled', ['enabled']), - - alertHistory: defineTable({ - configId: v.id('alertConfigurations'), - metricName: v.string(), - metricValue: v.number(), - threshold: v.number(), - timestamp: v.number(), - status: v.union(v.literal('triggered'), v.literal('resolved')), - notificationsSent: v.object({ - email: v.boolean(), - chat: v.boolean() - }) - }) - .index('by_timestamp', ['timestamp']) - .index('by_status', ['status']) - .index('by_config', ['configId', 'timestamp']), - - tickets: defineTable({ - numero: v.string(), - titulo: v.string(), - descricao: v.string(), - tipo: v.union( - v.literal('reclamacao'), - v.literal('elogio'), - v.literal('sugestao'), - v.literal('chamado') - ), - categoria: v.optional(v.string()), - status: v.union( - v.literal('aberto'), - v.literal('em_andamento'), - v.literal('aguardando_usuario'), - v.literal('resolvido'), - v.literal('encerrado'), - v.literal('cancelado') - ), - prioridade: v.union( - v.literal('baixa'), - v.literal('media'), - v.literal('alta'), - v.literal('critica') - ), - solicitanteId: v.id('usuarios'), - solicitanteNome: v.string(), - solicitanteEmail: v.string(), - responsavelId: v.optional(v.id('usuarios')), - setorResponsavel: v.optional(v.string()), - slaConfigId: v.optional(v.id('slaConfigs')), - conversaId: v.optional(v.id('conversas')), - prazoResposta: v.optional(v.number()), - prazoConclusao: v.optional(v.number()), - prazoEncerramento: v.optional(v.number()), - timeline: v.optional( - v.array( - v.object({ - etapa: v.string(), - status: v.union( - v.literal('pendente'), - v.literal('em_andamento'), - v.literal('concluido'), - v.literal('vencido') - ), - prazo: v.optional(v.number()), - concluidoEm: v.optional(v.number()), - observacao: v.optional(v.string()) - }) - ) - ), - alertasEmitidos: v.optional( - v.array( - v.object({ - tipo: v.union(v.literal('resposta'), v.literal('conclusao'), v.literal('encerramento')), - emitidoEm: v.number() - }) - ) - ), - anexos: v.optional( - v.array( - v.object({ - arquivoId: v.id('_storage'), - nome: v.optional(v.string()), - tipo: v.optional(v.string()), - tamanho: v.optional(v.number()) - }) - ) - ), - tags: v.optional(v.array(v.string())), - canalOrigem: v.optional(v.string()), - ultimaInteracaoEm: v.number(), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_numero', ['numero']) - .index('by_status', ['status']) - .index('by_solicitante', ['solicitanteId', 'status']) - .index('by_responsavel', ['responsavelId', 'status']) - .index('by_setor', ['setorResponsavel', 'status']), - - ticketInteractions: defineTable({ - ticketId: v.id('tickets'), - autorId: v.optional(v.id('usuarios')), - origem: v.union(v.literal('usuario'), v.literal('ti'), v.literal('sistema')), - tipo: v.union( - v.literal('mensagem'), - v.literal('status'), - v.literal('anexo'), - v.literal('alerta') - ), - conteudo: v.string(), - anexos: v.optional( - v.array( - v.object({ - arquivoId: v.id('_storage'), - nome: v.optional(v.string()), - tipo: v.optional(v.string()), - tamanho: v.optional(v.number()) - }) - ) - ), - statusAnterior: v.optional( - v.union( - v.literal('aberto'), - v.literal('em_andamento'), - v.literal('aguardando_usuario'), - v.literal('resolvido'), - v.literal('encerrado'), - v.literal('cancelado') - ) - ), - statusNovo: v.optional( - v.union( - v.literal('aberto'), - v.literal('em_andamento'), - v.literal('aguardando_usuario'), - v.literal('resolvido'), - v.literal('encerrado'), - v.literal('cancelado') - ) - ), - visibilidade: v.union(v.literal('publico'), v.literal('interno')), - criadoEm: v.number() - }) - .index('by_ticket', ['ticketId']) - .index('by_ticket_type', ['ticketId', 'tipo']) - .index('by_autor', ['autorId']), - - slaConfigs: defineTable({ - nome: v.string(), - descricao: v.optional(v.string()), - prioridade: v.optional( - v.union(v.literal('baixa'), v.literal('media'), v.literal('alta'), v.literal('critica')) - ), - tempoRespostaHoras: v.number(), - tempoConclusaoHoras: v.number(), - tempoEncerramentoHoras: v.optional(v.number()), - alertaAntecedenciaHoras: v.number(), - ativo: v.boolean(), - criadoPor: v.id('usuarios'), - atualizadoPor: v.optional(v.id('usuarios')), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_ativo', ['ativo']) - .index('by_prioridade', ['prioridade', 'ativo']) - .index('by_nome', ['nome']), - - ticketAssignments: defineTable({ - ticketId: v.id('tickets'), - responsavelId: v.id('usuarios'), - atribuidoPor: v.id('usuarios'), - motivo: v.optional(v.string()), - ativo: v.boolean(), - criadoEm: v.number(), - encerradoEm: v.optional(v.number()) - }) - .index('by_ticket', ['ticketId', 'ativo']) - .index('by_responsavel', ['responsavelId', 'ativo']), - - // Sistema de Segurança Cibernética - networkSensors: defineTable({ - nome: v.string(), - tipo: sensorSegurancaTipo, - status: sensorSegurancaStatus, - escopo: v.optional(v.string()), - ipMonitorado: v.optional(v.string()), - hostname: v.optional(v.string()), - regioes: v.optional(v.array(v.string())), - portasMonitoradas: v.optional(v.array(v.number())), - protocolos: v.optional(v.array(v.string())), - capacidades: v.optional(v.array(v.string())), - ultimaSincronizacao: v.number(), - ultimoHeartbeat: v.optional(v.number()), - latenciaMs: v.optional(v.number()), - errosConsecutivos: v.optional(v.number()), - agenteVersao: v.optional(v.string()), - notas: v.optional(v.string()) - }) - .index('by_tipo', ['tipo']) - .index('by_status', ['status']) - .index('by_hostname', ['hostname']), - - ipReputation: defineTable({ - indicador: v.string(), - categoria: v.union( - v.literal('ip'), - v.literal('dominio'), - v.literal('hash'), - v.literal('email') - ), - reputacao: v.number(), // -100 (malicioso) até 100 (confiável) - severidadeMax: severidadeSeguranca, - whitelist: v.boolean(), - blacklist: v.boolean(), - ocorrencias: v.number(), - primeiroRegistro: v.number(), - ultimoRegistro: v.number(), - bloqueadoAte: v.optional(v.number()), - origem: v.optional(v.string()), - comentarios: v.optional(v.string()), - classificacoes: v.optional(v.array(v.string())), - ultimaAcaoId: v.optional(v.id('incidentActions')) - }) - .index('by_indicador', ['indicador']) - .index('by_reputacao', ['reputacao']) - .index('by_blacklist', ['blacklist']) - .index('by_whitelist', ['whitelist']), - - portRules: defineTable({ - porta: v.number(), - protocolo: v.union( - v.literal('tcp'), - v.literal('udp'), - v.literal('icmp'), - v.literal('quic'), - v.literal('any') - ), - acao: v.union( - v.literal('permitir'), - v.literal('bloquear'), - v.literal('monitorar'), - v.literal('rate_limit') - ), - temporario: v.boolean(), - severidadeMin: severidadeSeguranca, - duracaoSegundos: v.optional(v.number()), - expiraEm: v.optional(v.number()), - criadoPor: v.id('usuarios'), - atualizadoPor: v.optional(v.id('usuarios')), - criadoEm: v.number(), - atualizadoEm: v.number(), - notas: v.optional(v.string()), - tags: v.optional(v.array(v.string())), - listaReferencia: v.optional(v.id('ipReputation')) - }) - .index('by_porta_protocolo', ['porta', 'protocolo']) - .index('by_acao', ['acao']) - .index('by_expiracao', ['expiraEm']), - - threatIntelFeeds: defineTable({ - nomeFonte: v.string(), - tipo: threatIntelTipo, - formato: threatIntelFormato, - url: v.optional(v.string()), - ativo: v.boolean(), - prioridade: v.union( - v.literal('baixa'), - v.literal('media'), - v.literal('alta'), - v.literal('critica') - ), - ultimaSincronizacao: v.optional(v.number()), - entradasProcessadas: v.optional(v.number()), - errosConsecutivos: v.optional(v.number()), - autenticacaoNecessaria: v.optional(v.boolean()), - configuracao: v.optional( - v.object({ - tokenId: v.optional(v.id('_storage')), - escopo: v.optional(v.string()) - }) - ), - criadoPor: v.id('usuarios'), - atualizadoPor: v.optional(v.id('usuarios')), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_tipo', ['tipo']) - .index('by_ativo', ['ativo']) - .index('by_prioridade', ['prioridade']), - - securityEvents: defineTable({ - referencia: v.string(), - timestamp: v.number(), - tipoAtaque: ataqueCiberneticoTipo, - severidade: severidadeSeguranca, - status: statusEventoSeguranca, - descricao: v.string(), - origemIp: v.optional(v.string()), - origemRegiao: v.optional(v.string()), - origemAsn: v.optional(v.string()), - destinoIp: v.optional(v.string()), - destinoPorta: v.optional(v.number()), - protocolo: v.optional(v.string()), - transporte: v.optional(v.string()), - sensorId: v.optional(v.id('networkSensors')), - detectadoPor: v.optional(v.string()), - mitreTechnique: v.optional(v.string()), - geolocalizacao: v.optional( - v.object({ - pais: v.optional(v.string()), - regiao: v.optional(v.string()), - cidade: v.optional(v.string()), - latitude: v.optional(v.number()), - longitude: v.optional(v.number()) - }) - ), - fingerprint: v.optional( - v.object({ - userAgent: v.optional(v.string()), - deviceId: v.optional(v.string()), - ja3: v.optional(v.string()), - tlsVersion: v.optional(v.string()) - }) - ), - indicadores: v.optional( - v.array( - v.object({ - tipo: v.string(), - valor: v.string(), - confianca: v.optional(v.number()) - }) - ) - ), - metricas: v.optional( - v.object({ - pps: v.optional(v.number()), - bps: v.optional(v.number()), - rpm: v.optional(v.number()), - errosPorSegundo: v.optional(v.number()), - hostsAfetados: v.optional(v.number()) - }) - ), - correlacoes: v.optional(v.array(v.id('securityEvents'))), - referenciasExternas: v.optional(v.array(v.string())), - tags: v.optional(v.array(v.string())), - criadoPor: v.optional(v.id('usuarios')), - atualizadoEm: v.number() - }) - .index('by_referencia', ['referencia']) - .index('by_timestamp', ['timestamp']) - .index('by_tipo', ['tipoAtaque', 'timestamp']) - .index('by_severidade', ['severidade', 'timestamp']) - .index('by_status', ['status', 'timestamp']), - - incidentActions: defineTable({ - eventoId: v.id('securityEvents'), - tipo: acaoIncidenteTipo, - origem: v.union(v.literal('automatico'), v.literal('manual')), - status: acaoIncidenteStatus, - executadoPor: v.optional(v.id('usuarios')), - detalhes: v.optional(v.string()), - resultado: v.optional(v.string()), - relacionadoA: v.optional(v.id('ipReputation')), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_evento', ['eventoId', 'status']) - .index('by_tipo', ['tipo', 'status']), - - reportRequests: defineTable({ - solicitanteId: v.id('usuarios'), - filtros: v.object({ - dataInicio: v.number(), - dataFim: v.number(), - severidades: v.optional(v.array(severidadeSeguranca)), - tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), - incluirIndicadores: v.optional(v.boolean()), - incluirMetricas: v.optional(v.boolean()), - incluirAcoes: v.optional(v.boolean()) - }), - status: reportStatus, - resultadoId: v.optional(v.id('_storage')), - observacoes: v.optional(v.string()), - criadoEm: v.number(), - atualizadoEm: v.number(), - concluidoEm: v.optional(v.number()), - erro: v.optional(v.string()) - }) - .index('by_status', ['status']) - .index('by_solicitante', ['solicitanteId', 'status']) - .index('by_criado_em', ['criadoEm']), - - rateLimitConfig: defineTable({ - nome: v.string(), - tipo: v.union( - v.literal('ip'), - v.literal('usuario'), - v.literal('endpoint'), - v.literal('global') - ), - identificador: v.optional(v.string()), - limite: v.number(), - janelaSegundos: v.number(), - estrategia: v.union( - v.literal('fixed_window'), - v.literal('sliding_window'), - v.literal('token_bucket') - ), - acaoExcedido: v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')), - bloqueioTemporarioSegundos: v.optional(v.number()), - ativo: v.boolean(), - prioridade: v.number(), - criadoPor: v.id('usuarios'), - atualizadoPor: v.optional(v.id('usuarios')), - criadoEm: v.number(), - atualizadoEm: v.number(), - notas: v.optional(v.string()), - tags: v.optional(v.array(v.string())) - }) - .index('by_tipo_identificador', ['tipo', 'identificador']) - .index('by_ativo', ['ativo']) - .index('by_prioridade', ['prioridade']), - alertConfigs: defineTable({ - nome: v.string(), - canais: v.object({ - email: v.boolean(), - chat: v.boolean() - }), - emails: v.array(v.string()), - chatUsers: v.array(v.string()), - severidadeMin: severidadeSeguranca, - tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), - reenvioMin: v.number(), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoEm: v.number() - }).index('by_criadoEm', ['criadoEm']), - - // Sistema de Controle de Ponto - registrosPonto: defineTable({ - funcionarioId: v.id('funcionarios'), - tipo: v.union( - v.literal('entrada'), - v.literal('saida_almoco'), - v.literal('retorno_almoco'), - v.literal('saida') - ), - data: v.string(), // YYYY-MM-DD - hora: v.number(), - minuto: v.number(), - segundo: v.number(), - timestamp: v.number(), // Timestamp completo para ordenação - imagemId: v.optional(v.id('_storage')), - sincronizadoComServidor: v.boolean(), - toleranciaMinutos: v.number(), - dentroDoPrazo: v.boolean(), - - // Informações de Rede - ipAddress: v.optional(v.string()), - ipPublico: v.optional(v.string()), - ipLocal: v.optional(v.string()), - - // Informações do Navegador - userAgent: v.optional(v.string()), - browser: v.optional(v.string()), - browserVersion: v.optional(v.string()), - engine: v.optional(v.string()), - - // Informações do Sistema - sistemaOperacional: v.optional(v.string()), - osVersion: v.optional(v.string()), - arquitetura: v.optional(v.string()), - plataforma: v.optional(v.string()), - - // Informações de Localização - latitude: v.optional(v.number()), - longitude: v.optional(v.number()), - precisao: v.optional(v.number()), - altitude: v.optional(v.union(v.number(), v.null())), - altitudeAccuracy: v.optional(v.union(v.number(), v.null())), - heading: v.optional(v.union(v.number(), v.null())), - speed: v.optional(v.union(v.number(), v.null())), - confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend) - scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend) - suspeitaSpoofing: v.optional(v.boolean()), - motivoSuspeita: v.optional(v.string()), - avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação - distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS - velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro - distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro - tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro - // Informações de Geofencing - enderecoMarcacaoEsperado: v.optional(v.id('enderecosMarcacao')), // Endereço mais próximo esperado - distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado - dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido - enderecoMarcacaoUsado: v.optional(v.id('enderecosMarcacao')), // Qual endereço foi usado na validação - raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros - endereco: v.optional(v.string()), - cidade: v.optional(v.string()), - estado: v.optional(v.string()), - pais: v.optional(v.string()), - timezone: v.optional(v.string()), - - // Informações do Dispositivo - deviceType: v.optional(v.string()), - deviceModel: v.optional(v.string()), - screenResolution: v.optional(v.string()), - coresTela: v.optional(v.number()), - idioma: v.optional(v.string()), - - // Informações Adicionais - isMobile: v.optional(v.boolean()), - isTablet: v.optional(v.boolean()), - isDesktop: v.optional(v.boolean()), - connectionType: v.optional(v.string()), - memoryInfo: v.optional(v.string()), - - // Informações de Sensores (Acelerômetro e Giroscópio) - acelerometroX: v.optional(v.number()), - acelerometroY: v.optional(v.number()), - acelerometroZ: v.optional(v.number()), - movimentoDetectado: v.optional(v.boolean()), - magnitudeMovimento: v.optional(v.number()), - variacaoAcelerometro: v.optional(v.number()), - giroscopioAlpha: v.optional(v.number()), - giroscopioBeta: v.optional(v.number()), - giroscopioGamma: v.optional(v.number()), - sensorDisponivel: v.optional(v.boolean()), - permissaoSensorNegada: v.optional(v.boolean()), - - // Justificativa opcional para o registro - justificativa: v.optional(v.string()), - - // Campos para homologação - editadoPorGestor: v.optional(v.boolean()), - homologacaoId: v.optional(v.id('homologacoesPonto')), - - criadoEm: v.number() - }) - .index('by_funcionario_data', ['funcionarioId', 'data']) - .index('by_data', ['data']) - .index('by_dentro_prazo', ['dentroDoPrazo', 'data']) - .index('by_funcionario_timestamp', ['funcionarioId', 'timestamp']), - - // Endereços de Marcação - Locais permitidos para registro de ponto - enderecosMarcacao: defineTable({ - nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC" - descricao: v.optional(v.string()), // Descrição opcional - // Coordenadas (obrigatórias) - latitude: v.number(), - longitude: v.number(), - // Endereço físico (para exibição) - endereco: v.string(), // Ex: "Rua Exemplo, 123" - bairro: v.optional(v.string()), // Bairro do endereço - cep: v.optional(v.string()), - cidade: v.string(), - estado: v.string(), - pais: v.optional(v.string()), // Padrão: "Brasil" - // Configurações - raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m) - ativo: v.boolean(), - // Tipos de uso - tipo: v.union( - v.literal('sede'), // Sede principal (para todos) - v.literal('home_office'), // Home office específico - v.literal('deslocamento'), // Deslocamento temporário - v.literal('cliente') // Local de cliente - ), - // Metadados - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoPor: v.optional(v.id('usuarios')), - atualizadoEm: v.optional(v.number()) - }) - .index('by_ativo', ['ativo']) - .index('by_tipo', ['tipo']) - .index('by_cidade', ['cidade']), - - // Associação Funcionário ↔ Endereço de Marcação - funcionarioEnderecosMarcacao: defineTable({ - funcionarioId: v.id('funcionarios'), - enderecoMarcacaoId: v.id('enderecosMarcacao'), - // Configurações específicas do funcionário - raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão - // Período de validade (para deslocamentos temporários) - dataInicio: v.optional(v.string()), // YYYY-MM-DD - dataFim: v.optional(v.string()), // YYYY-MM-DD - // Status - ativo: v.boolean(), - // Metadados - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_endereco', ['enderecoMarcacaoId']) - .index('by_funcionario_ativo', ['funcionarioId', 'ativo']) - .index('by_endereco_ativo', ['enderecoMarcacaoId', 'ativo']), - - configuracaoPonto: defineTable({ - horarioEntrada: v.string(), // HH:mm - horarioSaidaAlmoco: v.string(), // HH:mm - horarioRetornoAlmoco: v.string(), // HH:mm - horarioSaida: v.string(), // HH:mm - toleranciaMinutos: v.number(), - // Nomes personalizados dos tipos de registro - nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1" - nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1" - nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2" - nomeSaida: v.optional(v.string()), // Padrão: "Saída 2" - // Ajuste de fuso horário (GMT offset em horas) - gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC) - // Configurações de geofencing - validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização - toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros - ativo: v.boolean(), - atualizadoPor: v.id('usuarios'), - atualizadoEm: v.number() - }).index('by_ativo', ['ativo']), - - configuracaoRelogio: defineTable({ - servidorNTP: v.optional(v.string()), - portaNTP: v.optional(v.number()), - usarServidorExterno: v.boolean(), - fallbackParaPC: v.boolean(), - ultimaSincronizacao: v.optional(v.number()), - offsetSegundos: v.optional(v.number()), - // Ajuste de fuso horário (GMT offset em horas) - gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC) - atualizadoPor: v.id('usuarios'), - atualizadoEm: v.number() - }).index('by_ativo', ['usarServidorExterno']), - - // Banco de Horas - Saldo diário de horas trabalhadas - bancoHoras: defineTable({ - funcionarioId: v.id('funcionarios'), - data: v.string(), // YYYY-MM-DD - cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos) - horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos) - saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit) - registrosPontoIds: v.array(v.id('registrosPonto')), // IDs dos registros do dia - calculadoEm: v.number() - }) - .index('by_funcionario_data', ['funcionarioId', 'data']) - .index('by_funcionario', ['funcionarioId']) - .index('by_data', ['data']), - - // Homologações de Ponto - Edições e ajustes realizados pelo gestor - homologacoesPonto: defineTable({ - registroId: v.optional(v.id('registrosPonto')), // ID do registro editado (se for edição) - funcionarioId: v.id('funcionarios'), - gestorId: v.id('usuarios'), - // Dados do registro original (se for edição) - horaAnterior: v.optional(v.number()), - minutoAnterior: v.optional(v.number()), - // Dados do registro novo (se for edição) - horaNova: v.optional(v.number()), - minutoNova: v.optional(v.number()), - // Motivo e observações - motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações) - motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc) - motivoDescricao: v.optional(v.string()), // Descrição do motivo - observacoes: v.optional(v.string()), - // Tipo de ajuste (se for ajuste de banco de horas) - tipoAjuste: v.optional( - v.union(v.literal('compensar'), v.literal('abonar'), v.literal('descontar')) - ), - // Período do ajuste (se for ajuste de banco de horas) - periodoDias: v.optional(v.number()), - periodoHoras: v.optional(v.number()), - periodoMinutos: v.optional(v.number()), - // Ajuste em minutos (calculado) - ajusteMinutos: v.optional(v.number()), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_gestor', ['gestorId']) - .index('by_registro', ['registroId']) - .index('by_data', ['criadoEm']), - - // Configurações Gerais - config: defineTable({ - comprasSetorId: v.optional(v.id('setores')), - criadoPor: v.id('usuarios'), - atualizadoEm: v.number() - }), - - // Módulo de Pedidos/Compras - produtos: defineTable({ - nome: v.string(), - valorEstimado: v.string(), - tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')), - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .searchIndex('search_nome', { searchField: 'nome' }) - .index('by_nome', ['nome']) - .index('by_tipo', ['tipo']), - - acoes: defineTable({ - nome: v.string(), - tipo: v.union(v.literal('projeto'), v.literal('lei')), - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_nome', ['nome']) - .index('by_tipo', ['tipo']), - - pedidos: defineTable({ - numeroSei: v.optional(v.string()), - status: v.union( - v.literal('em_rascunho'), - v.literal('aguardando_aceite'), - v.literal('em_analise'), - v.literal('precisa_ajustes'), - v.literal('cancelado'), - v.literal('concluido') - ), - acaoId: v.optional(v.id('acoes')), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_numeroSei', ['numeroSei']) - .index('by_status', ['status']) - .index('by_criadoPor', ['criadoPor']) - .index('by_acaoId', ['acaoId']), - - pedidoItems: defineTable({ - pedidoId: v.id('pedidos'), - produtoId: v.id('produtos'), - valorEstimado: v.string(), - valorReal: v.optional(v.string()), - quantidade: v.number(), - adicionadoPor: v.id('funcionarios'), - criadoEm: v.number() - }) - .index('by_pedidoId', ['pedidoId']) - .index('by_produtoId', ['produtoId']) - .index('by_adicionadoPor', ['adicionadoPor']), - - historicoPedidos: defineTable({ - pedidoId: v.id('pedidos'), - usuarioId: v.id('usuarios'), - acao: v.string(), // "criacao", "alteracao_status", "adicao_item", "remocao_item", "edicao_item" - detalhes: v.optional(v.string()), // JSON string - data: v.number() - }) - .index('by_pedidoId', ['pedidoId']) - .index('by_usuarioId', ['usuarioId']) - .index('by_data', ['data']), - - // Logs de Login Detalhados - logsLogin: defineTable({ - usuarioId: v.optional(v.id('usuarios')), // pode ser null se falha antes de identificar usuário - matriculaOuEmail: v.string(), // tentativa de login - sucesso: v.boolean(), - motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente" - // Informações de Rede - ipAddress: v.optional(v.string()), - ipPublico: v.optional(v.string()), - ipLocal: v.optional(v.string()), - userAgent: v.optional(v.string()), - device: v.optional(v.string()), - browser: v.optional(v.string()), - sistema: v.optional(v.string()), - // Informações de Localização (por IP) - latitude: v.optional(v.number()), - longitude: v.optional(v.number()), - endereco: v.optional(v.string()), - cidade: v.optional(v.string()), - estado: v.optional(v.string()), - pais: v.optional(v.string()), - // Informações de Localização (GPS do navegador) - latitudeGPS: v.optional(v.number()), - longitudeGPS: v.optional(v.number()), - precisaoGPS: v.optional(v.number()), - enderecoGPS: v.optional(v.string()), - cidadeGPS: v.optional(v.string()), - estadoGPS: v.optional(v.string()), - paisGPS: v.optional(v.string()), - timestamp: v.number() - }) - .index('by_usuario', ['usuarioId']) - .index('by_sucesso', ['sucesso']) - .index('by_timestamp', ['timestamp']) - .index('by_ip', ['ipAddress']), - - // Templates de Mensagens - templatesMensagens: defineTable({ - codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc. - nome: v.string(), - tipo: v.union( - v.literal('sistema'), // predefinido, não editável - v.literal('customizado') // criado por TI_MASTER - ), - titulo: v.string(), - corpo: v.string(), // pode ter variáveis {{variavel}} - htmlCorpo: v.optional(v.string()), // versão HTML do corpo (com wrapper) - variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.] - categoria: v.optional(v.union(v.literal('email'), v.literal('chat'), v.literal('ambos'))), // categoria do template - tags: v.optional(v.array(v.string())), // tags para organização - criadoPor: v.optional(v.id('usuarios')), - criadoEm: v.number() - }) - .index('by_codigo', ['codigo']) - .index('by_tipo', ['tipo']) - .index('by_criado_por', ['criadoPor']) - .index('by_categoria', ['categoria']), - - // Configuração de Jitsi Meet - configuracaoJitsi: defineTable({ - domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com") - appId: v.string(), // ID da aplicação Jitsi - roomPrefix: v.string(), // Prefixo para nomes de salas - useHttps: v.boolean(), // Usar HTTPS - acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento) - ativo: v.boolean(), // Configuração ativa - testadoEm: v.optional(v.number()), // Timestamp do último teste de conexão - configuradoEm: v.optional(v.number()), // Timestamp da última configuração do servidor Docker - configuradoNoServidor: v.optional(v.boolean()), // Indica se a configuração foi aplicada no servidor - configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor - configuradoPor: v.id('usuarios'), // Usuário que configurou - atualizadoEm: v.number(), // Timestamp de atualização - jitsiConfigPath: v.optional(v.string()), // Caminho da configuração do Jitsi no servidor (ex: "~/.jitsi-meet-cfg") - sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor - sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada) - sshPort: v.optional(v.number()) // Porta SSH (padrão: 22) - }).index('by_ativo', ['ativo']), - - // Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto - dispensasRegistro: defineTable({ - funcionarioId: v.id('funcionarios'), - gestorId: v.id('usuarios'), - dataInicio: v.string(), // YYYY-MM-DD - horaInicio: v.number(), - minutoInicio: v.number(), - dataFim: v.string(), // YYYY-MM-DD - horaFim: v.number(), - minutoFim: v.number(), - motivo: v.string(), - isento: v.boolean(), // Se true, não expira (casos excepcionais) - ativo: v.boolean(), - criadoEm: v.number() - }) - .index('by_funcionario', ['funcionarioId']) - .index('by_gestor', ['gestorId']) - .index('by_ativo', ['ativo']) - .index('by_data_inicio', ['dataInicio']) - .index('by_data_fim', ['dataFim']) + ...setoresTables, + ...flowsTables, + ...contratosTables, + ...enderecosTables, + ...empresasTable, + ...funcionariosTables, + ...atestadosTables, + ...licencasTables, + ...feriasTables, + ...ausenciasTables, + ...timesTables, + ...cursosTables, + ...authTables, + ...systemTables, + ...chatTables, + ...ticketsTables, + ...securityTables, + ...pontoTables, + ...pedidosTables, + ...produtosTables }); diff --git a/packages/backend/convex/security.ts b/packages/backend/convex/security.ts index 9db7bdb..c78f30d 100644 --- a/packages/backend/convex/security.ts +++ b/packages/backend/convex/security.ts @@ -1,16 +1,12 @@ import { v } from 'convex/values'; -import { - internalMutation, - mutation, - query -} from './_generated/server'; +import { internalMutation, mutation, query } from './_generated/server'; import { internal } from './_generated/api'; import type { Id } from './_generated/dataModel'; import type { AtaqueCiberneticoTipo, SeveridadeSeguranca, StatusEventoSeguranca -} from './schema'; +} from './tables/security'; import type { MutationCtx, QueryCtx } from './_generated/server'; import { RateLimiter, SECOND } from '@convex-dev/rate-limiter'; import { components } from './_generated/api'; @@ -413,9 +409,9 @@ const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual') // Função para analisar string e detectar ataques function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null { if (!texto) return null; - + const textoLower = texto.toLowerCase(); - + // Verificar cada tipo de ataque em ordem de prioridade for (const tipo of ATAQUES_PRIORITARIOS) { const patterns = KEYWORDS[tipo]; @@ -423,7 +419,7 @@ function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null return tipo; } } - + return null; } @@ -591,14 +587,23 @@ export const registrarEventoSeguranca = mutation({ handler: async (ctx, args) => { // Aplicar rate limiting por IP se fornecido if (args.origemIp) { - const rateLimitResult = await aplicarRateLimit(ctx, 'ip', args.origemIp, 'registrarEventoSeguranca'); + const rateLimitResult = await aplicarRateLimit( + ctx, + 'ip', + args.origemIp, + 'registrarEventoSeguranca' + ); if (!rateLimitResult.permitido) { throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido'); } } const tipo = inferirTipoAtaque(args); - const severidade = calcularSeveridade(tipo, args.metricas ?? undefined, args.severidade ?? undefined); + const severidade = calcularSeveridade( + tipo, + args.metricas ?? undefined, + args.severidade ?? undefined + ); const status = statusInicial(severidade); const duplicado = await ctx.db @@ -727,10 +732,18 @@ export const listarEventosSeguranca = query({ const candidatos = await builder.order('desc').take(limit * 3); const filtrados = candidatos .filter((evento) => { - if (args.severidades && args.severidades.length > 0 && !args.severidades.includes(evento.severidade)) { + if ( + args.severidades && + args.severidades.length > 0 && + !args.severidades.includes(evento.severidade) + ) { return false; } - if (args.tiposAtaque && args.tiposAtaque.length > 0 && !args.tiposAtaque.includes(evento.tipoAtaque)) { + if ( + args.tiposAtaque && + args.tiposAtaque.length > 0 && + !args.tiposAtaque.includes(evento.tipoAtaque) + ) { return false; } if (args.status && args.status.length > 0 && !args.status.includes(evento.status)) { @@ -829,10 +842,7 @@ export const obterVisaoCamadas = query({ for (const evento of eventos) { const idx = Math.min( bucketCount - 1, - Math.max( - 0, - Math.floor((evento.timestamp - inicioJanela) / bucketSize) - ) + Math.max(0, Math.floor((evento.timestamp - inicioJanela) / bucketSize)) ); const bucket = series[idx]; if (evento.severidade === 'critico') criticos += 1; @@ -893,11 +903,12 @@ export const listarReputacoes = query({ handler: async (ctx, args) => { const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200; - const builder = args.lista === undefined - ? ctx.db.query('ipReputation') - : args.lista === 'blacklist' - ? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true)) - : ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true)); + const builder = + args.lista === undefined + ? ctx.db.query('ipReputation') + : args.lista === 'blacklist' + ? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true)) + : ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true)); const docs = await builder.order('desc').take(limit * 2); const filtrados = docs @@ -945,7 +956,12 @@ export const atualizarReputacaoIndicador = mutation({ }), handler: async (ctx, args) => { // Aplicar rate limiting por usuário - const rateLimitResult = await aplicarRateLimit(ctx, 'usuario', args.usuarioId, 'atualizarReputacaoIndicador'); + const rateLimitResult = await aplicarRateLimit( + ctx, + 'usuario', + args.usuarioId, + 'atualizarReputacaoIndicador' + ); if (!rateLimitResult.permitido) { throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido'); } @@ -1075,7 +1091,8 @@ export const configurarRegraPorta = mutation({ }), handler: async (ctx, args) => { const agora = Date.now(); - const expiraEm = args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined; + const expiraEm = + args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined; if (args.regraId) { await ctx.db.patch(args.regraId, { @@ -1172,7 +1189,14 @@ export const registrarAcaoIncidente = mutation({ tipo: acaoIncidenteValidator, origem: acaoOrigemValidator, executadoPor: v.optional(v.id('usuarios')), - status: v.optional(v.union(v.literal('pendente'), v.literal('executando'), v.literal('concluido'), v.literal('falhou'))), + status: v.optional( + v.union( + v.literal('pendente'), + v.literal('executando'), + v.literal('concluido'), + v.literal('falhou') + ) + ), detalhes: v.optional(v.string()), resultado: v.optional(v.string()), relacionadoA: v.optional(v.id('ipReputation')) @@ -1338,9 +1362,7 @@ export const processarRelatorioSegurancaInternal = internalMutation({ const eventos = await ctx.db .query('securityEvents') .withIndex('by_timestamp', (q) => - q - .gte('timestamp', relatorio.filtros.dataInicio) - .lte('timestamp', relatorio.filtros.dataFim) + q.gte('timestamp', relatorio.filtros.dataInicio).lte('timestamp', relatorio.filtros.dataFim) ) .collect(); @@ -1350,14 +1372,14 @@ export const processarRelatorioSegurancaInternal = internalMutation({ relatorio.filtros.severidades.length > 0 && !relatorio.filtros.severidades.includes(evento.severidade) ) { - return false; + return false; } if ( relatorio.filtros.tiposAtaque && relatorio.filtros.tiposAtaque.length > 0 && !relatorio.filtros.tiposAtaque.includes(evento.tipoAtaque) ) { - return false; + return false; } return true; }); @@ -1509,7 +1531,10 @@ export const dispararAlertasInternos = internalMutation({ const usuariosNotificados: Id<'usuarios'>[] = []; for (const role of rolesTi) { - const membros = await ctx.db.query('usuarios').withIndex('by_role', (q) => q.eq('roleId', role._id)).collect(); + const membros = await ctx.db + .query('usuarios') + .withIndex('by_role', (q) => q.eq('roleId', role._id)) + .collect(); for (const usuario of membros) { usuariosNotificados.push(usuario._id); } @@ -1602,7 +1627,9 @@ async function aplicarRateLimit( ): Promise<{ permitido: boolean; motivo?: string; retryAfter?: number }> { const configs = await ctx.db .query('rateLimitConfig') - .withIndex('by_tipo_identificador', (q) => q.eq('tipo', tipo).eq('identificador', identificador)) + .withIndex('by_tipo_identificador', (q) => + q.eq('tipo', tipo).eq('identificador', identificador) + ) .filter((q) => q.eq(q.field('ativo'), true)) .collect(); @@ -1611,7 +1638,9 @@ async function aplicarRateLimit( // Verificar configuração global const globalConfigs = await ctx.db .query('rateLimitConfig') - .withIndex('by_tipo_identificador', (q) => q.eq('tipo', 'global').eq('identificador', 'global')) + .withIndex('by_tipo_identificador', (q) => + q.eq('tipo', 'global').eq('identificador', 'global') + ) .filter((q) => q.eq(q.field('ativo'), true)) .collect(); @@ -1626,10 +1655,11 @@ async function aplicarRateLimit( // Converter janelaSegundos para período do rate-limiter const periodo = config.janelaSegundos * SECOND; - + // Determinar estratégia baseada na configuração // O rate-limiter suporta apenas 'token bucket' e 'fixed window' - const kind: 'token bucket' | 'fixed window' = config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window'; + const kind: 'token bucket' | 'fixed window' = + config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window'; // Criar namespace único para este rate limit const namespace = `${tipo}:${identificador}:${endpoint ?? 'default'}`; @@ -1643,7 +1673,10 @@ async function aplicarRateLimit( period: periodo, ...(config.estrategia === 'token_bucket' ? { capacity: config.limite } : {}) } - } as Record; + } as Record< + string, + { kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number } + >; const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig); @@ -1654,7 +1687,7 @@ async function aplicarRateLimit( if (!result.ok) { const retryAfter = result.retryAfter ?? periodo; - + if (config.acaoExcedido === 'bloquear') { return { permitido: false, @@ -1688,7 +1721,12 @@ export const criarConfigRateLimit = mutation({ args: { usuarioId: v.id('usuarios'), nome: v.string(), - tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')), + 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(), @@ -1737,13 +1775,11 @@ export const atualizarConfigRateLimit = mutation({ limite: v.optional(v.number()), janelaSegundos: v.optional(v.number()), estrategia: v.optional( - v.union( - v.literal('fixed_window'), - v.literal('sliding_window'), - v.literal('token_bucket') - ) + v.union(v.literal('fixed_window'), v.literal('sliding_window'), v.literal('token_bucket')) + ), + acaoExcedido: v.optional( + v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')) ), - acaoExcedido: v.optional(v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))), bloqueioTemporarioSegundos: v.optional(v.number()), ativo: v.optional(v.boolean()), prioridade: v.optional(v.number()), @@ -1794,7 +1830,9 @@ export const atualizarConfigRateLimit = mutation({ export const listarConfigsRateLimit = query({ args: { - tipo: v.optional(v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))), + tipo: v.optional( + v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')) + ), ativo: v.optional(v.boolean()), limit: v.optional(v.number()) }, @@ -1802,7 +1840,12 @@ export const listarConfigsRateLimit = query({ v.object({ _id: v.id('rateLimitConfig'), nome: v.string(), - tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')), + 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(), @@ -1882,8 +1925,12 @@ export const analisarRequisicaoHTTP = mutation({ args.url, args.method, args.body ?? '', - Object.entries(args.queryParams ?? {}).map(([k, v]) => `${k}=${v}`).join('&'), - Object.entries(args.headers ?? {}).map(([k, v]) => `${k}:${v}`).join('\n'), + Object.entries(args.queryParams ?? {}) + .map(([k, v]) => `${k}=${v}`) + .join('&'), + Object.entries(args.headers ?? {}) + .map(([k, v]) => `${k}:${v}`) + .join('\n'), args.userAgent ?? '' ].join('\n'); @@ -1904,7 +1951,8 @@ export const analisarRequisicaoHTTP = mutation({ // Permitir que o chamador informe o destino/protocolo via query string em cenários de dev/teste const destinoIp = - (args.queryParams && (args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) || + (args.queryParams && + (args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) || undefined; const protocolo = (args.queryParams && (args.queryParams['proto'] as string)) || 'http'; @@ -1922,9 +1970,11 @@ export const analisarRequisicaoHTTP = mutation({ protocolo, transporte: 'tcp', detectadoPor: 'analisador_http_automatico', - fingerprint: args.userAgent ? { - userAgent: args.userAgent - } : undefined, + fingerprint: args.userAgent + ? { + userAgent: args.userAgent + } + : undefined, destinoIp: destinoIp ?? undefined, tags: ['detecção_automática', 'http', tipoAtaque], atualizadoEm: agora @@ -1978,19 +2028,13 @@ export const detectarBruteForce = internalMutation({ tentativasFalhas = await ctx.db .query('logsLogin') .withIndex('by_ip', (q) => q.eq('ipAddress', args.ipAddress)) - .filter((q) => - q.gte(q.field('timestamp'), dataLimite) && - q.eq(q.field('sucesso'), false) - ) + .filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false)) .collect(); } else if (args.usuarioId) { tentativasFalhas = await ctx.db .query('logsLogin') .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId)) - .filter((q) => - q.gte(q.field('timestamp'), dataLimite) && - q.eq(q.field('sucesso'), false) - ) + .filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false)) .collect(); } else { // Buscar todas as tentativas falhas na janela @@ -2026,7 +2070,8 @@ export const detectarBruteForce = internalMutation({ const eventosIds: Id<'securityEvents'>[] = []; for (const { ip, count } of ipsSuspeitos) { - const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo'; + const severidade: SeveridadeSeguranca = + count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo'; const referencia = `brute_force_${ip}_${Date.now()}`; const agora = Date.now(); @@ -2058,10 +2103,12 @@ export const detectarBruteForce = internalMutation({ 'ip', delta, severidade, - severidade === 'alto' ? { - blacklist: true, - bloqueadoAte: agora + (60 * 60 * 1000) // Bloquear por 1 hora - } : undefined + severidade === 'alto' + ? { + blacklist: true, + bloqueadoAte: agora + 60 * 60 * 1000 // Bloquear por 1 hora + } + : undefined ); } @@ -2109,13 +2156,7 @@ export const criarEventosTeste = mutation({ ]; // IPs de teste - const ipsTeste = [ - '192.168.1.100', - '10.0.0.50', - '172.16.0.25', - '203.0.113.42', - '198.51.100.15' - ]; + const ipsTeste = ['192.168.1.100', '10.0.0.50', '172.16.0.25', '203.0.113.42', '198.51.100.15']; for (let i = 0; i < quantidade; i++) { const tipoAtaque = tiposAtaque[i % tiposAtaque.length]; @@ -2124,7 +2165,7 @@ export const criarEventosTeste = mutation({ const eventoId = await ctx.db.insert('securityEvents', { referencia, - timestamp: agora - (i * 60000), // Espaçar eventos em 1 minuto + timestamp: agora - i * 60000, // Espaçar eventos em 1 minuto tipoAtaque: tipoAtaque.tipo, severidade: tipoAtaque.severidade, status: statusInicial(tipoAtaque.severidade), @@ -2140,7 +2181,7 @@ export const criarEventosTeste = mutation({ pps: Math.floor(Math.random() * 50000) }, tags: ['teste', 'validação', tipoAtaque.tipo], - atualizadoEm: agora - (i * 60000) + atualizadoEm: agora - i * 60000 }); eventosIds.push(eventoId); @@ -2153,9 +2194,11 @@ export const criarEventosTeste = mutation({ 'ip', delta, tipoAtaque.severidade, - tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' ? { - blacklist: true - } : undefined + tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' + ? { + blacklist: true + } + : undefined ); } @@ -2208,7 +2251,8 @@ export const monitorarLogsLogin = internalMutation({ // Registrar eventos para cada IP suspeito let ipsBloqueados = 0; for (const { ip, count } of ipsSuspeitos) { - const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo'; + const severidade: SeveridadeSeguranca = + count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo'; const referencia = `brute_force_${ip}_${Date.now()}`; const agora = Date.now(); @@ -2238,10 +2282,12 @@ export const monitorarLogsLogin = internalMutation({ 'ip', delta, severidade, - severidade === 'alto' ? { - blacklist: true, - bloqueadoAte: agora + (60 * 60 * 1000) - } : undefined + severidade === 'alto' + ? { + blacklist: true, + bloqueadoAte: agora + 60 * 60 * 1000 + } + : undefined ); if (severidade === 'alto') { @@ -2302,7 +2348,12 @@ export const seedRateLimitDev = mutation({ const existing = await ctx.db .query('rateLimitConfig') .withIndex('by_tipo_identificador', (q) => - q.eq('tipo', params.tipo).eq('identificador', params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)), + q + .eq('tipo', params.tipo) + .eq( + 'identificador', + params.identificador ?? (params.tipo === 'global' ? 'global' : undefined) + ) ) .collect(); const agora = Date.now(); @@ -2315,9 +2366,9 @@ export const seedRateLimitDev = mutation({ estrategia: params.estrategia, acaoExcedido: params.acaoExcedido, ativo: true, - prioridade: params.prioridade ?? (doc.prioridade ?? 0), + prioridade: params.prioridade ?? doc.prioridade ?? 0, atualizadoEm: agora, - notas: params.notas, + notas: params.notas }); } else { await ctx.db.insert('rateLimitConfig', { @@ -2420,4 +2471,3 @@ export const deletarRegraPorta = mutation({ return null; } }); - diff --git a/packages/backend/convex/seed.ts b/packages/backend/convex/seed.ts index 3fee8cb..db4d0f5 100644 --- a/packages/backend/convex/seed.ts +++ b/packages/backend/convex/seed.ts @@ -618,13 +618,6 @@ export const clearDatabase = internalMutation({ } console.log(` ✅ ${roles.length} roles removidas`); - // 23. Todos (tabela de exemplo) - const todos = await ctx.db.query('todos').collect(); - for (const todo of todos) { - await ctx.db.delete(todo._id); - } - console.log(` ✅ ${todos.length} todos removidos`); - console.log('✨ Banco de dados completamente limpo!'); return null; } @@ -841,13 +834,6 @@ export const limparBanco = mutation({ } console.log(` ✅ ${roles.length} roles removidas`); - // 23. Todos (tabela de exemplo) - const todos = await ctx.db.query('todos').collect(); - for (const todo of todos) { - await ctx.db.delete(todo._id); - } - console.log(` ✅ ${todos.length} todos removidos`); - console.log('✨ Banco de dados completamente limpo!'); return null; } diff --git a/packages/backend/convex/simbolos.ts b/packages/backend/convex/simbolos.ts index 624c06f..d9dacc4 100644 --- a/packages/backend/convex/simbolos.ts +++ b/packages/backend/convex/simbolos.ts @@ -1,188 +1,190 @@ -import { v } from "convex/values"; -import { query, mutation } from "./_generated/server"; -import { simboloTipo } from "./schema"; +import { v } from 'convex/values'; +import { query, mutation } from './_generated/server'; +import { simboloTipo } from './tables/funcionarios'; export const getAll = query({ - args: {}, - returns: v.array( - v.object({ - _id: v.id("simbolos"), - _creationTime: v.number(), - nome: v.string(), - tipo: simboloTipo, - descricao: v.string(), - vencValor: v.string(), - repValor: v.string(), - valor: v.string(), - }) - ), - handler: async (ctx) => { - return await ctx.db.query("simbolos").collect(); - }, + args: {}, + returns: v.array( + v.object({ + _id: v.id('simbolos'), + _creationTime: v.number(), + nome: v.string(), + tipo: simboloTipo, + descricao: v.string(), + vencValor: v.string(), + repValor: v.string(), + valor: v.string() + }) + ), + handler: async (ctx) => { + return await ctx.db.query('simbolos').collect(); + } }); export const getById = query({ - args: { - id: v.id("simbolos"), - }, - returns: v.union( - v.object({ - _id: v.id("simbolos"), - _creationTime: v.number(), - nome: v.string(), - tipo: simboloTipo, - descricao: v.string(), - vencValor: v.string(), - repValor: v.string(), - valor: v.string(), - }), - v.null() - ), - handler: async (ctx, args) => { - return await ctx.db.get(args.id); - }, + args: { + id: v.id('simbolos') + }, + returns: v.union( + v.object({ + _id: v.id('simbolos'), + _creationTime: v.number(), + nome: v.string(), + tipo: simboloTipo, + descricao: v.string(), + vencValor: v.string(), + repValor: v.string(), + valor: v.string() + }), + v.null() + ), + handler: async (ctx, args) => { + return await ctx.db.get(args.id); + } }); export const create = mutation({ - args: { - nome: v.string(), - tipo: simboloTipo, - refValor: v.string(), - vencValor: v.string(), - descricao: v.string(), - valor: v.optional(v.string()), - }, - handler: async (ctx, args) => { - let refValor = args.refValor; - let vencValor = args.vencValor; - let valor = args.valor ?? ""; + args: { + nome: v.string(), + tipo: simboloTipo, + refValor: v.string(), + vencValor: v.string(), + descricao: v.string(), + valor: v.optional(v.string()) + }, + handler: async (ctx, args) => { + let refValor = args.refValor; + let vencValor = args.vencValor; + let valor = args.valor ?? ''; - if (args.tipo === "cargo_comissionado") { - if (!refValor || !vencValor) { - throw new Error( - "Valor de referência e valor de vencimento são obrigatórios para cargo comissionado" - ); - } - valor = (Number(refValor) + Number(vencValor)).toFixed(2); - } else { - if (!args.valor) { - throw new Error("Valor é obrigatório para função gratificada"); - } - refValor = ""; - vencValor = ""; - valor = args.valor; - } - const novoSimboloId = await ctx.db.insert("simbolos", { - nome: args.nome, - descricao: args.descricao, - repValor: refValor, - vencValor: vencValor, - tipo: args.tipo, - valor, - }); - return await ctx.db.get(novoSimboloId); - }, + if (args.tipo === 'cargo_comissionado') { + if (!refValor || !vencValor) { + throw new Error( + 'Valor de referência e valor de vencimento são obrigatórios para cargo comissionado' + ); + } + valor = (Number(refValor) + Number(vencValor)).toFixed(2); + } else { + if (!args.valor) { + throw new Error('Valor é obrigatório para função gratificada'); + } + refValor = ''; + vencValor = ''; + valor = args.valor; + } + const novoSimboloId = await ctx.db.insert('simbolos', { + nome: args.nome, + descricao: args.descricao, + repValor: refValor, + vencValor: vencValor, + tipo: args.tipo, + valor + }); + return await ctx.db.get(novoSimboloId); + } }); export const remove = mutation({ - args: { - id: v.id("simbolos"), - }, - returns: v.null(), - handler: async (ctx, args) => { - await ctx.db.delete(args.id); - return null; - }, + args: { + id: v.id('simbolos') + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.delete(args.id); + return null; + } }); export const update = mutation({ - args: { - id: v.id("simbolos"), - nome: v.string(), - tipo: simboloTipo, - refValor: v.string(), - vencValor: v.string(), - descricao: v.string(), - valor: v.optional(v.string()), - }, - returns: v.null(), - handler: async (ctx, args) => { - let refValor = args.refValor; - let vencValor = args.vencValor; - let valor = args.valor ?? ""; + args: { + id: v.id('simbolos'), + nome: v.string(), + tipo: simboloTipo, + refValor: v.string(), + vencValor: v.string(), + descricao: v.string(), + valor: v.optional(v.string()) + }, + returns: v.null(), + handler: async (ctx, args) => { + let refValor = args.refValor; + let vencValor = args.vencValor; + let valor = args.valor ?? ''; - if (args.tipo === "cargo_comissionado") { - if (!refValor || !vencValor) { - throw new Error( - "Valor de referência e valor de vencimento são obrigatórios para cargo comissionado" - ); - } - valor = (Number(refValor) + Number(vencValor)).toFixed(2); - } else { - if (!args.valor) { - throw new Error("Valor é obrigatório para função gratificada"); - } - refValor = ""; - vencValor = ""; - valor = args.valor; - } + if (args.tipo === 'cargo_comissionado') { + if (!refValor || !vencValor) { + throw new Error( + 'Valor de referência e valor de vencimento são obrigatórios para cargo comissionado' + ); + } + valor = (Number(refValor) + Number(vencValor)).toFixed(2); + } else { + if (!args.valor) { + throw new Error('Valor é obrigatório para função gratificada'); + } + refValor = ''; + vencValor = ''; + valor = args.valor; + } - await ctx.db.patch(args.id, { - nome: args.nome, - descricao: args.descricao, - repValor: refValor, - vencValor: vencValor, - tipo: args.tipo, - valor, - }); - return null; - }, + await ctx.db.patch(args.id, { + nome: args.nome, + descricao: args.descricao, + repValor: refValor, + vencValor: vencValor, + tipo: args.tipo, + valor + }); + return null; + } }); /** * Remove símbolos duplicados, mantendo apenas a primeira ocorrência de cada símbolo */ export const removerDuplicados = mutation({ - args: {}, - returns: v.object({ - removidos: v.number(), - mantidos: v.number(), - }), - handler: async (ctx) => { - const todosSimbolos = await ctx.db.query("simbolos").collect(); - - // Agrupar símbolos por nome - const simbolosPorNome = new Map(); - - for (const simbolo of todosSimbolos) { - const key = simbolo.nome.trim().toLowerCase(); - if (!simbolosPorNome.has(key)) { - simbolosPorNome.set(key, []); - } - simbolosPorNome.get(key)!.push(simbolo); - } - - let removidos = 0; - let mantidos = 0; - - // Para cada grupo de símbolos com o mesmo nome - for (const [nome, simbolos] of simbolosPorNome) { - // Ordenar por _creationTime (mais antigo primeiro) - simbolos.sort((a, b) => a._creationTime - b._creationTime); - - // Manter o primeiro (mais antigo) e remover os demais - const [primeiro, ...duplicados] = simbolos; - mantidos++; - - // Remover duplicados - for (const duplicado of duplicados) { - await ctx.db.delete(duplicado._id); - removidos++; - } - } - - console.log(`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`); - - return { removidos, mantidos }; - }, -}); \ No newline at end of file + args: {}, + returns: v.object({ + removidos: v.number(), + mantidos: v.number() + }), + handler: async (ctx) => { + const todosSimbolos = await ctx.db.query('simbolos').collect(); + + // Agrupar símbolos por nome + const simbolosPorNome = new Map(); + + for (const simbolo of todosSimbolos) { + const key = simbolo.nome.trim().toLowerCase(); + if (!simbolosPorNome.has(key)) { + simbolosPorNome.set(key, []); + } + simbolosPorNome.get(key)!.push(simbolo); + } + + let removidos = 0; + let mantidos = 0; + + // Para cada grupo de símbolos com o mesmo nome + for (const [nome, simbolos] of simbolosPorNome) { + // Ordenar por _creationTime (mais antigo primeiro) + simbolos.sort((a, b) => a._creationTime - b._creationTime); + + // Manter o primeiro (mais antigo) e remover os demais + const [primeiro, ...duplicados] = simbolos; + mantidos++; + + // Remover duplicados + for (const duplicado of duplicados) { + await ctx.db.delete(duplicado._id); + removidos++; + } + } + + console.log( + `✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos` + ); + + return { removidos, mantidos }; + } +}); diff --git a/packages/backend/convex/tables/atestados.ts b/packages/backend/convex/tables/atestados.ts new file mode 100644 index 0000000..2a95420 --- /dev/null +++ b/packages/backend/convex/tables/atestados.ts @@ -0,0 +1,20 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const atestadosTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/ausencias.ts b/packages/backend/convex/tables/ausencias.ts new file mode 100644 index 0000000..94fc88a --- /dev/null +++ b/packages/backend/convex/tables/ausencias.ts @@ -0,0 +1,36 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const ausenciasTables = { + // 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']) +}; diff --git a/packages/backend/convex/tables/auth.ts b/packages/backend/convex/tables/auth.ts new file mode 100644 index 0000000..42b8739 --- /dev/null +++ b/packages/backend/convex/tables/auth.ts @@ -0,0 +1,172 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const authTables = { + // 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']), + + // 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']), + + configuracaoAcesso: defineTable({ + chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc. + valor: v.string(), + descricao: v.string() + }).index('by_chave', ['chave']), + + // 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']) +}; diff --git a/packages/backend/convex/tables/chat.ts b/packages/backend/convex/tables/chat.ts new file mode 100644 index 0000000..5230eb3 --- /dev/null +++ b/packages/backend/convex/tables/chat.ts @@ -0,0 +1,173 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const chatTables = { + // 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']) +}; diff --git a/packages/backend/convex/tables/contratos.ts b/packages/backend/convex/tables/contratos.ts new file mode 100644 index 0000000..4643046 --- /dev/null +++ b/packages/backend/convex/tables/contratos.ts @@ -0,0 +1,37 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const situacaoContrato = v.union( + v.literal('em_execucao'), + v.literal('rescendido'), + v.literal('aguardando_assinatura'), + v.literal('finalizado') +); + +export const contratosTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/cursos.ts b/packages/backend/convex/tables/cursos.ts new file mode 100644 index 0000000..411c71d --- /dev/null +++ b/packages/backend/convex/tables/cursos.ts @@ -0,0 +1,11 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const cursosTables = { + cursos: defineTable({ + funcionarioId: v.id('funcionarios'), + descricao: v.string(), + data: v.string(), + certificadoId: v.optional(v.id('_storage')) + }).index('by_funcionario', ['funcionarioId']) +}; diff --git a/packages/backend/convex/tables/empresas.ts b/packages/backend/convex/tables/empresas.ts new file mode 100644 index 0000000..29f5fbe --- /dev/null +++ b/packages/backend/convex/tables/empresas.ts @@ -0,0 +1,29 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const empresasTable = { + 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']) +}; diff --git a/packages/backend/convex/tables/enderecos.ts b/packages/backend/convex/tables/enderecos.ts new file mode 100644 index 0000000..e47158b --- /dev/null +++ b/packages/backend/convex/tables/enderecos.ts @@ -0,0 +1,16 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const enderecosTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/ferias.ts b/packages/backend/convex/tables/ferias.ts new file mode 100644 index 0000000..ccd0c01 --- /dev/null +++ b/packages/backend/convex/tables/ferias.ts @@ -0,0 +1,55 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const feriasTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/flows.ts b/packages/backend/convex/tables/flows.ts new file mode 100644 index 0000000..5058967 --- /dev/null +++ b/packages/backend/convex/tables/flows.ts @@ -0,0 +1,132 @@ +import { defineTable } from 'convex/server'; +import { Infer, v } from 'convex/values'; + +// Status de templates de fluxo +export const flowTemplateStatus = v.union( + v.literal('draft'), + v.literal('published'), + v.literal('archived') +); +export type FlowTemplateStatus = Infer; + +// Status de instâncias de fluxo +export const flowInstanceStatus = v.union( + v.literal('active'), + v.literal('completed'), + v.literal('cancelled') +); +export type FlowInstanceStatus = Infer; + +// Status de passos de instância de fluxo +export const flowInstanceStepStatus = v.union( + v.literal('pending'), + v.literal('in_progress'), + v.literal('completed'), + v.literal('blocked') +); +export type FlowInstanceStepStatus = Infer; + +export const flowsTables = { + // 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']) +}; diff --git a/packages/backend/convex/tables/funcionarios.ts b/packages/backend/convex/tables/funcionarios.ts new file mode 100644 index 0000000..826ea19 --- /dev/null +++ b/packages/backend/convex/tables/funcionarios.ts @@ -0,0 +1,172 @@ +import { defineTable } from 'convex/server'; +import { Infer, v } from 'convex/values'; + +export const simboloTipo = v.union( + v.literal('cargo_comissionado'), + v.literal('funcao_gratificada') +); +export type SimboloTipo = Infer; + +export const funcionariosTables = { + simbolos: defineTable({ + nome: v.string(), + tipo: simboloTipo, + descricao: v.string(), + vencValor: v.string(), + repValor: v.string(), + valor: v.string() + }), + + 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']) +}; diff --git a/packages/backend/convex/tables/licencas.ts b/packages/backend/convex/tables/licencas.ts new file mode 100644 index 0000000..9597bb9 --- /dev/null +++ b/packages/backend/convex/tables/licencas.ts @@ -0,0 +1,22 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const licencasTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/pedidos.ts b/packages/backend/convex/tables/pedidos.ts new file mode 100644 index 0000000..23b1a99 --- /dev/null +++ b/packages/backend/convex/tables/pedidos.ts @@ -0,0 +1,48 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const pedidosTables = { + pedidos: defineTable({ + numeroSei: v.optional(v.string()), + status: v.union( + v.literal('em_rascunho'), + v.literal('aguardando_aceite'), + v.literal('em_analise'), + v.literal('precisa_ajustes'), + v.literal('cancelado'), + v.literal('concluido') + ), + acaoId: v.optional(v.id('acoes')), + criadoPor: v.id('usuarios'), + criadoEm: v.number(), + atualizadoEm: v.number() + }) + .index('by_numeroSei', ['numeroSei']) + .index('by_status', ['status']) + .index('by_criadoPor', ['criadoPor']) + .index('by_acaoId', ['acaoId']), + + pedidoItems: defineTable({ + pedidoId: v.id('pedidos'), + produtoId: v.id('produtos'), + valorEstimado: v.string(), + valorReal: v.optional(v.string()), + quantidade: v.number(), + adicionadoPor: v.id('funcionarios'), + criadoEm: v.number() + }) + .index('by_pedidoId', ['pedidoId']) + .index('by_produtoId', ['produtoId']) + .index('by_adicionadoPor', ['adicionadoPor']), + + historicoPedidos: defineTable({ + pedidoId: v.id('pedidos'), + usuarioId: v.id('usuarios'), + acao: v.string(), // "criacao", "alteracao_status", "adicao_item", "remocao_item", "edicao_item" + detalhes: v.optional(v.string()), // JSON string + data: v.number() + }) + .index('by_pedidoId', ['pedidoId']) + .index('by_usuarioId', ['usuarioId']) + .index('by_data', ['data']) +}; diff --git a/packages/backend/convex/tables/ponto.ts b/packages/backend/convex/tables/ponto.ts new file mode 100644 index 0000000..4233db6 --- /dev/null +++ b/packages/backend/convex/tables/ponto.ts @@ -0,0 +1,266 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const pontoTables = { + // Sistema de Controle de Ponto + registrosPonto: defineTable({ + funcionarioId: v.id('funcionarios'), + tipo: v.union( + v.literal('entrada'), + v.literal('saida_almoco'), + v.literal('retorno_almoco'), + v.literal('saida') + ), + data: v.string(), // YYYY-MM-DD + hora: v.number(), + minuto: v.number(), + segundo: v.number(), + timestamp: v.number(), // Timestamp completo para ordenação + imagemId: v.optional(v.id('_storage')), + sincronizadoComServidor: v.boolean(), + toleranciaMinutos: v.number(), + dentroDoPrazo: v.boolean(), + + // Informações de Rede + ipAddress: v.optional(v.string()), + ipPublico: v.optional(v.string()), + ipLocal: v.optional(v.string()), + + // Informações do Navegador + userAgent: v.optional(v.string()), + browser: v.optional(v.string()), + browserVersion: v.optional(v.string()), + engine: v.optional(v.string()), + + // Informações do Sistema + sistemaOperacional: v.optional(v.string()), + osVersion: v.optional(v.string()), + arquitetura: v.optional(v.string()), + plataforma: v.optional(v.string()), + + // Informações de Localização + latitude: v.optional(v.number()), + longitude: v.optional(v.number()), + precisao: v.optional(v.number()), + altitude: v.optional(v.union(v.number(), v.null())), + altitudeAccuracy: v.optional(v.union(v.number(), v.null())), + heading: v.optional(v.union(v.number(), v.null())), + speed: v.optional(v.union(v.number(), v.null())), + confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend) + scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend) + suspeitaSpoofing: v.optional(v.boolean()), + motivoSuspeita: v.optional(v.string()), + avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação + distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS + velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro + distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro + tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro + // Informações de Geofencing + enderecoMarcacaoEsperado: v.optional(v.id('enderecosMarcacao')), // Endereço mais próximo esperado + distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado + dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido + enderecoMarcacaoUsado: v.optional(v.id('enderecosMarcacao')), // Qual endereço foi usado na validação + raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros + endereco: v.optional(v.string()), + cidade: v.optional(v.string()), + estado: v.optional(v.string()), + pais: v.optional(v.string()), + timezone: v.optional(v.string()), + + // Informações do Dispositivo + deviceType: v.optional(v.string()), + deviceModel: v.optional(v.string()), + screenResolution: v.optional(v.string()), + coresTela: v.optional(v.number()), + idioma: v.optional(v.string()), + + // Informações Adicionais + isMobile: v.optional(v.boolean()), + isTablet: v.optional(v.boolean()), + isDesktop: v.optional(v.boolean()), + connectionType: v.optional(v.string()), + memoryInfo: v.optional(v.string()), + + // Informações de Sensores (Acelerômetro e Giroscópio) + acelerometroX: v.optional(v.number()), + acelerometroY: v.optional(v.number()), + acelerometroZ: v.optional(v.number()), + movimentoDetectado: v.optional(v.boolean()), + magnitudeMovimento: v.optional(v.number()), + variacaoAcelerometro: v.optional(v.number()), + giroscopioAlpha: v.optional(v.number()), + giroscopioBeta: v.optional(v.number()), + giroscopioGamma: v.optional(v.number()), + sensorDisponivel: v.optional(v.boolean()), + permissaoSensorNegada: v.optional(v.boolean()), + + // Justificativa opcional para o registro + justificativa: v.optional(v.string()), + + // Campos para homologação + editadoPorGestor: v.optional(v.boolean()), + homologacaoId: v.optional(v.id('homologacoesPonto')), + + criadoEm: v.number() + }) + .index('by_funcionario_data', ['funcionarioId', 'data']) + .index('by_data', ['data']) + .index('by_dentro_prazo', ['dentroDoPrazo', 'data']) + .index('by_funcionario_timestamp', ['funcionarioId', 'timestamp']), + + // Endereços de Marcação - Locais permitidos para registro de ponto + enderecosMarcacao: defineTable({ + nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC" + descricao: v.optional(v.string()), // Descrição opcional + // Coordenadas (obrigatórias) + latitude: v.number(), + longitude: v.number(), + // Endereço físico (para exibição) + endereco: v.string(), // Ex: "Rua Exemplo, 123" + bairro: v.optional(v.string()), // Bairro do endereço + cep: v.optional(v.string()), + cidade: v.string(), + estado: v.string(), + pais: v.optional(v.string()), // Padrão: "Brasil" + // Configurações + raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m) + ativo: v.boolean(), + // Tipos de uso + tipo: v.union( + v.literal('sede'), // Sede principal (para todos) + v.literal('home_office'), // Home office específico + v.literal('deslocamento'), // Deslocamento temporário + v.literal('cliente') // Local de cliente + ), + // Metadados + criadoPor: v.id('usuarios'), + criadoEm: v.number(), + atualizadoPor: v.optional(v.id('usuarios')), + atualizadoEm: v.optional(v.number()) + }) + .index('by_ativo', ['ativo']) + .index('by_tipo', ['tipo']) + .index('by_cidade', ['cidade']), + + // Associação Funcionário ↔ Endereço de Marcação + funcionarioEnderecosMarcacao: defineTable({ + funcionarioId: v.id('funcionarios'), + enderecoMarcacaoId: v.id('enderecosMarcacao'), + // Configurações específicas do funcionário + raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão + // Período de validade (para deslocamentos temporários) + dataInicio: v.optional(v.string()), // YYYY-MM-DD + dataFim: v.optional(v.string()), // YYYY-MM-DD + // Status + ativo: v.boolean(), + // Metadados + criadoPor: v.id('usuarios'), + criadoEm: v.number() + }) + .index('by_funcionario', ['funcionarioId']) + .index('by_endereco', ['enderecoMarcacaoId']) + .index('by_funcionario_ativo', ['funcionarioId', 'ativo']) + .index('by_endereco_ativo', ['enderecoMarcacaoId', 'ativo']), + + configuracaoPonto: defineTable({ + horarioEntrada: v.string(), // HH:mm + horarioSaidaAlmoco: v.string(), // HH:mm + horarioRetornoAlmoco: v.string(), // HH:mm + horarioSaida: v.string(), // HH:mm + toleranciaMinutos: v.number(), + // Nomes personalizados dos tipos de registro + nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1" + nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1" + nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2" + nomeSaida: v.optional(v.string()), // Padrão: "Saída 2" + // Ajuste de fuso horário (GMT offset em horas) + gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC) + // Configurações de geofencing + validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização + toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros + ativo: v.boolean(), + atualizadoPor: v.id('usuarios'), + atualizadoEm: v.number() + }).index('by_ativo', ['ativo']), + + configuracaoRelogio: defineTable({ + servidorNTP: v.optional(v.string()), + portaNTP: v.optional(v.number()), + usarServidorExterno: v.boolean(), + fallbackParaPC: v.boolean(), + ultimaSincronizacao: v.optional(v.number()), + offsetSegundos: v.optional(v.number()), + // Ajuste de fuso horário (GMT offset em horas) + gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC) + atualizadoPor: v.id('usuarios'), + atualizadoEm: v.number() + }).index('by_ativo', ['usarServidorExterno']), + + // Banco de Horas - Saldo diário de horas trabalhadas + bancoHoras: defineTable({ + funcionarioId: v.id('funcionarios'), + data: v.string(), // YYYY-MM-DD + cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos) + horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos) + saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit) + registrosPontoIds: v.array(v.id('registrosPonto')), // IDs dos registros do dia + 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']) +}; diff --git a/packages/backend/convex/tables/produtos.ts b/packages/backend/convex/tables/produtos.ts new file mode 100644 index 0000000..c4ee858 --- /dev/null +++ b/packages/backend/convex/tables/produtos.ts @@ -0,0 +1,24 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const produtosTables = { + produtos: defineTable({ + nome: v.string(), + valorEstimado: v.string(), + tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')), + criadoPor: v.id('usuarios'), + criadoEm: v.number() + }) + .searchIndex('search_nome', { searchField: 'nome' }) + .index('by_nome', ['nome']) + .index('by_tipo', ['tipo']), + + acoes: defineTable({ + nome: v.string(), + tipo: v.union(v.literal('projeto'), v.literal('lei')), + criadoPor: v.id('usuarios'), + criadoEm: v.number() + }) + .index('by_nome', ['nome']) + .index('by_tipo', ['tipo']) +}; diff --git a/packages/backend/convex/tables/security.ts b/packages/backend/convex/tables/security.ts new file mode 100644 index 0000000..dde1069 --- /dev/null +++ b/packages/backend/convex/tables/security.ts @@ -0,0 +1,330 @@ +import { defineTable } from 'convex/server'; +import { Infer, v } from 'convex/values'; + +export const ataqueCiberneticoTipo = v.union( + v.literal('phishing'), + v.literal('malware'), + v.literal('ransomware'), + v.literal('brute_force'), + v.literal('credential_stuffing'), + v.literal('sql_injection'), + v.literal('xss'), + v.literal('path_traversal'), + v.literal('command_injection'), + v.literal('nosql_injection'), + v.literal('xxe'), + v.literal('man_in_the_middle'), + v.literal('ddos'), + v.literal('engenharia_social'), + v.literal('cve_exploit'), + v.literal('apt'), + v.literal('zero_day'), + v.literal('supply_chain'), + v.literal('fileless_malware'), + v.literal('polymorphic_malware'), + v.literal('ransomware_lateral'), + v.literal('deepfake_phishing'), + v.literal('adversarial_ai'), + v.literal('side_channel'), + v.literal('firmware_bootloader'), + v.literal('bec'), + v.literal('botnet'), + v.literal('ot_ics'), + v.literal('quantum_attack') +); +export type AtaqueCiberneticoTipo = Infer; + +export const severidadeSeguranca = v.union( + v.literal('informativo'), + v.literal('baixo'), + v.literal('moderado'), + v.literal('alto'), + v.literal('critico') +); +export type SeveridadeSeguranca = Infer; + +export const statusEventoSeguranca = v.union( + v.literal('detectado'), + v.literal('investigando'), + v.literal('contido'), + v.literal('falso_positivo'), + v.literal('escalado'), + v.literal('resolvido') +); +export type StatusEventoSeguranca = Infer; + +export const sensorSegurancaTipo = v.union( + v.literal('network'), + v.literal('endpoint'), + v.literal('application'), + v.literal('gateway'), + v.literal('ot'), + v.literal('honeypot') +); +export type SensorSegurancaTipo = Infer; + +export const sensorSegurancaStatus = v.union( + v.literal('ativo'), + v.literal('inativo'), + v.literal('degradado'), + v.literal('manutencao') +); +export type SensorSegurancaStatus = Infer; + +export const threatIntelTipo = v.union( + v.literal('open_source'), + v.literal('commercial'), + v.literal('internal'), + v.literal('gov'), + v.literal('research') +); + +export const threatIntelFormato = v.union( + v.literal('json'), + v.literal('stix'), + v.literal('csv'), + v.literal('text'), + v.literal('custom') +); + +export const acaoIncidenteTipo = v.union( + v.literal('block_ip'), + v.literal('unblock_ip'), + v.literal('block_port'), + v.literal('liberar_porta'), + v.literal('notificar'), + v.literal('isolar_host'), + v.literal('gerar_relatorio'), + v.literal('criar_ticket'), + v.literal('ajuste_regra'), + v.literal('custom') +); + +export const acaoIncidenteStatus = v.union( + v.literal('pendente'), + v.literal('executando'), + v.literal('concluido'), + v.literal('falhou') +); + +export const reportStatus = v.union( + v.literal('pendente'), + v.literal('processando'), + v.literal('concluido'), + v.literal('falhou') +); + +export const securityTables = { + // 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']) +}; diff --git a/packages/backend/convex/tables/setores.ts b/packages/backend/convex/tables/setores.ts new file mode 100644 index 0000000..7b65fa0 --- /dev/null +++ b/packages/backend/convex/tables/setores.ts @@ -0,0 +1,24 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const setoresTables = { + // 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']) +}; diff --git a/packages/backend/convex/tables/system.ts b/packages/backend/convex/tables/system.ts new file mode 100644 index 0000000..395c3c8 --- /dev/null +++ b/packages/backend/convex/tables/system.ts @@ -0,0 +1,220 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; +import { ataqueCiberneticoTipo, severidadeSeguranca } from './security'; + +export const systemTables = { + // Logs de Atividades do Sistema + logsAtividades: defineTable({ + usuarioId: v.id('usuarios'), + acao: v.string(), + recurso: v.string(), + recursoId: v.optional(v.string()), + detalhes: v.optional(v.string()), + 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']), + + // Configuração de Email/SMTP + configuracaoEmail: defineTable({ + servidor: v.string(), // smtp.gmail.com + porta: v.number(), // 587, 465, etc. + usuario: v.string(), + senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP + emailRemetente: v.string(), + nomeRemetente: v.string(), + usarSSL: v.boolean(), + usarTLS: v.boolean(), + ativo: v.boolean(), + testadoEm: v.optional(v.number()), + configuradoPor: v.id('usuarios'), + atualizadoEm: v.number() + }).index('by_ativo', ['ativo']), + + // Fila de Emails + notificacoesEmail: defineTable({ + destinatario: v.string(), // email + destinatarioId: v.optional(v.id('usuarios')), + assunto: v.string(), + corpo: v.string(), // HTML ou texto + templateId: v.optional(v.id('templatesMensagens')), + status: v.union( + v.literal('pendente'), + v.literal('enviando'), + v.literal('enviado'), + v.literal('falha') + ), + tentativas: v.number(), + ultimaTentativa: v.optional(v.number()), + erroDetalhes: v.optional(v.string()), + enviadoPor: v.id('usuarios'), + criadoEm: v.number(), + enviadoEm: v.optional(v.number()), + agendadaPara: v.optional(v.number()) // timestamp para agendamento + }) + .index('by_status', ['status']) + .index('by_destinatario', ['destinatarioId']) + .index('by_enviado_por', ['enviadoPor']) + .index('by_criado_em', ['criadoEm']) + .index('by_agendamento', ['agendadaPara']), + + // 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']), + + // 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']), + + 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']), + + // Configurações Gerais + config: defineTable({ + comprasSetorId: v.optional(v.id('setores')), + criadoPor: v.id('usuarios'), + atualizadoEm: v.number() + }), + + // Templates de Mensagens + templatesMensagens: defineTable({ + codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc. + nome: v.string(), + tipo: v.union( + v.literal('sistema'), // predefinido, não editável + v.literal('customizado') // criado por TI_MASTER + ), + titulo: v.string(), + corpo: v.string(), // pode ter variáveis {{variavel}} + htmlCorpo: v.optional(v.string()), // versão HTML do corpo (com wrapper) + variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.] + categoria: v.optional(v.union(v.literal('email'), v.literal('chat'), v.literal('ambos'))), // categoria do template + tags: v.optional(v.array(v.string())), // tags para organização + criadoPor: v.optional(v.id('usuarios')), + criadoEm: v.number() + }) + .index('by_codigo', ['codigo']) + .index('by_tipo', ['tipo']) + .index('by_criado_por', ['criadoPor']) + .index('by_categoria', ['categoria']), + + // Configuração de Jitsi Meet + configuracaoJitsi: defineTable({ + domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com") + appId: v.string(), // ID da aplicação Jitsi + roomPrefix: v.string(), // Prefixo para nomes de salas + useHttps: v.boolean(), // Usar HTTPS + acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento) + ativo: v.boolean(), // Configuração ativa + testadoEm: v.optional(v.number()), // Timestamp do último teste de conexão + configuradoEm: v.optional(v.number()), // Timestamp da última configuração do servidor Docker + configuradoNoServidor: v.optional(v.boolean()), // Indica se a configuração foi aplicada no servidor + configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor + configuradoPor: v.id('usuarios'), // Usuário que configurou + atualizadoEm: v.number(), // Timestamp de atualização + jitsiConfigPath: v.optional(v.string()), // Caminho da configuração do Jitsi no servidor (ex: "~/.jitsi-meet-cfg") + sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor + sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada) + sshPort: v.optional(v.number()) // Porta SSH (padrão: 22) + }).index('by_ativo', ['ativo']) +}; diff --git a/packages/backend/convex/tables/tickets.ts b/packages/backend/convex/tables/tickets.ts new file mode 100644 index 0000000..00fdb8b --- /dev/null +++ b/packages/backend/convex/tables/tickets.ts @@ -0,0 +1,165 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const ticketsTables = { + 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']) +}; diff --git a/packages/backend/convex/tables/times.ts b/packages/backend/convex/tables/times.ts new file mode 100644 index 0000000..13c45df --- /dev/null +++ b/packages/backend/convex/tables/times.ts @@ -0,0 +1,26 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const timesTables = { + 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']) +}; diff --git a/packages/backend/convex/todos.ts b/packages/backend/convex/todos.ts deleted file mode 100644 index 070c9de..0000000 --- a/packages/backend/convex/todos.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { query, mutation } from "./_generated/server"; -import { v } from "convex/values"; - -export const getAll = query({ - handler: async (ctx) => { - return await ctx.db.query("todos").collect(); - }, -}); - -export const create = mutation({ - args: { - text: v.string(), - }, - handler: async (ctx, args) => { - const newTodoId = await ctx.db.insert("todos", { - text: args.text, - completed: false, - }); - return await ctx.db.get(newTodoId); - }, -}); - -export const toggle = mutation({ - args: { - id: v.id("todos"), - completed: v.boolean(), - }, - handler: async (ctx, args) => { - await ctx.db.patch(args.id, { completed: args.completed }); - return { success: true }; - }, -}); - -export const deleteTodo = mutation({ - args: { - id: v.id("todos"), - }, - handler: async (ctx, args) => { - await ctx.db.delete(args.id); - return { success: true }; - }, -});