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) => { 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) // 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 & { 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; }, });