feat: enhance vacation approval process by adding notification system for employees, including email alerts and in-app notifications; improve error handling and user feedback during vacation management
This commit is contained in:
@@ -5,7 +5,8 @@ import { type DataModel } from './_generated/dataModel';
|
||||
import { MutationCtx, query, QueryCtx } from './_generated/server';
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
const siteUrl = process.env.SITE_URL!;
|
||||
// Usar SITE_URL se disponível, caso contrário usar CONVEX_SITE_URL ou um valor padrão
|
||||
const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || 'http://localhost:5173';
|
||||
|
||||
// The component client has methods needed for integrating Convex with Better Auth,
|
||||
// as well as helper methods for general use.
|
||||
|
||||
@@ -859,7 +859,11 @@ export const marcarNotificacaoLida = mutation({
|
||||
if (!usuarioAtual) throw new Error('Não autenticado');
|
||||
|
||||
const notificacao = await ctx.db.get(args.notificacaoId);
|
||||
if (!notificacao) throw new Error('Notificação não encontrada');
|
||||
// Se a notificação não existe (já foi deletada), retornar sucesso silenciosamente
|
||||
// Isso evita erros quando múltiplas tentativas são feitas ou quando a notificação já foi removida
|
||||
if (!notificacao) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// SEGURANÇA: Verificar se a notificação pertence ao usuário atual
|
||||
if (notificacao.usuarioId !== usuarioAtual._id) {
|
||||
@@ -874,6 +878,11 @@ export const marcarNotificacaoLida = mutation({
|
||||
}
|
||||
}
|
||||
|
||||
// Se já está marcada como lida, retornar sucesso sem fazer nada
|
||||
if (notificacao.lida) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.notificacaoId, { lida: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ 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({
|
||||
@@ -433,13 +435,58 @@ export const aprovar = mutation({
|
||||
.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 ${registro.diasFerias} dias foi aprovado!`
|
||||
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.FRONTEND_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: `<p>Olá ${usuario.nome},</p>
|
||||
<p>Sua solicitação de férias foi <strong>aprovada</strong> pelo gestor ${gestorUsuario.nome}:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)}</li>
|
||||
<li><strong>Dias:</strong> ${registro.diasFerias} dias</li>
|
||||
</ul>`,
|
||||
enviadoPor: args.gestorId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +600,10 @@ export const ajustarEAprovar = mutation({
|
||||
]
|
||||
});
|
||||
|
||||
// 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
|
||||
@@ -561,13 +612,58 @@ export const ajustarEAprovar = mutation({
|
||||
.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: ${args.novaDataInicio} a ${args.novaDataFim}`
|
||||
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.FRONTEND_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: `<p>Olá ${usuario.nome},</p>
|
||||
<p>Sua solicitação de férias foi <strong>aprovada com ajuste de datas</strong> pelo gestor ${gestorUsuario.nome}:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${formatarDataBR(args.novaDataInicio)} até ${formatarDataBR(args.novaDataFim)}</li>
|
||||
<li><strong>Dias:</strong> ${novosDias} dias</li>
|
||||
</ul>`,
|
||||
enviadoPor: args.gestorId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,6 +814,111 @@ export const atualizarStatus = mutation({
|
||||
);
|
||||
}
|
||||
|
||||
// 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.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: '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: `<p>Olá ${funcionarioUsuario.nome},</p>
|
||||
<p>Sua solicitação de férias foi <strong>cancelada</strong> pelo setor de Recursos Humanos:</p>
|
||||
<ul>
|
||||
<li><strong>Período:</strong> ${formatarDataBR(registro.dataInicio)} até ${formatarDataBR(registro.dataFim)}</li>
|
||||
<li><strong>Dias:</strong> ${registro.diasFerias} dias</li>
|
||||
</ul>
|
||||
<p>Para mais informações, entre em contato com o setor de Recursos Humanos.</p>`,
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,6 +145,180 @@ export const listarAlertas = query({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Verificar configuração do sistema de alertas (diagnóstico)
|
||||
*/
|
||||
export const verificarConfiguracaoAlertas = query({
|
||||
args: {
|
||||
_refresh: v.optional(v.number()) // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
returns: v.object({
|
||||
templateExiste: v.boolean(),
|
||||
templateInfo: v.union(
|
||||
v.object({
|
||||
_id: v.id('templatesMensagens'),
|
||||
codigo: v.string(),
|
||||
nome: v.string(),
|
||||
htmlCorpo: v.optional(v.string())
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
roleTiMasterExiste: v.boolean(),
|
||||
usuariosTiMaster: v.array(
|
||||
v.object({
|
||||
_id: v.id('usuarios'),
|
||||
nome: v.string(),
|
||||
email: v.optional(v.string()),
|
||||
temEmail: v.boolean()
|
||||
})
|
||||
),
|
||||
configSmtpAtiva: v.boolean(),
|
||||
configSmtpInfo: v.union(
|
||||
v.object({
|
||||
_id: v.id('configuracaoEmail'),
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
emailRemetente: v.string(),
|
||||
ativo: v.boolean()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
emailsPendentes: v.number(),
|
||||
emailsFalha: v.number(),
|
||||
alertasAtivos: v.number(),
|
||||
alertasComEmail: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
try {
|
||||
// 1. Verificar template
|
||||
let template = null;
|
||||
try {
|
||||
template = await ctx.db
|
||||
.query('templatesMensagens')
|
||||
.withIndex('by_codigo', (q) => q.eq('codigo', 'monitoramento_alerta_sistema'))
|
||||
.first();
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar template:', error);
|
||||
}
|
||||
|
||||
// 2. Verificar role TI_MASTER
|
||||
let roleTiMaster = null;
|
||||
try {
|
||||
roleTiMaster = await ctx.db
|
||||
.query('roles')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', 'ti_master'))
|
||||
.first();
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar role TI_MASTER:', error);
|
||||
}
|
||||
|
||||
// 3. Verificar usuários TI_MASTER
|
||||
let usuariosTiMaster: Array<{
|
||||
_id: Id<'usuarios'>;
|
||||
nome: string;
|
||||
email?: string;
|
||||
temEmail: boolean;
|
||||
}> = [];
|
||||
|
||||
if (roleTiMaster) {
|
||||
try {
|
||||
const usuarios = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', roleTiMaster!._id))
|
||||
.collect();
|
||||
|
||||
usuariosTiMaster = usuarios.map((u) => ({
|
||||
_id: u._id,
|
||||
nome: u.nome,
|
||||
email: u.email,
|
||||
temEmail: !!u.email
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar usuários TI_MASTER:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Verificar configuração SMTP
|
||||
let configSmtp = null;
|
||||
try {
|
||||
configSmtp = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar configuração SMTP:', error);
|
||||
}
|
||||
|
||||
// 5. Verificar fila de emails
|
||||
let emailsPendentes = 0;
|
||||
let emailsFalha = 0;
|
||||
try {
|
||||
const todosEmails = await ctx.db.query('notificacoesEmail').collect();
|
||||
emailsPendentes = todosEmails.filter((e) => e.status === 'pendente').length;
|
||||
emailsFalha = todosEmails.filter((e) => e.status === 'falha').length;
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar emails:', error);
|
||||
}
|
||||
|
||||
// 6. Verificar alertas
|
||||
let alertasAtivos = 0;
|
||||
let alertasComEmail = 0;
|
||||
try {
|
||||
const todosAlertas = await ctx.db.query('alertConfigurations').collect();
|
||||
alertasAtivos = todosAlertas.filter((a) => a.enabled).length;
|
||||
alertasComEmail = todosAlertas.filter(
|
||||
(a) => a.enabled && a.notifyByEmail
|
||||
).length;
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar alertas:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
templateExiste: !!template,
|
||||
templateInfo: template
|
||||
? {
|
||||
_id: template._id,
|
||||
codigo: template.codigo,
|
||||
nome: template.nome,
|
||||
htmlCorpo: template.htmlCorpo
|
||||
}
|
||||
: null,
|
||||
roleTiMasterExiste: !!roleTiMaster,
|
||||
usuariosTiMaster,
|
||||
configSmtpAtiva: !!configSmtp,
|
||||
configSmtpInfo: configSmtp
|
||||
? {
|
||||
_id: configSmtp._id,
|
||||
servidor: configSmtp.servidor,
|
||||
porta: configSmtp.porta,
|
||||
emailRemetente: configSmtp.emailRemetente,
|
||||
ativo: configSmtp.ativo
|
||||
}
|
||||
: null,
|
||||
emailsPendentes,
|
||||
emailsFalha,
|
||||
alertasAtivos,
|
||||
alertasComEmail
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar configuração de alertas:', error);
|
||||
// Retornar valores padrão em caso de erro
|
||||
return {
|
||||
templateExiste: false,
|
||||
templateInfo: null,
|
||||
roleTiMasterExiste: false,
|
||||
usuariosTiMaster: [],
|
||||
configSmtpAtiva: false,
|
||||
configSmtpInfo: null,
|
||||
emailsPendentes: 0,
|
||||
emailsFalha: 0,
|
||||
alertasAtivos: 0,
|
||||
alertasComEmail: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter métricas com filtros
|
||||
*/
|
||||
@@ -340,62 +514,89 @@ export const verificarAlertasInternal = internalMutation({
|
||||
|
||||
// Criar notificação no chat se configurado
|
||||
if (alerta.notifyByChat) {
|
||||
// Buscar roles administrativas (nível <= 1) e filtrar usuários por roleId
|
||||
const rolesAdminOuTi = await ctx.db
|
||||
// Buscar apenas a role TI_MASTER
|
||||
const roleTiMaster = await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.lte(q.field('nivel'), 1))
|
||||
.collect();
|
||||
.withIndex('by_nome', (q) => q.eq('nome', 'ti_master'))
|
||||
.first();
|
||||
|
||||
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
|
||||
if (!roleTiMaster) {
|
||||
console.warn('Role TI_MASTER não encontrada. Notificações de chat não serão enviadas.');
|
||||
} else {
|
||||
// Buscar usuários com role TI_MASTER
|
||||
const usuarios = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', roleTiMaster._id))
|
||||
.collect();
|
||||
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId));
|
||||
|
||||
for (const usuario of usuariosTI) {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: usuario._id,
|
||||
tipo: 'nova_mensagem',
|
||||
titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`,
|
||||
descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
for (const usuario of usuarios) {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: usuario._id,
|
||||
tipo: 'nova_mensagem',
|
||||
titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`,
|
||||
descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar email se configurado (usar template HTML padronizado)
|
||||
if (alerta.notifyByEmail) {
|
||||
// Buscar usuários administradores/TI para receber o alerta por email
|
||||
const rolesAdminOuTi = await ctx.db
|
||||
// Buscar apenas a role TI_MASTER
|
||||
const roleTiMaster = await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.lte(q.field('nivel'), 1))
|
||||
.collect();
|
||||
.withIndex('by_nome', (q) => q.eq('nome', 'ti_master'))
|
||||
.first();
|
||||
|
||||
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId) && !!u.email);
|
||||
if (!roleTiMaster) {
|
||||
console.warn('⚠️ [Monitoramento] Role TI_MASTER não encontrada. Emails de alerta não serão enviados.');
|
||||
} else {
|
||||
// Buscar usuários com role TI_MASTER que possuem email
|
||||
const usuarios = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', roleTiMaster._id))
|
||||
.collect();
|
||||
|
||||
for (const usuario of usuariosTI) {
|
||||
const email = usuario.email;
|
||||
if (!email) continue;
|
||||
if (usuarios.length === 0) {
|
||||
console.warn('⚠️ [Monitoramento] Nenhum usuário TI_MASTER encontrado para receber alertas por email.');
|
||||
} else {
|
||||
// Usar o createdBy do alerta como enviadoPor (quem criou o alerta)
|
||||
const enviadoPorId = alerta.createdBy;
|
||||
|
||||
// Montar variáveis para template de alerta de sistema
|
||||
const variaveisEmail = {
|
||||
destinatarioNome: usuario.nome,
|
||||
metricName: alerta.metricName,
|
||||
metricValue: metricValue.toFixed(2),
|
||||
threshold: alerta.threshold.toString()
|
||||
};
|
||||
for (const usuario of usuarios) {
|
||||
const email = usuario.email;
|
||||
if (!email) {
|
||||
console.warn(`⚠️ [Monitoramento] Usuário ${usuario._id} (TI_MASTER) não possui email cadastrado.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Importante: usar api.email.enviarEmailComTemplate (action pública),
|
||||
// e não internal.email, para corresponder à tipagem gerada em ./_generated/api.
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: email,
|
||||
destinatarioId: usuario._id,
|
||||
templateCodigo: 'monitoramento_alerta_sistema',
|
||||
variaveis: variaveisEmail,
|
||||
enviadoPor: usuario._id
|
||||
});
|
||||
// Montar variáveis para template de alerta de sistema
|
||||
const variaveisEmail = {
|
||||
destinatarioNome: usuario.nome,
|
||||
metricName: alerta.metricName,
|
||||
metricValue: metricValue.toFixed(2),
|
||||
threshold: alerta.threshold.toString()
|
||||
};
|
||||
|
||||
try {
|
||||
// Importante: usar api.email.enviarEmailComTemplate (action pública),
|
||||
// e não internal.email, para corresponder à tipagem gerada em ./_generated/api.
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: email,
|
||||
destinatarioId: usuario._id,
|
||||
templateCodigo: 'monitoramento_alerta_sistema',
|
||||
variaveis: variaveisEmail,
|
||||
enviadoPor: enviadoPorId // ✅ CORRIGIDO: usar createdBy do alerta
|
||||
});
|
||||
console.log(`✅ [Monitoramento] Email de alerta agendado para ${email} (${usuario.nome})`);
|
||||
} catch (error) {
|
||||
console.error(`❌ [Monitoramento] Erro ao agendar email de alerta para ${email}:`, error);
|
||||
// Continuar tentando enviar para outros usuários mesmo se um falhar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ export const feriasTables = {
|
||||
v.literal('nova_solicitacao'),
|
||||
v.literal('aprovado'),
|
||||
v.literal('reprovado'),
|
||||
v.literal('data_ajustada')
|
||||
v.literal('data_ajustada'),
|
||||
v.literal('cancelado')
|
||||
),
|
||||
lida: v.boolean(),
|
||||
mensagem: v.string()
|
||||
|
||||
@@ -352,8 +352,27 @@ export const criarTemplatesPadrao = mutation({
|
||||
nome: 'Boas-vindas',
|
||||
titulo: 'Bem-vindo ao SGSE',
|
||||
corpo:
|
||||
'Olá {{nome}},\n\nSeja bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI',
|
||||
variaveis: ['nome', 'matricula', 'senha']
|
||||
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
|
||||
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
|
||||
"<h2 style='color: #2563EB;'>Bem-vindo ao SGSE</h2>" +
|
||||
"<p>Olá <strong>{{nome}}</strong>,</p>" +
|
||||
"<p>Seja bem-vindo ao <strong>SGSE - Sistema de Gerenciamento de Secretaria</strong>!</p>" +
|
||||
"<p>Seu cadastro foi realizado com sucesso.</p>" +
|
||||
"<div style='background-color: #F3F4F6; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||||
"<p style='margin: 0 0 10px 0;'><strong>Suas credenciais de acesso:</strong></p>" +
|
||||
"<ul style='margin: 0; padding-left: 20px;'>" +
|
||||
"<li><strong>E-mail:</strong> {{email}}</li>" +
|
||||
"{{credenciaisAdicionais}}" +
|
||||
"<li><strong>Senha temporária:</strong> {{senha}}</li>" +
|
||||
"</ul>" +
|
||||
"</div>" +
|
||||
"<p><strong>⚠️ Importante:</strong> Por favor, altere sua senha no primeiro acesso ao sistema.</p>" +
|
||||
"<p>Acesse o sistema através do link: <a href='{{urlSistema}}' style='color: #2563EB;'>{{urlSistema}}</a></p>" +
|
||||
"<p style='margin-top: 30px; color: #6B7280; font-size: 14px;'>Equipe de TI - Secretaria de Esportes</p>" +
|
||||
"</div></body></html>",
|
||||
variaveis: ['nome', 'email', 'credenciaisAdicionais', 'senha', 'urlSistema'],
|
||||
categoria: 'email' as const,
|
||||
tags: ['boas_vindas', 'cadastro', 'credenciais']
|
||||
},
|
||||
{
|
||||
codigo: 'chat_mensagem',
|
||||
@@ -545,6 +564,33 @@ export const criarTemplatesPadrao = mutation({
|
||||
'Recomenda-se verificar o painel de monitoramento do SGSE para detalhes adicionais e, se necessário, ' +
|
||||
'executar ações corretivas.\n\n' +
|
||||
'Esta é uma notificação automática do sistema de monitoramento SGSE.',
|
||||
htmlCorpo:
|
||||
'<div style="max-width: 600px; margin: 0 auto; padding: 20px;">' +
|
||||
'<div style="background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%); border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' +
|
||||
'<h2 style="color: #FFFFFF; margin: 0 0 10px 0; font-size: 24px; font-weight: bold;">⚠️ Alerta de Sistema</h2>' +
|
||||
'<p style="color: #FFFFFF; margin: 0; font-size: 16px; font-weight: 500;">Métrica: <strong>{{metricName}}</strong></p>' +
|
||||
'</div>' +
|
||||
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||||
'<div style="background-color: #FFF3CD; border-left: 4px solid #FFC107; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||||
'<p style="margin: 0 0 10px 0; color: #856404; font-weight: bold; font-size: 14px;">📊 Detalhes do Alerta:</p>' +
|
||||
'<ul style="margin: 0; padding-left: 20px; color: #856404; font-size: 14px; line-height: 1.8;">' +
|
||||
'<li><strong>Métrica:</strong> {{metricName}}</li>' +
|
||||
'<li><strong>Valor Atual:</strong> <span style="color: #DC3545; font-weight: bold;">{{metricValue}}</span></li>' +
|
||||
'<li><strong>Limite Configurado:</strong> {{threshold}}</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<p style="color: #333333; font-size: 14px; line-height: 1.6; margin: 20px 0;">' +
|
||||
'Recomenda-se verificar o <strong>painel de monitoramento do SGSE</strong> para detalhes adicionais e, se necessário, executar ações corretivas.' +
|
||||
'</p>' +
|
||||
'<div style="background-color: #E7F3FF; border-left: 4px solid #0052A5; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||||
'<p style="margin: 0; color: #004085; font-size: 14px; line-height: 1.6;">' +
|
||||
'<strong>💡 Dica:</strong> Acesse o painel de monitoramento para visualizar gráficos e histórico detalhado desta métrica.' +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'<p style="color: #666666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #E0E0E0;">' +
|
||||
'Esta é uma notificação automática do sistema de monitoramento SGSE.' +
|
||||
'</p>' +
|
||||
'</div>',
|
||||
variaveis: ['destinatarioNome', 'metricName', 'metricValue', 'threshold'],
|
||||
categoria: 'email' as const,
|
||||
tags: ['monitoramento', 'alerta', 'sistema', 'ti']
|
||||
@@ -702,6 +748,39 @@ export const criarTemplatesPadrao = mutation({
|
||||
categoria: 'email' as const,
|
||||
tags: ['ausencia', 'reprovacao', 'gestao']
|
||||
},
|
||||
{
|
||||
codigo: 'ferias_aprovada',
|
||||
nome: 'Férias Aprovada',
|
||||
titulo: 'Solicitação de Férias Aprovada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de férias foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Dias:</strong> {{diasFerias}} dias</li></ul>',
|
||||
variaveis: [
|
||||
'funcionarioNome',
|
||||
'gestorNome',
|
||||
'dataInicio',
|
||||
'dataFim',
|
||||
'diasFerias',
|
||||
'urlSistema'
|
||||
],
|
||||
categoria: 'email' as const,
|
||||
tags: ['ferias', 'aprovacao', 'gestao']
|
||||
},
|
||||
{
|
||||
codigo: 'ferias_cancelada_rh',
|
||||
nome: 'Férias Cancelada pelo RH',
|
||||
titulo: 'Solicitação de Férias Cancelada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de férias foi <strong>cancelada</strong> pelo setor de Recursos Humanos:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Dias:</strong> {{diasFerias}} dias</li></ul>\n\nPara mais informações, entre em contato com o setor de Recursos Humanos.',
|
||||
variaveis: [
|
||||
'funcionarioNome',
|
||||
'dataInicio',
|
||||
'dataFim',
|
||||
'diasFerias',
|
||||
'urlSistema'
|
||||
],
|
||||
categoria: 'email' as const,
|
||||
tags: ['ferias', 'cancelamento', 'recursos_humanos']
|
||||
},
|
||||
// ===================== ALERTAS DE SEGURANÇA CIBERNÉTICA =====================
|
||||
{
|
||||
codigo: 'incidente_critico',
|
||||
|
||||
@@ -124,6 +124,116 @@ export const criar = mutation({
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Obter usuário que está criando (para enviar email e chat)
|
||||
const usuarioCriador = await getCurrentUserFunction(ctx);
|
||||
if (!usuarioCriador) {
|
||||
// Se não conseguir obter o criador, retornar sucesso mesmo assim
|
||||
return { sucesso: true as const, usuarioId };
|
||||
}
|
||||
|
||||
// Buscar funcionário para obter matrícula se houver
|
||||
let matricula = '';
|
||||
if (args.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (funcionario?.matricula) {
|
||||
matricula = funcionario.matricula;
|
||||
}
|
||||
}
|
||||
|
||||
// Preparar credenciais adicionais (matrícula se houver)
|
||||
const credenciaisAdicionais = matricula
|
||||
? `<li><strong>Matrícula:</strong> ${matricula}</li>`
|
||||
: '';
|
||||
|
||||
// Obter URL do sistema
|
||||
let urlSistema = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
if (!urlSistema.match(/^https?:\/\//i)) {
|
||||
urlSistema = `http://${urlSistema}`;
|
||||
}
|
||||
|
||||
// Enviar email de boas-vindas usando template (agendado via scheduler)
|
||||
try {
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: args.email,
|
||||
destinatarioId: usuarioId,
|
||||
templateCodigo: 'BEM_VINDO',
|
||||
variaveis: {
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
credenciaisAdicionais,
|
||||
senha: senhaTemporaria,
|
||||
urlSistema
|
||||
},
|
||||
enviadoPor: usuarioCriador._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 BEM_VINDO, usando envio direto:',
|
||||
error
|
||||
);
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: args.email,
|
||||
destinatarioId: usuarioId,
|
||||
assunto: 'Bem-vindo ao SGSE',
|
||||
corpo: `<p>Olá <strong>${args.nome}</strong>,</p>
|
||||
<p>Seja bem-vindo ao <strong>SGSE - Sistema de Gerenciamento de Secretaria</strong>!</p>
|
||||
<p>Seu cadastro foi realizado com sucesso.</p>
|
||||
<div style='background-color: #F3F4F6; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>
|
||||
<p style='margin: 0 0 10px 0;'><strong>Suas credenciais de acesso:</strong></p>
|
||||
<ul style='margin: 0; padding-left: 20px;'>
|
||||
<li><strong>E-mail:</strong> ${args.email}</li>
|
||||
${credenciaisAdicionais}
|
||||
<li><strong>Senha temporária:</strong> ${senhaTemporaria}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><strong>⚠️ Importante:</strong> Por favor, altere sua senha no primeiro acesso ao sistema.</p>
|
||||
<p>Acesse o sistema através do link: <a href='${urlSistema}' style='color: #2563EB;'>${urlSistema}</a></p>
|
||||
<p style='margin-top: 30px; color: #6B7280; font-size: 14px;'>Equipe de TI - Secretaria de Esportes</p>`,
|
||||
enviadoPor: usuarioCriador._id
|
||||
});
|
||||
}
|
||||
|
||||
// Criar ou obter conversa entre criador e novo usuá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(usuarioCriador._id) &&
|
||||
conversa.participantes.includes(usuarioId)
|
||||
) {
|
||||
conversaId = conversa._id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversaId) {
|
||||
conversaId = await ctx.db.insert('conversas', {
|
||||
tipo: 'individual',
|
||||
participantes: [usuarioCriador._id, usuarioId],
|
||||
criadoPor: usuarioCriador._id,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Criar mensagem de chat (texto simples)
|
||||
const mensagemChat = matricula
|
||||
? `Bem-vindo ao SGSE! Seu cadastro foi realizado com sucesso. Suas credenciais de acesso: E-mail: ${args.email}, Matrícula: ${matricula}, Senha temporária: ${senhaTemporaria}. Por favor, altere sua senha no primeiro acesso.`
|
||||
: `Bem-vindo ao SGSE! Seu cadastro foi realizado com sucesso. Suas credenciais de acesso: E-mail: ${args.email}, Senha temporária: ${senhaTemporaria}. Por favor, altere sua senha no primeiro acesso.`;
|
||||
|
||||
await ctx.db.insert('mensagens', {
|
||||
conversaId,
|
||||
remetenteId: usuarioCriador._id,
|
||||
tipo: 'texto',
|
||||
conteudo: mensagemChat,
|
||||
enviadaEm: Date.now()
|
||||
});
|
||||
|
||||
return { sucesso: true as const, usuarioId };
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user