feat: enhance scheduling and management of email notifications

- Added functionality to cancel scheduled email notifications, improving user control over their email management.
- Implemented a query to list all scheduled emails for the current user, providing better visibility into upcoming notifications.
- Enhanced the email schema to support scheduling features, including a timestamp for scheduled delivery.
- Improved error handling and user feedback for email scheduling actions, ensuring a smoother user experience.
This commit is contained in:
2025-11-04 00:43:13 -03:00
parent 7fb1693717
commit 3b89c496c6
6 changed files with 2385 additions and 1109 deletions

View File

@@ -6,10 +6,44 @@ import {
internalMutation,
internalQuery,
} from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { Doc, Id } from "./_generated/dataModel";
import type { QueryCtx, MutationCtx } from "./_generated/server";
import { renderizarTemplate } from "./templatesMensagens";
import { internal, api } from "./_generated/api";
// ========== HELPERS ==========
/**
* Helper function para obter usuário autenticado (Better Auth ou Sessão)
*/
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx): Promise<Doc<"usuarios"> | null> {
// Tentar autenticação via Better Auth primeiro
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual: Doc<"usuarios"> | null = null;
if (identity && identity.email) {
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
// Se não encontrou via Better Auth, tentar via sessão mais recente
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
return usuarioAtual;
}
/**
* Enfileirar email para envio
*/
@@ -187,7 +221,7 @@ export const reenviarEmail = mutation({
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
handler: async (ctx, args): Promise<{ sucesso: boolean }> => {
const email = await ctx.db.get(args.emailId);
if (!email) {
return { sucesso: false };
@@ -205,6 +239,52 @@ export const reenviarEmail = mutation({
},
});
/**
* Cancelar agendamento de email
*/
export const cancelarAgendamentoEmail = mutation({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => {
const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) {
return { sucesso: false, erro: "Usuário não autenticado" };
}
const email = await ctx.db.get(args.emailId);
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Verificar se o email pertence ao usuário atual
if (email.enviadoPor !== usuarioAtual._id) {
return { sucesso: false, erro: "Você não tem permissão para cancelar este agendamento" };
}
// Verificar se o email está agendado
if (!email.agendadaPara) {
return { sucesso: false, erro: "Este email não está agendado" };
}
// Verificar se ainda não foi enviado
if (email.status === "enviado") {
return { sucesso: false, erro: "Este email já foi enviado" };
}
// Verificar se já passou a data de agendamento
if (email.agendadaPara <= Date.now()) {
return { sucesso: false, erro: "A data de agendamento já passou" };
}
// Deletar o email agendado
await ctx.db.delete(args.emailId);
return { sucesso: true };
},
});
/**
* Action para enviar email (será implementado com nodemailer)
*
@@ -225,8 +305,8 @@ export const buscarEmailsPorIds = query({
args: {
emailIds: v.array(v.id("notificacoesEmail")),
},
handler: async (ctx, args) => {
const emails = [];
handler: async (ctx, args): Promise<Doc<"notificacoesEmail">[]> => {
const emails: Doc<"notificacoesEmail">[] = [];
for (const emailId of args.emailIds) {
const email = await ctx.db.get(emailId);
if (email) {
@@ -237,6 +317,57 @@ export const buscarEmailsPorIds = query({
},
});
/**
* Listar agendamentos de email do usuário atual
*/
export const listarAgendamentosEmail = query({
args: {},
handler: async (ctx): Promise<Array<Doc<"notificacoesEmail"> & { destinatarioInfo: Doc<"usuarios"> | null; templateInfo: Doc<"templatesMensagens"> | null }>> => {
const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) {
return [];
}
// Buscar todos os emails do usuário
const todosEmails = await ctx.db
.query("notificacoesEmail")
.withIndex("by_enviado_por", (q) => q.eq("enviadoPor", usuarioAtual._id))
.collect();
// Filtrar apenas os que têm agendamento (passados ou futuros)
const emailsAgendados = todosEmails.filter((email) => email.agendadaPara !== undefined);
// Enriquecer com informações do destinatário e template
const emailsEnriquecidos = await Promise.all(
emailsAgendados.map(async (email) => {
let destinatarioInfo: Doc<"usuarios"> | null = null;
let templateInfo: Doc<"templatesMensagens"> | null = null;
if (email.destinatarioId) {
destinatarioInfo = await ctx.db.get(email.destinatarioId);
}
if (email.templateId) {
templateInfo = await ctx.db.get(email.templateId);
}
return {
...email,
destinatarioInfo,
templateInfo,
};
})
);
// Ordenar por data de agendamento (mais próximos primeiro)
return emailsEnriquecidos.sort((a, b) => {
const dataA = a.agendadaPara ?? 0;
const dataB = b.agendadaPara ?? 0;
return dataA - dataB;
});
},
});
export const getActiveEmailConfig = internalQuery({
args: {},
// Tipo inferido automaticamente pelo Convex