Merge remote-tracking branch 'origin' into feat-pedidos

This commit is contained in:
2025-12-11 10:08:12 -03:00
194 changed files with 30374 additions and 10247 deletions

View File

@@ -18,7 +18,16 @@ export const ausenciasTables = {
dataReprovacao: v.optional(v.number()),
motivoReprovacao: v.optional(v.string()),
observacao: v.optional(v.string()),
criadoEm: v.number()
criadoEm: 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_status', ['status'])

View File

@@ -23,12 +23,16 @@ export const chatTables = {
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
conteudo: v.string(), // texto ou nome do arquivo (pode ser criptografado)
conteudoBusca: v.optional(v.string()), // versão normalizada para busca (não criptografada se mensagem for E2E)
arquivoId: v.optional(v.id('_storage')),
arquivoNome: v.optional(v.string()),
arquivoTamanho: v.optional(v.number()),
arquivoTipo: v.optional(v.string()),
// Campos para criptografia E2E
criptografado: v.optional(v.boolean()), // Indica se a mensagem está criptografada
iv: v.optional(v.string()), // Initialization Vector (base64) para descriptografia
keyId: v.optional(v.string()), // Identificador da chave usada para criptografar
linkPreview: v.optional(
v.object({
url: v.string(),
@@ -169,5 +173,17 @@ export const chatTables = {
atualizadoEm: v.number()
})
.index('by_usuario_conversa', ['usuarioId', 'conversaId'])
.index('by_conversa', ['conversaId'])
.index('by_conversa', ['conversaId']),
// Chaves de Criptografia E2E por Conversa
chavesCriptografia: defineTable({
conversaId: v.id('conversas'),
chaveCompartilhada: v.string(), // Chave criptografada compartilhada (base64)
keyId: v.string(), // Identificador único da chave
criadoPor: v.id('usuarios'), // Usuário que criou/compartilhou a chave
criadoEm: v.number(),
ativo: v.boolean() // Se a chave está ativa (permite rotação de chaves)
})
.index('by_conversa', ['conversaId', 'ativo'])
.index('by_key_id', ['keyId'])
};

View File

@@ -45,7 +45,8 @@ export const feriasTables = {
v.literal('nova_solicitacao'),
v.literal('aprovado'),
v.literal('reprovado'),
v.literal('data_ajustada')
v.literal('data_ajustada'),
v.literal('cancelado')
),
lida: v.boolean(),
mensagem: v.string()

View File

@@ -35,7 +35,9 @@ export const funcionariosTables = {
simboloId: v.id('simbolos'),
simboloTipo: simboloTipo,
gestorId: v.optional(v.id('usuarios')),
statusFerias: v.optional(v.union(v.literal('ativo'), v.literal('em_ferias'))),
statusFerias: v.optional(
v.union(v.literal('ativo'), v.literal('em_ferias'), v.literal('em_licenca'))
),
// Regime de trabalho (para cálculo correto de férias)
regimeTrabalho: v.optional(

View File

@@ -0,0 +1,98 @@
import { defineTable } from 'convex/server';
import { v } from 'convex/values';
export const lgpdTables = {
// ========== LGPD - Lei Geral de Proteção de Dados ==========
// Solicitações de direitos LGPD
solicitacoesLGPD: defineTable({
tipo: v.union(
v.literal('acesso'),
v.literal('correcao'),
v.literal('exclusao'),
v.literal('portabilidade'),
v.literal('revogacao_consentimento'),
v.literal('informacao_compartilhamento')
),
usuarioId: v.id('usuarios'),
funcionarioId: v.optional(v.id('funcionarios')),
status: v.union(
v.literal('pendente'),
v.literal('em_analise'),
v.literal('concluida'),
v.literal('rejeitada'),
v.literal('cancelada')
),
dadosSolicitados: v.optional(v.string()), // JSON com detalhes da solicitação
resposta: v.optional(v.string()), // Resposta da solicitação
arquivoResposta: v.optional(v.id('_storage')), // Arquivo gerado (ex: exportação de dados)
respondidoPor: v.optional(v.id('usuarios')),
respondidoEm: v.optional(v.number()),
criadoEm: v.number(),
prazoResposta: v.number(), // Prazo legal (15 dias) - timestamp
observacoes: v.optional(v.string())
})
.index('by_usuario', ['usuarioId'])
.index('by_status', ['status'])
.index('by_tipo', ['tipo'])
.index('by_prazo', ['prazoResposta'])
.index('by_funcionario', ['funcionarioId']),
// Consentimentos dos usuários
consentimentos: defineTable({
usuarioId: v.id('usuarios'),
tipo: v.union(
v.literal('termo_uso'),
v.literal('politica_privacidade'),
v.literal('comunicacoes'),
v.literal('compartilhamento_dados')
),
aceito: v.boolean(),
versao: v.string(), // Versão do documento aceito (ex: "1.0")
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
aceitoEm: v.number(),
revogadoEm: v.optional(v.number()),
revogadoPor: v.optional(v.id('usuarios')) // Se revogado pelo próprio usuário ou por TI
})
.index('by_usuario', ['usuarioId'])
.index('by_tipo', ['tipo'])
.index('by_usuario_tipo', ['usuarioId', 'tipo'])
.index('by_versao', ['versao']),
// Registro de Operações de Tratamento (ROT)
registrosTratamento: defineTable({
finalidade: v.string(), // Finalidade do tratamento
baseLegal: v.string(), // Base legal (ex: "Art. 7º, II - Execução de políticas públicas")
categoriasDados: v.array(v.string()), // ["dados_identificacao", "dados_contato", "dados_profissionais"]
categoriasTitulares: v.array(v.string()), // ["funcionarios", "servidores", "colaboradores"]
medidasSeguranca: v.array(v.string()), // ["criptografia", "controle_acesso", "logs_auditoria"]
prazoRetencao: v.number(), // em dias
compartilhamentoTerceiros: v.boolean(),
terceiros: v.optional(v.array(v.string())), // Lista de terceiros com quem compartilha
responsavel: v.id('usuarios'), // Responsável pelo tratamento
criadoEm: v.number(),
atualizadoEm: v.number(),
ativo: v.boolean(),
descricao: v.optional(v.string()) // Descrição detalhada
})
.index('by_finalidade', ['finalidade'])
.index('by_ativo', ['ativo'])
.index('by_responsavel', ['responsavel']),
// Configurações LGPD
configuracaoLGPD: defineTable({
encarregadoNome: v.optional(v.string()),
encarregadoEmail: v.optional(v.string()),
encarregadoTelefone: v.optional(v.string()),
encarregadoHorarioAtendimento: v.optional(v.string()), // Ex: "Segunda a Sexta, das 8h às 17h"
prazoRespostaPadrao: v.number(), // em dias (padrão: 15)
diasAlertaVencimento: v.number(), // dias antes do prazo para alertar (padrão: 3)
termoObrigatorio: v.boolean(), // Se o termo de consentimento é obrigatório
versaoTermoAtual: v.string(), // Versão atual do termo (ex: "1.0")
politicaRetencao: v.optional(v.string()), // JSON com política de retenção por tipo de dado
ativo: v.boolean(),
atualizadoPor: v.id('usuarios'),
atualizadoEm: v.number()
}).index('by_ativo', ['ativo'])
};

View File

@@ -204,11 +204,48 @@ export const pontoTables = {
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
// Novos campos para sistema avançado
ajustesIds: v.optional(v.array(v.id('ajustesBancoHoras'))), // IDs dos ajustes aplicados no dia
motivoAbono: v.optional(v.string()), // Motivo do abono (atestado, licença, ausência, etc.)
tipoDia: v.optional(
v.union(
v.literal('normal'),
v.literal('atestado'),
v.literal('licenca'),
v.literal('ausencia'),
v.literal('abonado'),
v.literal('descontado')
)
), // Tipo do dia
inconsistenciasIds: v.optional(v.array(v.id('inconsistenciasBancoHoras'))), // IDs de inconsistências detectadas
calculadoEm: v.number()
})
.index('by_funcionario_data', ['funcionarioId', 'data'])
.index('by_funcionario', ['funcionarioId'])
.index('by_data', ['data']),
.index('by_data', ['data'])
.index('by_tipo_dia', ['tipoDia']),
// Banco de Horas Mensal - Agregação mensal do banco de horas
bancoHorasMensal: defineTable({
funcionarioId: v.id('funcionarios'),
mes: v.string(), // YYYY-MM (ex: "2024-01")
saldoInicialMinutos: v.number(), // Saldo acumulado do mês anterior (pode ser negativo)
saldoFinalMinutos: v.number(), // Saldo acumulado ao final do mês
saldoMesMinutos: v.number(), // Saldo apenas do mês atual (sem acumular)
diasTrabalhados: v.number(), // Quantidade de dias com registros no mês
horasExtras: v.number(), // Total de minutos positivos do mês
horasDeficit: v.number(), // Total de minutos negativos do mês (valor absoluto)
// Novos campos para sistema avançado
totalAjustes: v.optional(v.number()), // Total de ajustes aplicados no mês (em minutos)
totalAbonos: v.optional(v.number()), // Total de abonos no mês (em minutos)
totalDescontos: v.optional(v.number()), // Total de descontos no mês (em minutos)
inconsistenciasResolvidas: v.optional(v.number()), // Quantidade de inconsistências resolvidas
calculadoEm: v.number(),
atualizadoEm: v.number()
})
.index('by_funcionario_mes', ['funcionarioId', 'mes'])
.index('by_funcionario', ['funcionarioId'])
.index('by_mes', ['mes']),
// Homologações de Ponto - Edições e ajustes realizados pelo gestor
homologacoesPonto: defineTable({
@@ -262,5 +299,119 @@ export const pontoTables = {
.index('by_gestor', ['gestorId'])
.index('by_ativo', ['ativo'])
.index('by_data_inicio', ['dataInicio'])
.index('by_data_fim', ['dataFim'])
.index('by_data_fim', ['dataFim']),
// Configuração de Banco de Horas - Configurações gerais do sistema
configuracaoBancoHoras: defineTable({
// Limites de saldo
limiteSaldoPositivoMinutos: v.optional(v.number()), // Limite máximo de saldo positivo (em minutos)
limiteSaldoNegativoMinutos: v.optional(v.number()), // Limite máximo de saldo negativo (em minutos)
// Regras de cálculo
considerarAjustesAutomaticos: v.optional(v.boolean()), // Se deve considerar ajustes automáticos (atestados, licenças, ausências)
// Periodicidade de verificação
periodicidadeVerificacao: v.optional(
v.union(v.literal('diario'), v.literal('semanal'), v.literal('mensal'))
),
// Metadados
atualizadoPor: v.id('usuarios'),
atualizadoEm: v.number()
}).index('by_ativo', ['atualizadoEm']),
// Alertas de Banco de Horas - Configuração de alertas por tipo
alertasBancoHoras: defineTable({
tipoAlerta: v.union(
v.literal('saldo_negativo'),
v.literal('saldo_negativo_critico'),
v.literal('inconsistencia_detectada'),
v.literal('dias_sem_registro'),
v.literal('limite_saldo_excedido')
),
// Periodicidade
periodicidade: v.union(v.literal('diario'), v.literal('semanal'), v.literal('mensal')),
// Canais de envio
enviarEmail: v.boolean(),
enviarChat: v.boolean(),
// Destinatários específicos (opcional - se vazio, envia para gestor padrão)
destinatariosEmail: v.optional(v.array(v.id('usuarios'))), // IDs de usuários que receberão email
destinatariosChat: v.optional(v.array(v.id('usuarios'))), // IDs de usuários que receberão chat
// Thresholds e limites
threshold: v.optional(v.number()), // Valor limite para disparar alerta
limiteMinutos: v.optional(v.number()), // Limite em minutos (para saldo negativo)
// Status
ativo: v.boolean(),
// Metadados
criadoPor: v.id('usuarios'),
criadoEm: v.number(),
atualizadoPor: v.optional(v.id('usuarios')),
atualizadoEm: v.optional(v.number())
})
.index('by_tipo', ['tipoAlerta'])
.index('by_ativo', ['ativo'])
.index('by_tipo_ativo', ['tipoAlerta', 'ativo']),
// Ajustes de Banco de Horas - Registro de ajustes manuais e automáticos
ajustesBancoHoras: defineTable({
funcionarioId: v.id('funcionarios'),
tipo: v.union(v.literal('abonar'), v.literal('descontar'), v.literal('compensar')),
// Motivo vinculado
motivoTipo: v.optional(
v.union(
v.literal('atestado'),
v.literal('licenca'),
v.literal('ausencia'),
v.literal('manual')
)
),
motivoId: v.optional(v.string()), // ID do atestado, licença, ausência ou null para manual
motivoDescricao: v.optional(v.string()), // Descrição do motivo
// Valor do ajuste
valorMinutos: v.number(), // Valor em minutos (positivo para abonar, negativo para descontar)
// Data de aplicação
dataAplicacao: v.string(), // YYYY-MM-DD
// Gestor responsável (null se automático)
gestorId: v.optional(v.id('usuarios')),
// Observações
observacoes: v.optional(v.string()),
// Status
aplicado: v.boolean(), // Se já foi aplicado ao banco de horas
// Metadados
criadoEm: v.number(),
aplicadoEm: v.optional(v.number())
})
.index('by_funcionario', ['funcionarioId'])
.index('by_data_aplicacao', ['dataAplicacao'])
.index('by_funcionario_data', ['funcionarioId', 'dataAplicacao'])
.index('by_tipo', ['tipo'])
.index('by_aplicado', ['aplicado'])
.index('by_gestor', ['gestorId']),
// Inconsistências de Banco de Horas - Registro de inconsistências detectadas
inconsistenciasBancoHoras: defineTable({
funcionarioId: v.id('funcionarios'),
tipo: v.union(
v.literal('ponto_com_atestado'),
v.literal('ponto_com_licenca'),
v.literal('ponto_com_ausencia'),
v.literal('registro_duplicado'),
v.literal('sequencia_invalida'),
v.literal('saldo_inconsistente')
),
descricao: v.string(), // Descrição detalhada da inconsistência
dataDetectada: v.string(), // YYYY-MM-DD
dataInconsistencia: v.string(), // YYYY-MM-DD (data do dia com inconsistência)
// Status
status: v.union(v.literal('pendente'), v.literal('resolvida'), v.literal('ignorada')),
// Resolução
resolucao: v.optional(v.string()), // Descrição da resolução
resolvidoPor: v.optional(v.id('usuarios')), // Usuário que resolveu
resolvidoEm: v.optional(v.number()), // Timestamp da resolução
// Metadados
criadoEm: v.number()
})
.index('by_funcionario', ['funcionarioId'])
.index('by_status', ['status'])
.index('by_funcionario_status', ['funcionarioId', 'status'])
.index('by_data_detectada', ['dataDetectada'])
.index('by_tipo', ['tipo'])
.index('by_data_inconsistencia', ['dataInconsistencia'])
};

View File

@@ -164,6 +164,7 @@ export const systemTables = {
severidadeMin: severidadeSeguranca,
tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
reenvioMin: v.number(),
templateCodigo: v.optional(v.string()), // Template a ser usado para email/chat
criadoPor: v.id('usuarios'),
criadoEm: v.number(),
atualizadoEm: v.number()
@@ -222,5 +223,24 @@ export const systemTables = {
sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor
sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada)
sshPort: v.optional(v.number()) // Porta SSH (padrão: 22)
}).index('by_ativo', ['ativo'])
}).index('by_ativo', ['ativo']),
// Logs de Erros do Servidor (500, etc)
errosServidor: defineTable({
statusCode: v.number(), // Código HTTP do erro (500, 502, etc)
mensagem: v.string(), // Mensagem do erro
stack: v.optional(v.string()), // Stack trace do erro
url: v.optional(v.string()), // URL onde ocorreu o erro
method: v.optional(v.string()), // Método HTTP (GET, POST, etc)
ipAddress: v.optional(v.string()), // IP do cliente
userAgent: v.optional(v.string()), // User agent do navegador
usuarioId: v.optional(v.id('usuarios')), // Usuário autenticado (se houver)
notificado: v.boolean(), // Se a equipe técnica já foi notificada
notificadoEm: v.optional(v.number()), // Timestamp da notificação
criadoEm: v.number() // Timestamp do erro
})
.index('by_status_code', ['statusCode'])
.index('by_notificado', ['notificado'])
.index('by_criado_em', ['criadoEm'])
.index('by_usuario', ['usuarioId'])
};