Merge remote-tracking branch 'origin/master' into ajustes_gerais

This commit is contained in:
2025-12-02 14:03:30 -03:00
46 changed files with 5178 additions and 2605 deletions

View File

@@ -8,6 +8,7 @@
* @module
*/
import type * as acoes from "../acoes.js";
import type * as actions_email from "../actions/email.js";
import type * as actions_linkPreview from "../actions/linkPreview.js";
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
@@ -21,6 +22,7 @@ import type * as auth_utils from "../auth/utils.js";
import type * as chamadas from "../chamadas.js";
import type * as chamados from "../chamados.js";
import type * as chat from "../chat.js";
import type * as config from "../config.js";
import type * as configuracaoEmail from "../configuracaoEmail.js";
import type * as configuracaoJitsi from "../configuracaoJitsi.js";
import type * as configuracaoPonto from "../configuracaoPonto.js";
@@ -44,9 +46,11 @@ import type * as logsAcesso from "../logsAcesso.js";
import type * as logsAtividades from "../logsAtividades.js";
import type * as logsLogin from "../logsLogin.js";
import type * as monitoramento from "../monitoramento.js";
import type * as pedidos from "../pedidos.js";
import type * as permissoesAcoes from "../permissoesAcoes.js";
import type * as pontos from "../pontos.js";
import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
import type * as produtos from "../produtos.js";
import type * as pushNotifications from "../pushNotifications.js";
import type * as roles from "../roles.js";
import type * as saldoFerias from "../saldoFerias.js";
@@ -54,9 +58,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";
@@ -71,6 +94,7 @@ import type {
} from "convex/server";
declare const fullApi: ApiFromModules<{
acoes: typeof acoes;
"actions/email": typeof actions_email;
"actions/linkPreview": typeof actions_linkPreview;
"actions/pushNotifications": typeof actions_pushNotifications;
@@ -84,6 +108,7 @@ declare const fullApi: ApiFromModules<{
chamadas: typeof chamadas;
chamados: typeof chamados;
chat: typeof chat;
config: typeof config;
configuracaoEmail: typeof configuracaoEmail;
configuracaoJitsi: typeof configuracaoJitsi;
configuracaoPonto: typeof configuracaoPonto;
@@ -107,9 +132,11 @@ declare const fullApi: ApiFromModules<{
logsAtividades: typeof logsAtividades;
logsLogin: typeof logsLogin;
monitoramento: typeof monitoramento;
pedidos: typeof pedidos;
permissoesAcoes: typeof permissoesAcoes;
pontos: typeof pontos;
preferenciasNotificacao: typeof preferenciasNotificacao;
produtos: typeof produtos;
pushNotifications: typeof pushNotifications;
roles: typeof roles;
saldoFerias: typeof saldoFerias;
@@ -117,9 +144,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;

View File

@@ -0,0 +1,56 @@
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('acoes').collect();
}
});
export const create = mutation({
args: {
nome: v.string(),
tipo: v.union(v.literal('projeto'), v.literal('lei'))
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
return await ctx.db.insert('acoes', {
...args,
criadoPor: user._id,
criadoEm: Date.now()
});
}
});
export const update = mutation({
args: {
id: v.id('acoes'),
nome: v.string(),
tipo: v.union(v.literal('projeto'), v.literal('lei'))
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
await ctx.db.patch(args.id, {
nome: args.nome,
tipo: args.tipo
});
}
});
export const remove = mutation({
args: {
id: v.id('acoes')
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
await ctx.db.delete(args.id);
}
});

View File

@@ -0,0 +1,38 @@
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
export const getComprasSetor = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('config').first();
}
});
export const updateComprasSetor = mutation({
args: {
setorId: v.id('setores')
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
// Check if user has permission (e.g., admin or TI) - For now, assuming any auth user can set it,
// but in production should be restricted.
const existingConfig = await ctx.db.query('config').first();
if (existingConfig) {
await ctx.db.patch(existingConfig._id, {
comprasSetorId: args.setorId,
atualizadoEm: Date.now()
});
} else {
await ctx.db.insert('config', {
comprasSetorId: args.setorId,
criadoPor: user._id,
atualizadoEm: Date.now()
});
}
}
});

View File

@@ -1,200 +1,198 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { situacaoContrato } from "./schema";
import { getCurrentUserFunction } from "./auth";
import { internal } from "./_generated/api";
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
import { situacaoContrato } from './tables/contratos';
import { getCurrentUserFunction } from './auth';
import { internal } from './_generated/api';
export const listar = query({
args: {
responsavelId: v.optional(v.id("funcionarios")),
dataInicio: v.optional(v.string()),
dataFim: v.optional(v.string()),
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "contratos",
acao: "listar",
});
args: {
responsavelId: v.optional(v.id('funcionarios')),
dataInicio: v.optional(v.string()),
dataFim: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'contratos',
acao: 'listar'
});
let q = ctx.db.query("contratos");
let q = ctx.db.query('contratos');
if (args.responsavelId) {
q = q.withIndex("by_responsavel", (q) =>
q.eq("responsavelId", args.responsavelId!)
) as typeof q;
}
if (args.responsavelId) {
q = q.withIndex('by_responsavel', (q) =>
q.eq('responsavelId', args.responsavelId!)
) as typeof q;
}
const contratos = await q.collect();
const contratos = await q.collect();
// Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo)
// Se o volume for muito grande, ideal seria criar índices específicos ou usar search.
let resultado = contratos;
// Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo)
// Se o volume for muito grande, ideal seria criar índices específicos ou usar search.
let resultado = contratos;
if (args.dataInicio) {
resultado = resultado.filter(
(c) => c.dataInicioVigencia >= args.dataInicio!
);
}
if (args.dataInicio) {
resultado = resultado.filter((c) => c.dataInicioVigencia >= args.dataInicio!);
}
if (args.dataFim) {
resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!);
}
if (args.dataFim) {
resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!);
}
// Enriquecer com dados relacionados
const contratosEnriquecidos = await Promise.all(
resultado.map(async (c) => {
const contratada = await ctx.db.get(c.contratadaId);
const responsavel = await ctx.db.get(c.responsavelId);
return {
...c,
contratada,
responsavel,
};
})
);
// Enriquecer com dados relacionados
const contratosEnriquecidos = await Promise.all(
resultado.map(async (c) => {
const contratada = await ctx.db.get(c.contratadaId);
const responsavel = await ctx.db.get(c.responsavelId);
return {
...c,
contratada,
responsavel
};
})
);
return contratosEnriquecidos;
},
return contratosEnriquecidos;
}
});
export const obter = query({
args: { id: v.id("contratos") },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "contratos",
acao: "ver",
});
const contrato = await ctx.db.get(args.id);
if (!contrato) return null;
args: { id: v.id('contratos') },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'contratos',
acao: 'ver'
});
const contrato = await ctx.db.get(args.id);
if (!contrato) return null;
const contratada = await ctx.db.get(contrato.contratadaId);
const responsavel = await ctx.db.get(contrato.responsavelId);
const contratada = await ctx.db.get(contrato.contratadaId);
const responsavel = await ctx.db.get(contrato.responsavelId);
return {
...contrato,
contratada,
responsavel,
};
},
return {
...contrato,
contratada,
responsavel
};
}
});
export const criar = mutation({
args: {
contratadaId: v.id("empresas"),
objeto: v.string(),
numeroNotaEmpenho: v.string(),
responsavelId: v.id("funcionarios"),
departamento: v.string(),
situacao: situacaoContrato,
numeroProcessoLicitatorio: v.string(),
modalidade: v.string(),
numeroContrato: v.string(),
anoContrato: v.number(),
dataInicioVigencia: v.string(),
dataFimVigencia: v.string(),
nomeFiscal: v.string(),
valorTotal: v.string(),
dataAditivoPrazo: v.optional(v.string()),
diasAvisoVencimento: v.number(),
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "contratos",
acao: "criar",
});
args: {
contratadaId: v.id('empresas'),
objeto: v.string(),
numeroNotaEmpenho: v.string(),
responsavelId: v.id('funcionarios'),
departamento: v.string(),
situacao: situacaoContrato,
numeroProcessoLicitatorio: v.string(),
modalidade: v.string(),
numeroContrato: v.string(),
anoContrato: v.number(),
dataInicioVigencia: v.string(),
dataFimVigencia: v.string(),
nomeFiscal: v.string(),
valorTotal: v.string(),
dataAditivoPrazo: v.optional(v.string()),
diasAvisoVencimento: v.number()
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'contratos',
acao: 'criar'
});
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error("Não autenticado");
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Não autenticado');
const id = await ctx.db.insert("contratos", {
...args,
criadoPor: usuario._id,
criadoEm: Date.now(),
});
const id = await ctx.db.insert('contratos', {
...args,
criadoPor: usuario._id,
criadoEm: Date.now()
});
return id;
},
return id;
}
});
export const editar = mutation({
args: {
id: v.id("contratos"),
contratadaId: v.optional(v.id("empresas")),
objeto: v.optional(v.string()),
numeroNotaEmpenho: v.optional(v.string()),
responsavelId: v.optional(v.id("funcionarios")),
departamento: v.optional(v.string()),
situacao: v.optional(situacaoContrato),
numeroProcessoLicitatorio: v.optional(v.string()),
modalidade: v.optional(v.string()),
numeroContrato: v.optional(v.string()),
anoContrato: v.optional(v.number()),
dataInicioVigencia: v.optional(v.string()),
dataFimVigencia: v.optional(v.string()),
nomeFiscal: v.optional(v.string()),
valorTotal: v.optional(v.string()),
dataAditivoPrazo: v.optional(v.string()),
diasAvisoVencimento: v.optional(v.number()),
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "contratos",
acao: "editar",
});
args: {
id: v.id('contratos'),
contratadaId: v.optional(v.id('empresas')),
objeto: v.optional(v.string()),
numeroNotaEmpenho: v.optional(v.string()),
responsavelId: v.optional(v.id('funcionarios')),
departamento: v.optional(v.string()),
situacao: v.optional(situacaoContrato),
numeroProcessoLicitatorio: v.optional(v.string()),
modalidade: v.optional(v.string()),
numeroContrato: v.optional(v.string()),
anoContrato: v.optional(v.number()),
dataInicioVigencia: v.optional(v.string()),
dataFimVigencia: v.optional(v.string()),
nomeFiscal: v.optional(v.string()),
valorTotal: v.optional(v.string()),
dataAditivoPrazo: v.optional(v.string()),
diasAvisoVencimento: v.optional(v.number())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'contratos',
acao: 'editar'
});
const { id, ...campos } = args;
const { id, ...campos } = args;
await ctx.db.patch(id, {
...campos,
atualizadoEm: Date.now(),
});
},
await ctx.db.patch(id, {
...campos,
atualizadoEm: Date.now()
});
}
});
export const excluir = mutation({
args: { id: v.id("contratos") },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "contratos",
acao: "excluir",
});
await ctx.db.delete(args.id);
},
args: { id: v.id('contratos') },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'contratos',
acao: 'excluir'
});
await ctx.db.delete(args.id);
}
});
export const verificarVencimentos = query({
args: {},
handler: async (ctx) => {
// Esta query pode ser usada por um componente de notificação ou cron job
// Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento
args: {},
handler: async (ctx) => {
// Esta query pode ser usada por um componente de notificação ou cron job
// Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento
const hoje = new Date();
const hojeStr = hoje.toISOString().split("T")[0];
const hoje = new Date();
const hojeStr = hoje.toISOString().split('T')[0];
// Buscar contratos ativos (em execução ou aguardando assinatura)
const contratos = await ctx.db
.query("contratos")
.filter((q) =>
q.or(
q.eq(q.field("situacao"), "em_execucao"),
q.eq(q.field("situacao"), "aguardando_assinatura")
)
)
.collect();
// Buscar contratos ativos (em execução ou aguardando assinatura)
const contratos = await ctx.db
.query('contratos')
.filter((q) =>
q.or(
q.eq(q.field('situacao'), 'em_execucao'),
q.eq(q.field('situacao'), 'aguardando_assinatura')
)
)
.collect();
const proximosVencimento = contratos.filter((c) => {
if (!c.dataFimVigencia) return false;
const proximosVencimento = contratos.filter((c) => {
if (!c.dataFimVigencia) return false;
const dataFim = new Date(c.dataFimVigencia);
const dataAviso = new Date(dataFim);
dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento);
const dataFim = new Date(c.dataFimVigencia);
const dataAviso = new Date(dataFim);
dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento);
const dataAvisoStr = dataAviso.toISOString().split("T")[0];
const dataAvisoStr = dataAviso.toISOString().split('T')[0];
// Se hoje for maior ou igual a data de aviso e menor que a data fim
return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia;
});
// Se hoje for maior ou igual a data de aviso e menor que a data fim
return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia;
});
return proximosVencimento;
},
return proximosVencimento;
}
});

View File

@@ -4,7 +4,7 @@ import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
import type { Id, Doc } from './_generated/dataModel';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { flowTemplateStatus, flowInstanceStatus, flowInstanceStepStatus } from './schema';
import { flowInstanceStatus, flowInstanceStepStatus, flowTemplateStatus } from './tables/flows';
// ============================================
// HELPER FUNCTIONS
@@ -852,7 +852,7 @@ export const getInstanceWithSteps = query({
// Verificar permissão de visualização
const temPermissaoVerTodas = await verificarPermissaoVerTodasFluxos(ctx);
if (!temPermissaoVerTodas) {
// Verificar se usuário pertence a algum setor do fluxo ou é o manager
const pertenceAoSetor = await usuarioPertenceAAlgumSetorDoFluxo(
@@ -860,7 +860,7 @@ export const getInstanceWithSteps = query({
usuario._id,
instance._id
);
if (!pertenceAoSetor && instance.managerId !== usuario._id) {
return null; // Usuário não tem acesso
}
@@ -1066,7 +1066,8 @@ export const instantiateFlow = mutation({
for (let i = 0; i < templateSteps.length; i++) {
const step = templateSteps[i];
const dueDate = now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000;
const dueDate =
now + cumulativeDays * 24 * 60 * 60 * 1000 + step.expectedDuration * 24 * 60 * 60 * 1000;
cumulativeDays += step.expectedDuration;
const instanceStepId = await ctx.db.insert('flowInstanceSteps', {
@@ -1202,7 +1203,12 @@ export const completeStep = mutation({
if (nextSetor && nextFlowStep) {
const tituloProximoSetor = 'Nova Etapa de Fluxo Disponível';
const descricaoProximoSetor = `A etapa "${nextFlowStep.name}" do fluxo "${template?.name ?? 'Fluxo'}" está pronta para ser iniciada.`;
await criarNotificacaoParaSetor(ctx, nextStepData.setorId, tituloProximoSetor, descricaoProximoSetor);
await criarNotificacaoParaSetor(
ctx,
nextStepData.setorId,
tituloProximoSetor,
descricaoProximoSetor
);
}
}
} else {
@@ -1303,7 +1309,9 @@ export const alterarGestorFluxo = mutation({
const eCriador = template?.createdBy === usuario._id;
if (!eGestor && !temPermissao && !eCriador) {
throw new Error('Somente o gestor atual, criador do template ou usuário com permissão pode alterar o gestor');
throw new Error(
'Somente o gestor atual, criador do template ou usuário com permissão pode alterar o gestor'
);
}
// Verificar se novo gestor existe
@@ -1371,7 +1379,7 @@ export const reassignStep = mutation({
if (!eCriador) {
// Se não for criador, verificar regra normal
const etapaAnterior = await obterEtapaAnterior(ctx, args.instanceStepId);
if (etapaAnterior) {
// Se há etapa anterior, verificar se o usuário atual é a pessoa atribuída
if (etapaAnterior.assignedToId) {
@@ -1386,7 +1394,9 @@ export const reassignStep = mutation({
if (instance.managerId !== usuario._id) {
const temPermissao = await verificarPermissaoVerTodasFluxos(ctx);
if (!temPermissao) {
throw new Error('Somente o gerente do fluxo ou pessoa da etapa anterior pode atribuir esta etapa');
throw new Error(
'Somente o gerente do fluxo ou pessoa da etapa anterior pode atribuir esta etapa'
);
}
}
}
@@ -1408,9 +1418,7 @@ export const reassignStep = mutation({
}
// Verificar se o usuário atribuído corresponde a um funcionário do setor
const funcionarioDoUsuario = funcionariosDoSetor.find(
(f) => f.email === assignee.email
);
const funcionarioDoUsuario = funcionariosDoSetor.find((f) => f.email === assignee.email);
if (!funcionarioDoUsuario) {
throw new Error('O funcionário atribuído não pertence ao setor deste passo');
@@ -1441,7 +1449,7 @@ export const updateStepNotes = mutation({
throw new Error('Passo não encontrado');
}
await ctx.db.patch(args.instanceStepId, {
await ctx.db.patch(args.instanceStepId, {
notes: args.notes,
notesUpdatedBy: usuario._id,
notesUpdatedAt: Date.now()
@@ -1526,7 +1534,9 @@ export const listarSubEtapas = query({
} else if (args.flowInstanceStepId) {
subEtapas = await ctx.db
.query('flowSubSteps')
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId))
.withIndex('by_flowInstanceStepId', (q) =>
q.eq('flowInstanceStepId', args.flowInstanceStepId)
)
.collect();
} else {
return [];
@@ -1607,7 +1617,9 @@ export const criarSubEtapa = mutation({
} else if (args.flowInstanceStepId) {
const existingSubEtapas = await ctx.db
.query('flowSubSteps')
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId))
.withIndex('by_flowInstanceStepId', (q) =>
q.eq('flowInstanceStepId', args.flowInstanceStepId)
)
.collect();
if (existingSubEtapas.length > 0) {
maxPosition = Math.max(...existingSubEtapas.map((s) => s.position));
@@ -1766,7 +1778,9 @@ export const listarNotas = query({
} else if (args.flowInstanceStepId) {
notas = await ctx.db
.query('flowStepNotes')
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId))
.withIndex('by_flowInstanceStepId', (q) =>
q.eq('flowInstanceStepId', args.flowInstanceStepId)
)
.collect();
} else if (args.flowSubStepId) {
notas = await ctx.db
@@ -1784,17 +1798,15 @@ export const listarNotas = query({
const notasComDetalhes = await Promise.all(
notas.map(async (nota) => {
const criador = await ctx.db.get(nota.criadoPor);
// Obter informações dos arquivos
const arquivosComNome = await Promise.all(
nota.arquivos.map(async (storageId) => {
// Buscar documento que referencia este storageId
// Como não temos uma tabela direta, vamos buscar nos flowInstanceDocuments
const documentos = await ctx.db
.query('flowInstanceDocuments')
.collect();
const documentos = await ctx.db.query('flowInstanceDocuments').collect();
const documento = documentos.find((d) => d.storageId === storageId);
return {
storageId,
name: documento?.name ?? 'Arquivo'
@@ -2003,7 +2015,9 @@ export const listDocumentsByStep = query({
handler: async (ctx, args) => {
const documents = await ctx.db
.query('flowInstanceDocuments')
.withIndex('by_flowInstanceStepId', (q) => q.eq('flowInstanceStepId', args.flowInstanceStepId))
.withIndex('by_flowInstanceStepId', (q) =>
q.eq('flowInstanceStepId', args.flowInstanceStepId)
)
.collect();
const result: Array<{
@@ -2158,4 +2172,3 @@ export const getUsuariosBySetorForAssignment = query({
return usuarios;
}
});

View File

@@ -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 [];
}

View File

@@ -1,180 +1,180 @@
import { v } from "convex/values";
import { mutation, query, QueryCtx, MutationCtx } from "./_generated/server";
import { Doc, Id } from "./_generated/dataModel";
import { v } from 'convex/values';
import { MutationCtx, query } from './_generated/server';
import { Id } from './_generated/dataModel';
/**
* Helper function para registrar atividades no sistema
* Use em todas as mutations que modificam dados
*/
export async function registrarAtividade(
ctx: MutationCtx,
usuarioId: Id<"usuarios">,
acao: string,
recurso: string,
detalhes?: string,
recursoId?: string
ctx: MutationCtx,
usuarioId: Id<'usuarios'>,
acao: string,
recurso: string,
detalhes?: string,
recursoId?: string
) {
await ctx.db.insert("logsAtividades", {
usuarioId,
acao,
recurso,
recursoId,
detalhes,
timestamp: Date.now(),
});
await ctx.db.insert('logsAtividades', {
usuarioId,
acao,
recurso,
recursoId,
detalhes,
timestamp: Date.now()
});
}
/**
* Lista atividades com filtros
*/
export const listarAtividades = query({
args: {
usuarioId: v.optional(v.id("usuarios")),
acao: v.optional(v.string()),
recurso: v.optional(v.string()),
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
limite: v.optional(v.number()),
},
handler: async (ctx, args) => {
let atividades;
args: {
usuarioId: v.optional(v.id('usuarios')),
acao: v.optional(v.string()),
recurso: v.optional(v.string()),
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
limite: v.optional(v.number())
},
handler: async (ctx, args) => {
let atividades;
if (args.usuarioId) {
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId!))
.order("desc")
.take(args.limite || 100);
} else if (args.acao) {
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_acao", (q) => q.eq("acao", args.acao!))
.order("desc")
.take(args.limite || 100);
} else if (args.recurso) {
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso!))
.order("desc")
.take(args.limite || 100);
} else {
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_timestamp")
.order("desc")
.take(args.limite || 100);
}
if (args.usuarioId) {
atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId!))
.order('desc')
.take(args.limite || 100);
} else if (args.acao) {
atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_acao', (q) => q.eq('acao', args.acao!))
.order('desc')
.take(args.limite || 100);
} else if (args.recurso) {
atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_recurso', (q) => q.eq('recurso', args.recurso!))
.order('desc')
.take(args.limite || 100);
} else {
atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp')
.order('desc')
.take(args.limite || 100);
}
// Filtrar por range de datas se fornecido
if (args.dataInicio || args.dataFim) {
atividades = atividades.filter((log) => {
if (args.dataInicio && log.timestamp < args.dataInicio) return false;
if (args.dataFim && log.timestamp > args.dataFim) return false;
return true;
});
}
// Filtrar por range de datas se fornecido
if (args.dataInicio || args.dataFim) {
atividades = atividades.filter((log) => {
if (args.dataInicio && log.timestamp < args.dataInicio) return false;
if (args.dataFim && log.timestamp > args.dataFim) return false;
return true;
});
}
// Buscar informações dos usuários
const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = "N/A";
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || "N/A";
}
return {
...atividade,
usuarioNome: usuario?.nome || "Usuário Desconhecido",
usuarioMatricula: matricula,
};
})
);
// Buscar informações dos usuários
const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = 'N/A';
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || 'N/A';
}
return {
...atividade,
usuarioNome: usuario?.nome || 'Usuário Desconhecido',
usuarioMatricula: matricula
};
})
);
return atividadesComUsuarios;
},
return atividadesComUsuarios;
}
});
/**
* Obtém estatísticas de atividades
*/
export const obterEstatisticasAtividades = query({
args: {
periodo: v.optional(v.number()), // dias (ex: 7, 30)
},
handler: async (ctx, args) => {
const periodo = args.periodo || 30;
const dataInicio = Date.now() - periodo * 24 * 60 * 60 * 1000;
args: {
periodo: v.optional(v.number()) // dias (ex: 7, 30)
},
handler: async (ctx, args) => {
const periodo = args.periodo || 30;
const dataInicio = Date.now() - periodo * 24 * 60 * 60 * 1000;
const atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_timestamp")
.filter((q) => q.gte(q.field("timestamp"), dataInicio))
.collect();
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp')
.filter((q) => q.gte(q.field('timestamp'), dataInicio))
.collect();
// Agrupar por ação
const porAcao: Record<string, number> = {};
atividades.forEach((ativ) => {
porAcao[ativ.acao] = (porAcao[ativ.acao] || 0) + 1;
});
// Agrupar por ação
const porAcao: Record<string, number> = {};
atividades.forEach((ativ) => {
porAcao[ativ.acao] = (porAcao[ativ.acao] || 0) + 1;
});
// Agrupar por recurso
const porRecurso: Record<string, number> = {};
atividades.forEach((ativ) => {
porRecurso[ativ.recurso] = (porRecurso[ativ.recurso] || 0) + 1;
});
// Agrupar por recurso
const porRecurso: Record<string, number> = {};
atividades.forEach((ativ) => {
porRecurso[ativ.recurso] = (porRecurso[ativ.recurso] || 0) + 1;
});
// Agrupar por dia
const porDia: Record<string, number> = {};
atividades.forEach((ativ) => {
const data = new Date(ativ.timestamp);
const dia = data.toISOString().split("T")[0];
porDia[dia] = (porDia[dia] || 0) + 1;
});
// Agrupar por dia
const porDia: Record<string, number> = {};
atividades.forEach((ativ) => {
const data = new Date(ativ.timestamp);
const dia = data.toISOString().split('T')[0];
porDia[dia] = (porDia[dia] || 0) + 1;
});
return {
total: atividades.length,
porAcao,
porRecurso,
porDia,
};
},
return {
total: atividades.length,
porAcao,
porRecurso,
porDia
};
}
});
/**
* Obtém histórico de atividades de um recurso específico
*/
export const obterHistoricoRecurso = query({
args: {
recurso: v.string(),
recursoId: v.string(),
},
handler: async (ctx, args) => {
const atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_recurso_id", (q) =>
q.eq("recurso", args.recurso).eq("recursoId", args.recursoId)
)
.order("desc")
.collect();
args: {
recurso: v.string(),
recursoId: v.string()
},
handler: async (ctx, args) => {
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_recurso_id', (q) =>
q.eq('recurso', args.recurso).eq('recursoId', args.recursoId)
)
.order('desc')
.collect();
// Buscar informações dos usuários
const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = "N/A";
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || "N/A";
}
return {
...atividade,
usuarioNome: usuario?.nome || "Usuário Desconhecido",
usuarioMatricula: matricula,
};
})
);
// Buscar informações dos usuários
const atividadesComUsuarios = await Promise.all(
atividades.map(async (atividade) => {
const usuario = await ctx.db.get(atividade.usuarioId);
let matricula = 'N/A';
if (usuario?.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
matricula = funcionario?.matricula || 'N/A';
}
return {
...atividade,
usuarioNome: usuario?.nome || 'Usuário Desconhecido',
usuarioMatricula: matricula
};
})
);
return atividadesComUsuarios;
},
return atividadesComUsuarios;
}
});

View File

@@ -0,0 +1,596 @@
import { mutation, query, internalMutation } from './_generated/server';
import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
import { api, internal } from './_generated/api';
import type { Doc, Id } from './_generated/dataModel';
import type { QueryCtx, MutationCtx } from './_generated/server';
// ========== HELPERS ==========
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
return user;
}
// ========== QUERIES ==========
export const list = query({
args: {},
returns: v.array(
v.object({
_id: v.id('pedidos'),
_creationTime: v.number(),
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()
})
),
handler: async (ctx) => {
return await ctx.db.query('pedidos').collect();
}
});
export const get = query({
args: { id: v.id('pedidos') },
returns: v.union(
v.object({
_id: v.id('pedidos'),
_creationTime: v.number(),
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()
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
}
});
export const getItems = query({
args: { pedidoId: v.id('pedidos') },
returns: v.array(
v.object({
_id: v.id('pedidoItems'),
_creationTime: v.number(),
pedidoId: v.id('pedidos'),
produtoId: v.id('produtos'),
valorEstimado: v.string(),
valorReal: v.optional(v.string()),
quantidade: v.number(),
adicionadoPor: v.id('funcionarios'),
adicionadoPorNome: v.string(),
criadoEm: v.number()
})
),
handler: async (ctx, args) => {
const items = await ctx.db
.query('pedidoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.collect();
// Get employee names
const itemsWithNames = await Promise.all(
items.map(async (item) => {
const funcionario = await ctx.db.get(item.adicionadoPor);
return {
...item,
adicionadoPorNome: funcionario?.nome || 'Desconhecido'
};
})
);
return itemsWithNames;
}
});
export const getHistory = query({
args: { pedidoId: v.id('pedidos') },
handler: async (ctx, args) => {
const history = await ctx.db
.query('historicoPedidos')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.order('desc')
.collect();
// Get user names
const historyWithNames = await Promise.all(
history.map(async (entry) => {
const usuario = await ctx.db.get(entry.usuarioId);
return {
_id: entry._id,
_creationTime: entry._creationTime,
pedidoId: entry.pedidoId,
usuarioId: entry.usuarioId,
usuarioNome: usuario?.nome || 'Desconhecido',
acao: entry.acao,
detalhes: entry.detalhes,
data: entry.data
};
})
);
return historyWithNames;
}
});
export const checkExisting = query({
args: {
acaoId: v.optional(v.id('acoes')),
numeroSei: v.optional(v.string()),
produtoIds: v.optional(v.array(v.id('produtos')))
},
returns: v.array(
v.object({
_id: v.id('pedidos'),
_creationTime: v.number(),
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(),
matchingItems: v.optional(
v.array(
v.object({
produtoId: v.id('produtos'),
quantidade: v.number()
})
)
)
})
),
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) return [];
const openStatuses: Array<
'em_rascunho' | 'aguardando_aceite' | 'em_analise' | 'precisa_ajustes'
> = ['em_rascunho', 'aguardando_aceite', 'em_analise', 'precisa_ajustes'];
// 1) Buscar todos os pedidos "abertos" usando o índice by_status
let pedidosAbertos: Doc<'pedidos'>[] = [];
for (const status of openStatuses) {
const partial = await ctx.db
.query('pedidos')
.withIndex('by_status', (q) => q.eq('status', status))
.collect();
pedidosAbertos = pedidosAbertos.concat(partial);
}
// 2) Filtros opcionais: acaoId e numeroSei
pedidosAbertos = pedidosAbertos.filter((p) => {
if (args.acaoId && p.acaoId !== args.acaoId) return false;
if (args.numeroSei && p.numeroSei !== args.numeroSei) return false;
return true;
});
// 3) Filtro por produtos (se informado) e coleta de matchingItems
const resultados = [];
for (const pedido of pedidosAbertos) {
let include = true;
let matchingItems: { produtoId: Id<'produtos'>; quantidade: number }[] = [];
// Se houver filtro de produtos, verificamos se o pedido tem ALGUM dos produtos
if (args.produtoIds && args.produtoIds.length > 0) {
const items = await ctx.db
.query('pedidoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
.collect();
// const pedidoProdutoIds = new Set(items.map((i) => i.produtoId)); // Unused
const matching = items.filter((i) => args.produtoIds?.includes(i.produtoId));
if (matching.length > 0) {
matchingItems = matching.map((i) => ({
produtoId: i.produtoId,
quantidade: i.quantidade
}));
} else {
// Se foi pedido filtro por produtos e não tem nenhum match, ignoramos este pedido
// A MENOS que tenha dado match por numeroSei ou acaoId?
// A regra original era: "Filtro por produtos (se informado)"
// Se o usuário informou produtos, ele quer ver pedidos que tenham esses produtos.
// Mas se ele TAMBÉM informou numeroSei, talvez ele queira ver aquele pedido específico mesmo sem o produto?
// Vamos manter a lógica de "E": se informou produtos, tem que ter o produto.
include = false;
}
}
if (include) {
resultados.push({
_id: pedido._id,
_creationTime: pedido._creationTime,
numeroSei: pedido.numeroSei,
status: pedido.status,
acaoId: pedido.acaoId,
criadoPor: pedido.criadoPor,
criadoEm: pedido.criadoEm,
atualizadoEm: pedido.atualizadoEm,
matchingItems: matchingItems.length > 0 ? matchingItems : undefined
});
}
}
return resultados;
}
});
// ========== MUTATIONS ==========
export const create = mutation({
args: {
numeroSei: v.optional(v.string()),
acaoId: v.optional(v.id('acoes'))
},
returns: v.id('pedidos'),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
// 1. Check Config
const config = await ctx.db.query('config').first();
if (!config || !config.comprasSetorId) {
throw new Error('Setor de Compras não configurado. Contate o administrador.');
}
// 2. Check Existing (Double check)
if (args.acaoId) {
const existing = await ctx.db
.query('pedidos')
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
.filter((q) =>
q.or(
q.eq(q.field('status'), 'em_rascunho'),
q.eq(q.field('status'), 'aguardando_aceite'),
q.eq(q.field('status'), 'em_analise'),
q.eq(q.field('status'), 'precisa_ajustes')
)
)
.first();
if (existing) {
throw new Error('Já existe um pedido em andamento para esta ação.');
}
}
// 3. Create Order
const pedidoId = await ctx.db.insert('pedidos', {
numeroSei: args.numeroSei,
status: 'em_rascunho',
acaoId: args.acaoId,
criadoPor: user._id,
criadoEm: Date.now(),
atualizadoEm: Date.now()
});
// 4. Create History
await ctx.db.insert('historicoPedidos', {
pedidoId,
usuarioId: user._id,
acao: 'criacao',
detalhes: JSON.stringify({ numeroSei: args.numeroSei, acaoId: args.acaoId }),
data: Date.now()
});
return pedidoId;
}
});
export const updateSeiNumber = mutation({
args: {
pedidoId: v.id('pedidos'),
numeroSei: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
const pedido = await ctx.db.get(args.pedidoId);
if (!pedido) throw new Error('Pedido not found');
// Check if SEI number is already taken by another order
const existing = await ctx.db
.query('pedidos')
.filter((q) =>
q.and(q.eq(q.field('numeroSei'), args.numeroSei), q.neq(q.field('_id'), args.pedidoId))
)
.first();
if (existing) {
throw new Error('Este número SEI já está em uso por outro pedido.');
}
const oldSei = pedido.numeroSei;
await ctx.db.patch(args.pedidoId, {
numeroSei: args.numeroSei,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'atualizacao_sei',
detalhes: JSON.stringify({ de: oldSei, para: args.numeroSei }),
data: Date.now()
});
}
});
export const addItem = mutation({
args: {
pedidoId: v.id('pedidos'),
produtoId: v.id('produtos'),
valorEstimado: v.string(),
quantidade: v.number()
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
// Ensure user has a funcionarioId linked
if (!user.funcionarioId) {
throw new Error('Usuário não vinculado a um funcionário.');
}
await ctx.db.insert('pedidoItems', {
pedidoId: args.pedidoId,
produtoId: args.produtoId,
valorEstimado: args.valorEstimado,
quantidade: args.quantidade,
adicionadoPor: user.funcionarioId,
criadoEm: Date.now()
});
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'adicao_item',
detalhes: JSON.stringify({
produtoId: args.produtoId,
valor: args.valorEstimado,
quantidade: args.quantidade
}),
data: Date.now()
});
}
});
export const updateItemQuantity = mutation({
args: {
itemId: v.id('pedidoItems'),
novaQuantidade: v.number()
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
if (!user.funcionarioId) {
throw new Error('Usuário não vinculado a um funcionário.');
}
const item = await ctx.db.get(args.itemId);
if (!item) throw new Error('Item não encontrado.');
const quantidadeAnterior = item.quantidade;
// Check permission: only item owner can decrease quantity
const isOwner = item.adicionadoPor === user.funcionarioId;
const isDecreasing = args.novaQuantidade < quantidadeAnterior;
if (isDecreasing && !isOwner) {
throw new Error(
'Apenas quem adicionou este item pode diminuir a quantidade. Você pode apenas aumentar.'
);
}
// Update quantity
await ctx.db.patch(args.itemId, { quantidade: args.novaQuantidade });
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
// Create history entry
await ctx.db.insert('historicoPedidos', {
pedidoId: item.pedidoId,
usuarioId: user._id,
acao: 'alteracao_quantidade',
detalhes: JSON.stringify({
produtoId: item.produtoId,
quantidadeAnterior,
novaQuantidade: args.novaQuantidade
}),
data: Date.now()
});
}
});
export const removeItem = mutation({
args: {
itemId: v.id('pedidoItems')
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
const item = await ctx.db.get(args.itemId);
if (!item) throw new Error('Item not found');
await ctx.db.delete(args.itemId);
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
await ctx.db.insert('historicoPedidos', {
pedidoId: item.pedidoId,
usuarioId: user._id,
acao: 'remocao_item',
detalhes: JSON.stringify({ produtoId: item.produtoId, valor: item.valorEstimado }),
data: Date.now()
});
}
});
export const updateStatus = mutation({
args: {
pedidoId: v.id('pedidos'),
novoStatus: v.union(
v.literal('em_rascunho'),
v.literal('aguardando_aceite'),
v.literal('em_analise'),
v.literal('precisa_ajustes'),
v.literal('cancelado'),
v.literal('concluido')
)
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
const pedido = await ctx.db.get(args.pedidoId);
if (!pedido) throw new Error('Pedido not found');
const oldStatus = pedido.status;
await ctx.db.patch(args.pedidoId, {
status: args.novoStatus,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: args.novoStatus }),
data: Date.now()
});
// Trigger Notifications
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus: args.novoStatus,
actorId: user._id
});
}
});
// ========== INTERNAL (NOTIFICATIONS) ==========
export const notifyStatusChange = internalMutation({
args: {
pedidoId: v.id('pedidos'),
oldStatus: v.string(),
newStatus: v.string(),
actorId: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
const pedido = await ctx.db.get(args.pedidoId);
if (!pedido) return;
const actor = await ctx.db.get(args.actorId);
const actorName = actor ? actor.nome : 'Alguém';
const recipients = new Set<string>(); // Set of User IDs
// 1. If status is "aguardando_aceite", notify Purchasing Sector
if (args.newStatus === 'aguardando_aceite') {
const config = await ctx.db.query('config').first();
if (config && config.comprasSetorId) {
// Find all employees in this sector
const funcionarioSetores = await ctx.db
.query('funcionarioSetores')
.withIndex('by_setorId', (q) => q.eq('setorId', config.comprasSetorId!))
.collect();
const funcionarioIds = funcionarioSetores.map((fs) => fs.funcionarioId);
// Find users linked to these employees
for (const fId of funcionarioIds) {
const user = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', fId))
.first();
if (user) recipients.add(user._id);
}
}
}
// 2. Notify "Involved" users (Creator + Item Adders)
// Always notify creator (unless they are the actor)
if (pedido.criadoPor !== args.actorId) {
recipients.add(pedido.criadoPor);
}
// Notify item adders
const items = await ctx.db
.query('pedidoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.collect();
for (const item of items) {
const user = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', item.adicionadoPor))
.first();
if (user && user._id !== args.actorId) {
recipients.add(user._id);
}
}
// Send Notifications
for (const recipientId of recipients) {
const recipientIdTyped = recipientId as Id<'usuarios'>;
// 1. In-App Notification
await ctx.db.insert('notificacoes', {
usuarioId: recipientIdTyped,
tipo: 'alerta_seguranca', // Using alerta_seguranca as the closest match for system notifications
titulo: `Pedido ${pedido.numeroSei || 'sem número SEI'} atualizado`,
descricao: `Status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
lida: false,
criadaEm: Date.now(),
remetenteId: args.actorId
});
// 2. Email Notification (Async)
const recipientUser = await ctx.db.get(recipientIdTyped);
if (recipientUser && recipientUser.email) {
// Using enfileirarEmail directly
await ctx.scheduler.runAfter(0, api.email.enfileirarEmail, {
destinatario: recipientUser.email,
destinatarioId: recipientIdTyped,
assunto: `Atualização no Pedido ${pedido.numeroSei || 'sem número SEI'}`,
corpo: `O pedido ${pedido.numeroSei || 'sem número SEI'} teve seu status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
enviadoPor: args.actorId
});
}
}
}
});

View File

@@ -395,6 +395,100 @@ const PERMISSOES_BASE = {
recurso: 'fluxos_documentos',
acao: 'excluir',
descricao: 'Excluir documentos de fluxos'
},
// Pedidos
{
nome: 'pedidos.listar',
recurso: 'pedidos',
acao: 'listar',
descricao: 'Listar pedidos'
},
{
nome: 'pedidos.criar',
recurso: 'pedidos',
acao: 'criar',
descricao: 'Criar novos pedidos'
},
{
nome: 'pedidos.ver',
recurso: 'pedidos',
acao: 'ver',
descricao: 'Visualizar detalhes de pedidos'
},
{
nome: 'pedidos.editar_status',
recurso: 'pedidos',
acao: 'editar_status',
descricao: 'Alterar status de pedidos'
},
{
nome: 'pedidos.adicionar_item',
recurso: 'pedidos',
acao: 'adicionar_item',
descricao: 'Adicionar itens ao pedido'
},
{
nome: 'pedidos.remover_item',
recurso: 'pedidos',
acao: 'remover_item',
descricao: 'Remover itens do pedido'
},
// Produtos
{
nome: 'produtos.listar',
recurso: 'produtos',
acao: 'listar',
descricao: 'Listar produtos'
},
{
nome: 'produtos.criar',
recurso: 'produtos',
acao: 'criar',
descricao: 'Criar novos produtos'
},
{
nome: 'produtos.editar',
recurso: 'produtos',
acao: 'editar',
descricao: 'Editar produtos'
},
{
nome: 'produtos.excluir',
recurso: 'produtos',
acao: 'excluir',
descricao: 'Excluir produtos'
},
// Ações
{
nome: 'acoes.listar',
recurso: 'acoes',
acao: 'listar',
descricao: 'Listar ações'
},
{
nome: 'acoes.criar',
recurso: 'acoes',
acao: 'criar',
descricao: 'Criar novas ações'
},
{
nome: 'acoes.editar',
recurso: 'acoes',
acao: 'editar',
descricao: 'Editar ações'
},
{
nome: 'acoes.excluir',
recurso: 'acoes',
acao: 'excluir',
descricao: 'Excluir ações'
},
// Configuração Compras
{
nome: 'config.compras.gerenciar',
recurso: 'config',
acao: 'gerenciar_compras',
descricao: 'Gerenciar configurações de compras'
}
]
} as const;

View File

@@ -0,0 +1,69 @@
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('produtos').collect();
}
});
export const search = query({
args: { query: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query('produtos')
.withSearchIndex('search_nome', (q) => q.search('nome', args.query))
.take(10);
}
});
export const create = mutation({
args: {
nome: v.string(),
valorEstimado: v.string(),
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
return await ctx.db.insert('produtos', {
...args,
criadoPor: user._id,
criadoEm: Date.now()
});
}
});
export const update = mutation({
args: {
id: v.id('produtos'),
nome: v.string(),
valorEstimado: v.string(),
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
await ctx.db.patch(args.id, {
nome: args.nome,
valorEstimado: args.valorEstimado,
tipo: args.tipo
});
}
});
export const remove = mutation({
args: {
id: v.id('produtos')
},
handler: async (ctx, args) => {
const user = await getCurrentUserFunction(ctx);
if (!user) throw new Error('Unauthorized');
await ctx.db.delete(args.id);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,12 @@
import { v } from 'convex/values';
import {
internalMutation,
mutation,
query
} from './_generated/server';
import { internalMutation, mutation, query } from './_generated/server';
import { internal } from './_generated/api';
import type { Id } from './_generated/dataModel';
import type {
AtaqueCiberneticoTipo,
SeveridadeSeguranca,
StatusEventoSeguranca
} from './schema';
} from './tables/security';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
import { components } from './_generated/api';
@@ -413,9 +409,9 @@ const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual')
// Função para analisar string e detectar ataques
function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null {
if (!texto) return null;
const textoLower = texto.toLowerCase();
// Verificar cada tipo de ataque em ordem de prioridade
for (const tipo of ATAQUES_PRIORITARIOS) {
const patterns = KEYWORDS[tipo];
@@ -423,7 +419,7 @@ function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null
return tipo;
}
}
return null;
}
@@ -591,14 +587,23 @@ export const registrarEventoSeguranca = mutation({
handler: async (ctx, args) => {
// Aplicar rate limiting por IP se fornecido
if (args.origemIp) {
const rateLimitResult = await aplicarRateLimit(ctx, 'ip', args.origemIp, 'registrarEventoSeguranca');
const rateLimitResult = await aplicarRateLimit(
ctx,
'ip',
args.origemIp,
'registrarEventoSeguranca'
);
if (!rateLimitResult.permitido) {
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
}
}
const tipo = inferirTipoAtaque(args);
const severidade = calcularSeveridade(tipo, args.metricas ?? undefined, args.severidade ?? undefined);
const severidade = calcularSeveridade(
tipo,
args.metricas ?? undefined,
args.severidade ?? undefined
);
const status = statusInicial(severidade);
const duplicado = await ctx.db
@@ -727,10 +732,18 @@ export const listarEventosSeguranca = query({
const candidatos = await builder.order('desc').take(limit * 3);
const filtrados = candidatos
.filter((evento) => {
if (args.severidades && args.severidades.length > 0 && !args.severidades.includes(evento.severidade)) {
if (
args.severidades &&
args.severidades.length > 0 &&
!args.severidades.includes(evento.severidade)
) {
return false;
}
if (args.tiposAtaque && args.tiposAtaque.length > 0 && !args.tiposAtaque.includes(evento.tipoAtaque)) {
if (
args.tiposAtaque &&
args.tiposAtaque.length > 0 &&
!args.tiposAtaque.includes(evento.tipoAtaque)
) {
return false;
}
if (args.status && args.status.length > 0 && !args.status.includes(evento.status)) {
@@ -829,10 +842,7 @@ export const obterVisaoCamadas = query({
for (const evento of eventos) {
const idx = Math.min(
bucketCount - 1,
Math.max(
0,
Math.floor((evento.timestamp - inicioJanela) / bucketSize)
)
Math.max(0, Math.floor((evento.timestamp - inicioJanela) / bucketSize))
);
const bucket = series[idx];
if (evento.severidade === 'critico') criticos += 1;
@@ -893,11 +903,12 @@ export const listarReputacoes = query({
handler: async (ctx, args) => {
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200;
const builder = args.lista === undefined
? ctx.db.query('ipReputation')
: args.lista === 'blacklist'
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
const builder =
args.lista === undefined
? ctx.db.query('ipReputation')
: args.lista === 'blacklist'
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
const docs = await builder.order('desc').take(limit * 2);
const filtrados = docs
@@ -945,7 +956,12 @@ export const atualizarReputacaoIndicador = mutation({
}),
handler: async (ctx, args) => {
// Aplicar rate limiting por usuário
const rateLimitResult = await aplicarRateLimit(ctx, 'usuario', args.usuarioId, 'atualizarReputacaoIndicador');
const rateLimitResult = await aplicarRateLimit(
ctx,
'usuario',
args.usuarioId,
'atualizarReputacaoIndicador'
);
if (!rateLimitResult.permitido) {
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
}
@@ -1075,7 +1091,8 @@ export const configurarRegraPorta = mutation({
}),
handler: async (ctx, args) => {
const agora = Date.now();
const expiraEm = args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
const expiraEm =
args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
if (args.regraId) {
await ctx.db.patch(args.regraId, {
@@ -1172,7 +1189,14 @@ export const registrarAcaoIncidente = mutation({
tipo: acaoIncidenteValidator,
origem: acaoOrigemValidator,
executadoPor: v.optional(v.id('usuarios')),
status: v.optional(v.union(v.literal('pendente'), v.literal('executando'), v.literal('concluido'), v.literal('falhou'))),
status: v.optional(
v.union(
v.literal('pendente'),
v.literal('executando'),
v.literal('concluido'),
v.literal('falhou')
)
),
detalhes: v.optional(v.string()),
resultado: v.optional(v.string()),
relacionadoA: v.optional(v.id('ipReputation'))
@@ -1338,9 +1362,7 @@ export const processarRelatorioSegurancaInternal = internalMutation({
const eventos = await ctx.db
.query('securityEvents')
.withIndex('by_timestamp', (q) =>
q
.gte('timestamp', relatorio.filtros.dataInicio)
.lte('timestamp', relatorio.filtros.dataFim)
q.gte('timestamp', relatorio.filtros.dataInicio).lte('timestamp', relatorio.filtros.dataFim)
)
.collect();
@@ -1350,14 +1372,14 @@ export const processarRelatorioSegurancaInternal = internalMutation({
relatorio.filtros.severidades.length > 0 &&
!relatorio.filtros.severidades.includes(evento.severidade)
) {
return false;
return false;
}
if (
relatorio.filtros.tiposAtaque &&
relatorio.filtros.tiposAtaque.length > 0 &&
!relatorio.filtros.tiposAtaque.includes(evento.tipoAtaque)
) {
return false;
return false;
}
return true;
});
@@ -1509,7 +1531,10 @@ export const dispararAlertasInternos = internalMutation({
const usuariosNotificados: Id<'usuarios'>[] = [];
for (const role of rolesTi) {
const membros = await ctx.db.query('usuarios').withIndex('by_role', (q) => q.eq('roleId', role._id)).collect();
const membros = await ctx.db
.query('usuarios')
.withIndex('by_role', (q) => q.eq('roleId', role._id))
.collect();
for (const usuario of membros) {
usuariosNotificados.push(usuario._id);
}
@@ -1602,7 +1627,9 @@ async function aplicarRateLimit(
): Promise<{ permitido: boolean; motivo?: string; retryAfter?: number }> {
const configs = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', tipo).eq('identificador', identificador))
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', tipo).eq('identificador', identificador)
)
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
@@ -1611,7 +1638,9 @@ async function aplicarRateLimit(
// Verificar configuração global
const globalConfigs = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', 'global').eq('identificador', 'global'))
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', 'global').eq('identificador', 'global')
)
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
@@ -1626,10 +1655,11 @@ async function aplicarRateLimit(
// Converter janelaSegundos para período do rate-limiter
const periodo = config.janelaSegundos * SECOND;
// Determinar estratégia baseada na configuração
// O rate-limiter suporta apenas 'token bucket' e 'fixed window'
const kind: 'token bucket' | 'fixed window' = config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
const kind: 'token bucket' | 'fixed window' =
config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
// Criar namespace único para este rate limit
const namespace = `${tipo}:${identificador}:${endpoint ?? 'default'}`;
@@ -1643,7 +1673,10 @@ async function aplicarRateLimit(
period: periodo,
...(config.estrategia === 'token_bucket' ? { capacity: config.limite } : {})
}
} as Record<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);
@@ -1654,7 +1687,7 @@ async function aplicarRateLimit(
if (!result.ok) {
const retryAfter = result.retryAfter ?? periodo;
if (config.acaoExcedido === 'bloquear') {
return {
permitido: false,
@@ -1688,7 +1721,12 @@ export const criarConfigRateLimit = mutation({
args: {
usuarioId: v.id('usuarios'),
nome: v.string(),
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
tipo: v.union(
v.literal('ip'),
v.literal('usuario'),
v.literal('endpoint'),
v.literal('global')
),
identificador: v.optional(v.string()),
limite: v.number(),
janelaSegundos: v.number(),
@@ -1737,13 +1775,11 @@ export const atualizarConfigRateLimit = mutation({
limite: v.optional(v.number()),
janelaSegundos: v.optional(v.number()),
estrategia: v.optional(
v.union(
v.literal('fixed_window'),
v.literal('sliding_window'),
v.literal('token_bucket')
)
v.union(v.literal('fixed_window'), v.literal('sliding_window'), v.literal('token_bucket'))
),
acaoExcedido: v.optional(
v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))
),
acaoExcedido: v.optional(v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))),
bloqueioTemporarioSegundos: v.optional(v.number()),
ativo: v.optional(v.boolean()),
prioridade: v.optional(v.number()),
@@ -1794,7 +1830,9 @@ export const atualizarConfigRateLimit = mutation({
export const listarConfigsRateLimit = query({
args: {
tipo: v.optional(v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))),
tipo: v.optional(
v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))
),
ativo: v.optional(v.boolean()),
limit: v.optional(v.number())
},
@@ -1802,7 +1840,12 @@ export const listarConfigsRateLimit = query({
v.object({
_id: v.id('rateLimitConfig'),
nome: v.string(),
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
tipo: v.union(
v.literal('ip'),
v.literal('usuario'),
v.literal('endpoint'),
v.literal('global')
),
identificador: v.optional(v.string()),
limite: v.number(),
janelaSegundos: v.number(),
@@ -1882,8 +1925,12 @@ export const analisarRequisicaoHTTP = mutation({
args.url,
args.method,
args.body ?? '',
Object.entries(args.queryParams ?? {}).map(([k, v]) => `${k}=${v}`).join('&'),
Object.entries(args.headers ?? {}).map(([k, v]) => `${k}:${v}`).join('\n'),
Object.entries(args.queryParams ?? {})
.map(([k, v]) => `${k}=${v}`)
.join('&'),
Object.entries(args.headers ?? {})
.map(([k, v]) => `${k}:${v}`)
.join('\n'),
args.userAgent ?? ''
].join('\n');
@@ -1904,7 +1951,8 @@ export const analisarRequisicaoHTTP = mutation({
// Permitir que o chamador informe o destino/protocolo via query string em cenários de dev/teste
const destinoIp =
(args.queryParams && (args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
(args.queryParams &&
(args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
undefined;
const protocolo = (args.queryParams && (args.queryParams['proto'] as string)) || 'http';
@@ -1922,9 +1970,11 @@ export const analisarRequisicaoHTTP = mutation({
protocolo,
transporte: 'tcp',
detectadoPor: 'analisador_http_automatico',
fingerprint: args.userAgent ? {
userAgent: args.userAgent
} : undefined,
fingerprint: args.userAgent
? {
userAgent: args.userAgent
}
: undefined,
destinoIp: destinoIp ?? undefined,
tags: ['detecção_automática', 'http', tipoAtaque],
atualizadoEm: agora
@@ -1978,19 +2028,13 @@ export const detectarBruteForce = internalMutation({
tentativasFalhas = await ctx.db
.query('logsLogin')
.withIndex('by_ip', (q) => q.eq('ipAddress', args.ipAddress))
.filter((q) =>
q.gte(q.field('timestamp'), dataLimite) &&
q.eq(q.field('sucesso'), false)
)
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
.collect();
} else if (args.usuarioId) {
tentativasFalhas = await ctx.db
.query('logsLogin')
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.filter((q) =>
q.gte(q.field('timestamp'), dataLimite) &&
q.eq(q.field('sucesso'), false)
)
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
.collect();
} else {
// Buscar todas as tentativas falhas na janela
@@ -2026,7 +2070,8 @@ export const detectarBruteForce = internalMutation({
const eventosIds: Id<'securityEvents'>[] = [];
for (const { ip, count } of ipsSuspeitos) {
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const severidade: SeveridadeSeguranca =
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const referencia = `brute_force_${ip}_${Date.now()}`;
const agora = Date.now();
@@ -2058,10 +2103,12 @@ export const detectarBruteForce = internalMutation({
'ip',
delta,
severidade,
severidade === 'alto' ? {
blacklist: true,
bloqueadoAte: agora + (60 * 60 * 1000) // Bloquear por 1 hora
} : undefined
severidade === 'alto'
? {
blacklist: true,
bloqueadoAte: agora + 60 * 60 * 1000 // Bloquear por 1 hora
}
: undefined
);
}
@@ -2109,13 +2156,7 @@ export const criarEventosTeste = mutation({
];
// IPs de teste
const ipsTeste = [
'192.168.1.100',
'10.0.0.50',
'172.16.0.25',
'203.0.113.42',
'198.51.100.15'
];
const ipsTeste = ['192.168.1.100', '10.0.0.50', '172.16.0.25', '203.0.113.42', '198.51.100.15'];
for (let i = 0; i < quantidade; i++) {
const tipoAtaque = tiposAtaque[i % tiposAtaque.length];
@@ -2124,7 +2165,7 @@ export const criarEventosTeste = mutation({
const eventoId = await ctx.db.insert('securityEvents', {
referencia,
timestamp: agora - (i * 60000), // Espaçar eventos em 1 minuto
timestamp: agora - i * 60000, // Espaçar eventos em 1 minuto
tipoAtaque: tipoAtaque.tipo,
severidade: tipoAtaque.severidade,
status: statusInicial(tipoAtaque.severidade),
@@ -2140,7 +2181,7 @@ export const criarEventosTeste = mutation({
pps: Math.floor(Math.random() * 50000)
},
tags: ['teste', 'validação', tipoAtaque.tipo],
atualizadoEm: agora - (i * 60000)
atualizadoEm: agora - i * 60000
});
eventosIds.push(eventoId);
@@ -2153,9 +2194,11 @@ export const criarEventosTeste = mutation({
'ip',
delta,
tipoAtaque.severidade,
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' ? {
blacklist: true
} : undefined
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto'
? {
blacklist: true
}
: undefined
);
}
@@ -2208,7 +2251,8 @@ export const monitorarLogsLogin = internalMutation({
// Registrar eventos para cada IP suspeito
let ipsBloqueados = 0;
for (const { ip, count } of ipsSuspeitos) {
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const severidade: SeveridadeSeguranca =
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const referencia = `brute_force_${ip}_${Date.now()}`;
const agora = Date.now();
@@ -2238,10 +2282,12 @@ export const monitorarLogsLogin = internalMutation({
'ip',
delta,
severidade,
severidade === 'alto' ? {
blacklist: true,
bloqueadoAte: agora + (60 * 60 * 1000)
} : undefined
severidade === 'alto'
? {
blacklist: true,
bloqueadoAte: agora + 60 * 60 * 1000
}
: undefined
);
if (severidade === 'alto') {
@@ -2302,7 +2348,12 @@ export const seedRateLimitDev = mutation({
const existing = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', params.tipo).eq('identificador', params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)),
q
.eq('tipo', params.tipo)
.eq(
'identificador',
params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)
)
)
.collect();
const agora = Date.now();
@@ -2315,9 +2366,9 @@ export const seedRateLimitDev = mutation({
estrategia: params.estrategia,
acaoExcedido: params.acaoExcedido,
ativo: true,
prioridade: params.prioridade ?? (doc.prioridade ?? 0),
prioridade: params.prioridade ?? doc.prioridade ?? 0,
atualizadoEm: agora,
notas: params.notas,
notas: params.notas
});
} else {
await ctx.db.insert('rateLimitConfig', {
@@ -2420,4 +2471,3 @@ export const deletarRegraPorta = mutation({
return null;
}
});

View File

@@ -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;
}

View File

@@ -1,188 +1,190 @@
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { simboloTipo } from "./schema";
import { v } from 'convex/values';
import { query, mutation } from './_generated/server';
import { simboloTipo } from './tables/funcionarios';
export const getAll = query({
args: {},
returns: v.array(
v.object({
_id: v.id("simbolos"),
_creationTime: v.number(),
nome: v.string(),
tipo: simboloTipo,
descricao: v.string(),
vencValor: v.string(),
repValor: v.string(),
valor: v.string(),
})
),
handler: async (ctx) => {
return await ctx.db.query("simbolos").collect();
},
args: {},
returns: v.array(
v.object({
_id: v.id('simbolos'),
_creationTime: v.number(),
nome: v.string(),
tipo: simboloTipo,
descricao: v.string(),
vencValor: v.string(),
repValor: v.string(),
valor: v.string()
})
),
handler: async (ctx) => {
return await ctx.db.query('simbolos').collect();
}
});
export const getById = query({
args: {
id: v.id("simbolos"),
},
returns: v.union(
v.object({
_id: v.id("simbolos"),
_creationTime: v.number(),
nome: v.string(),
tipo: simboloTipo,
descricao: v.string(),
vencValor: v.string(),
repValor: v.string(),
valor: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
args: {
id: v.id('simbolos')
},
returns: v.union(
v.object({
_id: v.id('simbolos'),
_creationTime: v.number(),
nome: v.string(),
tipo: simboloTipo,
descricao: v.string(),
vencValor: v.string(),
repValor: v.string(),
valor: v.string()
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
}
});
export const create = mutation({
args: {
nome: v.string(),
tipo: simboloTipo,
refValor: v.string(),
vencValor: v.string(),
descricao: v.string(),
valor: v.optional(v.string()),
},
handler: async (ctx, args) => {
let refValor = args.refValor;
let vencValor = args.vencValor;
let valor = args.valor ?? "";
args: {
nome: v.string(),
tipo: simboloTipo,
refValor: v.string(),
vencValor: v.string(),
descricao: v.string(),
valor: v.optional(v.string())
},
handler: async (ctx, args) => {
let refValor = args.refValor;
let vencValor = args.vencValor;
let valor = args.valor ?? '';
if (args.tipo === "cargo_comissionado") {
if (!refValor || !vencValor) {
throw new Error(
"Valor de referência e valor de vencimento são obrigatórios para cargo comissionado"
);
}
valor = (Number(refValor) + Number(vencValor)).toFixed(2);
} else {
if (!args.valor) {
throw new Error("Valor é obrigatório para função gratificada");
}
refValor = "";
vencValor = "";
valor = args.valor;
}
const novoSimboloId = await ctx.db.insert("simbolos", {
nome: args.nome,
descricao: args.descricao,
repValor: refValor,
vencValor: vencValor,
tipo: args.tipo,
valor,
});
return await ctx.db.get(novoSimboloId);
},
if (args.tipo === 'cargo_comissionado') {
if (!refValor || !vencValor) {
throw new Error(
'Valor de referência e valor de vencimento são obrigatórios para cargo comissionado'
);
}
valor = (Number(refValor) + Number(vencValor)).toFixed(2);
} else {
if (!args.valor) {
throw new Error('Valor é obrigatório para função gratificada');
}
refValor = '';
vencValor = '';
valor = args.valor;
}
const novoSimboloId = await ctx.db.insert('simbolos', {
nome: args.nome,
descricao: args.descricao,
repValor: refValor,
vencValor: vencValor,
tipo: args.tipo,
valor
});
return await ctx.db.get(novoSimboloId);
}
});
export const remove = mutation({
args: {
id: v.id("simbolos"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
return null;
},
args: {
id: v.id('simbolos')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
return null;
}
});
export const update = mutation({
args: {
id: v.id("simbolos"),
nome: v.string(),
tipo: simboloTipo,
refValor: v.string(),
vencValor: v.string(),
descricao: v.string(),
valor: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
let refValor = args.refValor;
let vencValor = args.vencValor;
let valor = args.valor ?? "";
args: {
id: v.id('simbolos'),
nome: v.string(),
tipo: simboloTipo,
refValor: v.string(),
vencValor: v.string(),
descricao: v.string(),
valor: v.optional(v.string())
},
returns: v.null(),
handler: async (ctx, args) => {
let refValor = args.refValor;
let vencValor = args.vencValor;
let valor = args.valor ?? '';
if (args.tipo === "cargo_comissionado") {
if (!refValor || !vencValor) {
throw new Error(
"Valor de referência e valor de vencimento são obrigatórios para cargo comissionado"
);
}
valor = (Number(refValor) + Number(vencValor)).toFixed(2);
} else {
if (!args.valor) {
throw new Error("Valor é obrigatório para função gratificada");
}
refValor = "";
vencValor = "";
valor = args.valor;
}
if (args.tipo === 'cargo_comissionado') {
if (!refValor || !vencValor) {
throw new Error(
'Valor de referência e valor de vencimento são obrigatórios para cargo comissionado'
);
}
valor = (Number(refValor) + Number(vencValor)).toFixed(2);
} else {
if (!args.valor) {
throw new Error('Valor é obrigatório para função gratificada');
}
refValor = '';
vencValor = '';
valor = args.valor;
}
await ctx.db.patch(args.id, {
nome: args.nome,
descricao: args.descricao,
repValor: refValor,
vencValor: vencValor,
tipo: args.tipo,
valor,
});
return null;
},
await ctx.db.patch(args.id, {
nome: args.nome,
descricao: args.descricao,
repValor: refValor,
vencValor: vencValor,
tipo: args.tipo,
valor
});
return null;
}
});
/**
* Remove símbolos duplicados, mantendo apenas a primeira ocorrência de cada símbolo
*/
export const removerDuplicados = mutation({
args: {},
returns: v.object({
removidos: v.number(),
mantidos: v.number(),
}),
handler: async (ctx) => {
const todosSimbolos = await ctx.db.query("simbolos").collect();
// Agrupar símbolos por nome
const simbolosPorNome = new Map<string, typeof todosSimbolos>();
for (const simbolo of todosSimbolos) {
const key = simbolo.nome.trim().toLowerCase();
if (!simbolosPorNome.has(key)) {
simbolosPorNome.set(key, []);
}
simbolosPorNome.get(key)!.push(simbolo);
}
let removidos = 0;
let mantidos = 0;
// Para cada grupo de símbolos com o mesmo nome
for (const [nome, simbolos] of simbolosPorNome) {
// Ordenar por _creationTime (mais antigo primeiro)
simbolos.sort((a, b) => a._creationTime - b._creationTime);
// Manter o primeiro (mais antigo) e remover os demais
const [primeiro, ...duplicados] = simbolos;
mantidos++;
// Remover duplicados
for (const duplicado of duplicados) {
await ctx.db.delete(duplicado._id);
removidos++;
}
}
console.log(`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`);
return { removidos, mantidos };
},
});
args: {},
returns: v.object({
removidos: v.number(),
mantidos: v.number()
}),
handler: async (ctx) => {
const todosSimbolos = await ctx.db.query('simbolos').collect();
// Agrupar símbolos por nome
const simbolosPorNome = new Map<string, typeof todosSimbolos>();
for (const simbolo of todosSimbolos) {
const key = simbolo.nome.trim().toLowerCase();
if (!simbolosPorNome.has(key)) {
simbolosPorNome.set(key, []);
}
simbolosPorNome.get(key)!.push(simbolo);
}
let removidos = 0;
let mantidos = 0;
// Para cada grupo de símbolos com o mesmo nome
for (const [nome, simbolos] of simbolosPorNome) {
// Ordenar por _creationTime (mais antigo primeiro)
simbolos.sort((a, b) => a._creationTime - b._creationTime);
// Manter o primeiro (mais antigo) e remover os demais
const [primeiro, ...duplicados] = simbolos;
mantidos++;
// Remover duplicados
for (const duplicado of duplicados) {
await ctx.db.delete(duplicado._id);
removidos++;
}
}
console.log(
`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`
);
return { removidos, mantidos };
}
});

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View 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'])
};

View File

@@ -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 };
},
});