- Introduced a modal for managing user profiles, allowing for the creation and editing of profiles with improved state management. - Updated the role filtering logic to enhance type safety and readability. - Refactored UI components for better user experience, including improved button states and loading indicators. - Removed outdated code related to permissions and streamlined the overall structure for maintainability.
568 lines
16 KiB
TypeScript
568 lines
16 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query, internalMutation } from "./_generated/server";
|
|
import { internal } from "./_generated/api";
|
|
import { Id, Doc } 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)
|
|
// Retorna tipo inferido automaticamente pelo Convex
|
|
export const listarTodas = query({
|
|
args: {},
|
|
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 não especificado - TypeScript inferirá automaticamente o tipo correto
|
|
handler: async (ctx, args) => {
|
|
const solicitacoes = await ctx.db
|
|
.query("solicitacoesFerias")
|
|
.withIndex("by_funcionario", (q) =>
|
|
q.eq("funcionarioId", args.funcionarioId)
|
|
)
|
|
.order("desc")
|
|
.collect();
|
|
|
|
// Enriquecer com dados do funcionário e time
|
|
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;
|
|
},
|
|
});
|
|
|
|
// Query: Listar solicitações dos subordinados (para gestores)
|
|
// Retorna tipo inferido automaticamente pelo Convex
|
|
export const listarSolicitacoesSubordinados = query({
|
|
args: { gestorId: v.id("usuarios") },
|
|
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<Doc<"solicitacoesFerias"> & {
|
|
funcionario: Doc<"funcionarios"> | null;
|
|
time: Doc<"times"> | null;
|
|
}> = [];
|
|
|
|
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
|
|
// Retorna tipo inferido automaticamente pelo Convex
|
|
export const obterDetalhes = query({
|
|
args: { solicitacaoId: v.id("solicitacoesFerias") },
|
|
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") },
|
|
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;
|
|
},
|
|
});
|