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:
475
packages/backend/convex/ferias.ts
Normal file
475
packages/backend/convex/ferias.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user