diff --git a/apps/web/src/routes/(dashboard)/programas-esportivos/acoes/+page.svelte b/apps/web/src/routes/(dashboard)/programas-esportivos/acoes/+page.svelte
new file mode 100644
index 0000000..0bd5a59
--- /dev/null
+++ b/apps/web/src/routes/(dashboard)/programas-esportivos/acoes/+page.svelte
@@ -0,0 +1,210 @@
+
+
+
+
+
Ações
+
+
+
+ {#if loading}
+
Carregando...
+ {:else if error}
+
{error}
+ {:else}
+
+
+
+
+ | Nome |
+ Tipo |
+ Ações |
+
+
+
+ {#each acoes as acao (acao._id)}
+
+ | {acao.nome} |
+
+
+ {acao.tipo === 'projeto' ? 'Projeto' : 'Lei'}
+
+ |
+
+
+
+ |
+
+ {/each}
+ {#if acoes.length === 0}
+
+ | Nenhuma ação cadastrada. |
+
+ {/if}
+
+
+
+ {/if}
+
+ {#if showModal}
+
+
+
+
{editingId ? 'Editar' : 'Novo'} Ação
+
+
+
+
+ {/if}
+
diff --git a/apps/web/src/routes/(dashboard)/ti/+page.svelte b/apps/web/src/routes/(dashboard)/ti/+page.svelte
index faa37e5..84f40d8 100644
--- a/apps/web/src/routes/(dashboard)/ti/+page.svelte
+++ b/apps/web/src/routes/(dashboard)/ti/+page.svelte
@@ -367,6 +367,15 @@
palette: 'accent',
icon: 'building'
},
+ {
+ title: 'Configurações Gerais',
+ description:
+ 'Configure opções gerais do sistema, incluindo setor de compras e outras configurações administrativas.',
+ ctaLabel: 'Configurar',
+ href: '/(dashboard)/ti/configuracoes',
+ palette: 'secondary',
+ icon: 'control'
+ },
{
title: 'Documentação',
description:
diff --git a/apps/web/src/routes/(dashboard)/ti/configuracoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/configuracoes/+page.svelte
new file mode 100644
index 0000000..9c82ee7
--- /dev/null
+++ b/apps/web/src/routes/(dashboard)/ti/configuracoes/+page.svelte
@@ -0,0 +1,94 @@
+
+
+
+
Configurações Gerais
+
+ {#if loading}
+
Carregando...
+ {:else}
+
+
Setor de Compras
+
+ Selecione o setor responsável por receber e aprovar pedidos de compra.
+
+
+ {#if error}
+
+ {error}
+
+ {/if}
+
+ {#if success}
+
+ {success}
+
+ {/if}
+
+
+
+
+
+
+
+
+ {/if}
+
diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts
index 770c2b4..d5115b6 100644
--- a/packages/backend/convex/_generated/api.d.ts
+++ b/packages/backend/convex/_generated/api.d.ts
@@ -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";
@@ -43,9 +45,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";
@@ -67,6 +71,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;
@@ -80,6 +85,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;
@@ -102,9 +108,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;
diff --git a/packages/backend/convex/acoes.ts b/packages/backend/convex/acoes.ts
new file mode 100644
index 0000000..0097ef1
--- /dev/null
+++ b/packages/backend/convex/acoes.ts
@@ -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);
+ }
+});
diff --git a/packages/backend/convex/config.ts b/packages/backend/convex/config.ts
new file mode 100644
index 0000000..d12a2a6
--- /dev/null
+++ b/packages/backend/convex/config.ts
@@ -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()
+ });
+ }
+ }
+});
diff --git a/packages/backend/convex/pedidos.ts b/packages/backend/convex/pedidos.ts
new file mode 100644
index 0000000..96691c2
--- /dev/null
+++ b/packages/backend/convex/pedidos.ts
@@ -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
(); // 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
+ });
+ }
+ }
+ }
+});
diff --git a/packages/backend/convex/permissoesAcoes.ts b/packages/backend/convex/permissoesAcoes.ts
index 02cac86..aa93593 100644
--- a/packages/backend/convex/permissoesAcoes.ts
+++ b/packages/backend/convex/permissoesAcoes.ts
@@ -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;
diff --git a/packages/backend/convex/produtos.ts b/packages/backend/convex/produtos.ts
new file mode 100644
index 0000000..3161e36
--- /dev/null
+++ b/packages/backend/convex/produtos.ts
@@ -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);
+ }
+});
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts
index 905319f..01f8edb 100644
--- a/packages/backend/convex/schema.ts
+++ b/packages/backend/convex/schema.ts
@@ -1,1856 +1,1885 @@
-import { defineSchema, defineTable } from "convex/server";
-import { Infer, v } from "convex/values";
+import { defineSchema, defineTable } from 'convex/server';
+import { Infer, v } from 'convex/values';
export const simboloTipo = v.union(
- v.literal("cargo_comissionado"),
- v.literal("funcao_gratificada")
+ v.literal('cargo_comissionado'),
+ v.literal('funcao_gratificada')
);
export type SimboloTipo = Infer;
export const ataqueCiberneticoTipo = v.union(
- v.literal("phishing"),
- v.literal("malware"),
- v.literal("ransomware"),
- v.literal("brute_force"),
- v.literal("credential_stuffing"),
- v.literal("sql_injection"),
- v.literal("xss"),
- v.literal("path_traversal"),
- v.literal("command_injection"),
- v.literal("nosql_injection"),
- v.literal("xxe"),
- v.literal("man_in_the_middle"),
- v.literal("ddos"),
- v.literal("engenharia_social"),
- v.literal("cve_exploit"),
- v.literal("apt"),
- v.literal("zero_day"),
- v.literal("supply_chain"),
- v.literal("fileless_malware"),
- v.literal("polymorphic_malware"),
- v.literal("ransomware_lateral"),
- v.literal("deepfake_phishing"),
- v.literal("adversarial_ai"),
- v.literal("side_channel"),
- v.literal("firmware_bootloader"),
- v.literal("bec"),
- v.literal("botnet"),
- v.literal("ot_ics"),
- v.literal("quantum_attack")
+ v.literal('phishing'),
+ v.literal('malware'),
+ v.literal('ransomware'),
+ v.literal('brute_force'),
+ v.literal('credential_stuffing'),
+ v.literal('sql_injection'),
+ v.literal('xss'),
+ v.literal('path_traversal'),
+ v.literal('command_injection'),
+ v.literal('nosql_injection'),
+ v.literal('xxe'),
+ v.literal('man_in_the_middle'),
+ v.literal('ddos'),
+ v.literal('engenharia_social'),
+ v.literal('cve_exploit'),
+ v.literal('apt'),
+ v.literal('zero_day'),
+ v.literal('supply_chain'),
+ v.literal('fileless_malware'),
+ v.literal('polymorphic_malware'),
+ v.literal('ransomware_lateral'),
+ v.literal('deepfake_phishing'),
+ v.literal('adversarial_ai'),
+ v.literal('side_channel'),
+ v.literal('firmware_bootloader'),
+ v.literal('bec'),
+ v.literal('botnet'),
+ v.literal('ot_ics'),
+ v.literal('quantum_attack')
);
export type AtaqueCiberneticoTipo = Infer;
export const severidadeSeguranca = v.union(
- v.literal("informativo"),
- v.literal("baixo"),
- v.literal("moderado"),
- v.literal("alto"),
- v.literal("critico")
+ v.literal('informativo'),
+ v.literal('baixo'),
+ v.literal('moderado'),
+ v.literal('alto'),
+ v.literal('critico')
);
export type SeveridadeSeguranca = Infer;
export const statusEventoSeguranca = v.union(
- v.literal("detectado"),
- v.literal("investigando"),
- v.literal("contido"),
- v.literal("falso_positivo"),
- v.literal("escalado"),
- v.literal("resolvido")
+ v.literal('detectado'),
+ v.literal('investigando'),
+ v.literal('contido'),
+ v.literal('falso_positivo'),
+ v.literal('escalado'),
+ v.literal('resolvido')
);
export type StatusEventoSeguranca = Infer;
export const sensorSegurancaTipo = v.union(
- v.literal("network"),
- v.literal("endpoint"),
- v.literal("application"),
- v.literal("gateway"),
- v.literal("ot"),
- v.literal("honeypot")
+ v.literal('network'),
+ v.literal('endpoint'),
+ v.literal('application'),
+ v.literal('gateway'),
+ v.literal('ot'),
+ v.literal('honeypot')
);
export type SensorSegurancaTipo = Infer;
export const sensorSegurancaStatus = v.union(
- v.literal("ativo"),
- v.literal("inativo"),
- v.literal("degradado"),
- v.literal("manutencao")
+ v.literal('ativo'),
+ v.literal('inativo'),
+ v.literal('degradado'),
+ v.literal('manutencao')
);
export type SensorSegurancaStatus = Infer;
export const threatIntelTipo = v.union(
- v.literal("open_source"),
- v.literal("commercial"),
- v.literal("internal"),
- v.literal("gov"),
- v.literal("research")
+ v.literal('open_source'),
+ v.literal('commercial'),
+ v.literal('internal'),
+ v.literal('gov'),
+ v.literal('research')
);
export const threatIntelFormato = v.union(
- v.literal("json"),
- v.literal("stix"),
- v.literal("csv"),
- v.literal("text"),
- v.literal("custom")
+ v.literal('json'),
+ v.literal('stix'),
+ v.literal('csv'),
+ v.literal('text'),
+ v.literal('custom')
);
export const acaoIncidenteTipo = v.union(
- v.literal("block_ip"),
- v.literal("unblock_ip"),
- v.literal("block_port"),
- v.literal("liberar_porta"),
- v.literal("notificar"),
- v.literal("isolar_host"),
- v.literal("gerar_relatorio"),
- v.literal("criar_ticket"),
- v.literal("ajuste_regra"),
- v.literal("custom")
+ v.literal('block_ip'),
+ v.literal('unblock_ip'),
+ v.literal('block_port'),
+ v.literal('liberar_porta'),
+ v.literal('notificar'),
+ v.literal('isolar_host'),
+ v.literal('gerar_relatorio'),
+ v.literal('criar_ticket'),
+ v.literal('ajuste_regra'),
+ v.literal('custom')
);
export const acaoIncidenteStatus = v.union(
- v.literal("pendente"),
- v.literal("executando"),
- v.literal("concluido"),
- v.literal("falhou")
+ v.literal('pendente'),
+ v.literal('executando'),
+ v.literal('concluido'),
+ v.literal('falhou')
);
export const reportStatus = v.union(
- v.literal("pendente"),
- v.literal("processando"),
- v.literal("concluido"),
- v.literal("falhou")
+ v.literal('pendente'),
+ v.literal('processando'),
+ v.literal('concluido'),
+ v.literal('falhou')
);
// Status de templates de fluxo
export const flowTemplateStatus = v.union(
- v.literal("draft"),
- v.literal("published"),
- v.literal("archived")
+ v.literal('draft'),
+ v.literal('published'),
+ v.literal('archived')
);
export type FlowTemplateStatus = Infer;
// Status de instâncias de fluxo
export const flowInstanceStatus = v.union(
- v.literal("active"),
- v.literal("completed"),
- v.literal("cancelled")
+ v.literal('active'),
+ v.literal('completed'),
+ v.literal('cancelled')
);
export type FlowInstanceStatus = Infer;
// Status de passos de instância de fluxo
export const flowInstanceStepStatus = v.union(
- v.literal("pending"),
- v.literal("in_progress"),
- v.literal("completed"),
- v.literal("blocked")
+ v.literal('pending'),
+ v.literal('in_progress'),
+ v.literal('completed'),
+ v.literal('blocked')
);
export type FlowInstanceStepStatus = Infer;
export const situacaoContrato = v.union(
- v.literal("em_execucao"),
- v.literal("rescendido"),
- v.literal("aguardando_assinatura"),
- v.literal("finalizado")
+ v.literal('em_execucao'),
+ v.literal('rescendido'),
+ v.literal('aguardando_assinatura'),
+ v.literal('finalizado')
);
export default defineSchema({
- // Setores da organização
- setores: defineTable({
- nome: v.string(),
- sigla: v.string(),
- criadoPor: v.id("usuarios"),
- createdAt: v.number(),
- })
- .index("by_nome", ["nome"])
- .index("by_sigla", ["sigla"]),
-
- // Relação muitos-para-muitos entre funcionários e setores
- funcionarioSetores: defineTable({
- funcionarioId: v.id("funcionarios"),
- setorId: v.id("setores"),
- createdAt: v.number(),
- })
- .index("by_funcionarioId", ["funcionarioId"])
- .index("by_setorId", ["setorId"])
- .index("by_funcionarioId_and_setorId", ["funcionarioId", "setorId"]),
-
- // Templates de fluxo
- flowTemplates: defineTable({
- name: v.string(),
- description: v.optional(v.string()),
- status: flowTemplateStatus,
- createdBy: v.id("usuarios"),
- createdAt: v.number(),
- })
- .index("by_status", ["status"])
- .index("by_createdBy", ["createdBy"]),
-
- // Passos de template de fluxo
- flowSteps: defineTable({
- flowTemplateId: v.id("flowTemplates"),
- name: v.string(),
- description: v.optional(v.string()),
- position: v.number(),
- expectedDuration: v.number(), // em dias
- setorId: v.id("setores"),
- defaultAssigneeId: v.optional(v.id("usuarios")),
- requiredDocuments: v.optional(v.array(v.string())),
- })
- .index("by_flowTemplateId", ["flowTemplateId"])
- .index("by_flowTemplateId_and_position", ["flowTemplateId", "position"]),
-
- // Instâncias de fluxo
- flowInstances: defineTable({
- flowTemplateId: v.id("flowTemplates"),
- contratoId: v.optional(v.id("contratos")),
- managerId: v.id("usuarios"),
- status: flowInstanceStatus,
- startedAt: v.number(),
- finishedAt: v.optional(v.number()),
- currentStepId: v.optional(v.id("flowInstanceSteps")),
- })
- .index("by_flowTemplateId", ["flowTemplateId"])
- .index("by_contratoId", ["contratoId"])
- .index("by_managerId", ["managerId"])
- .index("by_status", ["status"]),
-
- // Passos de instância de fluxo
- flowInstanceSteps: defineTable({
- flowInstanceId: v.id("flowInstances"),
- flowStepId: v.id("flowSteps"),
- setorId: v.id("setores"),
- assignedToId: v.optional(v.id("usuarios")),
- status: flowInstanceStepStatus,
- startedAt: v.optional(v.number()),
- finishedAt: v.optional(v.number()),
- notes: v.optional(v.string()),
- notesUpdatedBy: v.optional(v.id("usuarios")),
- notesUpdatedAt: v.optional(v.number()),
- dueDate: v.optional(v.number()),
- })
- .index("by_flowInstanceId", ["flowInstanceId"])
- .index("by_flowInstanceId_and_status", ["flowInstanceId", "status"])
- .index("by_setorId", ["setorId"])
- .index("by_assignedToId", ["assignedToId"]),
-
- // Documentos de instância de fluxo
- flowInstanceDocuments: defineTable({
- flowInstanceStepId: v.id("flowInstanceSteps"),
- uploadedById: v.id("usuarios"),
- storageId: v.id("_storage"),
- name: v.string(),
- uploadedAt: v.number(),
- })
- .index("by_flowInstanceStepId", ["flowInstanceStepId"])
- .index("by_uploadedById", ["uploadedById"]),
-
- // Sub-etapas de fluxo (para templates e instâncias)
- flowSubSteps: defineTable({
- flowStepId: v.optional(v.id("flowSteps")), // Para templates
- flowInstanceStepId: v.optional(v.id("flowInstanceSteps")), // Para instâncias
- name: v.string(),
- description: v.optional(v.string()),
- status: v.union(
- v.literal("pending"),
- v.literal("in_progress"),
- v.literal("completed"),
- v.literal("blocked")
- ),
- position: v.number(),
- createdBy: v.id("usuarios"),
- createdAt: v.number(),
- })
- .index("by_flowStepId", ["flowStepId"])
- .index("by_flowInstanceStepId", ["flowInstanceStepId"]),
-
- // Notas de steps e sub-etapas
- flowStepNotes: defineTable({
- flowStepId: v.optional(v.id("flowSteps")),
- flowInstanceStepId: v.optional(v.id("flowInstanceSteps")),
- flowSubStepId: v.optional(v.id("flowSubSteps")),
- texto: v.string(),
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- arquivos: v.array(v.id("_storage")),
- })
- .index("by_flowStepId", ["flowStepId"])
- .index("by_flowInstanceStepId", ["flowInstanceStepId"])
- .index("by_flowSubStepId", ["flowSubStepId"]),
-
- contratos: defineTable({
- contratadaId: v.id("empresas"),
- objeto: v.string(),
- numeroNotaEmpenho: v.string(),
- responsavelId: v.id("funcionarios"),
- departamento: v.string(),
- situacao: situacaoContrato,
- numeroProcessoLicitatorio: v.string(),
- modalidade: v.string(),
- numeroContrato: v.string(),
- anoContrato: v.number(),
- dataInicioVigencia: v.string(),
- dataFimVigencia: v.string(),
- nomeFiscal: v.string(),
- valorTotal: v.string(),
- dataAditivoPrazo: v.optional(v.string()),
- diasAvisoVencimento: v.number(),
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- atualizadoEm: v.optional(v.number()),
- })
- .index("by_responsavel", ["responsavelId"])
- .index("by_situacao", ["situacao"])
- .index("by_vigencia_inicio", ["dataInicioVigencia"])
- .index("by_vigencia_fim", ["dataFimVigencia"]),
-
- todos: defineTable({
- text: v.string(),
- completed: v.boolean(),
- }),
- enderecos: defineTable({
- cep: v.string(),
- logradouro: v.string(),
- numero: v.string(),
- complemento: v.optional(v.string()),
- bairro: v.string(),
- cidade: v.string(),
- uf: v.string(),
- criadoPor: v.optional(v.id("usuarios")),
- atualizadoPor: v.optional(v.id("usuarios")),
- }).index("by_cep", ["cep"]),
- empresas: defineTable({
- razao_social: v.string(),
- nome_fantasia: v.optional(v.string()),
- cnpj: v.string(),
- telefone: v.string(),
- email: v.string(),
- descricao: v.optional(v.string()),
- enderecoId: v.optional(v.id("enderecos")),
- criadoPor: v.optional(v.id("usuarios")),
- })
- .index("by_razao_social", ["razao_social"])
- .index("by_cnpj", ["cnpj"]),
- contatosEmpresa: defineTable({
- empresaId: v.id("empresas"),
- nome: v.string(),
- funcao: v.string(),
- email: v.string(),
- telefone: v.string(),
- adicionadoPor: v.optional(v.id("usuarios")),
- descricao: v.optional(v.string()),
- })
- .index("by_empresa", ["empresaId"])
- .index("by_email", ["email"]),
- funcionarios: defineTable({
- // Campos obrigatórios existentes
- nome: v.string(),
- nascimento: v.string(),
- rg: v.string(),
- cpf: v.string(),
- endereco: v.string(),
- cep: v.string(),
- cidade: v.string(),
- uf: v.string(),
- telefone: v.string(),
- email: v.string(),
- matricula: v.optional(v.string()),
- admissaoData: v.optional(v.string()),
- desligamentoData: v.optional(v.string()),
- simboloId: v.id("simbolos"),
- simboloTipo: simboloTipo,
- gestorId: v.optional(v.id("usuarios")),
- statusFerias: v.optional(
- v.union(v.literal("ativo"), v.literal("em_ferias"))
- ),
-
- // Regime de trabalho (para cálculo correto de férias)
- regimeTrabalho: v.optional(
- v.union(
- v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
- v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
- v.literal("estatutario_federal"), // Servidor Público Federal
- v.literal("estatutario_municipal") // Servidor Público Municipal
- )
- ),
-
- // Dados Pessoais Adicionais (opcionais)
- nomePai: v.optional(v.string()),
- nomeMae: v.optional(v.string()),
- naturalidade: v.optional(v.string()),
- naturalidadeUF: v.optional(v.string()),
- sexo: v.optional(
- v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro"))
- ),
- estadoCivil: v.optional(
- v.union(
- v.literal("solteiro"),
- v.literal("casado"),
- v.literal("divorciado"),
- v.literal("viuvo"),
- v.literal("uniao_estavel")
- )
- ),
- nacionalidade: v.optional(v.string()),
-
- // Documentos Pessoais
- rgOrgaoExpedidor: v.optional(v.string()),
- rgDataEmissao: v.optional(v.string()),
- carteiraProfissionalNumero: v.optional(v.string()),
- carteiraProfissionalSerie: v.optional(v.string()),
- carteiraProfissionalDataEmissao: v.optional(v.string()),
- reservistaNumero: v.optional(v.string()),
- reservistaSerie: v.optional(v.string()),
- tituloEleitorNumero: v.optional(v.string()),
- tituloEleitorZona: v.optional(v.string()),
- tituloEleitorSecao: v.optional(v.string()),
- pisNumero: v.optional(v.string()),
-
- // Formação e Saúde
- grauInstrucao: v.optional(
- v.union(
- v.literal("fundamental"),
- v.literal("medio"),
- v.literal("superior"),
- v.literal("pos_graduacao"),
- v.literal("mestrado"),
- v.literal("doutorado")
- )
- ),
- formacao: v.optional(v.string()),
- formacaoRegistro: v.optional(v.string()),
- grupoSanguineo: v.optional(
- v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O"))
- ),
- fatorRH: v.optional(v.union(v.literal("positivo"), v.literal("negativo"))),
-
- // Cargo e Vínculo
- descricaoCargo: v.optional(v.string()),
- nomeacaoPortaria: v.optional(v.string()),
- nomeacaoData: v.optional(v.string()),
- nomeacaoDOE: v.optional(v.string()),
- pertenceOrgaoPublico: v.optional(v.boolean()),
- orgaoOrigem: v.optional(v.string()),
- aposentado: v.optional(
- v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss"))
- ),
-
- // Dados Bancários
- contaBradescoNumero: v.optional(v.string()),
- contaBradescoDV: v.optional(v.string()),
- contaBradescoAgencia: v.optional(v.string()),
-
- // Documentos Anexos (Storage IDs)
- certidaoAntecedentesPF: v.optional(v.id("_storage")),
- certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
- certidaoAntecedentesSDS: v.optional(v.id("_storage")),
- certidaoAntecedentesTJPE: v.optional(v.id("_storage")),
- certidaoImprobidade: v.optional(v.id("_storage")),
- rgFrente: v.optional(v.id("_storage")),
- rgVerso: v.optional(v.id("_storage")),
- cpfFrente: v.optional(v.id("_storage")),
- cpfVerso: v.optional(v.id("_storage")),
- situacaoCadastralCPF: v.optional(v.id("_storage")),
- tituloEleitorFrente: v.optional(v.id("_storage")),
- tituloEleitorVerso: v.optional(v.id("_storage")),
- comprovanteVotacao: v.optional(v.id("_storage")),
- carteiraProfissionalFrente: v.optional(v.id("_storage")),
- carteiraProfissionalVerso: v.optional(v.id("_storage")),
- comprovantePIS: v.optional(v.id("_storage")),
- certidaoRegistroCivil: v.optional(v.id("_storage")),
- certidaoNascimentoDependentes: v.optional(v.id("_storage")),
- cpfDependentes: v.optional(v.id("_storage")),
- reservistaDoc: v.optional(v.id("_storage")),
- comprovanteEscolaridade: v.optional(v.id("_storage")),
- comprovanteResidencia: v.optional(v.id("_storage")),
- comprovanteContaBradesco: v.optional(v.id("_storage")),
-
- // Dependentes do funcionário (uploads opcionais)
- dependentes: v.optional(
- v.array(
- v.object({
- parentesco: v.union(
- v.literal("filho"),
- v.literal("filha"),
- v.literal("conjuge"),
- v.literal("outro")
- ),
- nome: v.string(),
- cpf: v.string(),
- nascimento: v.string(),
- documentoId: v.optional(v.id("_storage")),
- // Benefícios/declarações por dependente
- salarioFamilia: v.optional(v.boolean()),
- impostoRenda: v.optional(v.boolean()),
- })
- )
- ),
-
- // Declarações (Storage IDs)
- declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
- declaracaoDependentesIR: v.optional(v.id("_storage")),
- declaracaoIdoneidade: v.optional(v.id("_storage")),
- termoNepotismo: v.optional(v.id("_storage")),
- termoOpcaoRemuneracao: v.optional(v.id("_storage")),
- })
- .index("by_matricula", ["matricula"])
- .index("by_nome", ["nome"])
- .index("by_simboloId", ["simboloId"])
- .index("by_simboloTipo", ["simboloTipo"])
- .index("by_cpf", ["cpf"])
- .index("by_rg", ["rg"])
- .index("by_gestor", ["gestorId"]),
-
- atestados: defineTable({
- funcionarioId: v.id("funcionarios"),
- tipo: v.union(
- v.literal("atestado_medico"),
- v.literal("declaracao_comparecimento")
- ),
- dataInicio: v.string(),
- dataFim: v.string(),
- cid: v.optional(v.string()), // Apenas para atestado médico
- observacoes: v.optional(v.string()),
- documentoId: v.optional(v.id("_storage")),
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_tipo", ["tipo"])
- .index("by_data_inicio", ["dataInicio"])
- .index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]),
-
- licencas: defineTable({
- funcionarioId: v.id("funcionarios"),
- tipo: v.union(v.literal("maternidade"), v.literal("paternidade")),
- dataInicio: v.string(),
- dataFim: v.string(),
- documentoId: v.optional(v.id("_storage")),
- observacoes: v.optional(v.string()),
- licencaOriginalId: v.optional(v.id("licencas")), // Para prorrogações
- ehProrrogacao: v.boolean(),
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_tipo", ["tipo"])
- .index("by_data_inicio", ["dataInicio"])
- .index("by_licenca_original", ["licencaOriginalId"])
- .index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]),
-
- ferias: defineTable({
- funcionarioId: v.id("funcionarios"),
- anoReferencia: v.number(),
- dataInicio: v.string(),
- dataFim: v.string(),
- diasFerias: v.number(),
- status: v.union(
- v.literal("aguardando_aprovacao"),
- v.literal("aprovado"),
- v.literal("reprovado"),
- v.literal("data_ajustada_aprovada"),
- v.literal("EmFérias"),
- v.literal("Cancelado_RH")
- ),
- gestorId: v.optional(v.id("usuarios")),
- observacao: v.optional(v.string()),
- motivoReprovacao: v.optional(v.string()),
- dataAprovacao: v.optional(v.number()),
- dataReprovacao: v.optional(v.number()),
- diasAbono: v.number(),
- historicoAlteracoes: v.optional(
- v.array(
- v.object({
- data: v.number(),
- usuarioId: v.id("usuarios"),
- acao: v.string(),
- })
- )
- ),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"])
- .index("by_funcionario_and_status", ["funcionarioId", "status"])
- .index("by_status", ["status"])
- .index("by_ano", ["anoReferencia"]),
-
- notificacoesFerias: defineTable({
- destinatarioId: v.id("usuarios"),
- feriasId: v.id("ferias"),
- tipo: v.union(
- v.literal("nova_solicitacao"),
- v.literal("aprovado"),
- v.literal("reprovado"),
- v.literal("data_ajustada")
- ),
- lida: v.boolean(),
- mensagem: v.string(),
- })
- .index("by_destinatario", ["destinatarioId"])
- .index("by_destinatario_and_lida", ["destinatarioId", "lida"]),
-
- // Solicitações de Ausências
- solicitacoesAusencias: defineTable({
- funcionarioId: v.id("funcionarios"),
- dataInicio: v.string(),
- dataFim: v.string(),
- motivo: v.string(),
- status: v.union(
- v.literal("aguardando_aprovacao"),
- v.literal("aprovado"),
- v.literal("reprovado")
- ),
- gestorId: v.optional(v.id("usuarios")),
- dataAprovacao: v.optional(v.number()),
- dataReprovacao: v.optional(v.number()),
- motivoReprovacao: v.optional(v.string()),
- observacao: v.optional(v.string()),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_status", ["status"])
- .index("by_funcionario_and_status", ["funcionarioId", "status"]),
-
- notificacoesAusencias: defineTable({
- destinatarioId: v.id("usuarios"),
- solicitacaoAusenciaId: v.id("solicitacoesAusencias"),
- tipo: v.union(
- v.literal("nova_solicitacao"),
- v.literal("aprovado"),
- v.literal("reprovado")
- ),
- lida: v.boolean(),
- mensagem: v.string(),
- })
- .index("by_destinatario", ["destinatarioId"])
- .index("by_destinatario_and_lida", ["destinatarioId", "lida"]),
-
-
- times: defineTable({
- nome: v.string(),
- descricao: v.optional(v.string()),
- gestorId: v.id("usuarios"),
- gestorSuperiorId: v.optional(v.id("usuarios")),
- ativo: v.boolean(),
- cor: v.optional(v.string()), // Cor para identificação visual
- })
- .index("by_gestor", ["gestorId"])
- .index("by_gestor_superior", ["gestorSuperiorId"]),
-
- timesMembros: defineTable({
- timeId: v.id("times"),
- funcionarioId: v.id("funcionarios"),
- dataEntrada: v.number(),
- dataSaida: v.optional(v.number()),
- ativo: v.boolean(),
- })
- .index("by_time", ["timeId"])
- .index("by_funcionario", ["funcionarioId"])
- .index("by_time_and_ativo", ["timeId", "ativo"]),
-
- cursos: defineTable({
- funcionarioId: v.id("funcionarios"),
- descricao: v.string(),
- data: v.string(),
- certificadoId: v.optional(v.id("_storage")),
- }).index("by_funcionario", ["funcionarioId"]),
-
- simbolos: defineTable({
- nome: v.string(),
- tipo: simboloTipo,
- descricao: v.string(),
- vencValor: v.string(),
- repValor: v.string(),
- valor: v.string(),
- }),
-
- // Sistema de Autenticação e Controle de Acesso
- usuarios: defineTable({
- authId: v.string(),
- nome: v.string(),
- email: v.string(),
- funcionarioId: v.optional(v.id("funcionarios")),
- roleId: v.id("roles"),
- ativo: v.boolean(),
- primeiroAcesso: v.boolean(),
- ultimoAcesso: v.optional(v.number()),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
-
- // Controle de Bloqueio e Segurança
- bloqueado: v.optional(v.boolean()),
- motivoBloqueio: v.optional(v.string()),
- dataBloqueio: v.optional(v.number()),
- tentativasLogin: v.optional(v.number()), // contador de tentativas falhas
- ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa
-
- // Campos de Chat e Perfil
-
- fotoPerfil: v.optional(v.id("_storage")),
- avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
- setor: v.optional(v.string()),
- statusMensagem: v.optional(v.string()), // max 100 chars
- statusPresenca: v.optional(
- v.union(
- v.literal("online"),
- v.literal("offline"),
- v.literal("ausente"),
- v.literal("externo"),
- v.literal("em_reuniao")
- )
- ),
- ultimaAtividade: v.optional(v.number()), // timestamp
- notificacoesAtivadas: v.optional(v.boolean()),
- somNotificacao: v.optional(v.boolean()),
- temaPreferido: v.optional(v.string()), // tema de aparência escolhido pelo usuário
- })
- .index("by_email", ["email"])
- .index("by_role", ["roleId"])
- .index("by_ativo", ["ativo"])
- .index("by_status_presenca", ["statusPresenca"])
- .index("by_bloqueado", ["bloqueado"])
- .index("by_funcionarioId", ["funcionarioId"])
- .index("authId", ["authId"]),
-
- roles: defineTable({
- nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
- descricao: v.string(),
- nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado
- setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc.
- customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER
- criadoPor: v.optional(v.id("usuarios")), // usuário TI_MASTER que criou este perfil
- editavel: v.optional(v.boolean()), // se pode ser editado (false para roles fixas)
- })
- .index("by_nome", ["nome"])
- .index("by_nivel", ["nivel"])
- .index("by_setor", ["setor"])
- .index("by_customizado", ["customizado"]),
-
- permissoes: defineTable({
- nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc.
- descricao: v.string(),
- recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc.
- acao: v.string(), // "criar", "ler", "editar", "excluir"
- })
- .index("by_recurso", ["recurso"])
- .index("by_recurso_e_acao", ["recurso", "acao"])
- .index("by_nome", ["nome"]),
-
- rolePermissoes: defineTable({
- roleId: v.id("roles"),
- permissaoId: v.id("permissoes"),
- })
- .index("by_role", ["roleId"])
- .index("by_permissao", ["permissaoId"]),
-
- sessoes: defineTable({
- usuarioId: v.id("usuarios"),
- token: v.string(),
- ipAddress: v.optional(v.string()),
- userAgent: v.optional(v.string()),
- criadoEm: v.number(),
- expiraEm: v.number(),
- ativo: v.boolean(),
- })
- .index("by_usuario", ["usuarioId"])
- .index("by_token", ["token"])
- .index("by_ativo", ["ativo"])
- .index("by_expiracao", ["expiraEm"]),
-
- logsAcesso: defineTable({
- usuarioId: v.id("usuarios"),
- tipo: v.union(
- v.literal("login"),
- v.literal("logout"),
- v.literal("acesso_negado"),
- v.literal("senha_alterada"),
- v.literal("sessao_expirada")
- ),
- ipAddress: v.optional(v.string()),
- userAgent: v.optional(v.string()),
- detalhes: v.optional(v.string()),
- timestamp: v.number(),
- })
- .index("by_usuario", ["usuarioId"])
- .index("by_tipo", ["tipo"])
- .index("by_timestamp", ["timestamp"]),
-
- // Logs de Login Detalhados
- logsLogin: defineTable({
- usuarioId: v.optional(v.id("usuarios")), // pode ser null se falha antes de identificar usuário
- matriculaOuEmail: v.string(), // tentativa de login
- sucesso: v.boolean(),
- motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente"
- ipAddress: v.optional(v.string()),
- userAgent: v.optional(v.string()),
- device: v.optional(v.string()),
- browser: v.optional(v.string()),
- sistema: v.optional(v.string()),
- timestamp: v.number(),
- })
- .index("by_usuario", ["usuarioId"])
- .index("by_sucesso", ["sucesso"])
- .index("by_timestamp", ["timestamp"])
- .index("by_ip", ["ipAddress"]),
-
- // Logs de Atividades
- logsAtividades: defineTable({
- usuarioId: v.id("usuarios"),
- acao: v.string(), // "criar", "editar", "excluir", "bloquear", "desbloquear", etc.
- recurso: v.string(), // "funcionarios", "simbolos", "usuarios", "perfis", etc.
- recursoId: v.optional(v.string()), // ID do recurso afetado
- detalhes: v.optional(v.string()), // JSON com detalhes da ação
- timestamp: v.number(),
- })
- .index("by_usuario", ["usuarioId"])
- .index("by_acao", ["acao"])
- .index("by_recurso", ["recurso"])
- .index("by_timestamp", ["timestamp"])
- .index("by_recurso_id", ["recurso", "recursoId"]),
-
- // Histórico de Bloqueios
- bloqueiosUsuarios: defineTable({
- usuarioId: v.id("usuarios"),
- motivo: v.string(),
- bloqueadoPor: v.id("usuarios"), // ID do TI_MASTER que bloqueou
- dataInicio: v.number(),
- dataFim: v.optional(v.number()), // quando foi desbloqueado
- desbloqueadoPor: v.optional(v.id("usuarios")),
- ativo: v.boolean(), // se é o bloqueio atual ativo
- })
- .index("by_usuario", ["usuarioId"])
- .index("by_bloqueado_por", ["bloqueadoPor"])
- .index("by_ativo", ["ativo"])
- .index("by_data_inicio", ["dataInicio"]),
-
- // Perfis Customizados
-
- // Templates de Mensagens
- templatesMensagens: defineTable({
- codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc.
- nome: v.string(),
- tipo: v.union(
- v.literal("sistema"), // predefinido, não editável
- v.literal("customizado") // criado por TI_MASTER
- ),
- titulo: v.string(),
- corpo: v.string(), // pode ter variáveis {{variavel}}
- variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.]
- criadoPor: v.optional(v.id("usuarios")),
- criadoEm: v.number(),
- })
- .index("by_codigo", ["codigo"])
- .index("by_tipo", ["tipo"])
- .index("by_criado_por", ["criadoPor"]),
-
- // Configuração de Email/SMTP
- configuracaoEmail: defineTable({
- servidor: v.string(), // smtp.gmail.com
- porta: v.number(), // 587, 465, etc.
- usuario: v.string(),
- senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP
- emailRemetente: v.string(),
- nomeRemetente: v.string(),
- usarSSL: v.boolean(),
- usarTLS: v.boolean(),
- ativo: v.boolean(),
- testadoEm: v.optional(v.number()),
- configuradoPor: v.id("usuarios"),
- atualizadoEm: v.number(),
- }).index("by_ativo", ["ativo"]),
-
- // Configuração de Jitsi Meet
- configuracaoJitsi: defineTable({
- domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com")
- appId: v.string(), // ID da aplicação Jitsi
- roomPrefix: v.string(), // Prefixo para nomes de salas
- useHttps: v.boolean(), // Usar HTTPS
- acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento)
- // Configurações SSH/Docker para configuração automática do servidor
- sshHost: v.optional(v.string()), // Host SSH para acesso ao servidor Docker (ex: "192.168.1.100" ou "servidor.local")
- sshPort: v.optional(v.number()), // Porta SSH (padrão: 22)
- sshUsername: v.optional(v.string()), // Usuário SSH
- sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada)
- sshKeyPath: v.optional(v.string()), // Caminho para chave SSH (alternativa à senha)
- dockerComposePath: v.optional(v.string()), // Caminho do docker-compose.yml (ex: "/home/user/jitsi-docker")
- jitsiConfigPath: v.optional(v.string()), // Caminho base das configurações Jitsi (ex: "~/.jitsi-meet-cfg")
- 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
- }).index("by_ativo", ["ativo"]),
-
- // Fila de Emails
- notificacoesEmail: defineTable({
- destinatario: v.string(), // email
- destinatarioId: v.optional(v.id("usuarios")),
- assunto: v.string(),
- corpo: v.string(), // HTML ou texto
- templateId: v.optional(v.id("templatesMensagens")),
- status: v.union(
- v.literal("pendente"),
- v.literal("enviando"),
- v.literal("enviado"),
- v.literal("falha")
- ),
- tentativas: v.number(),
- ultimaTentativa: v.optional(v.number()),
- erroDetalhes: v.optional(v.string()),
- enviadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- enviadoEm: v.optional(v.number()),
- agendadaPara: v.optional(v.number()), // timestamp para agendamento
- })
- .index("by_status", ["status"])
- .index("by_destinatario", ["destinatarioId"])
- .index("by_enviado_por", ["enviadoPor"])
- .index("by_criado_em", ["criadoEm"])
- .index("by_agendamento", ["agendadaPara"]),
-
- configuracaoAcesso: defineTable({
- chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc.
- valor: v.string(),
- descricao: v.string(),
- }).index("by_chave", ["chave"]),
-
- // Rate Limiting de Emails
- rateLimitEmails: defineTable({
- remetenteId: v.id("usuarios"),
- timestamp: v.number(),
- contador: v.number(), // quantidade de emails enviados neste período
- periodo: v.union(
- v.literal("minuto"), // último minuto
- v.literal("hora") // última hora
- ),
- })
- .index("by_remetente_periodo", ["remetenteId", "periodo", "timestamp"])
- .index("by_timestamp", ["timestamp"]),
-
- // Sistema de Chat
- conversas: defineTable({
- tipo: v.union(
- v.literal("individual"),
- v.literal("grupo"),
- v.literal("sala_reuniao")
- ),
- nome: v.optional(v.string()), // nome do grupo/sala
-
- participantes: v.array(v.id("usuarios")), // IDs dos participantes
- administradores: v.optional(v.array(v.id("usuarios"))), // IDs dos administradores (apenas para sala_reuniao)
- ultimaMensagem: v.optional(v.string()),
- ultimaMensagemTimestamp: v.optional(v.number()),
- ultimaMensagemRemetenteId: v.optional(v.id("usuarios")), // ID do remetente da última mensagem
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- })
- .index("by_criado_por", ["criadoPor"])
- .index("by_tipo", ["tipo"])
- .index("by_ultima_mensagem", ["ultimaMensagemTimestamp"]),
-
- mensagens: defineTable({
- conversaId: v.id("conversas"),
- remetenteId: v.id("usuarios"),
- tipo: v.union(
- v.literal("texto"),
- v.literal("arquivo"),
- v.literal("imagem")
- ),
- conteudo: v.string(), // texto ou nome do arquivo
- conteudoBusca: v.optional(v.string()), // versão normalizada para busca
- arquivoId: v.optional(v.id("_storage")),
- arquivoNome: v.optional(v.string()),
- arquivoTamanho: v.optional(v.number()),
- arquivoTipo: v.optional(v.string()),
- linkPreview: v.optional(
- v.object({
- url: v.string(),
- titulo: v.optional(v.string()),
- descricao: v.optional(v.string()),
- imagem: v.optional(v.string()),
- site: v.optional(v.string()),
- })
- ),
- reagiuPor: v.optional(
- v.array(
- v.object({
- usuarioId: v.id("usuarios"),
- emoji: v.string(),
- })
- )
- ),
- mencoes: v.optional(v.array(v.id("usuarios"))),
- respostaPara: v.optional(v.id("mensagens")), // ID da mensagem que está respondendo
- agendadaPara: v.optional(v.number()), // timestamp
- enviadaEm: v.number(),
- editadaEm: v.optional(v.number()),
- deletada: v.optional(v.boolean()),
- lidaPor: v.optional(v.array(v.id("usuarios"))), // IDs dos usuários que leram a mensagem
- })
- .index("by_conversa", ["conversaId", "enviadaEm"])
- .index("by_remetente", ["remetenteId"])
- .index("by_agendamento", ["agendadaPara"])
- .index("by_resposta", ["respostaPara"]),
-
- leituras: defineTable({
- conversaId: v.id("conversas"),
- usuarioId: v.id("usuarios"),
- ultimaMensagemLida: v.id("mensagens"),
- lidaEm: v.number(),
- })
- .index("by_conversa_usuario", ["conversaId", "usuarioId"])
- .index("by_usuario", ["usuarioId"]),
-
- // Sistema de Chamadas de Áudio/Vídeo
- chamadas: defineTable({
- conversaId: v.id("conversas"),
- tipo: v.union(v.literal("audio"), v.literal("video")),
- roomName: v.string(), // Nome único da sala Jitsi
- criadoPor: v.id("usuarios"), // Anfitrião/criador
- participantes: v.array(v.id("usuarios")),
- status: v.union(
- v.literal("aguardando"),
- v.literal("em_andamento"),
- v.literal("finalizada"),
- v.literal("cancelada")
- ),
- iniciadaEm: v.optional(v.number()),
- finalizadaEm: v.optional(v.number()),
- duracaoSegundos: v.optional(v.number()),
- gravando: v.boolean(),
- gravacaoIniciadaPor: v.optional(v.id("usuarios")),
- gravacaoIniciadaEm: v.optional(v.number()),
- gravacaoFinalizadaEm: v.optional(v.number()),
- configuracoes: v.optional(v.object({
- audioHabilitado: v.boolean(),
- videoHabilitado: v.boolean(),
- participantesConfig: v.optional(v.array(v.object({
- usuarioId: v.id("usuarios"),
- audioHabilitado: v.boolean(),
- videoHabilitado: v.boolean(),
- forcadoPeloAnfitriao: v.optional(v.boolean()), // Se foi forçado pelo anfitrião
- })))
- })),
- criadoEm: v.number(),
- })
- .index("by_conversa", ["conversaId", "status"])
- .index("by_criado_por", ["criadoPor"])
- .index("by_status", ["status"])
- .index("by_room_name", ["roomName"]),
-
- notificacoes: defineTable({
- usuarioId: v.id("usuarios"),
- tipo: v.union(
- v.literal("nova_mensagem"),
- v.literal("mencao"),
- v.literal("grupo_criado"),
- v.literal("adicionado_grupo"),
- v.literal("alerta_seguranca"),
- v.literal("etapa_fluxo_concluida")
- ),
- conversaId: v.optional(v.id("conversas")),
- mensagemId: v.optional(v.id("mensagens")),
- remetenteId: v.optional(v.id("usuarios")),
- titulo: v.string(),
- descricao: v.string(),
- lida: v.boolean(),
- criadaEm: v.number(),
- })
- .index("by_usuario", ["usuarioId", "lida", "criadaEm"])
- .index("by_usuario_lida", ["usuarioId", "lida"]),
-
- digitando: defineTable({
- conversaId: v.id("conversas"),
- usuarioId: v.id("usuarios"),
- iniciouEm: v.number(),
- })
- .index("by_conversa", ["conversaId", "iniciouEm"])
- .index("by_usuario", ["usuarioId"]),
-
- // Push Notifications
- pushSubscriptions: defineTable({
- usuarioId: v.id("usuarios"),
- endpoint: v.string(), // URL do serviço de push
- keys: v.object({
- p256dh: v.string(), // Chave pública
- auth: v.string(), // Chave de autenticação
- }),
- userAgent: v.optional(v.string()),
- criadoEm: v.number(),
- ultimaAtividade: v.number(),
- ativo: v.boolean(),
- })
- .index("by_usuario", ["usuarioId", "ativo"])
- .index("by_endpoint", ["endpoint"]),
-
- // Preferências de Notificação por Conversa
- preferenciasNotificacaoConversa: defineTable({
- usuarioId: v.id("usuarios"),
- conversaId: v.id("conversas"),
- pushAtivado: v.boolean(), // Receber push notifications
- emailAtivado: v.boolean(), // Receber emails quando offline
- somAtivado: v.boolean(), // Tocar som
- silenciado: v.boolean(), // Silenciar completamente
- apenasMencoes: v.boolean(), // Notificar apenas quando mencionado
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_usuario_conversa", ["usuarioId", "conversaId"])
- .index("by_conversa", ["conversaId"]),
-
- // Tabelas de Monitoramento do Sistema
- systemMetrics: defineTable({
- timestamp: v.number(),
- // Métricas de Sistema
- cpuUsage: v.optional(v.number()),
- memoryUsage: v.optional(v.number()),
- networkLatency: v.optional(v.number()),
- storageUsed: v.optional(v.number()),
- // Métricas de Aplicação
- usuariosOnline: v.optional(v.number()),
- mensagensPorMinuto: v.optional(v.number()),
- tempoRespostaMedio: v.optional(v.number()),
- errosCount: v.optional(v.number()),
- }).index("by_timestamp", ["timestamp"]),
-
- alertConfigurations: defineTable({
- metricName: v.string(),
- threshold: v.number(),
- operator: v.union(
- v.literal(">"),
- v.literal("<"),
- v.literal(">="),
- v.literal("<="),
- v.literal("==")
- ),
- enabled: v.boolean(),
- notifyByEmail: v.boolean(),
- notifyByChat: v.boolean(),
- createdBy: v.id("usuarios"),
- lastModified: v.number(),
- }).index("by_enabled", ["enabled"]),
-
- alertHistory: defineTable({
- configId: v.id("alertConfigurations"),
- metricName: v.string(),
- metricValue: v.number(),
- threshold: v.number(),
- timestamp: v.number(),
- status: v.union(v.literal("triggered"), v.literal("resolved")),
- notificationsSent: v.object({
- email: v.boolean(),
- chat: v.boolean(),
- }),
- })
- .index("by_timestamp", ["timestamp"])
- .index("by_status", ["status"])
- .index("by_config", ["configId", "timestamp"]),
-
- tickets: defineTable({
- numero: v.string(),
- titulo: v.string(),
- descricao: v.string(),
- tipo: v.union(
- v.literal("reclamacao"),
- v.literal("elogio"),
- v.literal("sugestao"),
- v.literal("chamado")
- ),
- categoria: v.optional(v.string()),
- status: v.union(
- v.literal("aberto"),
- v.literal("em_andamento"),
- v.literal("aguardando_usuario"),
- v.literal("resolvido"),
- v.literal("encerrado"),
- v.literal("cancelado")
- ),
- prioridade: v.union(
- v.literal("baixa"),
- v.literal("media"),
- v.literal("alta"),
- v.literal("critica")
- ),
- solicitanteId: v.id("usuarios"),
- solicitanteNome: v.string(),
- solicitanteEmail: v.string(),
- responsavelId: v.optional(v.id("usuarios")),
- setorResponsavel: v.optional(v.string()),
- slaConfigId: v.optional(v.id("slaConfigs")),
- conversaId: v.optional(v.id("conversas")),
- prazoResposta: v.optional(v.number()),
- prazoConclusao: v.optional(v.number()),
- prazoEncerramento: v.optional(v.number()),
- timeline: v.optional(
- v.array(
- v.object({
- etapa: v.string(),
- status: v.union(
- v.literal("pendente"),
- v.literal("em_andamento"),
- v.literal("concluido"),
- v.literal("vencido")
- ),
- prazo: v.optional(v.number()),
- concluidoEm: v.optional(v.number()),
- observacao: v.optional(v.string()),
- })
- )
- ),
- alertasEmitidos: v.optional(
- v.array(
- v.object({
- tipo: v.union(
- v.literal("resposta"),
- v.literal("conclusao"),
- v.literal("encerramento")
- ),
- emitidoEm: v.number(),
- })
- )
- ),
- anexos: v.optional(
- v.array(
- v.object({
- arquivoId: v.id("_storage"),
- nome: v.optional(v.string()),
- tipo: v.optional(v.string()),
- tamanho: v.optional(v.number()),
- })
- )
- ),
- tags: v.optional(v.array(v.string())),
- canalOrigem: v.optional(v.string()),
- ultimaInteracaoEm: v.number(),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_numero", ["numero"])
- .index("by_status", ["status"])
- .index("by_solicitante", ["solicitanteId", "status"])
- .index("by_responsavel", ["responsavelId", "status"])
- .index("by_setor", ["setorResponsavel", "status"]),
-
- ticketInteractions: defineTable({
- ticketId: v.id("tickets"),
- autorId: v.optional(v.id("usuarios")),
- origem: v.union(
- v.literal("usuario"),
- v.literal("ti"),
- v.literal("sistema")
- ),
- tipo: v.union(
- v.literal("mensagem"),
- v.literal("status"),
- v.literal("anexo"),
- v.literal("alerta")
- ),
- conteudo: v.string(),
- anexos: v.optional(
- v.array(
- v.object({
- arquivoId: v.id("_storage"),
- nome: v.optional(v.string()),
- tipo: v.optional(v.string()),
- tamanho: v.optional(v.number()),
- })
- )
- ),
- statusAnterior: v.optional(
- v.union(
- v.literal("aberto"),
- v.literal("em_andamento"),
- v.literal("aguardando_usuario"),
- v.literal("resolvido"),
- v.literal("encerrado"),
- v.literal("cancelado")
- )
- ),
- statusNovo: v.optional(
- v.union(
- v.literal("aberto"),
- v.literal("em_andamento"),
- v.literal("aguardando_usuario"),
- v.literal("resolvido"),
- v.literal("encerrado"),
- v.literal("cancelado")
- )
- ),
- visibilidade: v.union(
- v.literal("publico"),
- v.literal("interno")
- ),
- criadoEm: v.number(),
- })
- .index("by_ticket", ["ticketId"])
- .index("by_ticket_type", ["ticketId", "tipo"])
- .index("by_autor", ["autorId"]),
-
- slaConfigs: defineTable({
- nome: v.string(),
- descricao: v.optional(v.string()),
- prioridade: v.optional(
- v.union(
- v.literal("baixa"),
- v.literal("media"),
- v.literal("alta"),
- v.literal("critica")
- )
- ),
- tempoRespostaHoras: v.number(),
- tempoConclusaoHoras: v.number(),
- tempoEncerramentoHoras: v.optional(v.number()),
- alertaAntecedenciaHoras: v.number(),
- ativo: v.boolean(),
- criadoPor: v.id("usuarios"),
- atualizadoPor: v.optional(v.id("usuarios")),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_ativo", ["ativo"])
- .index("by_prioridade", ["prioridade", "ativo"])
- .index("by_nome", ["nome"]),
-
- ticketAssignments: defineTable({
- ticketId: v.id("tickets"),
- responsavelId: v.id("usuarios"),
- atribuidoPor: v.id("usuarios"),
- motivo: v.optional(v.string()),
- ativo: v.boolean(),
- criadoEm: v.number(),
- encerradoEm: v.optional(v.number()),
- })
- .index("by_ticket", ["ticketId", "ativo"])
- .index("by_responsavel", ["responsavelId", "ativo"]),
-
- // Sistema de Segurança Cibernética
- networkSensors: defineTable({
- nome: v.string(),
- tipo: sensorSegurancaTipo,
- status: sensorSegurancaStatus,
- escopo: v.optional(v.string()),
- ipMonitorado: v.optional(v.string()),
- hostname: v.optional(v.string()),
- regioes: v.optional(v.array(v.string())),
- portasMonitoradas: v.optional(v.array(v.number())),
- protocolos: v.optional(v.array(v.string())),
- capacidades: v.optional(v.array(v.string())),
- ultimaSincronizacao: v.number(),
- ultimoHeartbeat: v.optional(v.number()),
- latenciaMs: v.optional(v.number()),
- errosConsecutivos: v.optional(v.number()),
- agenteVersao: v.optional(v.string()),
- notas: v.optional(v.string()),
- })
- .index("by_tipo", ["tipo"])
- .index("by_status", ["status"])
- .index("by_hostname", ["hostname"]),
-
- ipReputation: defineTable({
- indicador: v.string(),
- categoria: v.union(
- v.literal("ip"),
- v.literal("dominio"),
- v.literal("hash"),
- v.literal("email")
- ),
- reputacao: v.number(), // -100 (malicioso) até 100 (confiável)
- severidadeMax: severidadeSeguranca,
- whitelist: v.boolean(),
- blacklist: v.boolean(),
- ocorrencias: v.number(),
- primeiroRegistro: v.number(),
- ultimoRegistro: v.number(),
- bloqueadoAte: v.optional(v.number()),
- origem: v.optional(v.string()),
- comentarios: v.optional(v.string()),
- classificacoes: v.optional(v.array(v.string())),
- ultimaAcaoId: v.optional(v.id("incidentActions")),
- })
- .index("by_indicador", ["indicador"])
- .index("by_reputacao", ["reputacao"])
- .index("by_blacklist", ["blacklist"])
- .index("by_whitelist", ["whitelist"]),
-
- portRules: defineTable({
- porta: v.number(),
- protocolo: v.union(
- v.literal("tcp"),
- v.literal("udp"),
- v.literal("icmp"),
- v.literal("quic"),
- v.literal("any")
- ),
- acao: v.union(
- v.literal("permitir"),
- v.literal("bloquear"),
- v.literal("monitorar"),
- v.literal("rate_limit")
- ),
- temporario: v.boolean(),
- severidadeMin: severidadeSeguranca,
- duracaoSegundos: v.optional(v.number()),
- expiraEm: v.optional(v.number()),
- criadoPor: v.id("usuarios"),
- atualizadoPor: v.optional(v.id("usuarios")),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- notas: v.optional(v.string()),
- tags: v.optional(v.array(v.string())),
- listaReferencia: v.optional(v.id("ipReputation")),
- })
- .index("by_porta_protocolo", ["porta", "protocolo"])
- .index("by_acao", ["acao"])
- .index("by_expiracao", ["expiraEm"]),
-
- threatIntelFeeds: defineTable({
- nomeFonte: v.string(),
- tipo: threatIntelTipo,
- formato: threatIntelFormato,
- url: v.optional(v.string()),
- ativo: v.boolean(),
- prioridade: v.union(
- v.literal("baixa"),
- v.literal("media"),
- v.literal("alta"),
- v.literal("critica")
- ),
- ultimaSincronizacao: v.optional(v.number()),
- entradasProcessadas: v.optional(v.number()),
- errosConsecutivos: v.optional(v.number()),
- autenticacaoNecessaria: v.optional(v.boolean()),
- configuracao: v.optional(
- v.object({
- tokenId: v.optional(v.id("_storage")),
- escopo: v.optional(v.string()),
- })
- ),
- criadoPor: v.id("usuarios"),
- atualizadoPor: v.optional(v.id("usuarios")),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_tipo", ["tipo"])
- .index("by_ativo", ["ativo"])
- .index("by_prioridade", ["prioridade"]),
-
- securityEvents: defineTable({
- referencia: v.string(),
- timestamp: v.number(),
- tipoAtaque: ataqueCiberneticoTipo,
- severidade: severidadeSeguranca,
- status: statusEventoSeguranca,
- descricao: v.string(),
- origemIp: v.optional(v.string()),
- origemRegiao: v.optional(v.string()),
- origemAsn: v.optional(v.string()),
- destinoIp: v.optional(v.string()),
- destinoPorta: v.optional(v.number()),
- protocolo: v.optional(v.string()),
- transporte: v.optional(v.string()),
- sensorId: v.optional(v.id("networkSensors")),
- detectadoPor: v.optional(v.string()),
- mitreTechnique: v.optional(v.string()),
- geolocalizacao: v.optional(
- v.object({
- pais: v.optional(v.string()),
- regiao: v.optional(v.string()),
- cidade: v.optional(v.string()),
- latitude: v.optional(v.number()),
- longitude: v.optional(v.number()),
- })
- ),
- fingerprint: v.optional(
- v.object({
- userAgent: v.optional(v.string()),
- deviceId: v.optional(v.string()),
- ja3: v.optional(v.string()),
- tlsVersion: v.optional(v.string()),
- })
- ),
- indicadores: v.optional(
- v.array(
- v.object({
- tipo: v.string(),
- valor: v.string(),
- confianca: v.optional(v.number()),
- })
- )
- ),
- metricas: v.optional(
- v.object({
- pps: v.optional(v.number()),
- bps: v.optional(v.number()),
- rpm: v.optional(v.number()),
- errosPorSegundo: v.optional(v.number()),
- hostsAfetados: v.optional(v.number()),
- })
- ),
- correlacoes: v.optional(v.array(v.id("securityEvents"))),
- referenciasExternas: v.optional(v.array(v.string())),
- tags: v.optional(v.array(v.string())),
- criadoPor: v.optional(v.id("usuarios")),
- atualizadoEm: v.number(),
- })
- .index("by_referencia", ["referencia"])
- .index("by_timestamp", ["timestamp"])
- .index("by_tipo", ["tipoAtaque", "timestamp"])
- .index("by_severidade", ["severidade", "timestamp"])
- .index("by_status", ["status", "timestamp"]),
-
- incidentActions: defineTable({
- eventoId: v.id("securityEvents"),
- tipo: acaoIncidenteTipo,
- origem: v.union(v.literal("automatico"), v.literal("manual")),
- status: acaoIncidenteStatus,
- executadoPor: v.optional(v.id("usuarios")),
- detalhes: v.optional(v.string()),
- resultado: v.optional(v.string()),
- relacionadoA: v.optional(v.id("ipReputation")),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_evento", ["eventoId", "status"])
- .index("by_tipo", ["tipo", "status"]),
-
- reportRequests: defineTable({
- solicitanteId: v.id("usuarios"),
- filtros: v.object({
- dataInicio: v.number(),
- dataFim: v.number(),
- severidades: v.optional(v.array(severidadeSeguranca)),
- tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
- incluirIndicadores: v.optional(v.boolean()),
- incluirMetricas: v.optional(v.boolean()),
- incluirAcoes: v.optional(v.boolean()),
- }),
- status: reportStatus,
- resultadoId: v.optional(v.id("_storage")),
- observacoes: v.optional(v.string()),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- concluidoEm: v.optional(v.number()),
- erro: v.optional(v.string()),
- })
- .index("by_status", ["status"])
- .index("by_solicitante", ["solicitanteId", "status"])
- .index("by_criado_em", ["criadoEm"]),
-
- rateLimitConfig: defineTable({
- nome: v.string(),
- tipo: v.union(
- v.literal("ip"),
- v.literal("usuario"),
- v.literal("endpoint"),
- v.literal("global")
- ),
- identificador: v.optional(v.string()),
- limite: v.number(),
- janelaSegundos: v.number(),
- estrategia: v.union(
- v.literal("fixed_window"),
- v.literal("sliding_window"),
- v.literal("token_bucket")
- ),
- acaoExcedido: v.union(
- v.literal("bloquear"),
- v.literal("throttle"),
- v.literal("alertar")
- ),
- bloqueioTemporarioSegundos: v.optional(v.number()),
- ativo: v.boolean(),
- prioridade: v.number(),
- criadoPor: v.id("usuarios"),
- atualizadoPor: v.optional(v.id("usuarios")),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- notas: v.optional(v.string()),
- tags: v.optional(v.array(v.string()))
- })
- .index("by_tipo_identificador", ["tipo", "identificador"])
- .index("by_ativo", ["ativo"])
- .index("by_prioridade", ["prioridade"])
- ,
- alertConfigs: defineTable({
- nome: v.string(),
- canais: v.object({
- email: v.boolean(),
- chat: v.boolean(),
- }),
- emails: v.array(v.string()),
- chatUsers: v.array(v.string()),
- severidadeMin: severidadeSeguranca,
- tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
- reenvioMin: v.number(),
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- atualizadoEm: v.number(),
- })
- .index("by_criadoEm", ["criadoEm"]),
-
- // Sistema de Controle de Ponto
- registrosPonto: defineTable({
- funcionarioId: v.id("funcionarios"),
- tipo: v.union(
- v.literal("entrada"),
- v.literal("saida_almoco"),
- v.literal("retorno_almoco"),
- v.literal("saida")
- ),
- data: v.string(), // YYYY-MM-DD
- hora: v.number(),
- minuto: v.number(),
- segundo: v.number(),
- timestamp: v.number(), // Timestamp completo para ordenação
- imagemId: v.optional(v.id("_storage")),
- sincronizadoComServidor: v.boolean(),
- toleranciaMinutos: v.number(),
- dentroDoPrazo: v.boolean(),
-
- // Informações de Rede
- ipAddress: v.optional(v.string()),
- ipPublico: v.optional(v.string()),
- ipLocal: v.optional(v.string()),
-
- // Informações do Navegador
- userAgent: v.optional(v.string()),
- browser: v.optional(v.string()),
- browserVersion: v.optional(v.string()),
- engine: v.optional(v.string()),
-
- // Informações do Sistema
- sistemaOperacional: v.optional(v.string()),
- osVersion: v.optional(v.string()),
- arquitetura: v.optional(v.string()),
- plataforma: v.optional(v.string()),
-
- // Informações de Localização
- latitude: v.optional(v.number()),
- longitude: v.optional(v.number()),
- precisao: v.optional(v.number()),
- altitude: v.optional(v.union(v.number(), v.null())),
- altitudeAccuracy: v.optional(v.union(v.number(), v.null())),
- heading: v.optional(v.union(v.number(), v.null())),
- speed: v.optional(v.union(v.number(), v.null())),
- confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend)
- scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend)
- suspeitaSpoofing: v.optional(v.boolean()),
- motivoSuspeita: v.optional(v.string()),
- avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação
- distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS
- velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro
- distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro
- tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro
- // Informações de Geofencing
- enderecoMarcacaoEsperado: v.optional(v.id("enderecosMarcacao")), // Endereço mais próximo esperado
- distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado
- dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido
- enderecoMarcacaoUsado: v.optional(v.id("enderecosMarcacao")), // Qual endereço foi usado na validação
- raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros
- endereco: v.optional(v.string()),
- cidade: v.optional(v.string()),
- estado: v.optional(v.string()),
- pais: v.optional(v.string()),
- timezone: v.optional(v.string()),
-
- // Informações do Dispositivo
- deviceType: v.optional(v.string()),
- deviceModel: v.optional(v.string()),
- screenResolution: v.optional(v.string()),
- coresTela: v.optional(v.number()),
- idioma: v.optional(v.string()),
-
- // Informações Adicionais
- isMobile: v.optional(v.boolean()),
- isTablet: v.optional(v.boolean()),
- isDesktop: v.optional(v.boolean()),
- connectionType: v.optional(v.string()),
- memoryInfo: v.optional(v.string()),
-
- // Informações de Sensores (Acelerômetro e Giroscópio)
- acelerometroX: v.optional(v.number()),
- acelerometroY: v.optional(v.number()),
- acelerometroZ: v.optional(v.number()),
- movimentoDetectado: v.optional(v.boolean()),
- magnitudeMovimento: v.optional(v.number()),
- variacaoAcelerometro: v.optional(v.number()),
- giroscopioAlpha: v.optional(v.number()),
- giroscopioBeta: v.optional(v.number()),
- giroscopioGamma: v.optional(v.number()),
- sensorDisponivel: v.optional(v.boolean()),
- permissaoSensorNegada: v.optional(v.boolean()),
-
- // Justificativa opcional para o registro
- justificativa: v.optional(v.string()),
-
- // Campos para homologação
- editadoPorGestor: v.optional(v.boolean()),
- homologacaoId: v.optional(v.id("homologacoesPonto")),
-
- criadoEm: v.number(),
- })
- .index("by_funcionario_data", ["funcionarioId", "data"])
- .index("by_data", ["data"])
- .index("by_dentro_prazo", ["dentroDoPrazo", "data"])
- .index("by_funcionario_timestamp", ["funcionarioId", "timestamp"]),
-
- // Endereços de Marcação - Locais permitidos para registro de ponto
- enderecosMarcacao: defineTable({
- nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC"
- descricao: v.optional(v.string()), // Descrição opcional
- // Coordenadas (obrigatórias)
- latitude: v.number(),
- longitude: v.number(),
- // Endereço físico (para exibição)
- endereco: v.string(), // Ex: "Rua Exemplo, 123"
- bairro: v.optional(v.string()), // Bairro do endereço
- cep: v.optional(v.string()),
- cidade: v.string(),
- estado: v.string(),
- pais: v.optional(v.string()), // Padrão: "Brasil"
- // Configurações
- raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m)
- ativo: v.boolean(),
- // Tipos de uso
- tipo: v.union(
- v.literal("sede"), // Sede principal (para todos)
- v.literal("home_office"), // Home office específico
- v.literal("deslocamento"), // Deslocamento temporário
- v.literal("cliente") // Local de cliente
- ),
- // Metadados
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- atualizadoPor: v.optional(v.id("usuarios")),
- atualizadoEm: v.optional(v.number()),
- })
- .index("by_ativo", ["ativo"])
- .index("by_tipo", ["tipo"])
- .index("by_cidade", ["cidade"]),
-
- // Associação Funcionário ↔ Endereço de Marcação
- funcionarioEnderecosMarcacao: defineTable({
- funcionarioId: v.id("funcionarios"),
- enderecoMarcacaoId: v.id("enderecosMarcacao"),
- // Configurações específicas do funcionário
- raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão
- // Período de validade (para deslocamentos temporários)
- dataInicio: v.optional(v.string()), // YYYY-MM-DD
- dataFim: v.optional(v.string()), // YYYY-MM-DD
- // Status
- ativo: v.boolean(),
- // Metadados
- criadoPor: v.id("usuarios"),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_endereco", ["enderecoMarcacaoId"])
- .index("by_funcionario_ativo", ["funcionarioId", "ativo"])
- .index("by_endereco_ativo", ["enderecoMarcacaoId", "ativo"]),
-
- configuracaoPonto: defineTable({
- horarioEntrada: v.string(), // HH:mm
- horarioSaidaAlmoco: v.string(), // HH:mm
- horarioRetornoAlmoco: v.string(), // HH:mm
- horarioSaida: v.string(), // HH:mm
- toleranciaMinutos: v.number(),
- // Nomes personalizados dos tipos de registro
- nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1"
- nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1"
- nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2"
- nomeSaida: v.optional(v.string()), // Padrão: "Saída 2"
- // Ajuste de fuso horário (GMT offset em horas)
- gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
- // Configurações de geofencing
- validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização
- toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros
- ativo: v.boolean(),
- atualizadoPor: v.id("usuarios"),
- atualizadoEm: v.number(),
- })
- .index("by_ativo", ["ativo"]),
-
- configuracaoRelogio: defineTable({
- servidorNTP: v.optional(v.string()),
- portaNTP: v.optional(v.number()),
- usarServidorExterno: v.boolean(),
- fallbackParaPC: v.boolean(),
- ultimaSincronizacao: v.optional(v.number()),
- offsetSegundos: v.optional(v.number()),
- // Ajuste de fuso horário (GMT offset em horas)
- gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
- atualizadoPor: v.id("usuarios"),
- atualizadoEm: v.number(),
- })
- .index("by_ativo", ["usarServidorExterno"]),
-
- // Banco de Horas - Saldo diário de horas trabalhadas
- bancoHoras: defineTable({
- funcionarioId: v.id("funcionarios"),
- data: v.string(), // YYYY-MM-DD
- cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos)
- horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos)
- saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit)
- registrosPontoIds: v.array(v.id("registrosPonto")), // IDs dos registros do dia
- calculadoEm: v.number(),
- })
- .index("by_funcionario_data", ["funcionarioId", "data"])
- .index("by_funcionario", ["funcionarioId"])
- .index("by_data", ["data"]),
-
- // Homologações de Ponto - Edições e ajustes realizados pelo gestor
- homologacoesPonto: defineTable({
- registroId: v.optional(v.id("registrosPonto")), // ID do registro editado (se for edição)
- funcionarioId: v.id("funcionarios"),
- gestorId: v.id("usuarios"),
- // Dados do registro original (se for edição)
- horaAnterior: v.optional(v.number()),
- minutoAnterior: v.optional(v.number()),
- // Dados do registro novo (se for edição)
- horaNova: v.optional(v.number()),
- minutoNova: v.optional(v.number()),
- // Motivo e observações
- motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações)
- motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc)
- motivoDescricao: v.optional(v.string()), // Descrição do motivo
- observacoes: v.optional(v.string()),
- // Tipo de ajuste (se for ajuste de banco de horas)
- tipoAjuste: v.optional(v.union(
- v.literal("compensar"),
- v.literal("abonar"),
- v.literal("descontar")
- )),
- // Período do ajuste (se for ajuste de banco de horas)
- periodoDias: v.optional(v.number()),
- periodoHoras: v.optional(v.number()),
- periodoMinutos: v.optional(v.number()),
- // Ajuste em minutos (calculado)
- ajusteMinutos: v.optional(v.number()),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_gestor", ["gestorId"])
- .index("by_registro", ["registroId"])
- .index("by_data", ["criadoEm"]),
-
- // Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto
- dispensasRegistro: defineTable({
- funcionarioId: v.id("funcionarios"),
- gestorId: v.id("usuarios"),
- dataInicio: v.string(), // YYYY-MM-DD
- horaInicio: v.number(),
- minutoInicio: v.number(),
- dataFim: v.string(), // YYYY-MM-DD
- horaFim: v.number(),
- minutoFim: v.number(),
- motivo: v.string(),
- isento: v.boolean(), // Se true, não expira (casos excepcionais)
- ativo: v.boolean(),
- criadoEm: v.number(),
- })
- .index("by_funcionario", ["funcionarioId"])
- .index("by_gestor", ["gestorId"])
- .index("by_ativo", ["ativo"])
- .index("by_data_inicio", ["dataInicio"])
- .index("by_data_fim", ["dataFim"]),
+ // Setores da organização
+ setores: defineTable({
+ nome: v.string(),
+ sigla: v.string(),
+ criadoPor: v.id('usuarios'),
+ createdAt: v.number()
+ })
+ .index('by_nome', ['nome'])
+ .index('by_sigla', ['sigla']),
+
+ // Relação muitos-para-muitos entre funcionários e setores
+ funcionarioSetores: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ setorId: v.id('setores'),
+ createdAt: v.number()
+ })
+ .index('by_funcionarioId', ['funcionarioId'])
+ .index('by_setorId', ['setorId'])
+ .index('by_funcionarioId_and_setorId', ['funcionarioId', 'setorId']),
+
+ // Templates de fluxo
+ flowTemplates: defineTable({
+ name: v.string(),
+ description: v.optional(v.string()),
+ status: flowTemplateStatus,
+ createdBy: v.id('usuarios'),
+ createdAt: v.number()
+ })
+ .index('by_status', ['status'])
+ .index('by_createdBy', ['createdBy']),
+
+ // Passos de template de fluxo
+ flowSteps: defineTable({
+ flowTemplateId: v.id('flowTemplates'),
+ name: v.string(),
+ description: v.optional(v.string()),
+ position: v.number(),
+ expectedDuration: v.number(), // em dias
+ setorId: v.id('setores'),
+ defaultAssigneeId: v.optional(v.id('usuarios')),
+ requiredDocuments: v.optional(v.array(v.string()))
+ })
+ .index('by_flowTemplateId', ['flowTemplateId'])
+ .index('by_flowTemplateId_and_position', ['flowTemplateId', 'position']),
+
+ // Instâncias de fluxo
+ flowInstances: defineTable({
+ flowTemplateId: v.id('flowTemplates'),
+ contratoId: v.optional(v.id('contratos')),
+ managerId: v.id('usuarios'),
+ status: flowInstanceStatus,
+ startedAt: v.number(),
+ finishedAt: v.optional(v.number()),
+ currentStepId: v.optional(v.id('flowInstanceSteps'))
+ })
+ .index('by_flowTemplateId', ['flowTemplateId'])
+ .index('by_contratoId', ['contratoId'])
+ .index('by_managerId', ['managerId'])
+ .index('by_status', ['status']),
+
+ // Passos de instância de fluxo
+ flowInstanceSteps: defineTable({
+ flowInstanceId: v.id('flowInstances'),
+ flowStepId: v.id('flowSteps'),
+ setorId: v.id('setores'),
+ assignedToId: v.optional(v.id('usuarios')),
+ status: flowInstanceStepStatus,
+ startedAt: v.optional(v.number()),
+ finishedAt: v.optional(v.number()),
+ notes: v.optional(v.string()),
+ notesUpdatedBy: v.optional(v.id('usuarios')),
+ notesUpdatedAt: v.optional(v.number()),
+ dueDate: v.optional(v.number())
+ })
+ .index('by_flowInstanceId', ['flowInstanceId'])
+ .index('by_flowInstanceId_and_status', ['flowInstanceId', 'status'])
+ .index('by_setorId', ['setorId'])
+ .index('by_assignedToId', ['assignedToId']),
+
+ // Documentos de instância de fluxo
+ flowInstanceDocuments: defineTable({
+ flowInstanceStepId: v.id('flowInstanceSteps'),
+ uploadedById: v.id('usuarios'),
+ storageId: v.id('_storage'),
+ name: v.string(),
+ uploadedAt: v.number()
+ })
+ .index('by_flowInstanceStepId', ['flowInstanceStepId'])
+ .index('by_uploadedById', ['uploadedById']),
+
+ // Sub-etapas de fluxo (para templates e instâncias)
+ flowSubSteps: defineTable({
+ flowStepId: v.optional(v.id('flowSteps')), // Para templates
+ flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), // Para instâncias
+ name: v.string(),
+ description: v.optional(v.string()),
+ status: v.union(
+ v.literal('pending'),
+ v.literal('in_progress'),
+ v.literal('completed'),
+ v.literal('blocked')
+ ),
+ position: v.number(),
+ createdBy: v.id('usuarios'),
+ createdAt: v.number()
+ })
+ .index('by_flowStepId', ['flowStepId'])
+ .index('by_flowInstanceStepId', ['flowInstanceStepId']),
+
+ // Notas de steps e sub-etapas
+ flowStepNotes: defineTable({
+ flowStepId: v.optional(v.id('flowSteps')),
+ flowInstanceStepId: v.optional(v.id('flowInstanceSteps')),
+ flowSubStepId: v.optional(v.id('flowSubSteps')),
+ texto: v.string(),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ arquivos: v.array(v.id('_storage'))
+ })
+ .index('by_flowStepId', ['flowStepId'])
+ .index('by_flowInstanceStepId', ['flowInstanceStepId'])
+ .index('by_flowSubStepId', ['flowSubStepId']),
+
+ contratos: defineTable({
+ contratadaId: v.id('empresas'),
+ objeto: v.string(),
+ numeroNotaEmpenho: v.string(),
+ responsavelId: v.id('funcionarios'),
+ departamento: v.string(),
+ situacao: situacaoContrato,
+ numeroProcessoLicitatorio: v.string(),
+ modalidade: v.string(),
+ numeroContrato: v.string(),
+ anoContrato: v.number(),
+ dataInicioVigencia: v.string(),
+ dataFimVigencia: v.string(),
+ nomeFiscal: v.string(),
+ valorTotal: v.string(),
+ dataAditivoPrazo: v.optional(v.string()),
+ diasAvisoVencimento: v.number(),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ atualizadoEm: v.optional(v.number())
+ })
+ .index('by_responsavel', ['responsavelId'])
+ .index('by_situacao', ['situacao'])
+ .index('by_vigencia_inicio', ['dataInicioVigencia'])
+ .index('by_vigencia_fim', ['dataFimVigencia']),
+
+ todos: defineTable({
+ text: v.string(),
+ completed: v.boolean()
+ }),
+ enderecos: defineTable({
+ cep: v.string(),
+ logradouro: v.string(),
+ numero: v.string(),
+ complemento: v.optional(v.string()),
+ bairro: v.string(),
+ cidade: v.string(),
+ uf: v.string(),
+ criadoPor: v.optional(v.id('usuarios')),
+ atualizadoPor: v.optional(v.id('usuarios'))
+ }).index('by_cep', ['cep']),
+ empresas: defineTable({
+ razao_social: v.string(),
+ nome_fantasia: v.optional(v.string()),
+ cnpj: v.string(),
+ telefone: v.string(),
+ email: v.string(),
+ descricao: v.optional(v.string()),
+ enderecoId: v.optional(v.id('enderecos')),
+ criadoPor: v.optional(v.id('usuarios'))
+ })
+ .index('by_razao_social', ['razao_social'])
+ .index('by_cnpj', ['cnpj']),
+ contatosEmpresa: defineTable({
+ empresaId: v.id('empresas'),
+ nome: v.string(),
+ funcao: v.string(),
+ email: v.string(),
+ telefone: v.string(),
+ adicionadoPor: v.optional(v.id('usuarios')),
+ descricao: v.optional(v.string())
+ })
+ .index('by_empresa', ['empresaId'])
+ .index('by_email', ['email']),
+ funcionarios: defineTable({
+ // Campos obrigatórios existentes
+ nome: v.string(),
+ nascimento: v.string(),
+ rg: v.string(),
+ cpf: v.string(),
+ endereco: v.string(),
+ cep: v.string(),
+ cidade: v.string(),
+ uf: v.string(),
+ telefone: v.string(),
+ email: v.string(),
+ matricula: v.optional(v.string()),
+ admissaoData: v.optional(v.string()),
+ desligamentoData: v.optional(v.string()),
+ simboloId: v.id('simbolos'),
+ simboloTipo: simboloTipo,
+ gestorId: v.optional(v.id('usuarios')),
+ statusFerias: v.optional(v.union(v.literal('ativo'), v.literal('em_ferias'))),
+
+ // Regime de trabalho (para cálculo correto de férias)
+ regimeTrabalho: v.optional(
+ v.union(
+ v.literal('clt'), // CLT - Consolidação das Leis do Trabalho
+ v.literal('estatutario_pe'), // Servidor Público Estadual de Pernambuco
+ v.literal('estatutario_federal'), // Servidor Público Federal
+ v.literal('estatutario_municipal') // Servidor Público Municipal
+ )
+ ),
+
+ // Dados Pessoais Adicionais (opcionais)
+ nomePai: v.optional(v.string()),
+ nomeMae: v.optional(v.string()),
+ naturalidade: v.optional(v.string()),
+ naturalidadeUF: v.optional(v.string()),
+ sexo: v.optional(v.union(v.literal('masculino'), v.literal('feminino'), v.literal('outro'))),
+ estadoCivil: v.optional(
+ v.union(
+ v.literal('solteiro'),
+ v.literal('casado'),
+ v.literal('divorciado'),
+ v.literal('viuvo'),
+ v.literal('uniao_estavel')
+ )
+ ),
+ nacionalidade: v.optional(v.string()),
+
+ // Documentos Pessoais
+ rgOrgaoExpedidor: v.optional(v.string()),
+ rgDataEmissao: v.optional(v.string()),
+ carteiraProfissionalNumero: v.optional(v.string()),
+ carteiraProfissionalSerie: v.optional(v.string()),
+ carteiraProfissionalDataEmissao: v.optional(v.string()),
+ reservistaNumero: v.optional(v.string()),
+ reservistaSerie: v.optional(v.string()),
+ tituloEleitorNumero: v.optional(v.string()),
+ tituloEleitorZona: v.optional(v.string()),
+ tituloEleitorSecao: v.optional(v.string()),
+ pisNumero: v.optional(v.string()),
+
+ // Formação e Saúde
+ grauInstrucao: v.optional(
+ v.union(
+ v.literal('fundamental'),
+ v.literal('medio'),
+ v.literal('superior'),
+ v.literal('pos_graduacao'),
+ v.literal('mestrado'),
+ v.literal('doutorado')
+ )
+ ),
+ formacao: v.optional(v.string()),
+ formacaoRegistro: v.optional(v.string()),
+ grupoSanguineo: v.optional(
+ v.union(v.literal('A'), v.literal('B'), v.literal('AB'), v.literal('O'))
+ ),
+ fatorRH: v.optional(v.union(v.literal('positivo'), v.literal('negativo'))),
+
+ // Cargo e Vínculo
+ descricaoCargo: v.optional(v.string()),
+ nomeacaoPortaria: v.optional(v.string()),
+ nomeacaoData: v.optional(v.string()),
+ nomeacaoDOE: v.optional(v.string()),
+ pertenceOrgaoPublico: v.optional(v.boolean()),
+ orgaoOrigem: v.optional(v.string()),
+ aposentado: v.optional(v.union(v.literal('nao'), v.literal('funape_ipsep'), v.literal('inss'))),
+
+ // Dados Bancários
+ contaBradescoNumero: v.optional(v.string()),
+ contaBradescoDV: v.optional(v.string()),
+ contaBradescoAgencia: v.optional(v.string()),
+
+ // Documentos Anexos (Storage IDs)
+ certidaoAntecedentesPF: v.optional(v.id('_storage')),
+ certidaoAntecedentesJFPE: v.optional(v.id('_storage')),
+ certidaoAntecedentesSDS: v.optional(v.id('_storage')),
+ certidaoAntecedentesTJPE: v.optional(v.id('_storage')),
+ certidaoImprobidade: v.optional(v.id('_storage')),
+ rgFrente: v.optional(v.id('_storage')),
+ rgVerso: v.optional(v.id('_storage')),
+ cpfFrente: v.optional(v.id('_storage')),
+ cpfVerso: v.optional(v.id('_storage')),
+ situacaoCadastralCPF: v.optional(v.id('_storage')),
+ tituloEleitorFrente: v.optional(v.id('_storage')),
+ tituloEleitorVerso: v.optional(v.id('_storage')),
+ comprovanteVotacao: v.optional(v.id('_storage')),
+ carteiraProfissionalFrente: v.optional(v.id('_storage')),
+ carteiraProfissionalVerso: v.optional(v.id('_storage')),
+ comprovantePIS: v.optional(v.id('_storage')),
+ certidaoRegistroCivil: v.optional(v.id('_storage')),
+ certidaoNascimentoDependentes: v.optional(v.id('_storage')),
+ cpfDependentes: v.optional(v.id('_storage')),
+ reservistaDoc: v.optional(v.id('_storage')),
+ comprovanteEscolaridade: v.optional(v.id('_storage')),
+ comprovanteResidencia: v.optional(v.id('_storage')),
+ comprovanteContaBradesco: v.optional(v.id('_storage')),
+
+ // Dependentes do funcionário (uploads opcionais)
+ dependentes: v.optional(
+ v.array(
+ v.object({
+ parentesco: v.union(
+ v.literal('filho'),
+ v.literal('filha'),
+ v.literal('conjuge'),
+ v.literal('outro')
+ ),
+ nome: v.string(),
+ cpf: v.string(),
+ nascimento: v.string(),
+ documentoId: v.optional(v.id('_storage')),
+ // Benefícios/declarações por dependente
+ salarioFamilia: v.optional(v.boolean()),
+ impostoRenda: v.optional(v.boolean())
+ })
+ )
+ ),
+
+ // Declarações (Storage IDs)
+ declaracaoAcumulacaoCargo: v.optional(v.id('_storage')),
+ declaracaoDependentesIR: v.optional(v.id('_storage')),
+ declaracaoIdoneidade: v.optional(v.id('_storage')),
+ termoNepotismo: v.optional(v.id('_storage')),
+ termoOpcaoRemuneracao: v.optional(v.id('_storage'))
+ })
+ .index('by_matricula', ['matricula'])
+ .index('by_nome', ['nome'])
+ .index('by_simboloId', ['simboloId'])
+ .index('by_simboloTipo', ['simboloTipo'])
+ .index('by_cpf', ['cpf'])
+ .index('by_rg', ['rg'])
+ .index('by_gestor', ['gestorId']),
+
+ atestados: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ tipo: v.union(v.literal('atestado_medico'), v.literal('declaracao_comparecimento')),
+ dataInicio: v.string(),
+ dataFim: v.string(),
+ cid: v.optional(v.string()), // Apenas para atestado médico
+ observacoes: v.optional(v.string()),
+ documentoId: v.optional(v.id('_storage')),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_tipo', ['tipo'])
+ .index('by_data_inicio', ['dataInicio'])
+ .index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']),
+
+ licencas: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ tipo: v.union(v.literal('maternidade'), v.literal('paternidade')),
+ dataInicio: v.string(),
+ dataFim: v.string(),
+ documentoId: v.optional(v.id('_storage')),
+ observacoes: v.optional(v.string()),
+ licencaOriginalId: v.optional(v.id('licencas')), // Para prorrogações
+ ehProrrogacao: v.boolean(),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_tipo', ['tipo'])
+ .index('by_data_inicio', ['dataInicio'])
+ .index('by_licenca_original', ['licencaOriginalId'])
+ .index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']),
+
+ ferias: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ anoReferencia: v.number(),
+ dataInicio: v.string(),
+ dataFim: v.string(),
+ diasFerias: v.number(),
+ status: v.union(
+ v.literal('aguardando_aprovacao'),
+ v.literal('aprovado'),
+ v.literal('reprovado'),
+ v.literal('data_ajustada_aprovada'),
+ v.literal('EmFérias'),
+ v.literal('Cancelado_RH')
+ ),
+ gestorId: v.optional(v.id('usuarios')),
+ observacao: v.optional(v.string()),
+ motivoReprovacao: v.optional(v.string()),
+ dataAprovacao: v.optional(v.number()),
+ dataReprovacao: v.optional(v.number()),
+ diasAbono: v.number(),
+ historicoAlteracoes: v.optional(
+ v.array(
+ v.object({
+ data: v.number(),
+ usuarioId: v.id('usuarios'),
+ acao: v.string()
+ })
+ )
+ )
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_funcionario_and_ano', ['funcionarioId', 'anoReferencia'])
+ .index('by_funcionario_and_status', ['funcionarioId', 'status'])
+ .index('by_status', ['status'])
+ .index('by_ano', ['anoReferencia']),
+
+ notificacoesFerias: defineTable({
+ destinatarioId: v.id('usuarios'),
+ feriasId: v.id('ferias'),
+ tipo: v.union(
+ v.literal('nova_solicitacao'),
+ v.literal('aprovado'),
+ v.literal('reprovado'),
+ v.literal('data_ajustada')
+ ),
+ lida: v.boolean(),
+ mensagem: v.string()
+ })
+ .index('by_destinatario', ['destinatarioId'])
+ .index('by_destinatario_and_lida', ['destinatarioId', 'lida']),
+
+ // Solicitações de Ausências
+ solicitacoesAusencias: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ dataInicio: v.string(),
+ dataFim: v.string(),
+ motivo: v.string(),
+ status: v.union(
+ v.literal('aguardando_aprovacao'),
+ v.literal('aprovado'),
+ v.literal('reprovado')
+ ),
+ gestorId: v.optional(v.id('usuarios')),
+ dataAprovacao: v.optional(v.number()),
+ dataReprovacao: v.optional(v.number()),
+ motivoReprovacao: v.optional(v.string()),
+ observacao: v.optional(v.string()),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_status', ['status'])
+ .index('by_funcionario_and_status', ['funcionarioId', 'status']),
+
+ notificacoesAusencias: defineTable({
+ destinatarioId: v.id('usuarios'),
+ solicitacaoAusenciaId: v.id('solicitacoesAusencias'),
+ tipo: v.union(v.literal('nova_solicitacao'), v.literal('aprovado'), v.literal('reprovado')),
+ lida: v.boolean(),
+ mensagem: v.string()
+ })
+ .index('by_destinatario', ['destinatarioId'])
+ .index('by_destinatario_and_lida', ['destinatarioId', 'lida']),
+
+ times: defineTable({
+ nome: v.string(),
+ descricao: v.optional(v.string()),
+ gestorId: v.id('usuarios'),
+ gestorSuperiorId: v.optional(v.id('usuarios')),
+ ativo: v.boolean(),
+ cor: v.optional(v.string()) // Cor para identificação visual
+ })
+ .index('by_gestor', ['gestorId'])
+ .index('by_gestor_superior', ['gestorSuperiorId']),
+
+ timesMembros: defineTable({
+ timeId: v.id('times'),
+ funcionarioId: v.id('funcionarios'),
+ dataEntrada: v.number(),
+ dataSaida: v.optional(v.number()),
+ ativo: v.boolean()
+ })
+ .index('by_time', ['timeId'])
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_time_and_ativo', ['timeId', 'ativo']),
+
+ cursos: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ descricao: v.string(),
+ data: v.string(),
+ certificadoId: v.optional(v.id('_storage'))
+ }).index('by_funcionario', ['funcionarioId']),
+
+ simbolos: defineTable({
+ nome: v.string(),
+ tipo: simboloTipo,
+ descricao: v.string(),
+ vencValor: v.string(),
+ repValor: v.string(),
+ valor: v.string()
+ }),
+
+ // Sistema de Autenticação e Controle de Acesso
+ usuarios: defineTable({
+ authId: v.string(),
+ nome: v.string(),
+ email: v.string(),
+ funcionarioId: v.optional(v.id('funcionarios')),
+ roleId: v.id('roles'),
+ ativo: v.boolean(),
+ primeiroAcesso: v.boolean(),
+ ultimoAcesso: v.optional(v.number()),
+ criadoEm: v.number(),
+ atualizadoEm: v.number(),
+
+ // Controle de Bloqueio e Segurança
+ bloqueado: v.optional(v.boolean()),
+ motivoBloqueio: v.optional(v.string()),
+ dataBloqueio: v.optional(v.number()),
+ tentativasLogin: v.optional(v.number()), // contador de tentativas falhas
+ ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa
+
+ // Campos de Chat e Perfil
+
+ fotoPerfil: v.optional(v.id('_storage')),
+ avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
+ setor: v.optional(v.string()),
+ statusMensagem: v.optional(v.string()), // max 100 chars
+ statusPresenca: v.optional(
+ v.union(
+ v.literal('online'),
+ v.literal('offline'),
+ v.literal('ausente'),
+ v.literal('externo'),
+ v.literal('em_reuniao')
+ )
+ ),
+ ultimaAtividade: v.optional(v.number()), // timestamp
+ notificacoesAtivadas: v.optional(v.boolean()),
+ somNotificacao: v.optional(v.boolean()),
+ temaPreferido: v.optional(v.string()) // tema de aparência escolhido pelo usuário
+ })
+ .index('by_email', ['email'])
+ .index('by_role', ['roleId'])
+ .index('by_ativo', ['ativo'])
+ .index('by_status_presenca', ['statusPresenca'])
+ .index('by_bloqueado', ['bloqueado'])
+ .index('by_funcionarioId', ['funcionarioId'])
+ .index('authId', ['authId']),
+
+ roles: defineTable({
+ nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
+ descricao: v.string(),
+ nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado
+ setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc.
+ customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER
+ criadoPor: v.optional(v.id('usuarios')), // usuário TI_MASTER que criou este perfil
+ editavel: v.optional(v.boolean()) // se pode ser editado (false para roles fixas)
+ })
+ .index('by_nome', ['nome'])
+ .index('by_nivel', ['nivel'])
+ .index('by_setor', ['setor'])
+ .index('by_customizado', ['customizado']),
+
+ permissoes: defineTable({
+ nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc.
+ descricao: v.string(),
+ recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc.
+ acao: v.string() // "criar", "ler", "editar", "excluir"
+ })
+ .index('by_recurso', ['recurso'])
+ .index('by_recurso_e_acao', ['recurso', 'acao'])
+ .index('by_nome', ['nome']),
+
+ rolePermissoes: defineTable({
+ roleId: v.id('roles'),
+ permissaoId: v.id('permissoes')
+ })
+ .index('by_role', ['roleId'])
+ .index('by_permissao', ['permissaoId']),
+
+ sessoes: defineTable({
+ usuarioId: v.id('usuarios'),
+ token: v.string(),
+ ipAddress: v.optional(v.string()),
+ userAgent: v.optional(v.string()),
+ criadoEm: v.number(),
+ expiraEm: v.number(),
+ ativo: v.boolean()
+ })
+ .index('by_usuario', ['usuarioId'])
+ .index('by_token', ['token'])
+ .index('by_ativo', ['ativo'])
+ .index('by_expiracao', ['expiraEm']),
+
+ logsAcesso: defineTable({
+ usuarioId: v.id('usuarios'),
+ tipo: v.union(
+ v.literal('login'),
+ v.literal('logout'),
+ v.literal('acesso_negado'),
+ v.literal('senha_alterada'),
+ v.literal('sessao_expirada')
+ ),
+ ipAddress: v.optional(v.string()),
+ userAgent: v.optional(v.string()),
+ detalhes: v.optional(v.string()),
+ timestamp: v.number()
+ })
+ .index('by_usuario', ['usuarioId'])
+ .index('by_tipo', ['tipo'])
+ .index('by_timestamp', ['timestamp']),
+
+ // Logs de Login Detalhados
+ logsLogin: defineTable({
+ usuarioId: v.optional(v.id('usuarios')), // pode ser null se falha antes de identificar usuário
+ matriculaOuEmail: v.string(), // tentativa de login
+ sucesso: v.boolean(),
+ motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente"
+ ipAddress: v.optional(v.string()),
+ userAgent: v.optional(v.string()),
+ device: v.optional(v.string()),
+ browser: v.optional(v.string()),
+ sistema: v.optional(v.string()),
+ timestamp: v.number()
+ })
+ .index('by_usuario', ['usuarioId'])
+ .index('by_sucesso', ['sucesso'])
+ .index('by_timestamp', ['timestamp'])
+ .index('by_ip', ['ipAddress']),
+
+ // Logs de Atividades
+ logsAtividades: defineTable({
+ usuarioId: v.id('usuarios'),
+ acao: v.string(), // "criar", "editar", "excluir", "bloquear", "desbloquear", etc.
+ recurso: v.string(), // "funcionarios", "simbolos", "usuarios", "perfis", etc.
+ recursoId: v.optional(v.string()), // ID do recurso afetado
+ detalhes: v.optional(v.string()), // JSON com detalhes da ação
+ timestamp: v.number()
+ })
+ .index('by_usuario', ['usuarioId'])
+ .index('by_acao', ['acao'])
+ .index('by_recurso', ['recurso'])
+ .index('by_timestamp', ['timestamp'])
+ .index('by_recurso_id', ['recurso', 'recursoId']),
+
+ // Histórico de Bloqueios
+ bloqueiosUsuarios: defineTable({
+ usuarioId: v.id('usuarios'),
+ motivo: v.string(),
+ bloqueadoPor: v.id('usuarios'), // ID do TI_MASTER que bloqueou
+ dataInicio: v.number(),
+ dataFim: v.optional(v.number()), // quando foi desbloqueado
+ desbloqueadoPor: v.optional(v.id('usuarios')),
+ ativo: v.boolean() // se é o bloqueio atual ativo
+ })
+ .index('by_usuario', ['usuarioId'])
+ .index('by_bloqueado_por', ['bloqueadoPor'])
+ .index('by_ativo', ['ativo'])
+ .index('by_data_inicio', ['dataInicio']),
+
+ // Perfis Customizados
+
+ // Templates de Mensagens
+ templatesMensagens: defineTable({
+ codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc.
+ nome: v.string(),
+ tipo: v.union(
+ v.literal('sistema'), // predefinido, não editável
+ v.literal('customizado') // criado por TI_MASTER
+ ),
+ titulo: v.string(),
+ corpo: v.string(), // pode ter variáveis {{variavel}}
+ variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.]
+ criadoPor: v.optional(v.id('usuarios')),
+ criadoEm: v.number()
+ })
+ .index('by_codigo', ['codigo'])
+ .index('by_tipo', ['tipo'])
+ .index('by_criado_por', ['criadoPor']),
+
+ // Configuração de Email/SMTP
+ configuracaoEmail: defineTable({
+ servidor: v.string(), // smtp.gmail.com
+ porta: v.number(), // 587, 465, etc.
+ usuario: v.string(),
+ senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP
+ emailRemetente: v.string(),
+ nomeRemetente: v.string(),
+ usarSSL: v.boolean(),
+ usarTLS: v.boolean(),
+ ativo: v.boolean(),
+ testadoEm: v.optional(v.number()),
+ configuradoPor: v.id('usuarios'),
+ atualizadoEm: v.number()
+ }).index('by_ativo', ['ativo']),
+
+ // Configuração de Jitsi Meet
+ configuracaoJitsi: defineTable({
+ domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com")
+ appId: v.string(), // ID da aplicação Jitsi
+ roomPrefix: v.string(), // Prefixo para nomes de salas
+ useHttps: v.boolean(), // Usar HTTPS
+ acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento)
+ // Configurações SSH/Docker para configuração automática do servidor
+ sshHost: v.optional(v.string()), // Host SSH para acesso ao servidor Docker (ex: "192.168.1.100" ou "servidor.local")
+ sshPort: v.optional(v.number()), // Porta SSH (padrão: 22)
+ sshUsername: v.optional(v.string()), // Usuário SSH
+ sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada)
+ sshKeyPath: v.optional(v.string()), // Caminho para chave SSH (alternativa à senha)
+ dockerComposePath: v.optional(v.string()), // Caminho do docker-compose.yml (ex: "/home/user/jitsi-docker")
+ jitsiConfigPath: v.optional(v.string()), // Caminho base das configurações Jitsi (ex: "~/.jitsi-meet-cfg")
+ 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
+ }).index('by_ativo', ['ativo']),
+
+ // Fila de Emails
+ notificacoesEmail: defineTable({
+ destinatario: v.string(), // email
+ destinatarioId: v.optional(v.id('usuarios')),
+ assunto: v.string(),
+ corpo: v.string(), // HTML ou texto
+ templateId: v.optional(v.id('templatesMensagens')),
+ status: v.union(
+ v.literal('pendente'),
+ v.literal('enviando'),
+ v.literal('enviado'),
+ v.literal('falha')
+ ),
+ tentativas: v.number(),
+ ultimaTentativa: v.optional(v.number()),
+ erroDetalhes: v.optional(v.string()),
+ enviadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ enviadoEm: v.optional(v.number()),
+ agendadaPara: v.optional(v.number()) // timestamp para agendamento
+ })
+ .index('by_status', ['status'])
+ .index('by_destinatario', ['destinatarioId'])
+ .index('by_enviado_por', ['enviadoPor'])
+ .index('by_criado_em', ['criadoEm'])
+ .index('by_agendamento', ['agendadaPara']),
+
+ configuracaoAcesso: defineTable({
+ chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc.
+ valor: v.string(),
+ descricao: v.string()
+ }).index('by_chave', ['chave']),
+
+ // Rate Limiting de Emails
+ rateLimitEmails: defineTable({
+ remetenteId: v.id('usuarios'),
+ timestamp: v.number(),
+ contador: v.number(), // quantidade de emails enviados neste período
+ periodo: v.union(
+ v.literal('minuto'), // último minuto
+ v.literal('hora') // última hora
+ )
+ })
+ .index('by_remetente_periodo', ['remetenteId', 'periodo', 'timestamp'])
+ .index('by_timestamp', ['timestamp']),
+
+ // Sistema de Chat
+ conversas: defineTable({
+ tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')),
+ nome: v.optional(v.string()), // nome do grupo/sala
+
+ participantes: v.array(v.id('usuarios')), // IDs dos participantes
+ administradores: v.optional(v.array(v.id('usuarios'))), // IDs dos administradores (apenas para sala_reuniao)
+ ultimaMensagem: v.optional(v.string()),
+ ultimaMensagemTimestamp: v.optional(v.number()),
+ ultimaMensagemRemetenteId: v.optional(v.id('usuarios')), // ID do remetente da última mensagem
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .index('by_criado_por', ['criadoPor'])
+ .index('by_tipo', ['tipo'])
+ .index('by_ultima_mensagem', ['ultimaMensagemTimestamp']),
+
+ mensagens: defineTable({
+ conversaId: v.id('conversas'),
+ remetenteId: v.id('usuarios'),
+ tipo: v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem')),
+ conteudo: v.string(), // texto ou nome do arquivo
+ conteudoBusca: v.optional(v.string()), // versão normalizada para busca
+ arquivoId: v.optional(v.id('_storage')),
+ arquivoNome: v.optional(v.string()),
+ arquivoTamanho: v.optional(v.number()),
+ arquivoTipo: v.optional(v.string()),
+ linkPreview: v.optional(
+ v.object({
+ url: v.string(),
+ titulo: v.optional(v.string()),
+ descricao: v.optional(v.string()),
+ imagem: v.optional(v.string()),
+ site: v.optional(v.string())
+ })
+ ),
+ reagiuPor: v.optional(
+ v.array(
+ v.object({
+ usuarioId: v.id('usuarios'),
+ emoji: v.string()
+ })
+ )
+ ),
+ mencoes: v.optional(v.array(v.id('usuarios'))),
+ respostaPara: v.optional(v.id('mensagens')), // ID da mensagem que está respondendo
+ agendadaPara: v.optional(v.number()), // timestamp
+ enviadaEm: v.number(),
+ editadaEm: v.optional(v.number()),
+ deletada: v.optional(v.boolean()),
+ lidaPor: v.optional(v.array(v.id('usuarios'))) // IDs dos usuários que leram a mensagem
+ })
+ .index('by_conversa', ['conversaId', 'enviadaEm'])
+ .index('by_remetente', ['remetenteId'])
+ .index('by_agendamento', ['agendadaPara'])
+ .index('by_resposta', ['respostaPara']),
+
+ leituras: defineTable({
+ conversaId: v.id('conversas'),
+ usuarioId: v.id('usuarios'),
+ ultimaMensagemLida: v.id('mensagens'),
+ lidaEm: v.number()
+ })
+ .index('by_conversa_usuario', ['conversaId', 'usuarioId'])
+ .index('by_usuario', ['usuarioId']),
+
+ // Sistema de Chamadas de Áudio/Vídeo
+ chamadas: defineTable({
+ conversaId: v.id('conversas'),
+ tipo: v.union(v.literal('audio'), v.literal('video')),
+ roomName: v.string(), // Nome único da sala Jitsi
+ criadoPor: v.id('usuarios'), // Anfitrião/criador
+ participantes: v.array(v.id('usuarios')),
+ status: v.union(
+ v.literal('aguardando'),
+ v.literal('em_andamento'),
+ v.literal('finalizada'),
+ v.literal('cancelada')
+ ),
+ iniciadaEm: v.optional(v.number()),
+ finalizadaEm: v.optional(v.number()),
+ duracaoSegundos: v.optional(v.number()),
+ gravando: v.boolean(),
+ gravacaoIniciadaPor: v.optional(v.id('usuarios')),
+ gravacaoIniciadaEm: v.optional(v.number()),
+ gravacaoFinalizadaEm: v.optional(v.number()),
+ configuracoes: v.optional(
+ v.object({
+ audioHabilitado: v.boolean(),
+ videoHabilitado: v.boolean(),
+ participantesConfig: v.optional(
+ v.array(
+ v.object({
+ usuarioId: v.id('usuarios'),
+ audioHabilitado: v.boolean(),
+ videoHabilitado: v.boolean(),
+ forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião
+ })
+ )
+ )
+ })
+ ),
+ criadoEm: v.number()
+ })
+ .index('by_conversa', ['conversaId', 'status'])
+ .index('by_criado_por', ['criadoPor'])
+ .index('by_status', ['status'])
+ .index('by_room_name', ['roomName']),
+
+ notificacoes: defineTable({
+ usuarioId: v.id('usuarios'),
+ tipo: v.union(
+ v.literal('nova_mensagem'),
+ v.literal('mencao'),
+ v.literal('grupo_criado'),
+ v.literal('adicionado_grupo'),
+ v.literal('alerta_seguranca'),
+ v.literal('etapa_fluxo_concluida')
+ ),
+ conversaId: v.optional(v.id('conversas')),
+ mensagemId: v.optional(v.id('mensagens')),
+ remetenteId: v.optional(v.id('usuarios')),
+ titulo: v.string(),
+ descricao: v.string(),
+ lida: v.boolean(),
+ criadaEm: v.number()
+ })
+ .index('by_usuario', ['usuarioId', 'lida', 'criadaEm'])
+ .index('by_usuario_lida', ['usuarioId', 'lida']),
+
+ digitando: defineTable({
+ conversaId: v.id('conversas'),
+ usuarioId: v.id('usuarios'),
+ iniciouEm: v.number()
+ })
+ .index('by_conversa', ['conversaId', 'iniciouEm'])
+ .index('by_usuario', ['usuarioId']),
+
+ // Push Notifications
+ pushSubscriptions: defineTable({
+ usuarioId: v.id('usuarios'),
+ endpoint: v.string(), // URL do serviço de push
+ keys: v.object({
+ p256dh: v.string(), // Chave pública
+ auth: v.string() // Chave de autenticação
+ }),
+ userAgent: v.optional(v.string()),
+ criadoEm: v.number(),
+ ultimaAtividade: v.number(),
+ ativo: v.boolean()
+ })
+ .index('by_usuario', ['usuarioId', 'ativo'])
+ .index('by_endpoint', ['endpoint']),
+
+ // Preferências de Notificação por Conversa
+ preferenciasNotificacaoConversa: defineTable({
+ usuarioId: v.id('usuarios'),
+ conversaId: v.id('conversas'),
+ pushAtivado: v.boolean(), // Receber push notifications
+ emailAtivado: v.boolean(), // Receber emails quando offline
+ somAtivado: v.boolean(), // Tocar som
+ silenciado: v.boolean(), // Silenciar completamente
+ apenasMencoes: v.boolean(), // Notificar apenas quando mencionado
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_usuario_conversa', ['usuarioId', 'conversaId'])
+ .index('by_conversa', ['conversaId']),
+
+ // Tabelas de Monitoramento do Sistema
+ systemMetrics: defineTable({
+ timestamp: v.number(),
+ // Métricas de Sistema
+ cpuUsage: v.optional(v.number()),
+ memoryUsage: v.optional(v.number()),
+ networkLatency: v.optional(v.number()),
+ storageUsed: v.optional(v.number()),
+ // Métricas de Aplicação
+ usuariosOnline: v.optional(v.number()),
+ mensagensPorMinuto: v.optional(v.number()),
+ tempoRespostaMedio: v.optional(v.number()),
+ errosCount: v.optional(v.number())
+ }).index('by_timestamp', ['timestamp']),
+
+ alertConfigurations: defineTable({
+ metricName: v.string(),
+ threshold: v.number(),
+ operator: v.union(
+ v.literal('>'),
+ v.literal('<'),
+ v.literal('>='),
+ v.literal('<='),
+ v.literal('==')
+ ),
+ enabled: v.boolean(),
+ notifyByEmail: v.boolean(),
+ notifyByChat: v.boolean(),
+ createdBy: v.id('usuarios'),
+ lastModified: v.number()
+ }).index('by_enabled', ['enabled']),
+
+ alertHistory: defineTable({
+ configId: v.id('alertConfigurations'),
+ metricName: v.string(),
+ metricValue: v.number(),
+ threshold: v.number(),
+ timestamp: v.number(),
+ status: v.union(v.literal('triggered'), v.literal('resolved')),
+ notificationsSent: v.object({
+ email: v.boolean(),
+ chat: v.boolean()
+ })
+ })
+ .index('by_timestamp', ['timestamp'])
+ .index('by_status', ['status'])
+ .index('by_config', ['configId', 'timestamp']),
+
+ tickets: defineTable({
+ numero: v.string(),
+ titulo: v.string(),
+ descricao: v.string(),
+ tipo: v.union(
+ v.literal('reclamacao'),
+ v.literal('elogio'),
+ v.literal('sugestao'),
+ v.literal('chamado')
+ ),
+ categoria: v.optional(v.string()),
+ status: v.union(
+ v.literal('aberto'),
+ v.literal('em_andamento'),
+ v.literal('aguardando_usuario'),
+ v.literal('resolvido'),
+ v.literal('encerrado'),
+ v.literal('cancelado')
+ ),
+ prioridade: v.union(
+ v.literal('baixa'),
+ v.literal('media'),
+ v.literal('alta'),
+ v.literal('critica')
+ ),
+ solicitanteId: v.id('usuarios'),
+ solicitanteNome: v.string(),
+ solicitanteEmail: v.string(),
+ responsavelId: v.optional(v.id('usuarios')),
+ setorResponsavel: v.optional(v.string()),
+ slaConfigId: v.optional(v.id('slaConfigs')),
+ conversaId: v.optional(v.id('conversas')),
+ prazoResposta: v.optional(v.number()),
+ prazoConclusao: v.optional(v.number()),
+ prazoEncerramento: v.optional(v.number()),
+ timeline: v.optional(
+ v.array(
+ v.object({
+ etapa: v.string(),
+ status: v.union(
+ v.literal('pendente'),
+ v.literal('em_andamento'),
+ v.literal('concluido'),
+ v.literal('vencido')
+ ),
+ prazo: v.optional(v.number()),
+ concluidoEm: v.optional(v.number()),
+ observacao: v.optional(v.string())
+ })
+ )
+ ),
+ alertasEmitidos: v.optional(
+ v.array(
+ v.object({
+ tipo: v.union(v.literal('resposta'), v.literal('conclusao'), v.literal('encerramento')),
+ emitidoEm: v.number()
+ })
+ )
+ ),
+ anexos: v.optional(
+ v.array(
+ v.object({
+ arquivoId: v.id('_storage'),
+ nome: v.optional(v.string()),
+ tipo: v.optional(v.string()),
+ tamanho: v.optional(v.number())
+ })
+ )
+ ),
+ tags: v.optional(v.array(v.string())),
+ canalOrigem: v.optional(v.string()),
+ ultimaInteracaoEm: v.number(),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_numero', ['numero'])
+ .index('by_status', ['status'])
+ .index('by_solicitante', ['solicitanteId', 'status'])
+ .index('by_responsavel', ['responsavelId', 'status'])
+ .index('by_setor', ['setorResponsavel', 'status']),
+
+ ticketInteractions: defineTable({
+ ticketId: v.id('tickets'),
+ autorId: v.optional(v.id('usuarios')),
+ origem: v.union(v.literal('usuario'), v.literal('ti'), v.literal('sistema')),
+ tipo: v.union(
+ v.literal('mensagem'),
+ v.literal('status'),
+ v.literal('anexo'),
+ v.literal('alerta')
+ ),
+ conteudo: v.string(),
+ anexos: v.optional(
+ v.array(
+ v.object({
+ arquivoId: v.id('_storage'),
+ nome: v.optional(v.string()),
+ tipo: v.optional(v.string()),
+ tamanho: v.optional(v.number())
+ })
+ )
+ ),
+ statusAnterior: v.optional(
+ v.union(
+ v.literal('aberto'),
+ v.literal('em_andamento'),
+ v.literal('aguardando_usuario'),
+ v.literal('resolvido'),
+ v.literal('encerrado'),
+ v.literal('cancelado')
+ )
+ ),
+ statusNovo: v.optional(
+ v.union(
+ v.literal('aberto'),
+ v.literal('em_andamento'),
+ v.literal('aguardando_usuario'),
+ v.literal('resolvido'),
+ v.literal('encerrado'),
+ v.literal('cancelado')
+ )
+ ),
+ visibilidade: v.union(v.literal('publico'), v.literal('interno')),
+ criadoEm: v.number()
+ })
+ .index('by_ticket', ['ticketId'])
+ .index('by_ticket_type', ['ticketId', 'tipo'])
+ .index('by_autor', ['autorId']),
+
+ slaConfigs: defineTable({
+ nome: v.string(),
+ descricao: v.optional(v.string()),
+ prioridade: v.optional(
+ v.union(v.literal('baixa'), v.literal('media'), v.literal('alta'), v.literal('critica'))
+ ),
+ tempoRespostaHoras: v.number(),
+ tempoConclusaoHoras: v.number(),
+ tempoEncerramentoHoras: v.optional(v.number()),
+ alertaAntecedenciaHoras: v.number(),
+ ativo: v.boolean(),
+ criadoPor: v.id('usuarios'),
+ atualizadoPor: v.optional(v.id('usuarios')),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_ativo', ['ativo'])
+ .index('by_prioridade', ['prioridade', 'ativo'])
+ .index('by_nome', ['nome']),
+
+ ticketAssignments: defineTable({
+ ticketId: v.id('tickets'),
+ responsavelId: v.id('usuarios'),
+ atribuidoPor: v.id('usuarios'),
+ motivo: v.optional(v.string()),
+ ativo: v.boolean(),
+ criadoEm: v.number(),
+ encerradoEm: v.optional(v.number())
+ })
+ .index('by_ticket', ['ticketId', 'ativo'])
+ .index('by_responsavel', ['responsavelId', 'ativo']),
+
+ // Sistema de Segurança Cibernética
+ networkSensors: defineTable({
+ nome: v.string(),
+ tipo: sensorSegurancaTipo,
+ status: sensorSegurancaStatus,
+ escopo: v.optional(v.string()),
+ ipMonitorado: v.optional(v.string()),
+ hostname: v.optional(v.string()),
+ regioes: v.optional(v.array(v.string())),
+ portasMonitoradas: v.optional(v.array(v.number())),
+ protocolos: v.optional(v.array(v.string())),
+ capacidades: v.optional(v.array(v.string())),
+ ultimaSincronizacao: v.number(),
+ ultimoHeartbeat: v.optional(v.number()),
+ latenciaMs: v.optional(v.number()),
+ errosConsecutivos: v.optional(v.number()),
+ agenteVersao: v.optional(v.string()),
+ notas: v.optional(v.string())
+ })
+ .index('by_tipo', ['tipo'])
+ .index('by_status', ['status'])
+ .index('by_hostname', ['hostname']),
+
+ ipReputation: defineTable({
+ indicador: v.string(),
+ categoria: v.union(
+ v.literal('ip'),
+ v.literal('dominio'),
+ v.literal('hash'),
+ v.literal('email')
+ ),
+ reputacao: v.number(), // -100 (malicioso) até 100 (confiável)
+ severidadeMax: severidadeSeguranca,
+ whitelist: v.boolean(),
+ blacklist: v.boolean(),
+ ocorrencias: v.number(),
+ primeiroRegistro: v.number(),
+ ultimoRegistro: v.number(),
+ bloqueadoAte: v.optional(v.number()),
+ origem: v.optional(v.string()),
+ comentarios: v.optional(v.string()),
+ classificacoes: v.optional(v.array(v.string())),
+ ultimaAcaoId: v.optional(v.id('incidentActions'))
+ })
+ .index('by_indicador', ['indicador'])
+ .index('by_reputacao', ['reputacao'])
+ .index('by_blacklist', ['blacklist'])
+ .index('by_whitelist', ['whitelist']),
+
+ portRules: defineTable({
+ porta: v.number(),
+ protocolo: v.union(
+ v.literal('tcp'),
+ v.literal('udp'),
+ v.literal('icmp'),
+ v.literal('quic'),
+ v.literal('any')
+ ),
+ acao: v.union(
+ v.literal('permitir'),
+ v.literal('bloquear'),
+ v.literal('monitorar'),
+ v.literal('rate_limit')
+ ),
+ temporario: v.boolean(),
+ severidadeMin: severidadeSeguranca,
+ duracaoSegundos: v.optional(v.number()),
+ expiraEm: v.optional(v.number()),
+ criadoPor: v.id('usuarios'),
+ atualizadoPor: v.optional(v.id('usuarios')),
+ criadoEm: v.number(),
+ atualizadoEm: v.number(),
+ notas: v.optional(v.string()),
+ tags: v.optional(v.array(v.string())),
+ listaReferencia: v.optional(v.id('ipReputation'))
+ })
+ .index('by_porta_protocolo', ['porta', 'protocolo'])
+ .index('by_acao', ['acao'])
+ .index('by_expiracao', ['expiraEm']),
+
+ threatIntelFeeds: defineTable({
+ nomeFonte: v.string(),
+ tipo: threatIntelTipo,
+ formato: threatIntelFormato,
+ url: v.optional(v.string()),
+ ativo: v.boolean(),
+ prioridade: v.union(
+ v.literal('baixa'),
+ v.literal('media'),
+ v.literal('alta'),
+ v.literal('critica')
+ ),
+ ultimaSincronizacao: v.optional(v.number()),
+ entradasProcessadas: v.optional(v.number()),
+ errosConsecutivos: v.optional(v.number()),
+ autenticacaoNecessaria: v.optional(v.boolean()),
+ configuracao: v.optional(
+ v.object({
+ tokenId: v.optional(v.id('_storage')),
+ escopo: v.optional(v.string())
+ })
+ ),
+ criadoPor: v.id('usuarios'),
+ atualizadoPor: v.optional(v.id('usuarios')),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_tipo', ['tipo'])
+ .index('by_ativo', ['ativo'])
+ .index('by_prioridade', ['prioridade']),
+
+ securityEvents: defineTable({
+ referencia: v.string(),
+ timestamp: v.number(),
+ tipoAtaque: ataqueCiberneticoTipo,
+ severidade: severidadeSeguranca,
+ status: statusEventoSeguranca,
+ descricao: v.string(),
+ origemIp: v.optional(v.string()),
+ origemRegiao: v.optional(v.string()),
+ origemAsn: v.optional(v.string()),
+ destinoIp: v.optional(v.string()),
+ destinoPorta: v.optional(v.number()),
+ protocolo: v.optional(v.string()),
+ transporte: v.optional(v.string()),
+ sensorId: v.optional(v.id('networkSensors')),
+ detectadoPor: v.optional(v.string()),
+ mitreTechnique: v.optional(v.string()),
+ geolocalizacao: v.optional(
+ v.object({
+ pais: v.optional(v.string()),
+ regiao: v.optional(v.string()),
+ cidade: v.optional(v.string()),
+ latitude: v.optional(v.number()),
+ longitude: v.optional(v.number())
+ })
+ ),
+ fingerprint: v.optional(
+ v.object({
+ userAgent: v.optional(v.string()),
+ deviceId: v.optional(v.string()),
+ ja3: v.optional(v.string()),
+ tlsVersion: v.optional(v.string())
+ })
+ ),
+ indicadores: v.optional(
+ v.array(
+ v.object({
+ tipo: v.string(),
+ valor: v.string(),
+ confianca: v.optional(v.number())
+ })
+ )
+ ),
+ metricas: v.optional(
+ v.object({
+ pps: v.optional(v.number()),
+ bps: v.optional(v.number()),
+ rpm: v.optional(v.number()),
+ errosPorSegundo: v.optional(v.number()),
+ hostsAfetados: v.optional(v.number())
+ })
+ ),
+ correlacoes: v.optional(v.array(v.id('securityEvents'))),
+ referenciasExternas: v.optional(v.array(v.string())),
+ tags: v.optional(v.array(v.string())),
+ criadoPor: v.optional(v.id('usuarios')),
+ atualizadoEm: v.number()
+ })
+ .index('by_referencia', ['referencia'])
+ .index('by_timestamp', ['timestamp'])
+ .index('by_tipo', ['tipoAtaque', 'timestamp'])
+ .index('by_severidade', ['severidade', 'timestamp'])
+ .index('by_status', ['status', 'timestamp']),
+
+ incidentActions: defineTable({
+ eventoId: v.id('securityEvents'),
+ tipo: acaoIncidenteTipo,
+ origem: v.union(v.literal('automatico'), v.literal('manual')),
+ status: acaoIncidenteStatus,
+ executadoPor: v.optional(v.id('usuarios')),
+ detalhes: v.optional(v.string()),
+ resultado: v.optional(v.string()),
+ relacionadoA: v.optional(v.id('ipReputation')),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_evento', ['eventoId', 'status'])
+ .index('by_tipo', ['tipo', 'status']),
+
+ reportRequests: defineTable({
+ solicitanteId: v.id('usuarios'),
+ filtros: v.object({
+ dataInicio: v.number(),
+ dataFim: v.number(),
+ severidades: v.optional(v.array(severidadeSeguranca)),
+ tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
+ incluirIndicadores: v.optional(v.boolean()),
+ incluirMetricas: v.optional(v.boolean()),
+ incluirAcoes: v.optional(v.boolean())
+ }),
+ status: reportStatus,
+ resultadoId: v.optional(v.id('_storage')),
+ observacoes: v.optional(v.string()),
+ criadoEm: v.number(),
+ atualizadoEm: v.number(),
+ concluidoEm: v.optional(v.number()),
+ erro: v.optional(v.string())
+ })
+ .index('by_status', ['status'])
+ .index('by_solicitante', ['solicitanteId', 'status'])
+ .index('by_criado_em', ['criadoEm']),
+
+ rateLimitConfig: defineTable({
+ nome: v.string(),
+ tipo: v.union(
+ v.literal('ip'),
+ v.literal('usuario'),
+ v.literal('endpoint'),
+ v.literal('global')
+ ),
+ identificador: v.optional(v.string()),
+ limite: v.number(),
+ janelaSegundos: v.number(),
+ estrategia: v.union(
+ v.literal('fixed_window'),
+ v.literal('sliding_window'),
+ v.literal('token_bucket')
+ ),
+ acaoExcedido: v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')),
+ bloqueioTemporarioSegundos: v.optional(v.number()),
+ ativo: v.boolean(),
+ prioridade: v.number(),
+ criadoPor: v.id('usuarios'),
+ atualizadoPor: v.optional(v.id('usuarios')),
+ criadoEm: v.number(),
+ atualizadoEm: v.number(),
+ notas: v.optional(v.string()),
+ tags: v.optional(v.array(v.string()))
+ })
+ .index('by_tipo_identificador', ['tipo', 'identificador'])
+ .index('by_ativo', ['ativo'])
+ .index('by_prioridade', ['prioridade']),
+ alertConfigs: defineTable({
+ nome: v.string(),
+ canais: v.object({
+ email: v.boolean(),
+ chat: v.boolean()
+ }),
+ emails: v.array(v.string()),
+ chatUsers: v.array(v.string()),
+ severidadeMin: severidadeSeguranca,
+ tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
+ reenvioMin: v.number(),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ }).index('by_criadoEm', ['criadoEm']),
+
+ // Sistema de Controle de Ponto
+ registrosPonto: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ tipo: v.union(
+ v.literal('entrada'),
+ v.literal('saida_almoco'),
+ v.literal('retorno_almoco'),
+ v.literal('saida')
+ ),
+ data: v.string(), // YYYY-MM-DD
+ hora: v.number(),
+ minuto: v.number(),
+ segundo: v.number(),
+ timestamp: v.number(), // Timestamp completo para ordenação
+ imagemId: v.optional(v.id('_storage')),
+ sincronizadoComServidor: v.boolean(),
+ toleranciaMinutos: v.number(),
+ dentroDoPrazo: v.boolean(),
+
+ // Informações de Rede
+ ipAddress: v.optional(v.string()),
+ ipPublico: v.optional(v.string()),
+ ipLocal: v.optional(v.string()),
+
+ // Informações do Navegador
+ userAgent: v.optional(v.string()),
+ browser: v.optional(v.string()),
+ browserVersion: v.optional(v.string()),
+ engine: v.optional(v.string()),
+
+ // Informações do Sistema
+ sistemaOperacional: v.optional(v.string()),
+ osVersion: v.optional(v.string()),
+ arquitetura: v.optional(v.string()),
+ plataforma: v.optional(v.string()),
+
+ // Informações de Localização
+ latitude: v.optional(v.number()),
+ longitude: v.optional(v.number()),
+ precisao: v.optional(v.number()),
+ altitude: v.optional(v.union(v.number(), v.null())),
+ altitudeAccuracy: v.optional(v.union(v.number(), v.null())),
+ heading: v.optional(v.union(v.number(), v.null())),
+ speed: v.optional(v.union(v.number(), v.null())),
+ confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend)
+ scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend)
+ suspeitaSpoofing: v.optional(v.boolean()),
+ motivoSuspeita: v.optional(v.string()),
+ avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação
+ distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS
+ velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro
+ distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro
+ tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro
+ // Informações de Geofencing
+ enderecoMarcacaoEsperado: v.optional(v.id('enderecosMarcacao')), // Endereço mais próximo esperado
+ distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado
+ dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido
+ enderecoMarcacaoUsado: v.optional(v.id('enderecosMarcacao')), // Qual endereço foi usado na validação
+ raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros
+ endereco: v.optional(v.string()),
+ cidade: v.optional(v.string()),
+ estado: v.optional(v.string()),
+ pais: v.optional(v.string()),
+ timezone: v.optional(v.string()),
+
+ // Informações do Dispositivo
+ deviceType: v.optional(v.string()),
+ deviceModel: v.optional(v.string()),
+ screenResolution: v.optional(v.string()),
+ coresTela: v.optional(v.number()),
+ idioma: v.optional(v.string()),
+
+ // Informações Adicionais
+ isMobile: v.optional(v.boolean()),
+ isTablet: v.optional(v.boolean()),
+ isDesktop: v.optional(v.boolean()),
+ connectionType: v.optional(v.string()),
+ memoryInfo: v.optional(v.string()),
+
+ // Informações de Sensores (Acelerômetro e Giroscópio)
+ acelerometroX: v.optional(v.number()),
+ acelerometroY: v.optional(v.number()),
+ acelerometroZ: v.optional(v.number()),
+ movimentoDetectado: v.optional(v.boolean()),
+ magnitudeMovimento: v.optional(v.number()),
+ variacaoAcelerometro: v.optional(v.number()),
+ giroscopioAlpha: v.optional(v.number()),
+ giroscopioBeta: v.optional(v.number()),
+ giroscopioGamma: v.optional(v.number()),
+ sensorDisponivel: v.optional(v.boolean()),
+ permissaoSensorNegada: v.optional(v.boolean()),
+
+ // Justificativa opcional para o registro
+ justificativa: v.optional(v.string()),
+
+ // Campos para homologação
+ editadoPorGestor: v.optional(v.boolean()),
+ homologacaoId: v.optional(v.id('homologacoesPonto')),
+
+ criadoEm: v.number()
+ })
+ .index('by_funcionario_data', ['funcionarioId', 'data'])
+ .index('by_data', ['data'])
+ .index('by_dentro_prazo', ['dentroDoPrazo', 'data'])
+ .index('by_funcionario_timestamp', ['funcionarioId', 'timestamp']),
+
+ // Endereços de Marcação - Locais permitidos para registro de ponto
+ enderecosMarcacao: defineTable({
+ nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC"
+ descricao: v.optional(v.string()), // Descrição opcional
+ // Coordenadas (obrigatórias)
+ latitude: v.number(),
+ longitude: v.number(),
+ // Endereço físico (para exibição)
+ endereco: v.string(), // Ex: "Rua Exemplo, 123"
+ bairro: v.optional(v.string()), // Bairro do endereço
+ cep: v.optional(v.string()),
+ cidade: v.string(),
+ estado: v.string(),
+ pais: v.optional(v.string()), // Padrão: "Brasil"
+ // Configurações
+ raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m)
+ ativo: v.boolean(),
+ // Tipos de uso
+ tipo: v.union(
+ v.literal('sede'), // Sede principal (para todos)
+ v.literal('home_office'), // Home office específico
+ v.literal('deslocamento'), // Deslocamento temporário
+ v.literal('cliente') // Local de cliente
+ ),
+ // Metadados
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ atualizadoPor: v.optional(v.id('usuarios')),
+ atualizadoEm: v.optional(v.number())
+ })
+ .index('by_ativo', ['ativo'])
+ .index('by_tipo', ['tipo'])
+ .index('by_cidade', ['cidade']),
+
+ // Associação Funcionário ↔ Endereço de Marcação
+ funcionarioEnderecosMarcacao: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ enderecoMarcacaoId: v.id('enderecosMarcacao'),
+ // Configurações específicas do funcionário
+ raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão
+ // Período de validade (para deslocamentos temporários)
+ dataInicio: v.optional(v.string()), // YYYY-MM-DD
+ dataFim: v.optional(v.string()), // YYYY-MM-DD
+ // Status
+ ativo: v.boolean(),
+ // Metadados
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_endereco', ['enderecoMarcacaoId'])
+ .index('by_funcionario_ativo', ['funcionarioId', 'ativo'])
+ .index('by_endereco_ativo', ['enderecoMarcacaoId', 'ativo']),
+
+ configuracaoPonto: defineTable({
+ horarioEntrada: v.string(), // HH:mm
+ horarioSaidaAlmoco: v.string(), // HH:mm
+ horarioRetornoAlmoco: v.string(), // HH:mm
+ horarioSaida: v.string(), // HH:mm
+ toleranciaMinutos: v.number(),
+ // Nomes personalizados dos tipos de registro
+ nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1"
+ nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1"
+ nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2"
+ nomeSaida: v.optional(v.string()), // Padrão: "Saída 2"
+ // Ajuste de fuso horário (GMT offset em horas)
+ gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
+ // Configurações de geofencing
+ validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização
+ toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros
+ ativo: v.boolean(),
+ atualizadoPor: v.id('usuarios'),
+ atualizadoEm: v.number()
+ }).index('by_ativo', ['ativo']),
+
+ configuracaoRelogio: defineTable({
+ servidorNTP: v.optional(v.string()),
+ portaNTP: v.optional(v.number()),
+ usarServidorExterno: v.boolean(),
+ fallbackParaPC: v.boolean(),
+ ultimaSincronizacao: v.optional(v.number()),
+ offsetSegundos: v.optional(v.number()),
+ // Ajuste de fuso horário (GMT offset em horas)
+ gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
+ atualizadoPor: v.id('usuarios'),
+ atualizadoEm: v.number()
+ }).index('by_ativo', ['usarServidorExterno']),
+
+ // Banco de Horas - Saldo diário de horas trabalhadas
+ bancoHoras: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ data: v.string(), // YYYY-MM-DD
+ cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos)
+ horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos)
+ saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit)
+ registrosPontoIds: v.array(v.id('registrosPonto')), // IDs dos registros do dia
+ calculadoEm: v.number()
+ })
+ .index('by_funcionario_data', ['funcionarioId', 'data'])
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_data', ['data']),
+
+ // Homologações de Ponto - Edições e ajustes realizados pelo gestor
+ homologacoesPonto: defineTable({
+ registroId: v.optional(v.id('registrosPonto')), // ID do registro editado (se for edição)
+ funcionarioId: v.id('funcionarios'),
+ gestorId: v.id('usuarios'),
+ // Dados do registro original (se for edição)
+ horaAnterior: v.optional(v.number()),
+ minutoAnterior: v.optional(v.number()),
+ // Dados do registro novo (se for edição)
+ horaNova: v.optional(v.number()),
+ minutoNova: v.optional(v.number()),
+ // Motivo e observações
+ motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações)
+ motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc)
+ motivoDescricao: v.optional(v.string()), // Descrição do motivo
+ observacoes: v.optional(v.string()),
+ // Tipo de ajuste (se for ajuste de banco de horas)
+ tipoAjuste: v.optional(
+ v.union(v.literal('compensar'), v.literal('abonar'), v.literal('descontar'))
+ ),
+ // Período do ajuste (se for ajuste de banco de horas)
+ periodoDias: v.optional(v.number()),
+ periodoHoras: v.optional(v.number()),
+ periodoMinutos: v.optional(v.number()),
+ // Ajuste em minutos (calculado)
+ ajusteMinutos: v.optional(v.number()),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_gestor', ['gestorId'])
+ .index('by_registro', ['registroId'])
+ .index('by_data', ['criadoEm']),
+
+ // Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto
+ dispensasRegistro: defineTable({
+ funcionarioId: v.id('funcionarios'),
+ gestorId: v.id('usuarios'),
+ dataInicio: v.string(), // YYYY-MM-DD
+ horaInicio: v.number(),
+ minutoInicio: v.number(),
+ dataFim: v.string(), // YYYY-MM-DD
+ horaFim: v.number(),
+ minutoFim: v.number(),
+ motivo: v.string(),
+ isento: v.boolean(), // Se true, não expira (casos excepcionais)
+ ativo: v.boolean(),
+ criadoEm: v.number()
+ })
+ .index('by_funcionario', ['funcionarioId'])
+ .index('by_gestor', ['gestorId'])
+ .index('by_ativo', ['ativo'])
+ .index('by_data_inicio', ['dataInicio'])
+ .index('by_data_fim', ['dataFim']),
+ // Configurações Gerais
+ config: defineTable({
+ comprasSetorId: v.optional(v.id('setores')),
+ criadoPor: v.id('usuarios'),
+ atualizadoEm: v.number()
+ }),
+
+ // Módulo de Pedidos/Compras
+ produtos: defineTable({
+ nome: v.string(),
+ valorEstimado: v.string(),
+ tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .searchIndex('search_nome', { searchField: 'nome' })
+ .index('by_nome', ['nome'])
+ .index('by_tipo', ['tipo']),
+
+ acoes: defineTable({
+ nome: v.string(),
+ tipo: v.union(v.literal('projeto'), v.literal('lei')),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number()
+ })
+ .index('by_nome', ['nome'])
+ .index('by_tipo', ['tipo']),
+
+ pedidos: defineTable({
+ numeroSei: v.optional(v.string()),
+ status: v.union(
+ v.literal('em_rascunho'),
+ v.literal('aguardando_aceite'),
+ v.literal('em_analise'),
+ v.literal('precisa_ajustes'),
+ v.literal('cancelado'),
+ v.literal('concluido')
+ ),
+ acaoId: v.optional(v.id('acoes')),
+ criadoPor: v.id('usuarios'),
+ criadoEm: v.number(),
+ atualizadoEm: v.number()
+ })
+ .index('by_numeroSei', ['numeroSei'])
+ .index('by_status', ['status'])
+ .index('by_criadoPor', ['criadoPor'])
+ .index('by_acaoId', ['acaoId']),
+
+ pedidoItems: defineTable({
+ pedidoId: v.id('pedidos'),
+ produtoId: v.id('produtos'),
+ valorEstimado: v.string(),
+ valorReal: v.optional(v.string()),
+ quantidade: v.number(),
+ adicionadoPor: v.id('funcionarios'),
+ criadoEm: v.number()
+ })
+ .index('by_pedidoId', ['pedidoId'])
+ .index('by_produtoId', ['produtoId'])
+ .index('by_adicionadoPor', ['adicionadoPor']),
+
+ historicoPedidos: defineTable({
+ pedidoId: v.id('pedidos'),
+ usuarioId: v.id('usuarios'),
+ acao: v.string(), // "criacao", "alteracao_status", "adicao_item", "remocao_item", "edicao_item"
+ detalhes: v.optional(v.string()), // JSON string
+ data: v.number()
+ })
+ .index('by_pedidoId', ['pedidoId'])
+ .index('by_usuarioId', ['usuarioId'])
+ .index('by_data', ['data'])
});