Files
sgse-app/packages/backend/convex/ferias.ts
killer-cf 5469c50d90 feat: add svelte-sonner dependency and enhance NotificationBell component
- Added `svelte-sonner` to dependencies for improved notification handling.
- Refactored the `NotificationBell.svelte` component for better readability and maintainability, including code formatting and structure improvements.
- Updated `package.json` and `bun.lock` to reflect the new dependency.
2025-10-30 14:55:51 -03:00

536 lines
15 KiB
TypeScript

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