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(), }); // Helper: Calcular dias entre duas datas function calcularDiasEntreDatas(dataInicio: string, dataFim: string): number { const inicio = new Date(dataInicio); const fim = new Date(dataFim); const diffTime = Math.abs(fim.getTime() - inicio.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; return diffDays; } // Helper: Agrupar registros de ferias por funcionarioId + anoReferencia function agruparPorSolicitacao( registros: Array> ): Array<{ funcionarioId: Id<"funcionarios">; anoReferencia: number; periodos: Array>; status: string; observacao?: string; motivoReprovacao?: string; gestorId?: Id<"usuarios">; dataAprovacao?: number; dataReprovacao?: number; historicoAlteracoes?: Array<{ data: number; usuarioId: Id<"usuarios">; acao: string; }>; }> { const grupos = new Map>>(); for (const registro of registros) { const chave = `${registro.funcionarioId}_${registro.anoReferencia}`; if (!grupos.has(chave)) { grupos.set(chave, []); } grupos.get(chave)!.push(registro); } return Array.from(grupos.entries()).map(([_, periodos]) => { // Ordenar por data de criação para manter ordem periodos.sort((a, b) => a._creationTime - b._creationTime); // Pegar informações da primeira solicitação (todos têm os mesmos campos compartilhados) const primeiro = periodos[0]; return { funcionarioId: primeiro.funcionarioId, anoReferencia: primeiro.anoReferencia, periodos, status: primeiro.status, observacao: primeiro.observacao, motivoReprovacao: primeiro.motivoReprovacao, gestorId: primeiro.gestorId, dataAprovacao: primeiro.dataAprovacao, dataReprovacao: primeiro.dataReprovacao, historicoAlteracoes: primeiro.historicoAlteracoes, }; }); } // Query: Listar TODAS as solicitações (para RH) - períodos individuais export const listarTodas = query({ args: {}, handler: async (ctx) => { const todasFerias = await ctx.db.query("ferias").collect(); const periodosComDetalhes = await Promise.all( todasFerias.map(async (ferias) => { const funcionario = await ctx.db.get(ferias.funcionarioId); // Buscar time do funcionário const membroTime = await ctx.db .query("timesMembros") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", ferias.funcionarioId) ) .filter((q) => q.eq(q.field("ativo"), true)) .first(); let time = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); } return { ...ferias, funcionario, time, }; }) ); return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime); }, }); // Query: Listar solicitações do funcionário - períodos individuais export const listarMinhasSolicitacoes = query({ args: { funcionarioId: v.id("funcionarios") }, handler: async (ctx, args) => { const todasFerias = await ctx.db .query("ferias") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId) ) .collect(); const funcionario = await ctx.db.get(args.funcionarioId); // Buscar time do funcionário const membroTime = await ctx.db .query("timesMembros") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId) ) .filter((q) => q.eq(q.field("ativo"), true)) .first(); let time = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); } // Retornar períodos individuais com detalhes return todasFerias.map((ferias) => ({ ...ferias, funcionario, time, })).sort((a, b) => b._creationTime - a._creationTime); }, }); // Query: Listar solicitações dos subordinados (para gestores) - períodos individuais 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 todasFerias: Array> = []; 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 férias de cada membro for (const membro of membros) { const ferias = await ctx.db .query("ferias") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", membro.funcionarioId) ) .collect(); todasFerias.push(...ferias); } } // Adicionar info do funcionário e time para cada período const periodosComDetalhes = await Promise.all( todasFerias.map(async (ferias) => { const funcionario = await ctx.db.get(ferias.funcionarioId); // Buscar time do funcionário const membroTime = await ctx.db .query("timesMembros") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", ferias.funcionarioId) ) .filter((q) => q.eq(q.field("ativo"), true)) .first(); let time = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); } return { ...ferias, funcionario, time, }; }) ); return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime); }, }); // Query: Obter detalhes de um período individual export const obterDetalhes = query({ args: { feriasId: v.id("ferias") }, handler: async (ctx, args) => { const ferias = await ctx.db.get(args.feriasId); if (!ferias) return null; const funcionario = await ctx.db.get(ferias.funcionarioId); let gestor = null; if (ferias.gestorId) { gestor = await ctx.db.get(ferias.gestorId); } // Buscar time do funcionário const membroTime = await ctx.db .query("timesMembros") .withIndex("by_funcionario", (q) => q.eq("funcionarioId", ferias.funcionarioId) ) .filter((q) => q.eq(q.field("ativo"), true)) .first(); let time = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); } return { ...ferias, funcionario, gestor, time, }; }, }); // Mutation: Criar solicitação de férias (cria um registro por período) export const criarSolicitacao = mutation({ args: { funcionarioId: v.id("funcionarios"), anoReferencia: v.number(), periodos: v.array(periodoValidator), observacao: v.optional(v.string()), }, returns: v.array(v.id("ferias")), 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"); // 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 historicoInicial = [ { data: Date.now(), usuarioId: usuario?._id || funcionario.gestorId!, acao: "Solicitação criada", }, ]; // Criar um registro para cada período const idsCriados: Array> = []; for (const periodo of args.periodos) { const feriasId = await ctx.db.insert("ferias", { funcionarioId: args.funcionarioId, anoReferencia: args.anoReferencia, dataInicio: periodo.dataInicio, dataFim: periodo.dataFim, diasFerias: periodo.diasCorridos, status: "aguardando_aprovacao", observacao: args.observacao, diasAbono: 0, historicoAlteracoes: historicoInicial, }); idsCriados.push(feriasId); } // Notificar gestor (usar o primeiro ID criado) if (funcionario.gestorId && idsCriados.length > 0) { await ctx.db.insert("notificacoesFerias", { destinatarioId: funcionario.gestorId, feriasId: idsCriados[0], tipo: "nova_solicitacao", lida: false, mensagem: `${funcionario.nome} solicitou férias`, }); } return idsCriados; }, }); // Mutation: Aprovar período de férias individual export const aprovar = mutation({ args: { feriasId: v.id("ferias"), gestorId: v.id("usuarios"), }, returns: v.null(), handler: async (ctx, args) => { // Buscar o registro específico const registro = await ctx.db.get(args.feriasId); if (!registro) { throw new Error("Período de férias não encontrado"); } // Verificar se está aguardando aprovação if (registro.status !== "aguardando_aprovacao") { throw new Error("Este período já foi processado"); } const funcionario = await ctx.db.get(registro.funcionarioId); // Atualizar o registro await ctx.db.patch(registro._id, { status: "aprovado", gestorId: args.gestorId, dataAprovacao: Date.now(), historicoAlteracoes: [ ...(registro.historicoAlteracoes || []), { data: Date.now(), usuarioId: args.gestorId, acao: "Aprovado", }, ], }); // 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, feriasId: registro._id, tipo: "aprovado", lida: false, mensagem: `Período de férias de ${registro.diasFerias} dias foi aprovado!`, }); } } return null; }, }); // Mutation: Reprovar período de férias individual export const reprovar = mutation({ args: { feriasId: v.id("ferias"), gestorId: v.id("usuarios"), motivoReprovacao: v.string(), }, returns: v.null(), handler: async (ctx, args) => { // Buscar o registro específico const registro = await ctx.db.get(args.feriasId); if (!registro) { throw new Error("Período de férias não encontrado"); } // Verificar se está aguardando aprovação if (registro.status !== "aguardando_aprovacao") { throw new Error("Este período já foi processado"); } const funcionario = await ctx.db.get(registro.funcionarioId); // Atualizar o registro await ctx.db.patch(registro._id, { status: "reprovado", gestorId: args.gestorId, dataReprovacao: Date.now(), motivoReprovacao: args.motivoReprovacao, historicoAlteracoes: [ ...(registro.historicoAlteracoes || []), { data: Date.now(), usuarioId: args.gestorId, acao: `Reprovado: ${args.motivoReprovacao}`, }, ], }); // 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, feriasId: registro._id, tipo: "reprovado", lida: false, mensagem: `Período de férias de ${registro.diasFerias} dias foi reprovado: ${args.motivoReprovacao}`, }); } } return null; }, }); // Mutation: Ajustar data e aprovar período individual export const ajustarEAprovar = mutation({ args: { feriasId: v.id("ferias"), gestorId: v.id("usuarios"), novaDataInicio: v.string(), novaDataFim: v.string(), }, returns: v.null(), handler: async (ctx, args) => { // Buscar o registro específico const registroAntigo = await ctx.db.get(args.feriasId); if (!registroAntigo) { throw new Error("Período de férias não encontrado"); } // Verificar se está aguardando aprovação if (registroAntigo.status !== "aguardando_aprovacao") { throw new Error("Este período já foi processado"); } const funcionario = await ctx.db.get(registroAntigo.funcionarioId); // Calcular novos dias const novosDias = calcularDiasEntreDatas(args.novaDataInicio, args.novaDataFim); // Atualizar o registro com novas datas await ctx.db.patch(registroAntigo._id, { dataInicio: args.novaDataInicio, dataFim: args.novaDataFim, diasFerias: novosDias, status: "data_ajustada_aprovada", gestorId: args.gestorId, dataAprovacao: Date.now(), historicoAlteracoes: [ ...(registroAntigo.historicoAlteracoes || []), { data: Date.now(), usuarioId: args.gestorId, acao: `Data ajustada e aprovada: ${registroAntigo.dataInicio} - ${registroAntigo.dataFim} → ${args.novaDataInicio} - ${args.novaDataFim}`, }, ], }); // 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, feriasId: registroAntigo._id, tipo: "data_ajustada", lida: false, mensagem: `Período de férias foi aprovado com ajuste de datas: ${args.novaDataInicio} a ${args.novaDataFim}`, }); } } 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 feriasAprovadas = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q.eq("funcionarioId", args.funcionarioId).eq("status", "aprovado") ) .collect(); const feriasAjustadas = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q .eq("funcionarioId", args.funcionarioId) .eq("status", "data_ajustada_aprovada") ) .collect(); const feriasEmFerias = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q.eq("funcionarioId", args.funcionarioId).eq("status", "EmFérias") ) .collect(); const todasFerias = [ ...feriasAprovadas, ...feriasAjustadas, ...feriasEmFerias, ]; for (const ferias of todasFerias) { const inicio = new Date(ferias.dataInicio); const fim = new Date(ferias.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; }, }); // Mutation: Atualizar status de um período individual export const atualizarStatus = mutation({ args: { feriasId: v.id("ferias"), novoStatus: v.union( v.literal("aguardando_aprovacao"), v.literal("aprovado"), v.literal("reprovado"), v.literal("data_ajustada_aprovada") ), usuarioId: v.id("usuarios"), }, returns: v.null(), handler: async (ctx, args) => { // Buscar o registro específico const registro = await ctx.db.get(args.feriasId); if (!registro) { throw new Error("Período de férias não encontrado"); } // Atualizar status e histórico const acao = `Status alterado para ${args.novoStatus}`; const updateData: { status: typeof args.novoStatus; historicoAlteracoes: Array<{ data: number; usuarioId: Id<"usuarios">; acao: string; }>; gestorId?: undefined; dataAprovacao?: undefined; dataReprovacao?: undefined; motivoReprovacao?: undefined; } = { status: args.novoStatus, historicoAlteracoes: [ ...(registro.historicoAlteracoes || []), { data: Date.now(), usuarioId: args.usuarioId, acao, }, ], }; // Se voltar para aguardando_aprovacao, limpar campos relacionados if (args.novoStatus === "aguardando_aprovacao") { await ctx.db.patch(registro._id, { ...updateData, gestorId: undefined, dataAprovacao: undefined, dataReprovacao: undefined, motivoReprovacao: undefined, }); } else { await ctx.db.patch(registro._id, updateData); } 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); // Buscar todos os registros de férias que podem estar em férias // Buscar por status específico para criar mapas de referência const feriasAprovadas = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q.eq("funcionarioId", func._id).eq("status", "aprovado") ) .collect(); const feriasAjustadas = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q.eq("funcionarioId", func._id).eq("status", "data_ajustada_aprovada") ) .collect(); const feriasEmFerias = await ctx.db .query("ferias") .withIndex("by_funcionario_and_status", (q) => q.eq("funcionarioId", func._id).eq("status", "EmFérias") ) .collect(); // Criar mapas para verificar status original // Quando um registro está "EmFérias", precisamos saber qual era o status anterior // Vamos usar o histórico ou verificar se o ID estava nas listas antes const idsAprovados = new Set(feriasAprovadas.map(f => f._id)); const idsAjustados = new Set(feriasAjustadas.map(f => f._id)); // Para registros que estão "EmFérias", verificar o histórico para determinar status anterior // Se não houver histórico claro, usar lógica: se foi aprovado recentemente, provavelmente era "aprovado" // Por enquanto, vamos usar uma heurística: se o registro está "EmFérias" e não está nas listas, // vamos verificar o histórico de alterações para encontrar o status anterior const statusAnteriorPorId = new Map, "aprovado" | "data_ajustada_aprovada">(); for (const ferias of feriasEmFerias) { // Verificar histórico para encontrar status anterior if (ferias.historicoAlteracoes && ferias.historicoAlteracoes.length > 0) { // Procurar pela última alteração que mudou para "EmFérias" ou antes disso const historico = ferias.historicoAlteracoes; for (let i = historico.length - 1; i >= 0; i--) { const entrada = historico[i]; if (entrada.acao.includes("Aprovado") || entrada.acao.includes("aprovado")) { statusAnteriorPorId.set(ferias._id, "aprovado"); break; } else if (entrada.acao.includes("Data ajustada") || entrada.acao.includes("ajustada")) { statusAnteriorPorId.set(ferias._id, "data_ajustada_aprovada"); break; } } } // Se não encontrou no histórico, usar fallback: assumir "aprovado" if (!statusAnteriorPorId.has(ferias._id)) { statusAnteriorPorId.set(ferias._id, "aprovado"); } } // Combinar todos os registros const todasFerias = [ ...feriasAprovadas, ...feriasAjustadas, ...feriasEmFerias, ]; let emFerias = false; for (const ferias of todasFerias) { const inicio = new Date(ferias.dataInicio); const fim = new Date(ferias.dataFim); inicio.setHours(0, 0, 0, 0); fim.setHours(23, 59, 59, 999); if (hoje >= inicio && hoje <= fim) { emFerias = true; // Atualizar status para "EmFérias" se ainda não estiver if (ferias.status !== "EmFérias") { await ctx.db.patch(ferias._id, { status: "EmFérias", }); } } else { // Se saiu do período e está "EmFérias", voltar para o status anterior if (ferias.status === "EmFérias") { // Determinar status anterior let statusAnterior: "aprovado" | "data_ajustada_aprovada"; if (idsAprovados.has(ferias._id)) { statusAnterior = "aprovado"; } else if (idsAjustados.has(ferias._id)) { statusAnterior = "data_ajustada_aprovada"; } else { // Usar histórico ou fallback statusAnterior = statusAnteriorPorId.get(ferias._id) || "aprovado"; } await ctx.db.patch(ferias._id, { status: statusAnterior, }); } } } const novoStatus = emFerias ? "em_ferias" : "ativo"; if (func.statusFerias !== novoStatus) { await ctx.db.patch(func._id, { statusFerias: novoStatus }); } } return null; }, });