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) { // Obter URL do sistema let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173"; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } // Enviar email ao gestor usando template (agendado via scheduler) try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: gestorUsuario.email, destinatarioId: gestorId, templateCodigo: "ausencia_solicitada", variaveis: { gestorNome: gestorUsuario.nome, funcionarioNome: funcionario.nome, dataInicio: new Date(args.dataInicio).toLocaleDateString("pt-BR"), dataFim: new Date(args.dataFim).toLocaleDateString("pt-BR"), motivo: args.motivo, urlSistema, }, enviadoPor: funcionarioUsuario._id, }); } catch (error) { // Fallback para envio direto se houver erro ao agendar ou processar o template console.warn( "Erro ao agendar envio de email com template ausencia_solicitada, usando envio direto:", error, ); 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:

Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.

`, enviadoPor: 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) { // Obter URL do sistema let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173"; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } // Enviar email ao funcionário usando template (agendado via scheduler) try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: funcionarioUsuario.email, destinatarioId: funcionarioUsuario._id, templateCodigo: "ausencia_aprovada", variaveis: { funcionarioNome: funcionarioUsuario.nome, gestorNome: gestorUsuario.nome, dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR"), dataFim: new Date(solicitacao.dataFim).toLocaleDateString("pt-BR"), motivo: solicitacao.motivo, urlSistema, }, enviadoPor: args.gestorId, }); } catch (error) { // Fallback para envio direto se houver erro ao agendar ou processar o template console.warn( "Erro ao agendar envio de email com template ausencia_aprovada, usando envio direto:", error, ); 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}:

`, enviadoPor: 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) { // Obter URL do sistema let urlSistema = process.env.FRONTEND_URL || "http://localhost:5173"; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } // Enviar email ao funcionário usando template (agendado via scheduler) try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: funcionarioUsuario.email, destinatarioId: funcionarioUsuario._id, templateCodigo: "ausencia_reprovada", variaveis: { funcionarioNome: funcionarioUsuario.nome, gestorNome: gestorUsuario.nome, dataInicio: new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR"), dataFim: new Date(solicitacao.dataFim).toLocaleDateString("pt-BR"), motivo: solicitacao.motivo, motivoReprovacao: args.motivoReprovacao, urlSistema, }, enviadoPor: args.gestorId, }); } catch (error) { // Fallback para envio direto se houver erro ao agendar ou processar o template console.warn( "Erro ao agendar envio de email com template ausencia_reprovada, usando envio direto:", error, ); 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}:

`, enviadoPor: 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; }, });