feat: implement vacation management system with request approval, notification handling, and employee training tracking; enhance UI components for improved user experience

This commit is contained in:
2025-10-29 22:05:29 -03:00
parent f219340cd8
commit 16bcd2ac25
21 changed files with 3910 additions and 617 deletions

View File

@@ -18,9 +18,11 @@ import type * as betterAuth_auth from "../betterAuth/auth.js";
import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.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";
@@ -29,6 +31,7 @@ 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";
@@ -37,6 +40,7 @@ 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";
@@ -66,9 +70,11 @@ declare const fullApi: ApiFromModules<{
chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail;
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;
@@ -77,6 +83,7 @@ declare const fullApi: ApiFromModules<{
logsAtividades: typeof logsAtividades;
logsLogin: typeof logsLogin;
menuPermissoes: typeof menuPermissoes;
migrarParaTimes: typeof migrarParaTimes;
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
monitoramento: typeof monitoramento;
perfisCustomizados: typeof perfisCustomizados;
@@ -85,6 +92,7 @@ declare const fullApi: ApiFromModules<{
simbolos: typeof simbolos;
solicitacoesAcesso: typeof solicitacoesAcesso;
templatesMensagens: typeof templatesMensagens;
times: typeof times;
todos: typeof todos;
usuarios: typeof usuarios;
verificarMatriculas: typeof verificarMatriculas;

View File

@@ -17,5 +17,13 @@ crons.interval(
internal.chat.limparIndicadoresDigitacao
);
// Atualizar status de férias dos funcionários diariamente
crons.interval(
"atualizar-status-ferias",
{ hours: 24 },
internal.ferias.atualizarStatusTodosFuncionarios,
{}
);
export default crons;

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

View File

@@ -0,0 +1,475 @@
import { v } from "convex/values";
import { mutation, query, internalMutation } from "./_generated/server";
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
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");
}
if (args.periodos.length > 3) {
throw new Error("Máximo de 3 períodos permitidos");
}
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) throw new Error("Funcionário não encontrado");
// 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",
},
],
});
// 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}`,
},
],
});
// 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");
}
if (args.novosPeriodos.length > 3) {
throw new Error("Máximo de 3 períodos permitidos");
}
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
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,
},
],
});
// 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;
},
});

View File

@@ -48,7 +48,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(),
@@ -149,13 +149,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);
@@ -168,7 +170,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(),
@@ -269,13 +271,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;
@@ -306,13 +310,52 @@ 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,
tipo: simbolo.tipo,
vencValor: simbolo.vencValor,
repValor: simbolo.repValor,
valor: simbolo.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;
},
});

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

View File

@@ -26,11 +26,16 @@ 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")
)),
// Dados Pessoais Adicionais (opcionais)
nomePai: v.optional(v.string()),
@@ -135,7 +140,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"),
@@ -145,11 +151,87 @@ 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"]),
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(),

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