import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import type { QueryCtx, MutationCtx } from './_generated/server'; import { api } from './_generated/api'; import { internal } from './_generated/api'; import { Id, Doc } from './_generated/dataModel'; import { parseLocalDate, formatarDataBR } from './utils/datas'; import { getCurrentUserFunction } from './auth'; // Query: Listar todas as solicitações (para RH) export const listarTodas = query({ args: {}, handler: async (ctx) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'listar' }); 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); } // Buscar usuário do funcionário para obter fotoPerfilUrl let fotoPerfilUrl: string | null = null; if (funcionario) { const usuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (usuario?.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } } return { ...s, funcionario: funcionario ? { ...funcionario, fotoPerfilUrl } : null, 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'), _refresh: v.optional(v.number()) // Parâmetro para forçar atualização no frontend }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'criar' }); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'aprovar' }); // 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'> & { fotoPerfilUrl: string | null }) | 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); // Buscar usuário do funcionário para obter fotoPerfilUrl let fotoPerfilUrl: string | null = null; if (funcionario) { const usuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (usuario?.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } } solicitacoes.push({ ...s, funcionario: funcionario ? { ...funcionario, fotoPerfilUrl } : null, 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'listar' }); const solicitacao = await ctx.db.get(args.solicitacaoId); if (!solicitacao) return null; const funcionario = await ctx.db.get(solicitacao.funcionarioId); // Buscar usuário do funcionário para obter fotoPerfilUrl let fotoPerfilUrl: string | null = null; if (funcionario) { const usuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (usuario?.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } } 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); } // Enriquecer histórico com nomes dos usuários let historicoComUsuarios = solicitacao.historicoAlteracoes; if (historicoComUsuarios && historicoComUsuarios.length > 0) { historicoComUsuarios = await Promise.all( historicoComUsuarios.map(async (hist) => { const usuario = await ctx.db.get(hist.usuarioId); return { ...hist, usuarioNome: usuario?.nome || 'Usuário Desconhecido' }; }) ); } return { ...solicitacao, funcionario: funcionario ? { ...funcionario, fotoPerfilUrl } : null, gestor, time, historicoAlteracoes: historicoComUsuarios }; } }); // Query: Obter notificações não lidas export const obterNotificacoesNaoLidas = query({ args: { usuarioId: v.id('usuarios') }, handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'criar' }); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'aprovar' }); // 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: Recalcular banco de horas em um período async function recalcularBancoHorasPeriodo( ctx: MutationCtx, funcionarioId: Id<'funcionarios'>, dataInicio: string, dataFim: string ): Promise { // Gerar todas as datas do período const dataInicioObj = new Date(dataInicio); const dataFimObj = new Date(dataFim); const datas: string[] = []; const dataAtual = new Date(dataInicioObj); while (dataAtual <= dataFimObj) { const ano = dataAtual.getFullYear(); const mes = String(dataAtual.getMonth() + 1).padStart(2, '0'); const dia = String(dataAtual.getDate()).padStart(2, '0'); datas.push(`${ano}-${mes}-${dia}`); dataAtual.setDate(dataAtual.getDate() + 1); } // Recalcular para cada data usando a mutation interna (agendar para execução assíncrona) for (let i = 0; i < datas.length; i++) { await ctx.scheduler.runAfter(i * 100, internal.pontos.recalcularBancoHorasData, { funcionarioId, data: datas[i]! }); } } // Helper: Verificar se há sobreposição de datas function verificarSobreposicao( inicio1: string, fim1: string, inicio2: string, fim2: string ): boolean { const d1Inicio = parseLocalDate(inicio1); const d1Fim = parseLocalDate(fim1); const d2Inicio = parseLocalDate(inicio2); const d2Fim = parseLocalDate(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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'criar' }); // Validações if (args.motivo.trim().length < 10) { throw new Error('O motivo deve ter no mínimo 10 caracteres'); } const dataInicio = parseLocalDate(args.dataInicio); const dataFim = parseLocalDate(args.dataFim); // Criar data de hoje em UTC para comparação const hoje = new Date(); const hojeUTC = new Date( Date.UTC(hoje.getUTCFullYear(), hoje.getUTCMonth(), hoje.getUTCDate(), 0, 0, 0, 0) ); if (dataInicio < hojeUTC) { 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'); } // Buscar usuário que está criando (pode não ser o próprio funcionário) const usuarioCriador = await getCurrentUserFunction(ctx); const usuarioFuncionario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId)) .first(); // Usar o usuário autenticado se disponível, senão usar o usuário do funcionário ou gestor const usuarioIdParaHistorico = usuarioCriador?._id || usuarioFuncionario?._id || funcionario.gestorId; // 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 histórico inicial const historicoInicial = usuarioIdParaHistorico ? [ { data: Date.now(), usuarioId: usuarioIdParaHistorico, acao: 'Solicitação criada' } ] : []; // 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(), historicoAlteracoes: historicoInicial }); // 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 ${formatarDataBR(args.dataInicio)} até ${formatarDataBR(args.dataFim)}` }); // 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: formatarDataBR(args.dataInicio), dataFim: formatarDataBR(args.dataFim), 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 ${formatarDataBR(args.dataInicio)} até ${formatarDataBR(args.dataFim)}. 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'aprovar' }); 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'); } // Buscar nome do gestor para o histórico const gestorUsuario = await ctx.db.get(args.gestorId); const nomeGestor = gestorUsuario?.nome || 'Gestor'; // Atualizar solicitação 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 por ${nomeGestor}` } ] }); // 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 ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)} 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: formatarDataBR(solicitacao.dataInicio), dataFim: formatarDataBR(solicitacao.dataFim), 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 ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}.`, enviadaEm: Date.now() }); } } // Recalcular banco de horas para todas as datas do período da ausência aprovada await recalcularBancoHorasPeriodo( ctx, solicitacao.funcionarioId, solicitacao.dataInicio, solicitacao.dataFim ); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'reprovar' }); 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'); } // Buscar nome do gestor para o histórico const gestorUsuario = await ctx.db.get(args.gestorId); const nomeGestor = gestorUsuario?.nome || 'Gestor'; // Atualizar solicitação await ctx.db.patch(args.solicitacaoId, { status: 'reprovado', gestorId: args.gestorId, dataReprovacao: Date.now(), motivoReprovacao: args.motivoReprovacao.trim(), historicoAlteracoes: [ ...(solicitacao.historicoAlteracoes || []), { data: Date.now(), usuarioId: args.gestorId, acao: `Reprovado por ${nomeGestor}: ${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 ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)} 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: formatarDataBR(solicitacao.dataInicio), dataFim: formatarDataBR(solicitacao.dataFim), 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 ${formatarDataBR(solicitacao.dataInicio)} até ${formatarDataBR(solicitacao.dataFim)}. 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.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ausencias', acao: 'criar' }); await ctx.db.patch(args.notificacaoId, { lida: true }); return null; } });