feat: implement absence management features in the dashboard
- Added functionality for managing absence requests, including listing, approving, and rejecting requests. - Enhanced the user interface to display statistics and pending requests for better oversight. - Updated backend schema to support absence requests and notifications, ensuring data integrity and efficient handling. - Integrated new components for absence request forms and approval workflows, improving user experience and administrative efficiency.
This commit is contained in:
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -11,6 +11,7 @@
|
||||
import type * as actions_email from "../actions/email.js";
|
||||
import type * as actions_smtp from "../actions/smtp.js";
|
||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||
import type * as ausencias from "../ausencias.js";
|
||||
import type * as autenticacao from "../autenticacao.js";
|
||||
import type * as auth_utils from "../auth/utils.js";
|
||||
import type * as chat from "../chat.js";
|
||||
@@ -60,6 +61,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"actions/email": typeof actions_email;
|
||||
"actions/smtp": typeof actions_smtp;
|
||||
atestadosLicencas: typeof atestadosLicencas;
|
||||
ausencias: typeof ausencias;
|
||||
autenticacao: typeof autenticacao;
|
||||
"auth/utils": typeof auth_utils;
|
||||
chat: typeof chat;
|
||||
|
||||
666
packages/backend/convex/ausencias.ts
Normal file
666
packages/backend/convex/ausencias.ts
Normal file
@@ -0,0 +1,666 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
||||
import { internal, api } from "./_generated/api";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
|
||||
// Query: Listar todas as solicitações (para RH)
|
||||
export const listarTodas = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const solicitacoes = await ctx.db.query("solicitacoesAusencias").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.criadoEm - a.criadoEm
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Listar solicitações do funcionário
|
||||
export const listarMinhasSolicitacoes = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
handler: async (ctx, args) => {
|
||||
const solicitacoes = await ctx.db
|
||||
.query("solicitacoesAusencias")
|
||||
.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)
|
||||
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<"solicitacoesAusencias"> & {
|
||||
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("solicitacoesAusencias")
|
||||
.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.criadoEm - a.criadoEm);
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter detalhes completos de uma solicitação
|
||||
export const obterDetalhes = query({
|
||||
args: { solicitacaoId: v.id("solicitacoesAusencias") },
|
||||
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);
|
||||
}
|
||||
|
||||
// Buscar time do funcionário
|
||||
const membroTime = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) =>
|
||||
q.eq("funcionarioId", solicitacao.funcionarioId)
|
||||
)
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
let time = null;
|
||||
if (membroTime) {
|
||||
time = await ctx.db.get(membroTime.timeId);
|
||||
}
|
||||
|
||||
return {
|
||||
...solicitacao,
|
||||
funcionario,
|
||||
gestor,
|
||||
time,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Obter notificações não lidas
|
||||
export const obterNotificacoesNaoLidas = query({
|
||||
args: { usuarioId: v.id("usuarios") },
|
||||
handler: async (ctx, args) => {
|
||||
const notificacoes = await ctx.db
|
||||
.query("notificacoesAusencias")
|
||||
.withIndex("by_destinatario_and_lida", (q) =>
|
||||
q.eq("destinatarioId", args.usuarioId).eq("lida", false)
|
||||
)
|
||||
.order("desc")
|
||||
.collect();
|
||||
|
||||
return notificacoes;
|
||||
},
|
||||
});
|
||||
|
||||
// Query: Contar solicitações pendentes para gestor
|
||||
export const contarPendentesGestor = 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();
|
||||
|
||||
let totalPendentes = 0;
|
||||
|
||||
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();
|
||||
|
||||
// Contar solicitações pendentes de cada membro
|
||||
for (const membro of membros) {
|
||||
const pendentes = await ctx.db
|
||||
.query("solicitacoesAusencias")
|
||||
.withIndex("by_funcionario_and_status", (q) =>
|
||||
q
|
||||
.eq("funcionarioId", membro.funcionarioId)
|
||||
.eq("status", "aguardando_aprovacao")
|
||||
)
|
||||
.collect();
|
||||
totalPendentes += pendentes.length;
|
||||
}
|
||||
}
|
||||
|
||||
return totalPendentes;
|
||||
},
|
||||
});
|
||||
|
||||
// Helper: Verificar se há sobreposição de datas
|
||||
function verificarSobreposicao(
|
||||
inicio1: string,
|
||||
fim1: string,
|
||||
inicio2: string,
|
||||
fim2: string
|
||||
): boolean {
|
||||
const d1Inicio = new Date(inicio1);
|
||||
const d1Fim = new Date(fim1);
|
||||
const d2Inicio = new Date(inicio2);
|
||||
const d2Fim = new Date(fim2);
|
||||
|
||||
return d1Inicio <= d2Fim && d2Inicio <= d1Fim;
|
||||
}
|
||||
|
||||
// Helper: Encontrar gestor do funcionário
|
||||
async function encontrarGestorDoFuncionario(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
funcionarioId: Id<"funcionarios">
|
||||
): Promise<Id<"usuarios"> | null> {
|
||||
const membroTime = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (!membroTime) return null;
|
||||
|
||||
const time = await ctx.db.get(membroTime.timeId);
|
||||
if (!time) return null;
|
||||
|
||||
return time.gestorId;
|
||||
}
|
||||
|
||||
// Mutation: Criar solicitação de ausência
|
||||
export const criarSolicitacao = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
motivo: v.string(),
|
||||
},
|
||||
returns: v.id("solicitacoesAusencias"),
|
||||
handler: async (ctx, args) => {
|
||||
// Validações
|
||||
if (args.motivo.trim().length < 10) {
|
||||
throw new Error("O motivo deve ter no mínimo 10 caracteres");
|
||||
}
|
||||
|
||||
const dataInicio = new Date(args.dataInicio);
|
||||
const dataFim = new Date(args.dataFim);
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
if (dataInicio < hoje) {
|
||||
throw new Error("A data de início não pode ser no passado");
|
||||
}
|
||||
|
||||
if (dataFim < dataInicio) {
|
||||
throw new Error("A data de fim deve ser maior ou igual à data de início");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
|
||||
// Verificar sobreposição com outras solicitações aprovadas ou pendentes
|
||||
const solicitacoesExistentes = await ctx.db
|
||||
.query("solicitacoesAusencias")
|
||||
.withIndex("by_funcionario", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
)
|
||||
.collect();
|
||||
|
||||
for (const solic of solicitacoesExistentes) {
|
||||
if (
|
||||
solic.status === "aprovado" ||
|
||||
solic.status === "aguardando_aprovacao"
|
||||
) {
|
||||
if (
|
||||
verificarSobreposicao(
|
||||
args.dataInicio,
|
||||
args.dataFim,
|
||||
solic.dataInicio,
|
||||
solic.dataFim
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Já existe uma solicitação aprovada ou pendente para este período"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Criar solicitação
|
||||
const solicitacaoId = await ctx.db.insert("solicitacoesAusencias", {
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim,
|
||||
motivo: args.motivo.trim(),
|
||||
status: "aguardando_aprovacao",
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Encontrar gestor do funcionário
|
||||
const gestorId = await encontrarGestorDoFuncionario(
|
||||
ctx,
|
||||
args.funcionarioId
|
||||
);
|
||||
|
||||
if (gestorId) {
|
||||
// Criar notificação in-app para gestor
|
||||
await ctx.db.insert("notificacoesAusencias", {
|
||||
destinatarioId: gestorId,
|
||||
solicitacaoAusenciaId: solicitacaoId,
|
||||
tipo: "nova_solicitacao",
|
||||
lida: false,
|
||||
mensagem: `${funcionario.nome} solicitou uma ausência de ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}`,
|
||||
});
|
||||
|
||||
// Buscar usuário do gestor para enviar email e chat
|
||||
const gestorUsuario = await ctx.db.get(gestorId);
|
||||
const funcionarioUsuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (gestorUsuario && funcionarioUsuario) {
|
||||
// Enviar email ao gestor
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: gestorUsuario.email,
|
||||
destinatarioId: gestorId,
|
||||
assunto: `Nova Solicitação de Ausência - ${funcionario.nome}`,
|
||||
corpo: `<p>Olá ${gestorUsuario.nome},</p>
|
||||
<p>O funcionário <strong>${funcionario.nome}</strong> solicitou uma ausência:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}</li>
|
||||
<li><strong>Motivo:</strong> ${args.motivo}</li>
|
||||
</ul>
|
||||
<p>Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.</p>`,
|
||||
enviadoPorId: funcionarioUsuario._id,
|
||||
});
|
||||
|
||||
// Criar ou obter conversa entre gestor e funcionário
|
||||
const conversasExistentes = await ctx.db
|
||||
.query("conversas")
|
||||
.filter((q) => q.eq(q.field("tipo"), "individual"))
|
||||
.collect();
|
||||
|
||||
let conversaId: Id<"conversas"> | null = null;
|
||||
for (const conversa of conversasExistentes) {
|
||||
if (
|
||||
conversa.participantes.length === 2 &&
|
||||
conversa.participantes.includes(gestorId) &&
|
||||
conversa.participantes.includes(funcionarioUsuario._id)
|
||||
) {
|
||||
conversaId = conversa._id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversaId) {
|
||||
conversaId = await ctx.db.insert("conversas", {
|
||||
tipo: "individual",
|
||||
participantes: [gestorId, funcionarioUsuario._id],
|
||||
criadoPor: funcionarioUsuario._id,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Criar mensagem de chat
|
||||
await ctx.db.insert("mensagens", {
|
||||
conversaId,
|
||||
remetenteId: funcionarioUsuario._id,
|
||||
tipo: "texto",
|
||||
conteudo: `Solicitei uma ausência de ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}. Motivo: ${args.motivo}`,
|
||||
enviadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return solicitacaoId;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Aprovar ausência
|
||||
export const aprovar = mutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesAusencias"),
|
||||
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");
|
||||
}
|
||||
|
||||
// Verificar se o gestor tem permissão (é gestor do time do funcionário)
|
||||
const gestorIdDoFuncionario = await encontrarGestorDoFuncionario(
|
||||
ctx,
|
||||
solicitacao.funcionarioId
|
||||
);
|
||||
|
||||
if (gestorIdDoFuncionario !== args.gestorId) {
|
||||
throw new Error("Você não tem permissão para aprovar esta solicitação");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
|
||||
// Atualizar solicitação
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "aprovado",
|
||||
gestorId: args.gestorId,
|
||||
dataAprovacao: Date.now(),
|
||||
});
|
||||
|
||||
// Buscar usuário do funcionário
|
||||
const funcionarioUsuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) =>
|
||||
q.eq("funcionarioId", solicitacao.funcionarioId)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (funcionarioUsuario) {
|
||||
// Criar notificação in-app para funcionário
|
||||
await ctx.db.insert("notificacoesAusencias", {
|
||||
destinatarioId: funcionarioUsuario._id,
|
||||
solicitacaoAusenciaId: args.solicitacaoId,
|
||||
tipo: "aprovado",
|
||||
lida: false,
|
||||
mensagem: `Sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")} foi aprovada!`,
|
||||
});
|
||||
|
||||
const gestorUsuario = await ctx.db.get(args.gestorId);
|
||||
|
||||
if (gestorUsuario) {
|
||||
// Enviar email ao funcionário
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: funcionarioUsuario.email,
|
||||
destinatarioId: funcionarioUsuario._id,
|
||||
assunto: "Solicitação de Ausência Aprovada",
|
||||
corpo: `<p>Olá ${funcionarioUsuario.nome},</p>
|
||||
<p>Sua solicitação de ausência foi <strong>aprovada</strong> pelo gestor ${gestorUsuario.nome}:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}</li>
|
||||
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
||||
</ul>`,
|
||||
enviadoPorId: args.gestorId,
|
||||
});
|
||||
|
||||
// Criar ou obter conversa
|
||||
const conversasExistentes = await ctx.db
|
||||
.query("conversas")
|
||||
.filter((q) => q.eq(q.field("tipo"), "individual"))
|
||||
.collect();
|
||||
|
||||
let conversaId: Id<"conversas"> | null = null;
|
||||
for (const conversa of conversasExistentes) {
|
||||
if (
|
||||
conversa.participantes.length === 2 &&
|
||||
conversa.participantes.includes(args.gestorId) &&
|
||||
conversa.participantes.includes(funcionarioUsuario._id)
|
||||
) {
|
||||
conversaId = conversa._id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversaId) {
|
||||
conversaId = await ctx.db.insert("conversas", {
|
||||
tipo: "individual",
|
||||
participantes: [args.gestorId, funcionarioUsuario._id],
|
||||
criadoPor: args.gestorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Criar mensagem de chat
|
||||
await ctx.db.insert("mensagens", {
|
||||
conversaId,
|
||||
remetenteId: args.gestorId,
|
||||
tipo: "texto",
|
||||
conteudo: `Aprovei sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}.`,
|
||||
enviadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Reprovar ausência
|
||||
export const reprovar = mutation({
|
||||
args: {
|
||||
solicitacaoId: v.id("solicitacoesAusencias"),
|
||||
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");
|
||||
}
|
||||
|
||||
// Verificar se o gestor tem permissão
|
||||
const gestorIdDoFuncionario = await encontrarGestorDoFuncionario(
|
||||
ctx,
|
||||
solicitacao.funcionarioId
|
||||
);
|
||||
|
||||
if (gestorIdDoFuncionario !== args.gestorId) {
|
||||
throw new Error("Você não tem permissão para reprovar esta solicitação");
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
|
||||
// Atualizar solicitação
|
||||
await ctx.db.patch(args.solicitacaoId, {
|
||||
status: "reprovado",
|
||||
gestorId: args.gestorId,
|
||||
dataReprovacao: Date.now(),
|
||||
motivoReprovacao: args.motivoReprovacao.trim(),
|
||||
});
|
||||
|
||||
// Buscar usuário do funcionário
|
||||
const funcionarioUsuario = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_funcionarioId", (q) =>
|
||||
q.eq("funcionarioId", solicitacao.funcionarioId)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (funcionarioUsuario) {
|
||||
// Criar notificação in-app para funcionário
|
||||
await ctx.db.insert("notificacoesAusencias", {
|
||||
destinatarioId: funcionarioUsuario._id,
|
||||
solicitacaoAusenciaId: args.solicitacaoId,
|
||||
tipo: "reprovado",
|
||||
lida: false,
|
||||
mensagem: `Sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")} foi reprovada. Motivo: ${args.motivoReprovacao}`,
|
||||
});
|
||||
|
||||
const gestorUsuario = await ctx.db.get(args.gestorId);
|
||||
|
||||
if (gestorUsuario) {
|
||||
// Enviar email ao funcionário
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: funcionarioUsuario.email,
|
||||
destinatarioId: funcionarioUsuario._id,
|
||||
assunto: "Solicitação de Ausência Reprovada",
|
||||
corpo: `<p>Olá ${funcionarioUsuario.nome},</p>
|
||||
<p>Sua solicitação de ausência foi <strong>reprovada</strong> pelo gestor ${gestorUsuario.nome}:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}</li>
|
||||
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
||||
<li><strong>Motivo da Reprovação:</strong> ${args.motivoReprovacao}</li>
|
||||
</ul>`,
|
||||
enviadoPorId: args.gestorId,
|
||||
});
|
||||
|
||||
// Criar ou obter conversa
|
||||
const conversasExistentes = await ctx.db
|
||||
.query("conversas")
|
||||
.filter((q) => q.eq(q.field("tipo"), "individual"))
|
||||
.collect();
|
||||
|
||||
let conversaId: Id<"conversas"> | null = null;
|
||||
for (const conversa of conversasExistentes) {
|
||||
if (
|
||||
conversa.participantes.length === 2 &&
|
||||
conversa.participantes.includes(args.gestorId) &&
|
||||
conversa.participantes.includes(funcionarioUsuario._id)
|
||||
) {
|
||||
conversaId = conversa._id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversaId) {
|
||||
conversaId = await ctx.db.insert("conversas", {
|
||||
tipo: "individual",
|
||||
participantes: [args.gestorId, funcionarioUsuario._id],
|
||||
criadoPor: args.gestorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Criar mensagem de chat
|
||||
await ctx.db.insert("mensagens", {
|
||||
conversaId,
|
||||
remetenteId: args.gestorId,
|
||||
tipo: "texto",
|
||||
conteudo: `Reprovei sua solicitação de ausência de ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}. Motivo: ${args.motivoReprovacao}`,
|
||||
enviadaEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: Marcar notificação como lida
|
||||
export const marcarComoLida = mutation({
|
||||
args: {
|
||||
notificacaoId: v.id("notificacoesAusencias"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.notificacaoId, {
|
||||
lida: true,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -266,6 +266,42 @@ export default defineSchema({
|
||||
.index("by_destinatario", ["destinatarioId"])
|
||||
.index("by_destinatario_and_lida", ["destinatarioId", "lida"]),
|
||||
|
||||
// Solicitações de Ausências
|
||||
solicitacoesAusencias: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
motivo: v.string(),
|
||||
status: v.union(
|
||||
v.literal("aguardando_aprovacao"),
|
||||
v.literal("aprovado"),
|
||||
v.literal("reprovado")
|
||||
),
|
||||
gestorId: v.optional(v.id("usuarios")),
|
||||
dataAprovacao: v.optional(v.number()),
|
||||
dataReprovacao: v.optional(v.number()),
|
||||
motivoReprovacao: v.optional(v.string()),
|
||||
observacao: v.optional(v.string()),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_status", ["status"])
|
||||
.index("by_funcionario_and_status", ["funcionarioId", "status"]),
|
||||
|
||||
notificacoesAusencias: defineTable({
|
||||
destinatarioId: v.id("usuarios"),
|
||||
solicitacaoAusenciaId: v.id("solicitacoesAusencias"),
|
||||
tipo: v.union(
|
||||
v.literal("nova_solicitacao"),
|
||||
v.literal("aprovado"),
|
||||
v.literal("reprovado")
|
||||
),
|
||||
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"),
|
||||
|
||||
Reference in New Issue
Block a user