import { v } from 'convex/values'; import { mutation, query, internalMutation } from './_generated/server'; import { internal } from './_generated/api'; import { Id, Doc } from './_generated/dataModel'; import { verificarLicencaAtiva } from './atestadosLicencas'; import { getCurrentUserFunction } from './auth'; import { formatarDataBR } from './utils/datas'; import { api } from './_generated/api'; // 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'listar' }); 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 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); } } // 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: Doc<'times'> | null = null; let gestor: { _id: Id<'usuarios'>; nome: string } | null = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); // Buscar gestor do time if (time?.gestorId) { const gestorUsuario = await ctx.db.get(time.gestorId); if (gestorUsuario?.funcionarioId) { // Buscar funcionário do gestor para obter o nome const gestorFuncionario = await ctx.db.get(gestorUsuario.funcionarioId); if (gestorFuncionario) { gestor = { _id: gestorUsuario._id, nome: gestorFuncionario.nome }; } } } } return { ...ferias, funcionario: funcionario ? { ...funcionario, fotoPerfilUrl } : null, time, gestor }; }) ); 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'), _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: 'ferias', acao: 'dashboard' }); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'dashboard' }); // 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 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); } } // 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: funcionario ? { ...funcionario, fotoPerfilUrl } : null, 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'ver' }); const ferias = await ctx.db.get(args.feriasId); if (!ferias) return null; const funcionario = await ctx.db.get(ferias.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 (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); } // Enriquecer histórico com nomes dos usuários let historicoComUsuarios = ferias.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 { ...ferias, funcionario: funcionario ? { ...funcionario, fotoPerfilUrl } : null, gestor, time, historicoAlteracoes: historicoComUsuarios }; } }); // 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'criar' }); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'aprovar' }); // 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); // Buscar nome do gestor para o histórico const gestorUsuario = await ctx.db.get(args.gestorId); const nomeGestor = gestorUsuario?.nome || 'Gestor'; // 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 por ${nomeGestor}` } ] }); // Notificar funcionário if (funcionario) { const usuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (usuario) { // Criar notificação in-app para funcionário await ctx.db.insert('notificacoesFerias', { destinatarioId: usuario._id, feriasId: registro._id, tipo: 'aprovado', lida: false, mensagem: `Período de férias de ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)} (${registro.diasFerias} dias) foi aprovado por ${nomeGestor}!` }); // Enviar email ao funcionário usando template (agendado via scheduler) if (gestorUsuario) { // Obter URL do sistema let urlSistema = process.env.SITE_URL || 'http://localhost:5173'; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuario.email, destinatarioId: usuario._id, templateCodigo: 'ferias_aprovada', variaveis: { funcionarioNome: usuario.nome, gestorNome: gestorUsuario.nome, dataInicio: formatarDataBR(registro.dataInicio), dataFim: formatarDataBR(registro.dataFim), diasFerias: registro.diasFerias.toString(), 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 ferias_aprovada, usando envio direto:', error ); await ctx.runMutation(api.email.enfileirarEmail, { destinatario: usuario.email, destinatarioId: usuario._id, assunto: 'Solicitação de Férias Aprovada', corpo: `

Olá ${usuario.nome},

Sua solicitação de férias foi aprovada pelo gestor ${gestorUsuario.nome}:

  • Período: ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)}
  • Dias: ${registro.diasFerias} dias
`, enviadoPor: args.gestorId }); } } } } 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'reprovar' }); // 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'aprovar' }); // 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}` } ] }); // Buscar nome do gestor const gestorUsuario = await ctx.db.get(args.gestorId); const nomeGestor = gestorUsuario?.nome || 'Gestor'; // Notificar funcionário if (funcionario) { const usuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (usuario) { // Criar notificação in-app para funcionário 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: ${formatarDataBR(args.novaDataInicio)} a ${formatarDataBR(args.novaDataFim)} (${novosDias} dias) por ${nomeGestor}` }); // Enviar email ao funcionário usando template (agendado via scheduler) if (gestorUsuario) { // Obter URL do sistema let urlSistema = process.env.SITE_URL || 'http://localhost:5173'; if (!urlSistema.match(/^https?:\/\//i)) { urlSistema = `http://${urlSistema}`; } try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuario.email, destinatarioId: usuario._id, templateCodigo: 'ferias_aprovada', variaveis: { funcionarioNome: usuario.nome, gestorNome: gestorUsuario.nome, dataInicio: formatarDataBR(args.novaDataInicio), dataFim: formatarDataBR(args.novaDataFim), diasFerias: novosDias.toString(), 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 ferias_aprovada, usando envio direto:', error ); await ctx.runMutation(api.email.enfileirarEmail, { destinatario: usuario.email, destinatarioId: usuario._id, assunto: 'Solicitação de Férias Aprovada (com Ajuste de Datas)', corpo: `

Olá ${usuario.nome},

Sua solicitação de férias foi aprovada com ajuste de datas pelo gestor ${gestorUsuario.nome}:

  • Período: ${formatarDataBR(args.novaDataInicio)} até ${formatarDataBR(args.novaDataFim)}
  • Dias: ${novosDias} dias
`, enviadoPor: args.gestorId }); } } } } 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'ver' }); 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) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'dashboard' }); 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.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'dashboard' }); 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'), v.literal('Cancelado_RH') ), usuarioId: v.id('usuarios') }, returns: v.null(), handler: async (ctx, args) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'editar_status' }); // 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); } // Recalcular imediatamente o status de férias/licença do funcionário // para refletir o cancelamento (ou outra mudança) sem depender apenas do cron diário try { await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, { funcionarioId: registro.funcionarioId }); } catch (error) { console.error( '[ferias.atualizarStatus] Erro ao atualizar statusFerias do funcionário:', error ); } // Se o status foi alterado para Cancelado_RH, notificar o funcionário if (args.novoStatus === 'Cancelado_RH') { const funcionario = await ctx.db.get(registro.funcionarioId); if (funcionario) { const funcionarioUsuario = await ctx.db .query('usuarios') .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id)) .first(); if (funcionarioUsuario) { // Buscar usuário do RH que está cancelando const usuarioRH = await ctx.db.get(args.usuarioId); const nomeRH = usuarioRH?.nome || 'Recursos Humanos'; // Criar notificação in-app para funcionário await ctx.db.insert('notificacoesFerias', { destinatarioId: funcionarioUsuario._id, feriasId: registro._id, tipo: 'cancelado', lida: false, mensagem: `Sua solicitação de férias de ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)} (${registro.diasFerias} dias) foi cancelada pelo setor de Recursos Humanos.` }); // Obter URL do sistema let urlSistema = process.env.SITE_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: 'ferias_cancelada_rh', variaveis: { funcionarioNome: funcionarioUsuario.nome, dataInicio: formatarDataBR(registro.dataInicio), dataFim: formatarDataBR(registro.dataFim), diasFerias: registro.diasFerias.toString(), urlSistema }, enviadoPor: args.usuarioId }); } 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 ferias_cancelada_rh, usando envio direto:', error ); await ctx.runMutation(api.email.enfileirarEmail, { destinatario: funcionarioUsuario.email, destinatarioId: funcionarioUsuario._id, assunto: 'Solicitação de Férias Cancelada', corpo: `

Olá ${funcionarioUsuario.nome},

Sua solicitação de férias foi cancelada pelo setor de Recursos Humanos:

  • Período: ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)}
  • Dias: ${registro.diasFerias} dias

Para mais informações, entre em contato com o setor de Recursos Humanos.

`, enviadoPor: args.usuarioId }); } // Criar ou obter conversa entre RH 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(args.usuarioId) && conversa.participantes.includes(funcionarioUsuario._id) ) { conversaId = conversa._id; break; } } if (!conversaId) { conversaId = await ctx.db.insert('conversas', { tipo: 'individual', participantes: [args.usuarioId, funcionarioUsuario._id], criadoPor: args.usuarioId, criadoEm: Date.now() }); } // Criar mensagem de chat (texto simples) await ctx.db.insert('mensagens', { conversaId, remetenteId: args.usuarioId, tipo: 'texto', conteudo: `Sua solicitação de férias de ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)} (${registro.diasFerias} dias) foi cancelada pelo setor de Recursos Humanos. Para mais informações, entre em contato conosco.`, enviadaEm: Date.now() }); } } } 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 }); } } } // Determinar o status: férias tem prioridade sobre licença let novoStatus: 'ativo' | 'em_ferias' | 'em_licenca'; if (emFerias) { novoStatus = 'em_ferias'; } else { // Se não está em férias, verificar se está em licença const emLicenca = await verificarLicencaAtiva(ctx, func._id, hoje); novoStatus = emLicenca ? 'em_licenca' : 'ativo'; } if (func.statusFerias !== novoStatus) { await ctx.db.patch(func._id, { statusFerias: novoStatus }); } } return null; } }); // Internal Mutation: Atualizar status de um funcionário específico export const atualizarStatusFuncionario = internalMutation({ args: { funcionarioId: v.id('funcionarios') }, returns: v.null(), handler: async (ctx, args) => { const func = await ctx.db.get(args.funcionarioId); if (!func) return null; const hoje = new Date(); hoje.setHours(0, 0, 0, 0); // Buscar todos os registros de férias que podem estar em férias 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(); const idsAprovados = new Set(feriasAprovadas.map((f) => f._id)); const idsAjustados = new Set(feriasAjustadas.map((f) => f._id)); const statusAnteriorPorId = new Map, 'aprovado' | 'data_ajustada_aprovada'>(); for (const ferias of feriasEmFerias) { if (ferias.historicoAlteracoes && ferias.historicoAlteracoes.length > 0) { 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; } } } if (!statusAnteriorPorId.has(ferias._id)) { statusAnteriorPorId.set(ferias._id, 'aprovado'); } } 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; if (ferias.status !== 'EmFérias') { await ctx.db.patch(ferias._id, { status: 'EmFérias' }); } } else { if (ferias.status === 'EmFérias') { let statusAnterior: 'aprovado' | 'data_ajustada_aprovada'; if (idsAprovados.has(ferias._id)) { statusAnterior = 'aprovado'; } else if (idsAjustados.has(ferias._id)) { statusAnterior = 'data_ajustada_aprovada'; } else { statusAnterior = statusAnteriorPorId.get(ferias._id) || 'aprovado'; } await ctx.db.patch(ferias._id, { status: statusAnterior }); } } } // Determinar o status: férias tem prioridade sobre licença let novoStatus: 'ativo' | 'em_ferias' | 'em_licenca'; if (emFerias) { novoStatus = 'em_ferias'; console.log(`[atualizarStatusFuncionario] Funcionário ${func._id} está em férias`); } else { // Se não está em férias, verificar se está em licença console.log( `[atualizarStatusFuncionario] Verificando licença ativa para funcionário ${func._id}, data: ${hoje.toISOString()}` ); const emLicenca = await verificarLicencaAtiva(ctx, func._id, hoje); novoStatus = emLicenca ? 'em_licenca' : 'ativo'; console.log( `[atualizarStatusFuncionario] Funcionário ${func._id}: emLicenca=${emLicenca}, statusAtual=${func.statusFerias}, novoStatus=${novoStatus}` ); } if (func.statusFerias !== novoStatus) { console.log( `[atualizarStatusFuncionario] ⚠️ ATUALIZANDO status de "${func.statusFerias}" para "${novoStatus}"` ); await ctx.db.patch(func._id, { statusFerias: novoStatus }); console.log(`[atualizarStatusFuncionario] ✅ Status atualizado com sucesso!`); } else { console.log(`[atualizarStatusFuncionario] Status já está correto: ${novoStatus}`); } return null; } }); // Mutation pública para atualizar status do funcionário atual (útil para debug/teste) export const atualizarMeuStatus = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { recurso: 'ferias', acao: 'atualizar_status' }); const usuario = await getCurrentUserFunction(ctx); if (!usuario || !usuario.funcionarioId) { throw new Error('Usuário não encontrado ou não possui funcionário associado'); } await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, { funcionarioId: usuario.funcionarioId }); return null; } });