feat: Introduce structured table definitions in convex/tables for v…
#56
42
packages/backend/convex/_generated/api.d.ts
vendored
42
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
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")),
|
||||
responsavelId: v.optional(v.id('funcionarios')),
|
||||
dataInicio: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "contratos",
|
||||
acao: "listar",
|
||||
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!)
|
||||
q = q.withIndex('by_responsavel', (q) =>
|
||||
q.eq('responsavelId', args.responsavelId!)
|
||||
) as typeof q;
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ export const listar = query({
|
||||
let resultado = contratos;
|
||||
|
||||
if (args.dataInicio) {
|
||||
resultado = resultado.filter(
|
||||
(c) => c.dataInicioVigencia >= args.dataInicio!
|
||||
);
|
||||
resultado = resultado.filter((c) => c.dataInicioVigencia >= args.dataInicio!);
|
||||
}
|
||||
|
||||
if (args.dataFim) {
|
||||
@@ -48,21 +46,21 @@ export const listar = query({
|
||||
return {
|
||||
...c,
|
||||
contratada,
|
||||
responsavel,
|
||||
responsavel
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return contratosEnriquecidos;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const obter = query({
|
||||
args: { id: v.id("contratos") },
|
||||
args: { id: v.id('contratos') },
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "contratos",
|
||||
acao: "ver",
|
||||
recurso: 'contratos',
|
||||
acao: 'ver'
|
||||
});
|
||||
const contrato = await ctx.db.get(args.id);
|
||||
if (!contrato) return null;
|
||||
@@ -73,17 +71,17 @@ export const obter = query({
|
||||
return {
|
||||
...contrato,
|
||||
contratada,
|
||||
responsavel,
|
||||
responsavel
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
contratadaId: v.id("empresas"),
|
||||
contratadaId: v.id('empresas'),
|
||||
objeto: v.string(),
|
||||
numeroNotaEmpenho: v.string(),
|
||||
responsavelId: v.id("funcionarios"),
|
||||
responsavelId: v.id('funcionarios'),
|
||||
departamento: v.string(),
|
||||
situacao: situacaoContrato,
|
||||
numeroProcessoLicitatorio: v.string(),
|
||||
@@ -95,34 +93,34 @@ export const criar = mutation({
|
||||
nomeFiscal: v.string(),
|
||||
valorTotal: v.string(),
|
||||
dataAditivoPrazo: v.optional(v.string()),
|
||||
diasAvisoVencimento: v.number(),
|
||||
diasAvisoVencimento: v.number()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "contratos",
|
||||
acao: "criar",
|
||||
recurso: 'contratos',
|
||||
acao: 'criar'
|
||||
});
|
||||
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) throw new Error("Não autenticado");
|
||||
if (!usuario) throw new Error('Não autenticado');
|
||||
|
||||
const id = await ctx.db.insert("contratos", {
|
||||
const id = await ctx.db.insert('contratos', {
|
||||
...args,
|
||||
criadoPor: usuario._id,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return id;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const editar = mutation({
|
||||
args: {
|
||||
id: v.id("contratos"),
|
||||
contratadaId: v.optional(v.id("empresas")),
|
||||
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")),
|
||||
responsavelId: v.optional(v.id('funcionarios')),
|
||||
departamento: v.optional(v.string()),
|
||||
situacao: v.optional(situacaoContrato),
|
||||
numeroProcessoLicitatorio: v.optional(v.string()),
|
||||
@@ -134,32 +132,32 @@ export const editar = mutation({
|
||||
nomeFiscal: v.optional(v.string()),
|
||||
valorTotal: v.optional(v.string()),
|
||||
dataAditivoPrazo: v.optional(v.string()),
|
||||
diasAvisoVencimento: v.optional(v.number()),
|
||||
diasAvisoVencimento: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "contratos",
|
||||
acao: "editar",
|
||||
recurso: 'contratos',
|
||||
acao: 'editar'
|
||||
});
|
||||
|
||||
const { id, ...campos } = args;
|
||||
|
||||
await ctx.db.patch(id, {
|
||||
...campos,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const excluir = mutation({
|
||||
args: { id: v.id("contratos") },
|
||||
args: { id: v.id('contratos') },
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "contratos",
|
||||
acao: "excluir",
|
||||
recurso: 'contratos',
|
||||
acao: 'excluir'
|
||||
});
|
||||
await ctx.db.delete(args.id);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const verificarVencimentos = query({
|
||||
@@ -169,15 +167,15 @@ export const verificarVencimentos = query({
|
||||
// Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento
|
||||
|
||||
const hoje = new Date();
|
||||
const hojeStr = hoje.toISOString().split("T")[0];
|
||||
const hojeStr = hoje.toISOString().split('T')[0];
|
||||
|
||||
// Buscar contratos ativos (em execução ou aguardando assinatura)
|
||||
const contratos = await ctx.db
|
||||
.query("contratos")
|
||||
.query('contratos')
|
||||
.filter((q) =>
|
||||
q.or(
|
||||
q.eq(q.field("situacao"), "em_execucao"),
|
||||
q.eq(q.field("situacao"), "aguardando_assinatura")
|
||||
q.eq(q.field('situacao'), 'em_execucao'),
|
||||
q.eq(q.field('situacao'), 'aguardando_assinatura')
|
||||
)
|
||||
)
|
||||
.collect();
|
||||
@@ -189,12 +187,12 @@ export const verificarVencimentos = query({
|
||||
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;
|
||||
});
|
||||
|
||||
return proximosVencimento;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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');
|
||||
@@ -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
|
||||
@@ -1790,9 +1804,7 @@ export const listarNotas = query({
|
||||
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 {
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
@@ -8,19 +8,19 @@ import { Doc, Id } from "./_generated/dataModel";
|
||||
*/
|
||||
export async function registrarAtividade(
|
||||
ctx: MutationCtx,
|
||||
usuarioId: Id<"usuarios">,
|
||||
usuarioId: Id<'usuarios'>,
|
||||
acao: string,
|
||||
recurso: string,
|
||||
detalhes?: string,
|
||||
recursoId?: string
|
||||
) {
|
||||
await ctx.db.insert("logsAtividades", {
|
||||
await ctx.db.insert('logsAtividades', {
|
||||
usuarioId,
|
||||
acao,
|
||||
recurso,
|
||||
recursoId,
|
||||
detalhes,
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,39 +29,39 @@ export async function registrarAtividade(
|
||||
*/
|
||||
export const listarAtividades = query({
|
||||
args: {
|
||||
usuarioId: v.optional(v.id("usuarios")),
|
||||
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()),
|
||||
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")
|
||||
.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")
|
||||
.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")
|
||||
.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")
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp')
|
||||
.order('desc')
|
||||
.take(args.limite || 100);
|
||||
}
|
||||
|
||||
@@ -78,21 +78,21 @@ export const listarAtividades = query({
|
||||
const atividadesComUsuarios = await Promise.all(
|
||||
atividades.map(async (atividade) => {
|
||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
||||
let matricula = "N/A";
|
||||
let matricula = 'N/A';
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
matricula = funcionario?.matricula || 'N/A';
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: matricula,
|
||||
usuarioNome: usuario?.nome || 'Usuário Desconhecido',
|
||||
usuarioMatricula: matricula
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return atividadesComUsuarios;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -100,16 +100,16 @@ export const listarAtividades = query({
|
||||
*/
|
||||
export const obterEstatisticasAtividades = query({
|
||||
args: {
|
||||
periodo: v.optional(v.number()), // dias (ex: 7, 30)
|
||||
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))
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp')
|
||||
.filter((q) => q.gte(q.field('timestamp'), dataInicio))
|
||||
.collect();
|
||||
|
||||
// Agrupar por ação
|
||||
@@ -128,7 +128,7 @@ export const obterEstatisticasAtividades = query({
|
||||
const porDia: Record<string, number> = {};
|
||||
atividades.forEach((ativ) => {
|
||||
const data = new Date(ativ.timestamp);
|
||||
const dia = data.toISOString().split("T")[0];
|
||||
const dia = data.toISOString().split('T')[0];
|
||||
porDia[dia] = (porDia[dia] || 0) + 1;
|
||||
});
|
||||
|
||||
@@ -136,9 +136,9 @@ export const obterEstatisticasAtividades = query({
|
||||
total: atividades.length,
|
||||
porAcao,
|
||||
porRecurso,
|
||||
porDia,
|
||||
porDia
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -147,34 +147,34 @@ export const obterEstatisticasAtividades = query({
|
||||
export const obterHistoricoRecurso = query({
|
||||
args: {
|
||||
recurso: v.string(),
|
||||
recursoId: 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)
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_recurso_id', (q) =>
|
||||
q.eq('recurso', args.recurso).eq('recursoId', args.recursoId)
|
||||
)
|
||||
.order("desc")
|
||||
.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";
|
||||
let matricula = 'N/A';
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
matricula = funcionario?.matricula || 'N/A';
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: matricula,
|
||||
usuarioNome: usuario?.nome || 'Usuário Desconhecido',
|
||||
usuarioMatricula: matricula
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return atividadesComUsuarios;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
@@ -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,7 +903,8 @@ 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
|
||||
const builder =
|
||||
args.lista === undefined
|
||||
? ctx.db.query('ipReputation')
|
||||
: args.lista === 'blacklist'
|
||||
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1629,7 +1658,8 @@ async function aplicarRateLimit(
|
||||
|
||||
// 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<string, { kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }>;
|
||||
} as Record<
|
||||
string,
|
||||
{ kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }
|
||||
>;
|
||||
|
||||
const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig);
|
||||
|
||||
@@ -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 ? {
|
||||
fingerprint: args.userAgent
|
||||
? {
|
||||
userAgent: args.userAgent
|
||||
} : undefined,
|
||||
}
|
||||
: 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' ? {
|
||||
severidade === 'alto'
|
||||
? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + (60 * 60 * 1000) // Bloquear por 1 hora
|
||||
} : undefined
|
||||
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' ? {
|
||||
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto'
|
||||
? {
|
||||
blacklist: true
|
||||
} : undefined
|
||||
}
|
||||
: 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' ? {
|
||||
severidade === 'alto'
|
||||
? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + (60 * 60 * 1000)
|
||||
} : undefined
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
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"),
|
||||
_id: v.id('simbolos'),
|
||||
_creationTime: v.number(),
|
||||
nome: v.string(),
|
||||
tipo: simboloTipo,
|
||||
descricao: v.string(),
|
||||
vencValor: v.string(),
|
||||
repValor: v.string(),
|
||||
valor: v.string(),
|
||||
valor: v.string()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query("simbolos").collect();
|
||||
},
|
||||
return await ctx.db.query('simbolos').collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const getById = query({
|
||||
args: {
|
||||
id: v.id("simbolos"),
|
||||
id: v.id('simbolos')
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("simbolos"),
|
||||
_id: v.id('simbolos'),
|
||||
_creationTime: v.number(),
|
||||
nome: v.string(),
|
||||
tipo: simboloTipo,
|
||||
descricao: v.string(),
|
||||
vencValor: v.string(),
|
||||
repValor: v.string(),
|
||||
valor: v.string(),
|
||||
valor: v.string()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
@@ -50,80 +50,80 @@ export const create = mutation({
|
||||
refValor: v.string(),
|
||||
vencValor: v.string(),
|
||||
descricao: v.string(),
|
||||
valor: v.optional(v.string()),
|
||||
valor: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let refValor = args.refValor;
|
||||
let vencValor = args.vencValor;
|
||||
let valor = args.valor ?? "";
|
||||
let valor = args.valor ?? '';
|
||||
|
||||
if (args.tipo === "cargo_comissionado") {
|
||||
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 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");
|
||||
throw new Error('Valor é obrigatório para função gratificada');
|
||||
}
|
||||
refValor = "";
|
||||
vencValor = "";
|
||||
refValor = '';
|
||||
vencValor = '';
|
||||
valor = args.valor;
|
||||
}
|
||||
const novoSimboloId = await ctx.db.insert("simbolos", {
|
||||
const novoSimboloId = await ctx.db.insert('simbolos', {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
repValor: refValor,
|
||||
vencValor: vencValor,
|
||||
tipo: args.tipo,
|
||||
valor,
|
||||
valor
|
||||
});
|
||||
return await ctx.db.get(novoSimboloId);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: {
|
||||
id: v.id("simbolos"),
|
||||
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"),
|
||||
id: v.id('simbolos'),
|
||||
nome: v.string(),
|
||||
tipo: simboloTipo,
|
||||
refValor: v.string(),
|
||||
vencValor: v.string(),
|
||||
descricao: v.string(),
|
||||
valor: v.optional(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 ?? "";
|
||||
let valor = args.valor ?? '';
|
||||
|
||||
if (args.tipo === "cargo_comissionado") {
|
||||
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 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");
|
||||
throw new Error('Valor é obrigatório para função gratificada');
|
||||
}
|
||||
refValor = "";
|
||||
vencValor = "";
|
||||
refValor = '';
|
||||
vencValor = '';
|
||||
valor = args.valor;
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ export const update = mutation({
|
||||
repValor: refValor,
|
||||
vencValor: vencValor,
|
||||
tipo: args.tipo,
|
||||
valor,
|
||||
valor
|
||||
});
|
||||
return null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -146,10 +146,10 @@ export const removerDuplicados = mutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
removidos: v.number(),
|
||||
mantidos: v.number(),
|
||||
mantidos: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const todosSimbolos = await ctx.db.query("simbolos").collect();
|
||||
const todosSimbolos = await ctx.db.query('simbolos').collect();
|
||||
|
||||
// Agrupar símbolos por nome
|
||||
const simbolosPorNome = new Map<string, typeof todosSimbolos>();
|
||||
@@ -181,8 +181,10 @@ export const removerDuplicados = mutation({
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`);
|
||||
console.log(
|
||||
`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`
|
||||
);
|
||||
|
||||
return { removidos, mantidos };
|
||||
},
|
||||
}
|
||||
});
|
||||
20
packages/backend/convex/tables/atestados.ts
Normal file
20
packages/backend/convex/tables/atestados.ts
Normal file
@@ -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'])
|
||||
};
|
||||
36
packages/backend/convex/tables/ausencias.ts
Normal file
36
packages/backend/convex/tables/ausencias.ts
Normal file
@@ -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'])
|
||||
};
|
||||
172
packages/backend/convex/tables/auth.ts
Normal file
172
packages/backend/convex/tables/auth.ts
Normal file
@@ -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'])
|
||||
};
|
||||
173
packages/backend/convex/tables/chat.ts
Normal file
173
packages/backend/convex/tables/chat.ts
Normal file
@@ -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'])
|
||||
};
|
||||
37
packages/backend/convex/tables/contratos.ts
Normal file
37
packages/backend/convex/tables/contratos.ts
Normal file
@@ -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'])
|
||||
};
|
||||
11
packages/backend/convex/tables/cursos.ts
Normal file
11
packages/backend/convex/tables/cursos.ts
Normal file
@@ -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'])
|
||||
};
|
||||
29
packages/backend/convex/tables/empresas.ts
Normal file
29
packages/backend/convex/tables/empresas.ts
Normal file
@@ -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'])
|
||||
};
|
||||
16
packages/backend/convex/tables/enderecos.ts
Normal file
16
packages/backend/convex/tables/enderecos.ts
Normal file
@@ -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'])
|
||||
};
|
||||
55
packages/backend/convex/tables/ferias.ts
Normal file
55
packages/backend/convex/tables/ferias.ts
Normal file
@@ -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'])
|
||||
};
|
||||
132
packages/backend/convex/tables/flows.ts
Normal file
132
packages/backend/convex/tables/flows.ts
Normal file
@@ -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<typeof flowTemplateStatus>;
|
||||
|
||||
// Status de instâncias de fluxo
|
||||
export const flowInstanceStatus = v.union(
|
||||
v.literal('active'),
|
||||
v.literal('completed'),
|
||||
v.literal('cancelled')
|
||||
);
|
||||
export type FlowInstanceStatus = Infer<typeof flowInstanceStatus>;
|
||||
|
||||
// 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<typeof flowInstanceStepStatus>;
|
||||
|
||||
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'])
|
||||
};
|
||||
172
packages/backend/convex/tables/funcionarios.ts
Normal file
172
packages/backend/convex/tables/funcionarios.ts
Normal file
@@ -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<typeof simboloTipo>;
|
||||
|
||||
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'])
|
||||
};
|
||||
22
packages/backend/convex/tables/licencas.ts
Normal file
22
packages/backend/convex/tables/licencas.ts
Normal file
@@ -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'])
|
||||
};
|
||||
48
packages/backend/convex/tables/pedidos.ts
Normal file
48
packages/backend/convex/tables/pedidos.ts
Normal file
@@ -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'])
|
||||
};
|
||||
266
packages/backend/convex/tables/ponto.ts
Normal file
266
packages/backend/convex/tables/ponto.ts
Normal file
@@ -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'])
|
||||
};
|
||||
24
packages/backend/convex/tables/produtos.ts
Normal file
24
packages/backend/convex/tables/produtos.ts
Normal file
@@ -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'])
|
||||
};
|
||||
330
packages/backend/convex/tables/security.ts
Normal file
330
packages/backend/convex/tables/security.ts
Normal file
@@ -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<typeof ataqueCiberneticoTipo>;
|
||||
|
||||
export const severidadeSeguranca = v.union(
|
||||
v.literal('informativo'),
|
||||
v.literal('baixo'),
|
||||
v.literal('moderado'),
|
||||
v.literal('alto'),
|
||||
v.literal('critico')
|
||||
);
|
||||
export type SeveridadeSeguranca = Infer<typeof severidadeSeguranca>;
|
||||
|
||||
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<typeof statusEventoSeguranca>;
|
||||
|
||||
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<typeof sensorSegurancaTipo>;
|
||||
|
||||
export const sensorSegurancaStatus = v.union(
|
||||
v.literal('ativo'),
|
||||
v.literal('inativo'),
|
||||
v.literal('degradado'),
|
||||
v.literal('manutencao')
|
||||
);
|
||||
export type SensorSegurancaStatus = Infer<typeof sensorSegurancaStatus>;
|
||||
|
||||
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'])
|
||||
};
|
||||
24
packages/backend/convex/tables/setores.ts
Normal file
24
packages/backend/convex/tables/setores.ts
Normal file
@@ -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'])
|
||||
};
|
||||
220
packages/backend/convex/tables/system.ts
Normal file
220
packages/backend/convex/tables/system.ts
Normal file
@@ -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'])
|
||||
};
|
||||
165
packages/backend/convex/tables/tickets.ts
Normal file
165
packages/backend/convex/tables/tickets.ts
Normal file
@@ -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'])
|
||||
};
|
||||
26
packages/backend/convex/tables/times.ts
Normal file
26
packages/backend/convex/tables/times.ts
Normal file
@@ -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'])
|
||||
};
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user