import { v } from 'convex/values'; import { api, internal } from './_generated/api'; import type { Doc, Id } from './_generated/dataModel'; import type { MutationCtx, QueryCtx } from './_generated/server'; import { mutation, query } from './_generated/server'; // 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 | 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; } });