@@ -1716,3 +2117,56 @@
(mostrarWizard = false)}>
{/if}
+
+
+{#if mostrarWizardAusencia && funcionarioIdDisponivel}
+
+
+
+ Nova Solicitação de Ausência
+
+
+ {
+ mostrarWizardAusencia = false;
+ }}
+ onCancelar={() => (mostrarWizardAusencia = false)}
+ />
+
+
+
+
+ (mostrarWizardAusencia = false)}>
+
+{/if}
+
+
+{#if solicitacaoAusenciaAprovar && authStore.usuario}
+ {#await client.query(api.ausencias.obterDetalhes, {
+ solicitacaoId: solicitacaoAusenciaAprovar,
+ }) then detalhes}
+ {#if detalhes}
+
+
+
{
+ solicitacaoAusenciaAprovar = null;
+ }}
+ onCancelar={() => (solicitacaoAusenciaAprovar = null)}
+ />
+
+
+
+ {/if}
+ {/await}
+{/if}
diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/ausencias/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/ausencias/+page.svelte
new file mode 100644
index 0000000..8341aae
--- /dev/null
+++ b/apps/web/src/routes/(dashboard)/recursos-humanos/ausencias/+page.svelte
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard de Ausências
+
+ Visão geral de todas as solicitações de ausências
+
+
+
+
goto("/recursos-humanos")}
+ >
+
+
+
+ Voltar
+
+
+
+
+
+
+
+
+
Total
+
{stats.total}
+
Solicitações
+
+
+
+
+
Pendentes
+
{stats.aguardando}
+
Aguardando
+
+
+
+
+
Aprovadas
+
{stats.aprovadas}
+
Deferidas
+
+
+
+
+
Reprovadas
+
{stats.reprovadas}
+
Indeferidas
+
+
+
+
+
+
+
Filtros
+
+
+
+ Status
+
+
+ Todos
+ Aguardando Aprovação
+ Aprovado
+ Reprovado
+
+
+
+
+
+
+
+
+
+
+ Todas as Solicitações ({ausenciasFiltradas.length})
+
+
+ {#if ausenciasFiltradas.length === 0}
+
+
+
+
+
Nenhuma solicitação encontrada com os filtros aplicados.
+
+ {:else}
+
+
+
+
+ Funcionário
+ Time
+ Período
+ Dias
+ Motivo
+ Status
+ Solicitado em
+ Ações
+
+
+
+ {#each ausenciasFiltradas as ausencia}
+
+
+ {ausencia.funcionario?.nome || "N/A"}
+
+
+ {#if ausencia.time}
+
+ {ausencia.time.nome}
+
+ {:else}
+ Sem time
+ {/if}
+
+
+ {new Date(ausencia.dataInicio).toLocaleDateString("pt-BR")} até{" "}
+ {new Date(ausencia.dataFim).toLocaleDateString("pt-BR")}
+
+
+ {calcularDias(ausencia.dataInicio, ausencia.dataFim)} dias
+
+
+ {ausencia.motivo}
+
+
+
+ {getStatusTexto(ausencia.status)}
+
+
+
+ {new Date(ausencia.criadoEm).toLocaleDateString("pt-BR")}
+
+
+ {#if ausencia.status === "aguardando_aprovacao"}
+ selecionarSolicitacao(ausencia._id)}
+ >
+
+
+
+
+ Ver Detalhes
+
+ {:else}
+ selecionarSolicitacao(ausencia._id)}
+ >
+
+
+
+
+ Ver Detalhes
+
+ {/if}
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+
+{#if solicitacaoSelecionada && authStore.usuario}
+ {#await client.query(api.ausencias.obterDetalhes, {
+ solicitacaoId: solicitacaoSelecionada,
+ }) then detalhes}
+ {#if detalhes}
+
+
+
(solicitacaoSelecionada = null)}
+ />
+
+
+
+ {/if}
+ {/await}
+{/if}
+
diff --git a/apps/web/src/routes/(dashboard)/secretaria-executiva/+page.svelte b/apps/web/src/routes/(dashboard)/secretaria-executiva/+page.svelte
index 6335814..e92c758 100644
--- a/apps/web/src/routes/(dashboard)/secretaria-executiva/+page.svelte
+++ b/apps/web/src/routes/(dashboard)/secretaria-executiva/+page.svelte
@@ -1,48 +1,214 @@
-
-
-
-
-
-
-
-
-
-
Secretaria Executiva
-
Gestão executiva e administrativa
-
-
-
-
-
-
-
-
-
Módulo em Desenvolvimento
-
- O módulo da Secretaria Executiva está sendo desenvolvido e em breve estará disponível com funcionalidades completas de gestão executiva e administrativa.
-
-
-
-
-
- Em Desenvolvimento
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
Secretaria Executiva
+
Gestão executiva e administrativa
+
+
+
+
+
+
+
+
+
+
+
+
+ Gestão de Ausências
+
+
goto("/recursos-humanos/ausencias")}
+ >
+ Ver Todas
+
+
+
+
+
+
+
Total
+
{stats.total}
+
Solicitações
+
+
+
Pendentes
+
{stats.pendentes}
+
Aguardando
+
+
+
Aprovadas
+
{stats.aprovadas}
+
Deferidas
+
+
+
Reprovadas
+
{stats.reprovadas}
+
Indeferidas
+
+
+
+
+
+
Solicitações Pendentes de Aprovação
+ {#if pendentes.length === 0}
+
+
+
+
+
Nenhuma solicitação pendente no momento.
+
+ {:else}
+
+
+
+
+ Funcionário
+ Período
+ Dias
+ Status
+ Solicitado em
+
+
+
+ {#each pendentes as ausencia}
+
+
+ {ausencia.funcionario?.nome || "N/A"}
+
+
+ {new Date(ausencia.dataInicio).toLocaleDateString("pt-BR")} até{" "}
+ {new Date(ausencia.dataFim).toLocaleDateString("pt-BR")}
+
+
+ {calcularDias(ausencia.dataInicio, ausencia.dataFim)} dias
+
+
+
+ {getStatusTexto(ausencia.status)}
+
+
+
+ {new Date(ausencia.criadoEm).toLocaleDateString("pt-BR")}
+
+
+ {/each}
+
+
+
+ {#if stats.pendentes > 5}
+
+ goto("/recursos-humanos/ausencias")}
+ >
+ Ver todas as {stats.pendentes} pendentes
+
+
+ {/if}
+ {/if}
+
+
+
+
diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts
index a0b2e5f..e33dc3d 100644
--- a/packages/backend/convex/_generated/api.d.ts
+++ b/packages/backend/convex/_generated/api.d.ts
@@ -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;
diff --git a/packages/backend/convex/ausencias.ts b/packages/backend/convex/ausencias.ts
new file mode 100644
index 0000000..93306bd
--- /dev/null
+++ b/packages/backend/convex/ausencias.ts
@@ -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
& {
+ 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 | 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: `Olá ${gestorUsuario.nome},
+ O funcionário ${funcionario.nome} solicitou uma ausência:
+
+ Período: ${new Date(args.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(args.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${args.motivo}
+
+ Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.
`,
+ 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: `Olá ${funcionarioUsuario.nome},
+ Sua solicitação de ausência foi aprovada pelo gestor ${gestorUsuario.nome}:
+
+ Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${solicitacao.motivo}
+ `,
+ 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: `Olá ${funcionarioUsuario.nome},
+ Sua solicitação de ausência foi reprovada pelo gestor ${gestorUsuario.nome}:
+
+ Período: ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
+ Motivo: ${solicitacao.motivo}
+ Motivo da Reprovação: ${args.motivoReprovacao}
+ `,
+ 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;
+ },
+});
+
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts
index 0f07dbc..0662bab 100644
--- a/packages/backend/convex/schema.ts
+++ b/packages/backend/convex/schema.ts
@@ -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"),