Merge remote-tracking branch 'origin/master' into feat-ajuste-acesso
This commit is contained in:
16
packages/backend/convex/_generated/api.d.ts
vendored
16
packages/backend/convex/_generated/api.d.ts
vendored
@@ -14,10 +14,14 @@ import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js
|
||||
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
|
||||
import type * as chat from "../chat.js";
|
||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||
import type * as criarFuncionarioTeste from "../criarFuncionarioTeste.js";
|
||||
import type * as criarUsuarioTeste from "../criarUsuarioTeste.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as cursos from "../cursos.js";
|
||||
import type * as dashboard from "../dashboard.js";
|
||||
import type * as documentos from "../documentos.js";
|
||||
import type * as email from "../email.js";
|
||||
import type * as ferias from "../ferias.js";
|
||||
import type * as funcionarios from "../funcionarios.js";
|
||||
import type * as healthCheck from "../healthCheck.js";
|
||||
import type * as http from "../http.js";
|
||||
@@ -25,15 +29,19 @@ import type * as limparPerfisAntigos from "../limparPerfisAntigos.js";
|
||||
import type * as logsAcesso from "../logsAcesso.js";
|
||||
import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
import type * as menuPermissoes from "../menuPermissoes.js";
|
||||
import type * as migrarParaTimes from "../migrarParaTimes.js";
|
||||
import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as perfisCustomizados from "../perfisCustomizados.js";
|
||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as saldoFerias from "../saldoFerias.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as simbolos from "../simbolos.js";
|
||||
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
|
||||
import type * as templatesMensagens from "../templatesMensagens.js";
|
||||
import type * as times from "../times.js";
|
||||
import type * as todos from "../todos.js";
|
||||
import type * as usuarios from "../usuarios.js";
|
||||
import type * as verificarMatriculas from "../verificarMatriculas.js";
|
||||
@@ -59,10 +67,14 @@ declare const fullApi: ApiFromModules<{
|
||||
"betterAuth/_generated/server": typeof betterAuth__generated_server;
|
||||
chat: typeof chat;
|
||||
configuracaoEmail: typeof configuracaoEmail;
|
||||
criarFuncionarioTeste: typeof criarFuncionarioTeste;
|
||||
criarUsuarioTeste: typeof criarUsuarioTeste;
|
||||
crons: typeof crons;
|
||||
cursos: typeof cursos;
|
||||
dashboard: typeof dashboard;
|
||||
documentos: typeof documentos;
|
||||
email: typeof email;
|
||||
ferias: typeof ferias;
|
||||
funcionarios: typeof funcionarios;
|
||||
healthCheck: typeof healthCheck;
|
||||
http: typeof http;
|
||||
@@ -70,15 +82,19 @@ declare const fullApi: ApiFromModules<{
|
||||
logsAcesso: typeof logsAcesso;
|
||||
logsAtividades: typeof logsAtividades;
|
||||
logsLogin: typeof logsLogin;
|
||||
menuPermissoes: typeof menuPermissoes;
|
||||
migrarParaTimes: typeof migrarParaTimes;
|
||||
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
|
||||
monitoramento: typeof monitoramento;
|
||||
perfisCustomizados: typeof perfisCustomizados;
|
||||
permissoesAcoes: typeof permissoesAcoes;
|
||||
roles: typeof roles;
|
||||
saldoFerias: typeof saldoFerias;
|
||||
seed: typeof seed;
|
||||
simbolos: typeof simbolos;
|
||||
solicitacoesAcesso: typeof solicitacoesAcesso;
|
||||
templatesMensagens: typeof templatesMensagens;
|
||||
times: typeof times;
|
||||
todos: typeof todos;
|
||||
usuarios: typeof usuarios;
|
||||
verificarMatriculas: typeof verificarMatriculas;
|
||||
|
||||
@@ -48,15 +48,8 @@ export const criarConversa = mutation({
|
||||
avatar: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
// Validar participantes
|
||||
if (!args.participantes.includes(usuarioAtual._id)) {
|
||||
@@ -194,6 +187,7 @@ export const enviarMensagem = mutation({
|
||||
arquivoTamanho: v.optional(v.number()),
|
||||
arquivoTipo: v.optional(v.string()),
|
||||
mencoes: v.optional(v.array(v.id("usuarios"))),
|
||||
permitirNotificacaoParaSiMesmo: v.optional(v.boolean()), // ✅ NOVO: Permite criar notificação para si mesmo
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
@@ -226,28 +220,38 @@ export const enviarMensagem = mutation({
|
||||
ultimaMensagemTimestamp: Date.now(),
|
||||
});
|
||||
|
||||
// Criar notificações para outros participantes
|
||||
for (const participanteId of conversa.participantes) {
|
||||
if (participanteId !== usuarioAtual._id) {
|
||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||
? "mencao"
|
||||
: "nova_mensagem";
|
||||
// Criar notificações para participantes (com tratamento de erro)
|
||||
try {
|
||||
for (const participanteId of conversa.participantes) {
|
||||
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
||||
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
||||
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
|
||||
if (deveCriarNotificacao) {
|
||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||
? "mencao"
|
||||
: "nova_mensagem";
|
||||
|
||||
await ctx.db.insert("notificacoes", {
|
||||
usuarioId: participanteId,
|
||||
tipo: tipoNotificacao,
|
||||
conversaId: args.conversaId,
|
||||
mensagemId,
|
||||
remetenteId: usuarioAtual._id,
|
||||
titulo:
|
||||
tipoNotificacao === "mencao"
|
||||
? `${usuarioAtual.nome} mencionou você`
|
||||
: `Nova mensagem de ${usuarioAtual.nome}`,
|
||||
descricao: args.conteudo.substring(0, 100),
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
await ctx.db.insert("notificacoes", {
|
||||
usuarioId: participanteId,
|
||||
tipo: tipoNotificacao,
|
||||
conversaId: args.conversaId,
|
||||
mensagemId,
|
||||
remetenteId: usuarioAtual._id,
|
||||
titulo:
|
||||
tipoNotificacao === "mencao"
|
||||
? `${usuarioAtual.nome} mencionou você`
|
||||
: `Nova mensagem de ${usuarioAtual.nome}`,
|
||||
descricao: args.conteudo.substring(0, 100),
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log do erro mas não falhar o envio da mensagem
|
||||
console.error("Erro ao criar notificações:", error);
|
||||
// A mensagem já foi criada, então retornamos o ID normalmente
|
||||
}
|
||||
|
||||
return mensagemId;
|
||||
@@ -264,15 +268,8 @@ export const agendarMensagem = mutation({
|
||||
agendadaPara: v.number(), // timestamp
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
// Validar data futura
|
||||
if (args.agendadaPara <= Date.now()) {
|
||||
@@ -308,15 +305,8 @@ export const cancelarMensagemAgendada = mutation({
|
||||
mensagemId: v.id("mensagens"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
const mensagem = await ctx.db.get(args.mensagemId);
|
||||
if (!mensagem) throw new Error("Mensagem não encontrada");
|
||||
@@ -338,15 +328,8 @@ export const reagirMensagem = mutation({
|
||||
emoji: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
const mensagem = await ctx.db.get(args.mensagemId);
|
||||
if (!mensagem) throw new Error("Mensagem não encontrada");
|
||||
@@ -496,20 +479,13 @@ export const uploadArquivoChat = mutation({
|
||||
conversaId: v.id("conversas"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
// Verificar se usuário pertence à conversa
|
||||
const conversa = await ctx.db.get(args.conversaId);
|
||||
if (!conversa) throw new Error("Conversa não encontrada");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
|
||||
if (!conversa.participantes.includes(usuarioAtual._id)) {
|
||||
throw new Error("Você não pertence a esta conversa");
|
||||
}
|
||||
@@ -526,8 +502,8 @@ export const marcarNotificacaoLida = mutation({
|
||||
notificacaoId: v.id("notificacoes"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
await ctx.db.patch(args.notificacaoId, { lida: true });
|
||||
return true;
|
||||
@@ -540,15 +516,8 @@ export const marcarNotificacaoLida = mutation({
|
||||
export const marcarTodasNotificacoesLidas = mutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
const notificacoes = await ctx.db
|
||||
.query("notificacoes")
|
||||
@@ -573,15 +542,8 @@ export const deletarMensagem = mutation({
|
||||
mensagemId: v.id("mensagens"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Não autenticado");
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) throw new Error("Não autenticado");
|
||||
|
||||
const mensagem = await ctx.db.get(args.mensagemId);
|
||||
if (!mensagem) throw new Error("Mensagem não encontrada");
|
||||
@@ -607,14 +569,7 @@ export const deletarMensagem = mutation({
|
||||
export const listarConversas = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
// Buscar todas as conversas do usuário
|
||||
@@ -641,10 +596,26 @@ export const listarConversas = query({
|
||||
// Para conversas individuais, pegar o outro usuário
|
||||
let outroUsuario = null;
|
||||
if (conversa.tipo === "individual") {
|
||||
outroUsuario = participantes.find((p) => p?._id !== usuarioAtual._id);
|
||||
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id);
|
||||
if (outroUsuarioRaw) {
|
||||
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
||||
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
||||
|
||||
if (usuarioAtualizado) {
|
||||
// Adicionar URL da foto de perfil
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuarioAtualizado.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil);
|
||||
}
|
||||
outroUsuario = {
|
||||
...usuarioAtualizado,
|
||||
fotoPerfilUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contar mensagens não lidas
|
||||
// Contar mensagens não lidas (apenas mensagens NÃO agendadas)
|
||||
const leitura = await ctx.db
|
||||
.query("leituras")
|
||||
.withIndex("by_conversa_usuario", (q) =>
|
||||
@@ -652,11 +623,13 @@ export const listarConversas = query({
|
||||
)
|
||||
.first();
|
||||
|
||||
const mensagens = await ctx.db
|
||||
// CORRIGIDO: Buscar apenas mensagens NÃO agendadas (agendadaPara === undefined)
|
||||
const todasMensagens = await ctx.db
|
||||
.query("mensagens")
|
||||
.withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id))
|
||||
.filter((q) => q.neq(q.field("agendadaPara"), undefined))
|
||||
.collect();
|
||||
|
||||
const mensagens = todasMensagens.filter((m) => !m.agendadaPara);
|
||||
|
||||
let naoLidas = 0;
|
||||
if (leitura) {
|
||||
@@ -693,14 +666,7 @@ export const obterMensagens = query({
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
// Verificar se usuário pertence à conversa
|
||||
@@ -747,25 +713,17 @@ export const obterMensagensAgendadas = query({
|
||||
conversaId: v.id("conversas"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
// Buscar mensagens agendadas
|
||||
const mensagens = await ctx.db
|
||||
const todasMensagens = await ctx.db
|
||||
.query("mensagens")
|
||||
.withIndex("by_conversa", (q) => q.eq("conversaId", args.conversaId))
|
||||
.filter((q) => q.neq(q.field("agendadaPara"), undefined))
|
||||
.collect();
|
||||
|
||||
// Filtrar apenas as do usuário atual
|
||||
const minhasMensagensAgendadas = mensagens.filter(
|
||||
// Filtrar apenas as agendadas do usuário atual
|
||||
const minhasMensagensAgendadas = todasMensagens.filter(
|
||||
(m) =>
|
||||
m.remetenteId === usuarioAtual._id &&
|
||||
m.agendadaPara &&
|
||||
@@ -786,14 +744,7 @@ export const obterNotificacoes = query({
|
||||
apenasPendentes: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
let query = ctx.db
|
||||
@@ -834,14 +785,7 @@ export const obterNotificacoes = query({
|
||||
export const contarNotificacoesNaoLidas = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return 0;
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return 0;
|
||||
|
||||
const notificacoes = await ctx.db
|
||||
@@ -861,8 +805,8 @@ export const contarNotificacoesNaoLidas = query({
|
||||
export const obterUsuariosOnline = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
const usuarios = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -888,14 +832,7 @@ export const obterUsuariosOnline = query({
|
||||
export const listarTodosUsuarios = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
const usuarios = await ctx.db
|
||||
@@ -929,14 +866,7 @@ export const buscarMensagens = query({
|
||||
conversaId: v.optional(v.id("conversas")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
// Buscar em todas as conversas do usuário
|
||||
@@ -1001,14 +931,7 @@ export const obterDigitando = query({
|
||||
conversaId: v.id("conversas"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return [];
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
|
||||
// Buscar indicadores de digitação (últimos 10 segundos)
|
||||
@@ -1043,14 +966,7 @@ export const contarNaoLidas = query({
|
||||
conversaId: v.id("conversas"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return 0;
|
||||
|
||||
const usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return 0;
|
||||
|
||||
const leitura = await ctx.db
|
||||
|
||||
127
packages/backend/convex/criarFuncionarioTeste.ts
Normal file
127
packages/backend/convex/criarFuncionarioTeste.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from "./_generated/server";
|
||||
|
||||
/**
|
||||
* Mutation de teste para criar um funcionário e associar ao usuário TI Master
|
||||
* Isso permite testar o sistema de férias completo
|
||||
*/
|
||||
export const criarFuncionarioParaTIMaster = mutation({
|
||||
args: {
|
||||
usuarioEmail: v.string(), // Email do usuário TI Master
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), funcionarioId: v.id("funcionarios") }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar usuário
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", args.usuarioEmail))
|
||||
.first();
|
||||
|
||||
if (!usuario) {
|
||||
return { sucesso: false as const, erro: "Usuário não encontrado" };
|
||||
}
|
||||
|
||||
// Verificar se já tem funcionário associado
|
||||
if (usuario.funcionarioId) {
|
||||
return { sucesso: false as const, erro: "Usuário já tem funcionário associado" };
|
||||
}
|
||||
|
||||
// Buscar um símbolo qualquer (pegamos o primeiro)
|
||||
const simbolo = await ctx.db.query("simbolos").first();
|
||||
|
||||
if (!simbolo) {
|
||||
return { sucesso: false as const, erro: "Nenhum símbolo encontrado no sistema" };
|
||||
}
|
||||
|
||||
// Criar funcionário de teste
|
||||
const funcionarioId = await ctx.db.insert("funcionarios", {
|
||||
nome: usuario.nome,
|
||||
cpf: "000.000.000-00", // CPF de teste
|
||||
rg: "0000000",
|
||||
endereco: "Endereço de Teste",
|
||||
bairro: "Centro",
|
||||
cidade: "Recife",
|
||||
uf: "PE",
|
||||
telefone: "(81) 99999-9999",
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula,
|
||||
admissaoData: "2023-01-01", // Data de admissão: 1 ano atrás
|
||||
simboloId: simbolo._id,
|
||||
simboloTipo: simbolo.tipo,
|
||||
statusFerias: "ativo",
|
||||
|
||||
// IMPORTANTE: Definir regime de trabalho
|
||||
// Altere aqui para testar diferentes regimes:
|
||||
// - "clt" = CLT (máx 3 períodos, mín 5 dias)
|
||||
// - "estatutario_pe" = Servidor Público PE (máx 2 períodos, mín 10 dias)
|
||||
regimeTrabalho: "clt",
|
||||
|
||||
// Dados opcionais
|
||||
descricaoCargo: "Gestor de TI - Cargo de Teste",
|
||||
nomePai: "Pai de Teste",
|
||||
nomeMae: "Mãe de Teste",
|
||||
naturalidade: "Recife",
|
||||
naturalidadeUF: "PE",
|
||||
sexo: "masculino",
|
||||
estadoCivil: "solteiro",
|
||||
nacionalidade: "Brasileira",
|
||||
grauInstrucao: "superior_completo",
|
||||
tipoSanguineo: "O+",
|
||||
});
|
||||
|
||||
// Associar funcionário ao usuário
|
||||
await ctx.db.patch(usuario._id, {
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
return { sucesso: true as const, funcionarioId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation para alterar o regime de trabalho de um funcionário
|
||||
* Útil para testar diferentes regras (CLT vs Servidor PE)
|
||||
*/
|
||||
export const alterarRegimeTrabalho = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novoRegime: v.union(
|
||||
v.literal("clt"),
|
||||
v.literal("estatutario_pe"),
|
||||
v.literal("estatutario_federal"),
|
||||
v.literal("estatutario_municipal")
|
||||
),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
regimeTrabalho: args.novoRegime,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation para alterar data de admissão
|
||||
* Útil para testar diferentes períodos aquisitivos
|
||||
*/
|
||||
export const alterarDataAdmissao = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novaData: v.string(), // Formato: "YYYY-MM-DD"
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
admissaoData: args.novaData,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
118
packages/backend/convex/criarUsuarioTeste.ts
Normal file
118
packages/backend/convex/criarUsuarioTeste.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from "./_generated/server";
|
||||
import { hashPassword } from "./auth/utils";
|
||||
|
||||
/**
|
||||
* Cria um usuário de teste com funcionário associado
|
||||
* para testar o sistema de férias
|
||||
*/
|
||||
export const criarUsuarioParaTesteFerias = mutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
login: v.string(),
|
||||
senha: v.string(),
|
||||
mensagem: v.string(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const loginTeste = "teste.ferias";
|
||||
const senhaTeste = "Teste@2025";
|
||||
const emailTeste = "teste.ferias@sgse.pe.gov.br";
|
||||
const nomeTeste = "João Silva (Teste)";
|
||||
|
||||
// Verificar se já existe
|
||||
const usuarioExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", loginTeste))
|
||||
.first();
|
||||
|
||||
if (usuarioExistente) {
|
||||
return {
|
||||
sucesso: true,
|
||||
login: loginTeste,
|
||||
senha: senhaTeste,
|
||||
mensagem: "Usuário de teste já existe! Use as credenciais abaixo.",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar role padrão (usuário comum)
|
||||
const roleUsuario = await ctx.db
|
||||
.query("roles")
|
||||
.filter((q) => q.eq(q.field("nome"), "usuario"))
|
||||
.first();
|
||||
|
||||
if (!roleUsuario) {
|
||||
return {
|
||||
sucesso: false,
|
||||
login: "",
|
||||
senha: "",
|
||||
mensagem: "Erro: Role 'usuario' não encontrada",
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar um símbolo qualquer
|
||||
const simbolo = await ctx.db.query("simbolos").first();
|
||||
|
||||
if (!simbolo) {
|
||||
return {
|
||||
sucesso: false,
|
||||
login: "",
|
||||
senha: "",
|
||||
mensagem: "Erro: Nenhum símbolo encontrado. Crie um símbolo primeiro.",
|
||||
};
|
||||
}
|
||||
|
||||
// Criar funcionário
|
||||
const funcionarioId = await ctx.db.insert("funcionarios", {
|
||||
nome: nomeTeste,
|
||||
cpf: "111.222.333-44",
|
||||
rg: "1234567",
|
||||
nascimento: "1990-05-15",
|
||||
endereco: "Rua de Teste, 123",
|
||||
bairro: "Centro",
|
||||
cidade: "Recife",
|
||||
uf: "PE",
|
||||
cep: "50000-000",
|
||||
telefone: "(81) 98765-4321",
|
||||
email: emailTeste,
|
||||
matricula: loginTeste,
|
||||
admissaoData: "2023-01-15", // Admitido em jan/2023 (quase 2 anos)
|
||||
simboloId: simbolo._id,
|
||||
simboloTipo: simbolo.tipo,
|
||||
statusFerias: "ativo",
|
||||
regimeTrabalho: "clt", // CLT para testar
|
||||
descricaoCargo: "Analista Administrativo",
|
||||
nomePai: "José Silva",
|
||||
nomeMae: "Maria Silva",
|
||||
naturalidade: "Recife",
|
||||
naturalidadeUF: "PE",
|
||||
sexo: "masculino",
|
||||
estadoCivil: "solteiro",
|
||||
nacionalidade: "Brasileira",
|
||||
grauInstrucao: "superior",
|
||||
});
|
||||
|
||||
// Criar usuário
|
||||
const senhaHash = await hashPassword(senhaTeste);
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: loginTeste,
|
||||
senhaHash,
|
||||
nome: nomeTeste,
|
||||
email: emailTeste,
|
||||
funcionarioId,
|
||||
roleId: roleUsuario._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: false, // Já consideramos que fez primeiro acesso
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
login: loginTeste,
|
||||
senha: senhaTeste,
|
||||
mensagem: "Usuário de teste criado com sucesso!",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,5 +17,29 @@ crons.interval(
|
||||
internal.chat.limparIndicadoresDigitacao
|
||||
);
|
||||
|
||||
// Atualizar status de férias dos funcionários diariamente
|
||||
crons.interval(
|
||||
"atualizar-status-ferias",
|
||||
{ hours: 24 },
|
||||
internal.ferias.atualizarStatusTodosFuncionarios,
|
||||
{}
|
||||
);
|
||||
|
||||
// Criar períodos aquisitivos de férias automaticamente (diariamente)
|
||||
crons.interval(
|
||||
"criar-periodos-aquisitivos",
|
||||
{ hours: 24 },
|
||||
internal.saldoFerias.criarPeriodosAquisitivos,
|
||||
{}
|
||||
);
|
||||
|
||||
// Processar fila de emails pendentes a cada 2 minutos
|
||||
crons.interval(
|
||||
"processar-fila-emails",
|
||||
{ minutes: 2 },
|
||||
internal.email.processarFilaEmails,
|
||||
{}
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
|
||||
67
packages/backend/convex/cursos.ts
Normal file
67
packages/backend/convex/cursos.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation } from "./_generated/server";
|
||||
|
||||
export const listarPorFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("cursos"),
|
||||
_creationTime: v.number(),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("cursos")
|
||||
.withIndex("by_funcionario", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
)
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
},
|
||||
returns: v.id("cursos"),
|
||||
handler: async (ctx, args) => {
|
||||
const cursoId = await ctx.db.insert("cursos", args);
|
||||
return cursoId;
|
||||
},
|
||||
});
|
||||
|
||||
export const atualizar = mutation({
|
||||
args: {
|
||||
id: v.id("cursos"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...updates } = args;
|
||||
await ctx.db.patch(id, updates);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const excluir = mutation({
|
||||
args: {
|
||||
id: v.id("cursos"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -229,7 +229,9 @@ export const enviarEmailAction = action({
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
// TODO: Implementar com nodemailer quando instalado
|
||||
"use node";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
try {
|
||||
// Buscar email da fila
|
||||
@@ -248,7 +250,17 @@ export const enviarEmailAction = action({
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Configuração de email não encontrada ou inativa",
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.testadoEm) {
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!",
|
||||
};
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
@@ -256,15 +268,32 @@ export const enviarEmailAction = action({
|
||||
emailId: args.emailId,
|
||||
});
|
||||
|
||||
// TODO: Enviar email real com nodemailer aqui
|
||||
console.log(
|
||||
"⚠️ AVISO: Envio de email simulado (nodemailer não instalado)"
|
||||
);
|
||||
// Criar transporter do nodemailer
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: (config as any).smtpHost,
|
||||
port: (config as any).smtpPort,
|
||||
secure: (config as any).smtpSecure,
|
||||
auth: {
|
||||
user: (config as any).smtpUser,
|
||||
pass: (config as any).smtpPassword,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Enviar email REAL
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${(config as any).remetenteNome}" <${(config as any).remetenteEmail}>`,
|
||||
to: (email as any).destinatario,
|
||||
subject: (email as any).assunto,
|
||||
html: (email as any).corpo,
|
||||
});
|
||||
|
||||
console.log("✅ Email enviado com sucesso!");
|
||||
console.log(" Para:", (email as any).destinatario);
|
||||
console.log(" Assunto:", (email as any).assunto);
|
||||
|
||||
// Simular delay de envio
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log(" Message ID:", info.messageId);
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
@@ -273,6 +302,8 @@ export const enviarEmailAction = action({
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Erro ao enviar email:", error.message);
|
||||
|
||||
// Marcar como falha
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
@@ -309,17 +340,19 @@ export const processarFilaEmails = internalMutation({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Agendar envio (será feito por uma action separada)
|
||||
// Por enquanto, apenas marca como enviado para não bloquear
|
||||
await ctx.db.patch(email._id, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
// Agendar envio via action
|
||||
// IMPORTANTE: Não podemos chamar action diretamente de mutation
|
||||
// Por isso, usaremos o scheduler
|
||||
await ctx.scheduler.runAfter(0, "email:enviarEmailAction" as any, {
|
||||
emailId: email._id,
|
||||
});
|
||||
|
||||
processados++;
|
||||
}
|
||||
|
||||
console.log(`📧 Fila de emails processada: ${processados} emails`);
|
||||
console.log(
|
||||
`📧 Fila de emails processada: ${processados} emails agendados para envio`
|
||||
);
|
||||
|
||||
return { processados };
|
||||
},
|
||||
|
||||
513
packages/backend/convex/ferias.ts
Normal file
513
packages/backend/convex/ferias.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, internalMutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
// Validador para períodos
|
||||
const periodoValidator = v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
});
|
||||
|
||||
// Query: Listar TODAS as solicitações (para RH)
|
||||
export const listarTodas = query({
|
||||
args: {},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx) => {
|
||||
const solicitacoes = await ctx.db.query("solicitacoesFerias").collect();
|
||||
|
||||
const solicitacoesComDetalhes = await Promise.all(
|
||||
solicitacoes.map(async (s) => {
|
||||
const funcionario = await ctx.db.get(s.funcionarioId);
|
||||
|
||||
// Buscar time do funcionário
|
||||
const membroTime = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", s.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
let time = null;
|
||||
if (membroTime) {
|
||||
time = await ctx.db.get(membroTime.timeId);
|
||||
}
|
||||
|
||||
return {
|
||||
...s,
|
||||
funcionario,
|
||||
time,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return solicitacoesComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Listar solicitações do funcionário
|
||||
export const listarMinhasSolicitacoes = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.order("desc")
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Listar solicitações dos subordinados (para gestores)
|
||||
export const listarSolicitacoesSubordinados = query({
|
||||
args: { gestorId: v.id("usuarios") },
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar times onde o usuário é gestor
|
||||
const timesGestor = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor", (q) => q.eq("gestorId", args.gestorId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
const solicitacoes: Array<any> = [];
|
||||
|
||||
for (const time of timesGestor) {
|
||||
// Buscar membros do time
|
||||
const membros = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
// Buscar solicitações de cada membro
|
||||
for (const membro of membros) {
|
||||
const solic = await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", membro.funcionarioId))
|
||||
.collect();
|
||||
|
||||
// Adicionar info do funcionário
|
||||
for (const s of solic) {
|
||||
const funcionario = await ctx.db.get(s.funcionarioId);
|
||||
solicitacoes.push({
|
||||
...s,
|
||||
funcionario,
|
||||
time,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return solicitacoes.sort((a, b) => b._creationTime - a._creationTime);
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter detalhes completos de uma solicitação
|
||||
export const obterDetalhes = query({
|
||||
args: { solicitacaoId: v.id("solicitacoesFerias") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) return null;
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
let gestor = null;
|
||||
if (solicitacao.gestorId) {
|
||||
gestor = await ctx.db.get(solicitacao.gestorId);
|
||||
}
|
||||
|
||||
return {
|
||||
...solicitacao,
|
||||
funcionario,
|
||||
gestor,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Criar solicitação de férias (com validação de saldo)
|
||||
export const criarSolicitacao = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
periodos: v.array(periodoValidator),
|
||||
observacao: v.optional(v.string()),
|
||||
},
|
||||
returns: v.id("solicitacoesFerias"),
|
||||
handler: async (ctx, args) => {
|
||||
if (args.periodos.length === 0) {
|
||||
throw new Error("É necessário adicionar pelo menos 1 período");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) throw new Error("Funcionário não encontrado");
|
||||
|
||||
// Calcular total de dias
|
||||
let totalDias = 0;
|
||||
for (const p of args.periodos) {
|
||||
totalDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
// Reservar dias no saldo (impede uso duplo)
|
||||
await ctx.runMutation(internal.saldoFerias.reservarDias, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
anoReferencia: args.anoReferencia,
|
||||
totalDias,
|
||||
});
|
||||
|
||||
// Buscar usuário que está criando (pode não ser o próprio funcionário)
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.first();
|
||||
|
||||
const solicitacaoId = await ctx.db.insert("solicitacoesFerias", {
|
||||
funcionarioId: args.funcionarioId,
|
||||
anoReferencia: args.anoReferencia,
|
||||
status: "aguardando_aprovacao",
|
||||
periodos: args.periodos,
|
||||
observacao: args.observacao,
|
||||
historicoAlteracoes: [{
|
||||
data: Date.now(),
|
||||
usuarioId: usuario?._id || funcionario.gestorId!,
|
||||
acao: "Solicitação criada",
|
||||
}],
|
||||
});
|
||||
|
||||
// Notificar gestor
|
||||
if (funcionario.gestorId) {
|
||||
await ctx.db.insert("notificacoesFerias", {
|
||||
destinatarioId: funcionario.gestorId,
|
||||
solicitacaoFeriasId: solicitacaoId,
|
||||
tipo: "nova_solicitacao",
|
||||
lida: false,
|
||||
mensagem: `${funcionario.nome} solicitou férias`,
|
||||
});
|
||||
}
|
||||
|
||||
return solicitacaoId;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Aprovar férias
|
||||
export const aprovar = mutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesFerias"),
|
||||
gestorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) throw new Error("Solicitação não encontrada");
|
||||
|
||||
if (solicitacao.status !== "aguardando_aprovacao") {
|
||||
throw new Error("Esta solicitação já foi processada");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "aprovado",
|
||||
gestorId: args.gestorId,
|
||||
dataAprovacao: Date.now(),
|
||||
historicoAlteracoes: [
|
||||
...(solicitacao.historicoAlteracoes || []),
|
||||
{
|
||||
data: Date.now(),
|
||||
usuarioId: args.gestorId,
|
||||
acao: "Aprovado",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Atualizar saldo (de pendente para usado)
|
||||
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
||||
.first();
|
||||
|
||||
if (usuario) {
|
||||
await ctx.db.insert("notificacoesFerias", {
|
||||
destinatarioId: usuario._id,
|
||||
solicitacaoFeriasId: args.solicitacaoId,
|
||||
tipo: "aprovado",
|
||||
lida: false,
|
||||
mensagem: "Suas férias foram aprovadas!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Reprovar férias
|
||||
export const reprovar = mutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesFerias"),
|
||||
gestorId: v.id("usuarios"),
|
||||
motivoReprovacao: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) throw new Error("Solicitação não encontrada");
|
||||
|
||||
if (solicitacao.status !== "aguardando_aprovacao") {
|
||||
throw new Error("Esta solicitação já foi processada");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "reprovado",
|
||||
gestorId: args.gestorId,
|
||||
dataReprovacao: Date.now(),
|
||||
motivoReprovacao: args.motivoReprovacao,
|
||||
historicoAlteracoes: [
|
||||
...(solicitacao.historicoAlteracoes || []),
|
||||
{
|
||||
data: Date.now(),
|
||||
usuarioId: args.gestorId,
|
||||
acao: `Reprovado: ${args.motivoReprovacao}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Liberar dias reservados de volta ao saldo
|
||||
await ctx.runMutation(internal.saldoFerias.liberarDias, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
||||
.first();
|
||||
|
||||
if (usuario) {
|
||||
await ctx.db.insert("notificacoesFerias", {
|
||||
destinatarioId: usuario._id,
|
||||
solicitacaoFeriasId: args.solicitacaoId,
|
||||
tipo: "reprovado",
|
||||
lida: false,
|
||||
mensagem: `Suas férias foram reprovadas: ${args.motivoReprovacao}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Ajustar data e aprovar
|
||||
export const ajustarEAprovar = mutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesFerias"),
|
||||
gestorId: v.id("usuarios"),
|
||||
novosPeriodos: v.array(periodoValidator),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) throw new Error("Solicitação não encontrada");
|
||||
|
||||
if (solicitacao.status !== "aguardando_aprovacao") {
|
||||
throw new Error("Esta solicitação já foi processada");
|
||||
}
|
||||
|
||||
if (args.novosPeriodos.length === 0) {
|
||||
throw new Error("É necessário adicionar pelo menos 1 período");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
|
||||
// Liberar dias antigos
|
||||
await ctx.runMutation(internal.saldoFerias.liberarDias, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Calcular novos dias e reservar
|
||||
let totalNovosDias = 0;
|
||||
for (const p of args.novosPeriodos) {
|
||||
totalNovosDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.saldoFerias.reservarDias, {
|
||||
funcionarioId: solicitacao.funcionarioId,
|
||||
anoReferencia: solicitacao.anoReferencia,
|
||||
totalDias: totalNovosDias,
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "data_ajustada_aprovada",
|
||||
periodos: args.novosPeriodos,
|
||||
gestorId: args.gestorId,
|
||||
dataAprovacao: Date.now(),
|
||||
historicoAlteracoes: [
|
||||
...(solicitacao.historicoAlteracoes || []),
|
||||
{
|
||||
data: Date.now(),
|
||||
usuarioId: args.gestorId,
|
||||
acao: "Data ajustada e aprovada",
|
||||
periodosAnteriores: solicitacao.periodos,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Atualizar saldo (marcar como usado)
|
||||
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
|
||||
solicitacaoId: args.solicitacaoId,
|
||||
});
|
||||
|
||||
// Notificar funcionário
|
||||
if (funcionario) {
|
||||
const usuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
||||
.first();
|
||||
|
||||
if (usuario) {
|
||||
await ctx.db.insert("notificacoesFerias", {
|
||||
destinatarioId: usuario._id,
|
||||
solicitacaoFeriasId: args.solicitacaoId,
|
||||
tipo: "data_ajustada",
|
||||
lida: false,
|
||||
mensagem: "Suas férias foram aprovadas com ajuste de datas",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Verificar status de férias automático
|
||||
export const verificarStatusFerias = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
returns: v.union(v.literal("ativo"), v.literal("em_ferias")),
|
||||
handler: async (ctx, args) => {
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
const solicitacoesAprovadas = await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario_and_status", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
.eq("status", "aprovado")
|
||||
)
|
||||
.collect();
|
||||
|
||||
const solicitacoesAjustadas = await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario_and_status", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
.eq("status", "data_ajustada_aprovada")
|
||||
)
|
||||
.collect();
|
||||
|
||||
const todasSolicitacoes = [...solicitacoesAprovadas, ...solicitacoesAjustadas];
|
||||
|
||||
for (const solicitacao of todasSolicitacoes) {
|
||||
for (const periodo of solicitacao.periodos) {
|
||||
const inicio = new Date(periodo.dataInicio);
|
||||
const fim = new Date(periodo.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(23, 59, 59, 999);
|
||||
|
||||
if (hoje >= inicio && hoje <= fim) {
|
||||
return "em_ferias";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "ativo";
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter notificações não lidas
|
||||
export const obterNotificacoesNaoLidas = query({
|
||||
args: { usuarioId: v.id("usuarios") },
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("notificacoesFerias")
|
||||
.withIndex("by_destinatario_and_lida", (q) =>
|
||||
q.eq("destinatarioId", args.usuarioId).eq("lida", false)
|
||||
)
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Marcar notificação como lida
|
||||
export const marcarComoLida = mutation({
|
||||
args: { notificacaoId: v.id("notificacoesFerias") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.notificacaoId, { lida: true });
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Internal Mutation: Atualizar status de todos os funcionários
|
||||
export const atualizarStatusTodosFuncionarios = internalMutation({
|
||||
args: {},
|
||||
returns: v.null(),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
|
||||
for (const func of funcionarios) {
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
const solicitacoesAprovadas = await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario_and_status", (q) =>
|
||||
q.eq("funcionarioId", func._id)
|
||||
.eq("status", "aprovado")
|
||||
)
|
||||
.collect();
|
||||
|
||||
const solicitacoesAjustadas = await ctx.db
|
||||
.query("solicitacoesFerias")
|
||||
.withIndex("by_funcionario_and_status", (q) =>
|
||||
q.eq("funcionarioId", func._id)
|
||||
.eq("status", "data_ajustada_aprovada")
|
||||
)
|
||||
.collect();
|
||||
|
||||
const todasSolicitacoes = [...solicitacoesAprovadas, ...solicitacoesAjustadas];
|
||||
|
||||
let emFerias = false;
|
||||
for (const solicitacao of todasSolicitacoes) {
|
||||
for (const periodo of solicitacao.periodos) {
|
||||
const inicio = new Date(periodo.dataInicio);
|
||||
const fim = new Date(periodo.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(23, 59, 59, 999);
|
||||
|
||||
if (hoje >= inicio && hoje <= fim) {
|
||||
emFerias = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (emFerias) break;
|
||||
}
|
||||
|
||||
const novoStatus = emFerias ? "em_ferias" : "ativo";
|
||||
|
||||
if (func.statusFerias !== novoStatus) {
|
||||
await ctx.db.patch(func._id, { statusFerias: novoStatus });
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ const aposentadoValidator = v.optional(
|
||||
|
||||
export const getAll = query({
|
||||
args: {},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx) => {
|
||||
// Autorização: listar funcionários
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
@@ -70,6 +71,7 @@ export const getAll = query({
|
||||
|
||||
export const getById = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
// Autorização: ver funcionário
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
@@ -84,7 +86,7 @@ export const create = mutation({
|
||||
args: {
|
||||
// Campos obrigatórios
|
||||
nome: v.string(),
|
||||
matricula: v.string(),
|
||||
matricula: v.optional(v.string()),
|
||||
simboloId: v.id("simbolos"),
|
||||
nascimento: v.string(),
|
||||
rg: v.string(),
|
||||
@@ -190,13 +192,15 @@ export const create = mutation({
|
||||
throw new Error("CPF já cadastrado");
|
||||
}
|
||||
|
||||
// Unicidade: Matrícula
|
||||
const matriculaExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.unique();
|
||||
if (matriculaExists) {
|
||||
throw new Error("Matrícula já cadastrada");
|
||||
// Unicidade: Matrícula (apenas se fornecida)
|
||||
if (args.matricula) {
|
||||
const matriculaExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.unique();
|
||||
if (matriculaExists) {
|
||||
throw new Error("Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco.");
|
||||
}
|
||||
}
|
||||
|
||||
const novoFuncionarioId = await ctx.db.insert("funcionarios", args as any);
|
||||
@@ -209,7 +213,7 @@ export const update = mutation({
|
||||
id: v.id("funcionarios"),
|
||||
// Campos obrigatórios
|
||||
nome: v.string(),
|
||||
matricula: v.string(),
|
||||
matricula: v.optional(v.string()),
|
||||
simboloId: v.id("simbolos"),
|
||||
nascimento: v.string(),
|
||||
rg: v.string(),
|
||||
@@ -315,13 +319,15 @@ export const update = mutation({
|
||||
throw new Error("CPF já cadastrado");
|
||||
}
|
||||
|
||||
// Unicidade: Matrícula (excluindo o próprio registro)
|
||||
const matriculaExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.unique();
|
||||
if (matriculaExists && matriculaExists._id !== args.id) {
|
||||
throw new Error("Matrícula já cadastrada");
|
||||
// Unicidade: Matrícula (apenas se fornecida, excluindo o próprio registro)
|
||||
if (args.matricula) {
|
||||
const matriculaExists = await ctx.db
|
||||
.query("funcionarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.unique();
|
||||
if (matriculaExists && matriculaExists._id !== args.id) {
|
||||
throw new Error("Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco.");
|
||||
}
|
||||
}
|
||||
|
||||
const { id, ...updateData } = args;
|
||||
@@ -348,6 +354,7 @@ export const remove = mutation({
|
||||
// Query para obter ficha completa para impressão
|
||||
export const getFichaCompleta = query({
|
||||
args: { id: v.id("funcionarios") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "funcionarios",
|
||||
@@ -361,15 +368,55 @@ export const getFichaCompleta = query({
|
||||
// Buscar informações do símbolo
|
||||
const simbolo = await ctx.db.get(funcionario.simboloId);
|
||||
|
||||
// Buscar cursos do funcionário
|
||||
const cursos = await ctx.db
|
||||
.query("cursos")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.id))
|
||||
.collect();
|
||||
|
||||
// Buscar URLs dos certificados
|
||||
const cursosComUrls = await Promise.all(
|
||||
cursos.map(async (curso) => {
|
||||
let certificadoUrl = null;
|
||||
if (curso.certificadoId) {
|
||||
certificadoUrl = await ctx.storage.getUrl(curso.certificadoId);
|
||||
}
|
||||
return {
|
||||
...curso,
|
||||
certificadoUrl,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...funcionario,
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
descricao: simbolo.descricao,
|
||||
valor: simbolo.valor,
|
||||
// campos adicionais, se existirem no símbolo
|
||||
tipo: (simbolo as any).tipo,
|
||||
vencValor: (simbolo as any).vencValor,
|
||||
repValor: (simbolo as any).repValor,
|
||||
valor: (simbolo as any).valor,
|
||||
}
|
||||
: null,
|
||||
cursos: cursosComUrls,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Configurar gestor (apenas para TI_MASTER)
|
||||
export const configurarGestor = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
gestorId: v.optional(v.id("usuarios")),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
gestorId: args.gestorId,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
171
packages/backend/convex/migrarParaTimes.ts
Normal file
171
packages/backend/convex/migrarParaTimes.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { internalMutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
/**
|
||||
* Migração: Converte estrutura antiga de gestores individuais para times
|
||||
*
|
||||
* Esta função cria automaticamente times baseados nos gestores existentes
|
||||
* e adiciona os funcionários subordinados aos respectivos times.
|
||||
*
|
||||
* Execute uma vez via dashboard do Convex:
|
||||
* Settings > Functions > Internal > migrarParaTimes > executar
|
||||
*/
|
||||
export const executar = internalMutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
timesCreated: v.number(),
|
||||
funcionariosAtribuidos: v.number(),
|
||||
erros: v.array(v.string()),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const erros: string[] = [];
|
||||
let timesCreated = 0;
|
||||
let funcionariosAtribuidos = 0;
|
||||
|
||||
try {
|
||||
// 1. Buscar todos os funcionários que têm gestor definido
|
||||
const funcionariosComGestor = await ctx.db
|
||||
.query("funcionarios")
|
||||
.filter((q) => q.neq(q.field("gestorId"), undefined))
|
||||
.collect();
|
||||
|
||||
if (funcionariosComGestor.length === 0) {
|
||||
return {
|
||||
timesCreated: 0,
|
||||
funcionariosAtribuidos: 0,
|
||||
erros: ["Nenhum funcionário com gestor configurado encontrado"],
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Agrupar funcionários por gestor
|
||||
const gestoresMap = new Map<string, any[]>();
|
||||
|
||||
for (const funcionario of funcionariosComGestor) {
|
||||
if (!funcionario.gestorId) continue;
|
||||
|
||||
const gestorId = funcionario.gestorId;
|
||||
if (!gestoresMap.has(gestorId)) {
|
||||
gestoresMap.set(gestorId, []);
|
||||
}
|
||||
gestoresMap.get(gestorId)!.push(funcionario);
|
||||
}
|
||||
|
||||
// 3. Para cada gestor, criar um time
|
||||
for (const [gestorId, subordinados] of gestoresMap.entries()) {
|
||||
try {
|
||||
const gestor = await ctx.db.get(gestorId as any);
|
||||
|
||||
if (!gestor) {
|
||||
erros.push(`Gestor ${gestorId} não encontrado`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar se já existe time para este gestor
|
||||
const timeExistente = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor", (q) => q.eq("gestorId", gestorId as any))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
let timeId;
|
||||
|
||||
if (timeExistente) {
|
||||
timeId = timeExistente._id;
|
||||
} else {
|
||||
// Criar novo time
|
||||
timeId = await ctx.db.insert("times", {
|
||||
nome: `Equipe ${gestor.nome}`,
|
||||
descricao: `Time gerenciado por ${gestor.nome} (migração automática)`,
|
||||
gestorId: gestorId as any,
|
||||
ativo: true,
|
||||
cor: "#3B82F6",
|
||||
});
|
||||
timesCreated++;
|
||||
}
|
||||
|
||||
// Adicionar membros ao time
|
||||
for (const funcionario of subordinados) {
|
||||
try {
|
||||
// Verificar se já está em algum time
|
||||
const membroExistente = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", funcionario._id))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (!membroExistente) {
|
||||
await ctx.db.insert("timesMembros", {
|
||||
timeId: timeId,
|
||||
funcionarioId: funcionario._id,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
funcionariosAtribuidos++;
|
||||
}
|
||||
} catch (e: any) {
|
||||
erros.push(`Erro ao adicionar ${funcionario.nome} ao time: ${e.message}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
erros.push(`Erro ao processar gestor ${gestorId}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timesCreated,
|
||||
funcionariosAtribuidos,
|
||||
erros,
|
||||
};
|
||||
} catch (e: any) {
|
||||
erros.push(`Erro geral na migração: ${e.message}`);
|
||||
return {
|
||||
timesCreated,
|
||||
funcionariosAtribuidos,
|
||||
erros,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Função auxiliar para limpar times inativos antigos
|
||||
*/
|
||||
export const limparTimesInativos = internalMutation({
|
||||
args: {
|
||||
diasInativos: v.optional(v.number()),
|
||||
},
|
||||
returns: v.number(),
|
||||
handler: async (ctx, args) => {
|
||||
const diasLimite = args.diasInativos || 30;
|
||||
const dataLimite = Date.now() - (diasLimite * 24 * 60 * 60 * 1000);
|
||||
|
||||
const timesInativos = await ctx.db
|
||||
.query("times")
|
||||
.filter((q) => q.eq(q.field("ativo"), false))
|
||||
.collect();
|
||||
|
||||
let removidos = 0;
|
||||
|
||||
for (const time of timesInativos) {
|
||||
if (time._creationTime < dataLimite) {
|
||||
// Remover membros inativos do time
|
||||
const membrosInativos = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time", (q) => q.eq("timeId", time._id))
|
||||
.filter((q) => q.eq(q.field("ativo"), false))
|
||||
.collect();
|
||||
|
||||
for (const membro of membrosInativos) {
|
||||
await ctx.db.delete(membro._id);
|
||||
}
|
||||
|
||||
// Remover o time
|
||||
await ctx.db.delete(time._id);
|
||||
removidos++;
|
||||
}
|
||||
}
|
||||
|
||||
return removidos;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -208,3 +208,4 @@ export const removerAdminAntigo = internalMutation({
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,146 +1,562 @@
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, internalMutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Obter estatísticas em tempo real do sistema
|
||||
* Helper para obter usuário autenticado
|
||||
*/
|
||||
export const getStatusSistema = query({
|
||||
args: {},
|
||||
async function getUsuarioAutenticado(ctx: any) {
|
||||
const usuariosOnline = await ctx.db.query("usuarios").collect();
|
||||
const usuarioOnline = usuariosOnline.find(
|
||||
(u: any) => u.statusPresenca === "online"
|
||||
);
|
||||
return usuarioOnline || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Salvar métricas do sistema
|
||||
*/
|
||||
export const salvarMetricas = mutation({
|
||||
args: {
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
networkLatency: v.optional(v.number()),
|
||||
storageUsed: v.optional(v.number()),
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
},
|
||||
returns: v.object({
|
||||
usuariosOnline: v.number(),
|
||||
totalRegistros: v.number(),
|
||||
tempoMedioResposta: v.number(),
|
||||
memoriaUsada: v.number(),
|
||||
cpuUsada: v.number(),
|
||||
ultimaAtualizacao: v.number(),
|
||||
success: v.boolean(),
|
||||
metricId: v.optional(v.id("systemMetrics")),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
// Contar usuários online (sessões ativas nos últimos 5 minutos)
|
||||
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
|
||||
const sessoesAtivas = await ctx.db
|
||||
.query("sessoes")
|
||||
.filter((q) =>
|
||||
q.and(
|
||||
q.eq(q.field("ativo"), true),
|
||||
q.gt(q.field("criadoEm"), cincoMinutosAtras)
|
||||
)
|
||||
)
|
||||
handler: async (ctx, args) => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Salvar métricas
|
||||
const metricId = await ctx.db.insert("systemMetrics", {
|
||||
timestamp,
|
||||
cpuUsage: args.cpuUsage,
|
||||
memoryUsage: args.memoryUsage,
|
||||
networkLatency: args.networkLatency,
|
||||
storageUsed: args.storageUsed,
|
||||
usuariosOnline: args.usuariosOnline,
|
||||
mensagensPorMinuto: args.mensagensPorMinuto,
|
||||
tempoRespostaMedio: args.tempoRespostaMedio,
|
||||
errosCount: args.errosCount,
|
||||
});
|
||||
|
||||
// Verificar alertas após salvar métricas
|
||||
await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, {
|
||||
metricId,
|
||||
});
|
||||
|
||||
// Limpar métricas antigas (mais de 30 dias)
|
||||
const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
const metricasAntigas = await ctx.db
|
||||
.query("systemMetrics")
|
||||
.withIndex("by_timestamp", (q) => q.lt("timestamp", dataLimite))
|
||||
.collect();
|
||||
const usuariosOnline = sessoesAtivas.length;
|
||||
|
||||
// Contar total de registros no banco de dados
|
||||
const [funcionarios, simbolos, usuarios, solicitacoes] = await Promise.all([
|
||||
ctx.db.query("funcionarios").collect(),
|
||||
ctx.db.query("simbolos").collect(),
|
||||
ctx.db.query("usuarios").collect(),
|
||||
ctx.db.query("solicitacoesAcesso").collect(),
|
||||
]);
|
||||
const totalRegistros = funcionarios.length + simbolos.length + usuarios.length + solicitacoes.length;
|
||||
|
||||
// Calcular tempo médio de resposta (simulado baseado em logs recentes)
|
||||
const logsRecentes = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.order("desc")
|
||||
.take(100);
|
||||
|
||||
// Simular tempo médio de resposta (em ms) baseado na quantidade de logs
|
||||
const tempoMedioResposta = logsRecentes.length > 0
|
||||
? Math.round(50 + Math.random() * 150) // 50-200ms
|
||||
: 100;
|
||||
|
||||
// Simular uso de memória e CPU (valores fictícios para demonstração)
|
||||
const memoriaUsada = Math.round(45 + Math.random() * 15); // 45-60%
|
||||
const cpuUsada = Math.round(20 + Math.random() * 30); // 20-50%
|
||||
for (const metrica of metricasAntigas) {
|
||||
await ctx.db.delete(metrica._id);
|
||||
}
|
||||
|
||||
return {
|
||||
usuariosOnline,
|
||||
totalRegistros,
|
||||
tempoMedioResposta,
|
||||
memoriaUsada,
|
||||
cpuUsada,
|
||||
ultimaAtualizacao: Date.now(),
|
||||
success: true,
|
||||
metricId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter histórico de atividades do banco de dados (últimos 60 segundos)
|
||||
* Configurar ou atualizar alerta
|
||||
*/
|
||||
export const getAtividadeBancoDados = query({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
historico: v.array(
|
||||
v.object({
|
||||
timestamp: v.number(),
|
||||
entradas: v.number(),
|
||||
saidas: v.number(),
|
||||
})
|
||||
export const configurarAlerta = mutation({
|
||||
args: {
|
||||
alertId: v.optional(v.id("alertConfigurations")),
|
||||
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(),
|
||||
},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
alertId: v.id("alertConfigurations"),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const agora = Date.now();
|
||||
const umMinutoAtras = agora - 60 * 1000;
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getUsuarioAutenticado(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error("Não autenticado");
|
||||
}
|
||||
|
||||
// Obter logs de acesso do último minuto
|
||||
const logsRecentes = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.filter((q) => q.gt(q.field("timestamp"), umMinutoAtras))
|
||||
.collect();
|
||||
let alertId: Id<"alertConfigurations">;
|
||||
|
||||
// Agrupar por segundos (intervalos de 5 segundos para suavizar)
|
||||
const historico: Array<{ timestamp: number; entradas: number; saidas: number }> = [];
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const timestampInicio = umMinutoAtras + i * 5000;
|
||||
const timestampFim = timestampInicio + 5000;
|
||||
|
||||
const logsNoIntervalo = logsRecentes.filter(
|
||||
(log) => log.timestamp >= timestampInicio && log.timestamp < timestampFim
|
||||
);
|
||||
|
||||
const entradas = logsNoIntervalo.filter((log) => log.tipo === "login").length;
|
||||
const saidas = logsNoIntervalo.filter((log) => log.tipo === "logout").length;
|
||||
|
||||
historico.push({
|
||||
timestamp: timestampInicio,
|
||||
entradas: entradas + Math.round(Math.random() * 3), // Adicionar variação simulada
|
||||
saidas: saidas + Math.round(Math.random() * 2),
|
||||
if (args.alertId) {
|
||||
// Atualizar alerta existente
|
||||
await ctx.db.patch(args.alertId, {
|
||||
metricName: args.metricName,
|
||||
threshold: args.threshold,
|
||||
operator: args.operator,
|
||||
enabled: args.enabled,
|
||||
notifyByEmail: args.notifyByEmail,
|
||||
notifyByChat: args.notifyByChat,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
alertId = args.alertId;
|
||||
} else {
|
||||
// Criar novo alerta
|
||||
alertId = await ctx.db.insert("alertConfigurations", {
|
||||
metricName: args.metricName,
|
||||
threshold: args.threshold,
|
||||
operator: args.operator,
|
||||
enabled: args.enabled,
|
||||
notifyByEmail: args.notifyByEmail,
|
||||
notifyByChat: args.notifyByChat,
|
||||
createdBy: usuario._id,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return { historico };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter distribuição de tipos de requisições
|
||||
*/
|
||||
export const getDistribuicaoRequisicoes = query({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
queries: v.number(),
|
||||
mutations: v.number(),
|
||||
leituras: v.number(),
|
||||
escritas: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const logs = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.order("desc")
|
||||
.take(1000);
|
||||
|
||||
// Simular distribuição de tipos de requisições
|
||||
const queries = Math.round(logs.length * 0.6 + Math.random() * 50);
|
||||
const mutations = Math.round(logs.length * 0.3 + Math.random() * 30);
|
||||
const leituras = Math.round(logs.length * 0.7 + Math.random() * 40);
|
||||
const escritas = Math.round(logs.length * 0.3 + Math.random() * 20);
|
||||
|
||||
return {
|
||||
queries,
|
||||
mutations,
|
||||
leituras,
|
||||
escritas,
|
||||
success: true,
|
||||
alertId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar todas as configurações de alerta
|
||||
*/
|
||||
export const listarAlertas = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("alertConfigurations"),
|
||||
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(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const alertas = await ctx.db.query("alertConfigurations").collect();
|
||||
return alertas;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter métricas com filtros
|
||||
*/
|
||||
export const obterMetricas = query({
|
||||
args: {
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number()),
|
||||
metricName: v.optional(v.string()),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
networkLatency: v.optional(v.number()),
|
||||
storageUsed: v.optional(v.number()),
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
let query = ctx.db.query("systemMetrics");
|
||||
|
||||
// Filtrar por data se fornecido
|
||||
if (args.dataInicio !== undefined || args.dataFim !== undefined) {
|
||||
query = query.withIndex("by_timestamp", (q) => {
|
||||
if (args.dataInicio !== undefined && args.dataFim !== undefined) {
|
||||
return q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim);
|
||||
} else if (args.dataInicio !== undefined) {
|
||||
return q.gte("timestamp", args.dataInicio);
|
||||
} else {
|
||||
return q.lte("timestamp", args.dataFim!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let metricas = await query.order("desc").collect();
|
||||
|
||||
// Limitar resultados
|
||||
if (args.limit !== undefined && args.limit > 0) {
|
||||
metricas = metricas.slice(0, args.limit);
|
||||
}
|
||||
|
||||
return metricas;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter métricas mais recentes (última hora)
|
||||
*/
|
||||
export const obterMetricasRecentes = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
networkLatency: v.optional(v.number()),
|
||||
storageUsed: v.optional(v.number()),
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
const metricas = await ctx.db
|
||||
.query("systemMetrics")
|
||||
.withIndex("by_timestamp", (q) => q.gte("timestamp", umaHoraAtras))
|
||||
.order("desc")
|
||||
.take(100);
|
||||
|
||||
return metricas;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter última métrica salva
|
||||
*/
|
||||
export const obterUltimaMetrica = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
networkLatency: v.optional(v.number()),
|
||||
storageUsed: v.optional(v.number()),
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const metrica = await ctx.db
|
||||
.query("systemMetrics")
|
||||
.order("desc")
|
||||
.first();
|
||||
|
||||
return metrica || null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verificar alertas (internal)
|
||||
*/
|
||||
export const verificarAlertasInternal = internalMutation({
|
||||
args: {
|
||||
metricId: v.id("systemMetrics"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const metrica = await ctx.db.get(args.metricId);
|
||||
if (!metrica) return null;
|
||||
|
||||
// Buscar configurações de alerta ativas
|
||||
const alertasAtivos = await ctx.db
|
||||
.query("alertConfigurations")
|
||||
.withIndex("by_enabled", (q) => q.eq("enabled", true))
|
||||
.collect();
|
||||
|
||||
for (const alerta of alertasAtivos) {
|
||||
// Obter valor da métrica correspondente
|
||||
const metricValue = (metrica as any)[alerta.metricName];
|
||||
|
||||
if (metricValue === undefined) continue;
|
||||
|
||||
// Verificar se o alerta deve ser disparado
|
||||
let shouldTrigger = false;
|
||||
switch (alerta.operator) {
|
||||
case ">":
|
||||
shouldTrigger = metricValue > alerta.threshold;
|
||||
break;
|
||||
case "<":
|
||||
shouldTrigger = metricValue < alerta.threshold;
|
||||
break;
|
||||
case ">=":
|
||||
shouldTrigger = metricValue >= alerta.threshold;
|
||||
break;
|
||||
case "<=":
|
||||
shouldTrigger = metricValue <= alerta.threshold;
|
||||
break;
|
||||
case "==":
|
||||
shouldTrigger = metricValue === alerta.threshold;
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldTrigger) {
|
||||
// Verificar se já existe um alerta triggered recente (últimos 5 minutos)
|
||||
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
|
||||
const alertaRecente = await ctx.db
|
||||
.query("alertHistory")
|
||||
.withIndex("by_config", (q) =>
|
||||
q.eq("configId", alerta._id).gte("timestamp", cincoMinutosAtras)
|
||||
)
|
||||
.filter((q) => q.eq(q.field("status"), "triggered"))
|
||||
.first();
|
||||
|
||||
// Se já existe alerta recente, não disparar novamente
|
||||
if (alertaRecente) continue;
|
||||
|
||||
// Registrar alerta no histórico
|
||||
await ctx.db.insert("alertHistory", {
|
||||
configId: alerta._id,
|
||||
metricName: alerta.metricName,
|
||||
metricValue,
|
||||
threshold: alerta.threshold,
|
||||
timestamp: Date.now(),
|
||||
status: "triggered",
|
||||
notificationsSent: {
|
||||
email: alerta.notifyByEmail,
|
||||
chat: alerta.notifyByChat,
|
||||
},
|
||||
});
|
||||
|
||||
// Criar notificação no chat se configurado
|
||||
if (alerta.notifyByChat) {
|
||||
// Buscar usuários TI para notificar
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
const usuariosTI = usuarios.filter(
|
||||
(u: any) => u.role?.nome === "ti" || u.role?.nivel === 0
|
||||
);
|
||||
|
||||
for (const usuario of usuariosTI) {
|
||||
await ctx.db.insert("notificacoes", {
|
||||
usuarioId: usuario._id,
|
||||
tipo: "nova_mensagem",
|
||||
titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`,
|
||||
descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`,
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Enviar email se configurado (integração com sistema de email)
|
||||
// if (alerta.notifyByEmail) {
|
||||
// await enviarEmailAlerta(alerta, metricValue);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Gerar relatório de métricas
|
||||
*/
|
||||
export const gerarRelatorio = query({
|
||||
args: {
|
||||
dataInicio: v.number(),
|
||||
dataFim: v.number(),
|
||||
metricNames: v.optional(v.array(v.string())),
|
||||
},
|
||||
returns: v.object({
|
||||
periodo: v.object({
|
||||
inicio: v.number(),
|
||||
fim: v.number(),
|
||||
}),
|
||||
metricas: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
networkLatency: v.optional(v.number()),
|
||||
storageUsed: v.optional(v.number()),
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
estatisticas: v.object({
|
||||
cpuUsage: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
memoryUsage: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
networkLatency: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
storageUsed: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
usuariosOnline: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
mensagensPorMinuto: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
tempoRespostaMedio: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
errosCount: v.optional(v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar métricas no período
|
||||
const metricas = await ctx.db
|
||||
.query("systemMetrics")
|
||||
.withIndex("by_timestamp", (q) =>
|
||||
q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim)
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Calcular estatísticas
|
||||
const calcularEstatisticas = (
|
||||
valores: number[]
|
||||
): { min: number; max: number; avg: number } | undefined => {
|
||||
if (valores.length === 0) return undefined;
|
||||
return {
|
||||
min: Math.min(...valores),
|
||||
max: Math.max(...valores),
|
||||
avg: valores.reduce((a, b) => a + b, 0) / valores.length,
|
||||
};
|
||||
};
|
||||
|
||||
const estatisticas = {
|
||||
cpuUsage: calcularEstatisticas(
|
||||
metricas.map((m) => m.cpuUsage).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
memoryUsage: calcularEstatisticas(
|
||||
metricas.map((m) => m.memoryUsage).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
networkLatency: calcularEstatisticas(
|
||||
metricas.map((m) => m.networkLatency).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
storageUsed: calcularEstatisticas(
|
||||
metricas.map((m) => m.storageUsed).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
usuariosOnline: calcularEstatisticas(
|
||||
metricas.map((m) => m.usuariosOnline).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
mensagensPorMinuto: calcularEstatisticas(
|
||||
metricas.map((m) => m.mensagensPorMinuto).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
tempoRespostaMedio: calcularEstatisticas(
|
||||
metricas.map((m) => m.tempoRespostaMedio).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
errosCount: calcularEstatisticas(
|
||||
metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
periodo: {
|
||||
inicio: args.dataInicio,
|
||||
fim: args.dataFim,
|
||||
},
|
||||
metricas,
|
||||
estatisticas,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletar configuração de alerta
|
||||
*/
|
||||
export const deletarAlerta = mutation({
|
||||
args: {
|
||||
alertId: v.id("alertConfigurations"),
|
||||
},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.alertId);
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter histórico de alertas
|
||||
*/
|
||||
export const obterHistoricoAlertas = query({
|
||||
args: {
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("alertHistory"),
|
||||
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(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const limit = args.limit || 50;
|
||||
|
||||
const historico = await ctx.db
|
||||
.query("alertHistory")
|
||||
.order("desc")
|
||||
.take(limit);
|
||||
|
||||
return historico;
|
||||
},
|
||||
});
|
||||
|
||||
556
packages/backend/convex/saldoFerias.ts
Normal file
556
packages/backend/convex/saldoFerias.ts
Normal file
@@ -0,0 +1,556 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation, internalMutation } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* SISTEMA DE CÁLCULO DE SALDO DE FÉRIAS
|
||||
* Suporte a múltiplos regimes de trabalho
|
||||
*
|
||||
* ============================================
|
||||
* REGRAS CLT (Consolidação das Leis do Trabalho):
|
||||
* ============================================
|
||||
* - 30 dias de férias por ano trabalhado
|
||||
* - Período aquisitivo: 12 meses de trabalho
|
||||
* - Período concessivo: 12 meses após aquisitivo
|
||||
* - Pode dividir em até 3 períodos
|
||||
* - Um período deve ter no mínimo 14 dias
|
||||
* - Demais períodos: mínimo 5 dias cada
|
||||
* - Abono pecuniário: vender 1/3 das férias (10 dias) - OPCIONAL
|
||||
*
|
||||
* ============================================
|
||||
* REGRAS SERVIDOR PÚBLICO ESTADUAL DE PERNAMBUCO
|
||||
* Lei nº 6.123/1968 - Estatuto dos Funcionários Públicos Civis do Estado de PE
|
||||
* ============================================
|
||||
* - 30 dias de férias por ano de exercício
|
||||
* - Pode dividir em até 2 períodos (NÃO 3)
|
||||
* - Nenhum período pode ser inferior a 10 dias (NÃO 5)
|
||||
* - NÃO permite abono pecuniário (venda de férias)
|
||||
* - Férias devem ser gozadas no ano subsequente
|
||||
* - Servidor com mais de 10 anos: pode acumular até 2 períodos
|
||||
* - Preferência: férias no período de 20/12 a 10/01 para docentes
|
||||
* - Gestante: pode antecipar ou prorrogar férias
|
||||
*/
|
||||
|
||||
type RegimeTrabalho = "clt" | "estatutario_pe" | "estatutario_federal" | "estatutario_municipal";
|
||||
|
||||
// Configurações por regime
|
||||
const REGIMES_CONFIG = {
|
||||
clt: {
|
||||
nome: "CLT - Consolidação das Leis do Trabalho",
|
||||
maxPeriodos: 3,
|
||||
minDiasPeriodo: 5,
|
||||
minDiasPeriodoPrincipal: 14,
|
||||
abonoPermitido: true,
|
||||
maxDiasAbono: 10,
|
||||
},
|
||||
estatutario_pe: {
|
||||
nome: "Servidor Público Estadual de Pernambuco",
|
||||
maxPeriodos: 2,
|
||||
minDiasPeriodo: 10,
|
||||
minDiasPeriodoPrincipal: null, // Não há essa regra
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
},
|
||||
estatutario_federal: {
|
||||
nome: "Servidor Público Federal",
|
||||
maxPeriodos: 3,
|
||||
minDiasPeriodo: 5,
|
||||
minDiasPeriodoPrincipal: 14,
|
||||
abonoPermitido: true,
|
||||
maxDiasAbono: 10,
|
||||
},
|
||||
estatutario_municipal: {
|
||||
nome: "Servidor Público Municipal",
|
||||
maxPeriodos: 3,
|
||||
minDiasPeriodo: 10,
|
||||
minDiasPeriodoPrincipal: null,
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Helper: Calcular dias entre duas datas
|
||||
function calcularDiasEntreDatas(dataInicio: string, dataFim: string): number {
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 para incluir ambos os dias
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
// Helper: Calcular data de fim do período aquisitivo
|
||||
function calcularDataFimPeriodo(dataAdmissao: string, anosPassados: number): string {
|
||||
const dataInicio = new Date(dataAdmissao);
|
||||
dataInicio.setFullYear(dataInicio.getFullYear() + anosPassados);
|
||||
return dataInicio.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// Helper: Obter regime de trabalho do funcionário
|
||||
async function obterRegimeTrabalho(ctx: any, funcionarioId: Id<"funcionarios">): Promise<RegimeTrabalho> {
|
||||
const funcionario = await ctx.db.get(funcionarioId);
|
||||
return funcionario?.regimeTrabalho || "clt"; // Default CLT
|
||||
}
|
||||
|
||||
/**
|
||||
* Query: Obter saldo de férias de um funcionário para um ano específico
|
||||
*/
|
||||
export const obterSaldo = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal("ativo"), v.literal("vencido"), v.literal("concluido")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
regimeTrabalho: v.string(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar período aquisitivo
|
||||
const periodo = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!periodo) {
|
||||
// Se não existe, criar automaticamente
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario || !funcionario.admissaoData) return null;
|
||||
|
||||
const regime = funcionario.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
// Calcular anos desde admissão
|
||||
const dataAdmissao = new Date(funcionario.admissaoData);
|
||||
const anosDesdeAdmissao = args.anoReferencia - dataAdmissao.getFullYear();
|
||||
|
||||
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
|
||||
|
||||
const dataInicio = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao - 1);
|
||||
const dataFim = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao);
|
||||
|
||||
// Criar período aquisitivo
|
||||
await ctx.db.insert("periodosAquisitivos", {
|
||||
funcionarioId: args.funcionarioId,
|
||||
anoReferencia: args.anoReferencia,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
diasDireito: 30,
|
||||
diasUsados: 0,
|
||||
diasPendentes: 0,
|
||||
diasDisponiveis: 30,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
diasAbono: 0,
|
||||
status: "ativo",
|
||||
});
|
||||
|
||||
return {
|
||||
anoReferencia: args.anoReferencia,
|
||||
diasDireito: 30,
|
||||
diasUsados: 0,
|
||||
diasPendentes: 0,
|
||||
diasDisponiveis: 30,
|
||||
diasAbono: 0,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
status: "ativo" as const,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
regimeTrabalho: config.nome,
|
||||
};
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
const regime = funcionario?.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
return {
|
||||
anoReferencia: periodo.anoReferencia,
|
||||
diasDireito: periodo.diasDireito,
|
||||
diasUsados: periodo.diasUsados,
|
||||
diasPendentes: periodo.diasPendentes,
|
||||
diasDisponiveis: periodo.diasDisponiveis,
|
||||
diasAbono: periodo.diasAbono,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
status: periodo.status,
|
||||
dataInicio: periodo.dataInicio,
|
||||
dataFim: periodo.dataFim,
|
||||
regimeTrabalho: config.nome,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Query: Listar todos os saldos de um funcionário
|
||||
*/
|
||||
export const listarSaldos = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("periodosAquisitivos"),
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal("ativo"), v.literal("vencido"), v.literal("concluido")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const periodos = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.collect();
|
||||
|
||||
return periodos.map((p) => ({
|
||||
_id: p._id,
|
||||
anoReferencia: p.anoReferencia,
|
||||
diasDireito: p.diasDireito,
|
||||
diasUsados: p.diasUsados,
|
||||
diasPendentes: p.diasPendentes,
|
||||
diasDisponiveis: p.diasDisponiveis,
|
||||
diasAbono: p.diasAbono,
|
||||
abonoPermitido: p.abonoPermitido,
|
||||
status: p.status,
|
||||
dataInicio: p.dataInicio,
|
||||
dataFim: p.dataFim,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Query: Validar solicitação de férias (regras CLT ou Servidor Público PE)
|
||||
*/
|
||||
export const validarSolicitacao = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
periodos: v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
})
|
||||
),
|
||||
},
|
||||
returns: v.object({
|
||||
valido: v.boolean(),
|
||||
erros: v.array(v.string()),
|
||||
avisos: v.array(v.string()),
|
||||
totalDias: v.number(),
|
||||
regimeTrabalho: v.string(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const erros: string[] = [];
|
||||
const avisos: string[] = [];
|
||||
let totalDias = 0;
|
||||
|
||||
// Obter regime de trabalho
|
||||
const regime = await obterRegimeTrabalho(ctx, args.funcionarioId);
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
// Validação 1: Número de períodos
|
||||
if (args.periodos.length === 0) {
|
||||
erros.push("É necessário adicionar pelo menos 1 período de férias");
|
||||
}
|
||||
|
||||
if (args.periodos.length > config.maxPeriodos) {
|
||||
erros.push(
|
||||
`Máximo de ${config.maxPeriodos} períodos permitidos para ${config.nome}`
|
||||
);
|
||||
}
|
||||
|
||||
// Calcular dias de cada período e validar
|
||||
const diasPorPeriodo: number[] = [];
|
||||
for (const periodo of args.periodos) {
|
||||
const dias = calcularDiasEntreDatas(periodo.dataInicio, periodo.dataFim);
|
||||
diasPorPeriodo.push(dias);
|
||||
totalDias += dias;
|
||||
|
||||
// Validação 2: Mínimo de dias por período
|
||||
if (dias < config.minDiasPeriodo) {
|
||||
erros.push(
|
||||
`Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 3: CLT requer um período com 14+ dias se dividir
|
||||
if (regime === "clt" && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) {
|
||||
const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal);
|
||||
if (!temPeriodo14Dias) {
|
||||
erros.push(
|
||||
`Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 4: Verificar saldo disponível
|
||||
const periodo = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!periodo) {
|
||||
erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`);
|
||||
} else {
|
||||
if (totalDias > periodo.diasDisponiveis) {
|
||||
erros.push(
|
||||
`Total solicitado (${totalDias} dias) excede saldo disponível (${periodo.diasDisponiveis} dias)`
|
||||
);
|
||||
}
|
||||
|
||||
// Aviso: Saldo baixo
|
||||
if (periodo.diasDisponiveis < 15 && periodo.diasDisponiveis > totalDias) {
|
||||
avisos.push(
|
||||
`Após essa solicitação, restará ${periodo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}`
|
||||
);
|
||||
}
|
||||
|
||||
// Aviso: Férias vencendo
|
||||
const hoje = new Date();
|
||||
const dataFim = new Date(periodo.dataFim);
|
||||
const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diasAteVencer < 90 && diasAteVencer > 0) {
|
||||
avisos.push(
|
||||
`⚠️ Atenção: Seu período aquisitivo ${periodo.anoReferencia} vence em ${diasAteVencer} dias!`
|
||||
);
|
||||
}
|
||||
|
||||
if (diasAteVencer < 0) {
|
||||
avisos.push(
|
||||
`⚠️ URGENTE: Seu período aquisitivo ${periodo.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 5: Verificar conflitos de datas (sobreposição)
|
||||
for (let i = 0; i < args.periodos.length; i++) {
|
||||
for (let j = i + 1; j < args.periodos.length; j++) {
|
||||
const inicio1 = new Date(args.periodos[i].dataInicio);
|
||||
const fim1 = new Date(args.periodos[i].dataFim);
|
||||
const inicio2 = new Date(args.periodos[j].dataInicio);
|
||||
const fim2 = new Date(args.periodos[j].dataFim);
|
||||
|
||||
if (
|
||||
(inicio1 <= fim2 && fim1 >= inicio2) ||
|
||||
(inicio2 <= fim1 && fim2 >= inicio1)
|
||||
) {
|
||||
erros.push("Os períodos não podem se sobrepor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 6: Datas no futuro (aviso)
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
for (const periodo of args.periodos) {
|
||||
const inicio = new Date(periodo.dataInicio);
|
||||
if (inicio < hoje) {
|
||||
avisos.push("⚠️ Período(s) com data de início no passado ou hoje");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 7: Servidor PE - aviso sobre período preferencial para docentes
|
||||
if (regime === "estatutario_pe") {
|
||||
for (const periodo of args.periodos) {
|
||||
const mes = new Date(periodo.dataInicio).getMonth() + 1;
|
||||
if (mes === 12 || mes === 1) {
|
||||
avisos.push("📅 Período preferencial para docentes (20/12 a 10/01)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valido: erros.length === 0,
|
||||
erros,
|
||||
avisos,
|
||||
totalDias,
|
||||
regimeTrabalho: config.nome,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal Mutation: Atualizar saldo após aprovação de férias
|
||||
*/
|
||||
export const atualizarSaldoAposAprovacao = internalMutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesFerias"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) return null;
|
||||
|
||||
// Buscar período aquisitivo
|
||||
const periodo = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!periodo) return null;
|
||||
|
||||
// Calcular total de dias
|
||||
let totalDias = 0;
|
||||
for (const p of solicitacao.periodos) {
|
||||
totalDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
// Atualizar saldo
|
||||
await ctx.db.patch(periodo._id, {
|
||||
diasPendentes: periodo.diasPendentes - totalDias,
|
||||
diasUsados: periodo.diasUsados + totalDias,
|
||||
diasDisponiveis: periodo.diasDireito - (periodo.diasUsados + totalDias) - periodo.diasAbono,
|
||||
status: periodo.diasDireito - (periodo.diasUsados + totalDias) <= 0 ? "concluido" : periodo.status,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal Mutation: Reservar dias (ao criar solicitação)
|
||||
*/
|
||||
export const reservarDias = internalMutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
totalDias: v.number(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const periodo = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!periodo) return null;
|
||||
|
||||
await ctx.db.patch(periodo._id, {
|
||||
diasPendentes: periodo.diasPendentes + args.totalDias,
|
||||
diasDisponiveis: periodo.diasDisponiveis - args.totalDias,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal Mutation: Liberar dias (ao reprovar solicitação)
|
||||
*/
|
||||
export const liberarDias = internalMutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesFerias"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
||||
if (!solicitacao) return null;
|
||||
|
||||
const periodo = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (!periodo) return null;
|
||||
|
||||
let totalDias = 0;
|
||||
for (const p of solicitacao.periodos) {
|
||||
totalDias += p.diasCorridos;
|
||||
}
|
||||
|
||||
await ctx.db.patch(periodo._id, {
|
||||
diasPendentes: periodo.diasPendentes - totalDias,
|
||||
diasDisponiveis: periodo.diasDisponiveis + totalDias,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal Mutation: Criar períodos aquisitivos para todos os funcionários
|
||||
*/
|
||||
export const criarPeriodosAquisitivos = internalMutation({
|
||||
args: {},
|
||||
returns: v.null(),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
const anoAtual = new Date().getFullYear();
|
||||
|
||||
for (const func of funcionarios) {
|
||||
if (!func.admissaoData) continue;
|
||||
|
||||
const regime = func.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
const dataAdmissao = new Date(func.admissaoData);
|
||||
const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
|
||||
|
||||
// Criar períodos para os últimos 2 anos (atual e anterior)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const ano = anoAtual - i;
|
||||
const anosPeriodo = ano - dataAdmissao.getFullYear();
|
||||
|
||||
if (anosPeriodo < 1) continue;
|
||||
|
||||
// Verificar se já existe
|
||||
const periodoExistente = await ctx.db
|
||||
.query("periodosAquisitivos")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", func._id).eq("anoReferencia", ano)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (periodoExistente) continue;
|
||||
|
||||
const dataInicio = calcularDataFimPeriodo(func.admissaoData, anosPeriodo - 1);
|
||||
const dataFim = calcularDataFimPeriodo(func.admissaoData, anosPeriodo);
|
||||
|
||||
await ctx.db.insert("periodosAquisitivos", {
|
||||
funcionarioId: func._id,
|
||||
anoReferencia: ano,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
diasDireito: 30,
|
||||
diasUsados: 0,
|
||||
diasPendentes: 0,
|
||||
diasDisponiveis: 30,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
diasAbono: 0,
|
||||
status: "ativo",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -24,11 +24,24 @@ export default defineSchema({
|
||||
uf: v.string(),
|
||||
telefone: v.string(),
|
||||
email: v.string(),
|
||||
matricula: 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()),
|
||||
@@ -133,7 +146,8 @@ export default defineSchema({
|
||||
.index("by_simboloId", ["simboloId"])
|
||||
.index("by_simboloTipo", ["simboloTipo"])
|
||||
.index("by_cpf", ["cpf"])
|
||||
.index("by_rg", ["rg"]),
|
||||
.index("by_rg", ["rg"])
|
||||
.index("by_gestor", ["gestorId"]),
|
||||
|
||||
atestados: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
@@ -143,11 +157,109 @@ export default defineSchema({
|
||||
descricao: v.string(),
|
||||
}),
|
||||
|
||||
ferias: defineTable({
|
||||
solicitacoesFerias: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
}),
|
||||
anoReferencia: v.number(),
|
||||
status: v.union(
|
||||
v.literal("aguardando_aprovacao"),
|
||||
v.literal("aprovado"),
|
||||
v.literal("reprovado"),
|
||||
v.literal("data_ajustada_aprovada")
|
||||
),
|
||||
periodos: v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
})
|
||||
),
|
||||
observacao: v.optional(v.string()),
|
||||
motivoReprovacao: v.optional(v.string()),
|
||||
gestorId: v.optional(v.id("usuarios")),
|
||||
dataAprovacao: v.optional(v.number()),
|
||||
dataReprovacao: v.optional(v.number()),
|
||||
historicoAlteracoes: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
data: v.number(),
|
||||
usuarioId: v.id("usuarios"),
|
||||
acao: v.string(),
|
||||
periodosAnteriores: v.optional(v.array(v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
}))),
|
||||
})
|
||||
)
|
||||
),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_status", ["status"])
|
||||
.index("by_funcionario_and_status", ["funcionarioId", "status"])
|
||||
.index("by_ano", ["anoReferencia"]),
|
||||
|
||||
notificacoesFerias: defineTable({
|
||||
destinatarioId: v.id("usuarios"),
|
||||
solicitacaoFeriasId: v.id("solicitacoesFerias"),
|
||||
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"]),
|
||||
|
||||
// Períodos aquisitivos e saldos de férias
|
||||
periodosAquisitivos: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(), // Ano do período aquisitivo (ex: 2024)
|
||||
dataInicio: v.string(), // Data de início do período aquisitivo
|
||||
dataFim: v.string(), // Data de fim do período aquisitivo
|
||||
diasDireito: v.number(), // Dias de férias que tem direito (30 ou proporcional)
|
||||
diasUsados: v.number(), // Dias já usados
|
||||
diasPendentes: v.number(), // Dias em solicitações aguardando aprovação
|
||||
diasDisponiveis: v.number(), // Dias disponíveis = direito - usados - pendentes
|
||||
abonoPermitido: v.boolean(), // Se pode vender 1/3 das férias
|
||||
diasAbono: v.number(), // Dias vendidos como abono pecuniário
|
||||
status: v.union(
|
||||
v.literal("ativo"), // Período vigente
|
||||
v.literal("vencido"), // Período vencido (não tirou férias)
|
||||
v.literal("concluido") // Período totalmente utilizado
|
||||
),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"])
|
||||
.index("by_funcionario_and_status", ["funcionarioId", "status"]),
|
||||
|
||||
times: defineTable({
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id("usuarios"),
|
||||
ativo: v.boolean(),
|
||||
cor: v.optional(v.string()), // Cor para identificação visual
|
||||
}).index("by_gestor", ["gestorId"]),
|
||||
|
||||
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(),
|
||||
@@ -220,7 +332,8 @@ export default defineSchema({
|
||||
.index("by_role", ["roleId"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_status_presenca", ["statusPresenca"])
|
||||
.index("by_bloqueado", ["bloqueado"]),
|
||||
.index("by_bloqueado", ["bloqueado"])
|
||||
.index("by_funcionarioId", ["funcionarioId"]),
|
||||
|
||||
roles: defineTable({
|
||||
nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
|
||||
@@ -520,4 +633,54 @@ export default defineSchema({
|
||||
})
|
||||
.index("by_conversa", ["conversaId", "iniciouEm"])
|
||||
.index("by_usuario", ["usuarioId"]),
|
||||
|
||||
// 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"]),
|
||||
});
|
||||
|
||||
270
packages/backend/convex/times.ts
Normal file
270
packages/backend/convex/times.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
// Query: Listar todos os times
|
||||
export const listar = query({
|
||||
args: {},
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx) => {
|
||||
const times = await ctx.db.query("times").collect();
|
||||
|
||||
// Buscar gestor e contar membros de cada time
|
||||
const timesComDetalhes = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosAtivos = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
totalMembros: membrosAtivos.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComDetalhes;
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter time por ID com membros
|
||||
export const obterPorId = query({
|
||||
args: { id: v.id("times") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const time = await ctx.db.get(args.id);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", args.id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
// Buscar dados completos dos membros
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
membros,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter time do funcionário
|
||||
export const obterTimeFuncionario = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
returns: v.union(v.any(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const relacao = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (!relacao) return null;
|
||||
|
||||
const time = await ctx.db.get(relacao.timeId);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter times do gestor
|
||||
export const listarPorGestor = query({
|
||||
args: { gestorId: v.id("usuarios") },
|
||||
returns: v.array(v.any()),
|
||||
handler: async (ctx, args) => {
|
||||
const times = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor", (q) => q.eq("gestorId", args.gestorId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
const timesComMembros = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
membros,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComMembros;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Criar time
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id("usuarios"),
|
||||
cor: v.optional(v.string()),
|
||||
},
|
||||
returns: v.id("times"),
|
||||
handler: async (ctx, args) => {
|
||||
const timeId = await ctx.db.insert("times", {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
gestorId: args.gestorId,
|
||||
ativo: true,
|
||||
cor: args.cor || "#3B82F6",
|
||||
});
|
||||
|
||||
return timeId;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Atualizar time
|
||||
export const atualizar = mutation({
|
||||
args: {
|
||||
id: v.id("times"),
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id("usuarios"),
|
||||
cor: v.optional(v.string()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...dados } = args;
|
||||
await ctx.db.patch(id, dados);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Desativar time
|
||||
export const desativar = mutation({
|
||||
args: { id: v.id("times") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar o time
|
||||
await ctx.db.patch(args.id, { ativo: false });
|
||||
|
||||
// Desativar todos os membros
|
||||
const membros = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", args.id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
for (const membro of membros) {
|
||||
await ctx.db.patch(membro._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Adicionar membro ao time
|
||||
export const adicionarMembro = mutation({
|
||||
args: {
|
||||
timeId: v.id("times"),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.id("timesMembros"),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se já não está em outro time ativo
|
||||
const membroExistente = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (membroExistente) {
|
||||
throw new Error("Funcionário já está em um time ativo");
|
||||
}
|
||||
|
||||
const membroId = await ctx.db.insert("timesMembros", {
|
||||
timeId: args.timeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
|
||||
return membroId;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Remover membro do time
|
||||
export const removerMembro = mutation({
|
||||
args: { membroId: v.id("timesMembros") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.membroId, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Transferir membro para outro time
|
||||
export const transferirMembro = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novoTimeId: v.id("times"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar do time atual
|
||||
const relacaoAtual = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (relacaoAtual) {
|
||||
await ctx.db.patch(relacaoAtual._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Adicionar ao novo time
|
||||
await ctx.db.insert("timesMembros", {
|
||||
timeId: args.novoTimeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,62 @@ import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { api } from "./_generated/api";
|
||||
|
||||
/**
|
||||
* Associar funcionário a um usuário
|
||||
*/
|
||||
export const associarFuncionario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se o funcionário existe
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
|
||||
// Verificar se o funcionário já está associado a outro usuário
|
||||
const usuarioExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
||||
throw new Error(
|
||||
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})`
|
||||
);
|
||||
}
|
||||
|
||||
// Associar funcionário ao usuário
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Desassociar funcionário de um usuário
|
||||
*/
|
||||
export const desassociarFuncionario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
funcionarioId: undefined,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar novo usuário (apenas TI)
|
||||
*/
|
||||
@@ -413,6 +469,7 @@ export const obterPerfil = query({
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
funcionarioId: v.optional(v.id("funcionarios")),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
fotoPerfilUrl: v.union(v.string(), v.null()),
|
||||
@@ -500,6 +557,7 @@ export const obterPerfil = query({
|
||||
nome: usuarioAtual.nome,
|
||||
email: usuarioAtual.email,
|
||||
matricula: usuarioAtual.matricula,
|
||||
funcionarioId: usuarioAtual.funcionarioId,
|
||||
avatar: usuarioAtual.avatar,
|
||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
@@ -522,7 +580,7 @@ export const listarParaChat = query({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
matricula: v.optional(v.string()),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
fotoPerfilUrl: v.union(v.string(), v.null()),
|
||||
@@ -558,7 +616,7 @@ export const listarParaChat = query({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula,
|
||||
matricula: usuario.matricula || undefined,
|
||||
avatar: usuario.avatar,
|
||||
fotoPerfil: usuario.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
|
||||
@@ -99,3 +99,4 @@ export const removerDuplicatas = internalMutation({
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,19 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/pako": "^2.0.4",
|
||||
"@types/raf": "^3.4.3",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dicebear/avataaars": "^9.2.4",
|
||||
"convex": "^1.28.0"
|
||||
"convex": "^1.28.0",
|
||||
"nodemailer": "^7.0.10"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user