feat: implement scheduling for notifications with enhanced validation
- Added functionality to schedule notifications for future delivery, including date and time selection in the UI. - Implemented validation to ensure scheduled times are in the future and correctly formatted. - Updated backend email handling to support scheduled sending, with appropriate checks for agendamentos. - Enhanced user feedback for both immediate and scheduled notifications, improving overall user experience.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
|
||||
@@ -81,6 +83,35 @@
|
||||
let criandoTemplates = $state(false);
|
||||
let progressoEnvio = $state({ total: 0, enviados: 0, falhas: 0 });
|
||||
|
||||
// Estados para agendamento
|
||||
let agendarEnvio = $state(false);
|
||||
let dataAgendamento = $state("");
|
||||
let horaAgendamento = $state("");
|
||||
|
||||
// Calcular data/hora mínimas
|
||||
const now = new Date();
|
||||
const minDate = format(now, "yyyy-MM-dd");
|
||||
|
||||
// Hora mínima recalculada baseada na data selecionada
|
||||
const horaMinima = $derived.by(() => {
|
||||
if (!dataAgendamento) return format(now, "HH:mm");
|
||||
const hoje = format(now, "yyyy-MM-dd");
|
||||
if (dataAgendamento === hoje) {
|
||||
return format(now, "HH:mm");
|
||||
}
|
||||
return undefined; // Sem restrição se for data futura
|
||||
});
|
||||
|
||||
function getPreviewAgendamento(): string {
|
||||
if (!agendarEnvio || !dataAgendamento || !horaAgendamento) return "";
|
||||
try {
|
||||
const dataHora = new Date(`${dataAgendamento}T${horaAgendamento}`);
|
||||
return `Será enviada em ${format(dataHora, "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Estados para modal de novo template
|
||||
let modalNovoTemplateAberto = $state(false);
|
||||
let codigoTemplate = $state("");
|
||||
@@ -201,6 +232,27 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar agendamento se marcado
|
||||
let agendadaPara: number | undefined = undefined;
|
||||
if (agendarEnvio) {
|
||||
if (!dataAgendamento || !horaAgendamento) {
|
||||
alert("Preencha a data e hora para agendamento");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataHora = new Date(`${dataAgendamento}T${horaAgendamento}`);
|
||||
if (dataHora.getTime() <= Date.now()) {
|
||||
alert("A data e hora devem ser futuras");
|
||||
return;
|
||||
}
|
||||
agendadaPara = dataHora.getTime();
|
||||
} catch (error) {
|
||||
alert("Data ou hora inválida");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
processando = true;
|
||||
progressoEnvio = { total: 0, enviados: 0, falhas: 0 };
|
||||
|
||||
@@ -236,12 +288,22 @@
|
||||
? templateSelecionado?.corpo || ""
|
||||
: mensagemPersonalizada;
|
||||
|
||||
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
});
|
||||
if (agendadaPara) {
|
||||
// Agendar mensagem
|
||||
resultadoChat = await client.mutation(api.chat.agendarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
} else {
|
||||
// Envio imediato
|
||||
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao enviar chat:", error);
|
||||
@@ -264,6 +326,7 @@
|
||||
matricula: destinatario.matricula,
|
||||
},
|
||||
enviadoPorId: destinatario._id as any,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -273,6 +336,7 @@
|
||||
assunto: "Notificação do Sistema",
|
||||
corpo: mensagemPersonalizada,
|
||||
enviadoPorId: destinatario._id as any,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -282,19 +346,26 @@
|
||||
}
|
||||
|
||||
// Feedback de sucesso
|
||||
let mensagem = "Notificação enviada com sucesso!";
|
||||
if (canal === "ambos") {
|
||||
if (resultadoChat && resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Chat e Email!";
|
||||
} else if (resultadoChat) {
|
||||
mensagem = "✅ Notificação enviada para Chat. Email falhou.";
|
||||
} else if (resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Email. Chat falhou.";
|
||||
let mensagem = agendadaPara
|
||||
? `✅ Notificação agendada com sucesso!`
|
||||
: "Notificação enviada com sucesso!";
|
||||
if (agendadaPara) {
|
||||
const dataFormatada = format(new Date(agendadaPara), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
||||
mensagem += `\n\nSerá enviada em: ${dataFormatada}`;
|
||||
} else {
|
||||
if (canal === "ambos") {
|
||||
if (resultadoChat && resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Chat e Email!";
|
||||
} else if (resultadoChat) {
|
||||
mensagem = "✅ Notificação enviada para Chat. Email falhou.";
|
||||
} else if (resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Email. Chat falhou.";
|
||||
}
|
||||
} else if (canal === "chat" && resultadoChat) {
|
||||
mensagem = "✅ Mensagem enviada no Chat!";
|
||||
} else if (canal === "email" && resultadoEmail) {
|
||||
mensagem = "✅ Email enfileirado para envio!";
|
||||
}
|
||||
} else if (canal === "chat" && resultadoChat) {
|
||||
mensagem = "✅ Mensagem enviada no Chat!";
|
||||
} else if (canal === "email" && resultadoEmail) {
|
||||
mensagem = "✅ Email enfileirado para envio!";
|
||||
}
|
||||
|
||||
alert(mensagem);
|
||||
@@ -323,12 +394,20 @@
|
||||
? templateSelecionado?.corpo || ""
|
||||
: mensagemPersonalizada;
|
||||
|
||||
await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
});
|
||||
if (agendadaPara) {
|
||||
await client.mutation(api.chat.agendarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
} else {
|
||||
await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto",
|
||||
permitirNotificacaoParaSiMesmo: true,
|
||||
});
|
||||
}
|
||||
sucessosChat++;
|
||||
} else {
|
||||
falhasChat++;
|
||||
@@ -355,6 +434,7 @@
|
||||
matricula: destinatario.matricula || "",
|
||||
},
|
||||
enviadoPorId: destinatario._id as any,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
sucessosEmail++;
|
||||
} else {
|
||||
@@ -367,6 +447,7 @@
|
||||
assunto: "Notificação do Sistema",
|
||||
corpo: mensagemPersonalizada,
|
||||
enviadoPorId: destinatario._id as any,
|
||||
agendadaPara: agendadaPara,
|
||||
});
|
||||
sucessosEmail++;
|
||||
}
|
||||
@@ -387,14 +468,22 @@
|
||||
}
|
||||
|
||||
// Feedback de envio em massa
|
||||
let mensagem = `✅ Envio em massa concluído!\n\n`;
|
||||
let mensagem = agendadaPara
|
||||
? `✅ Agendamento em massa concluído!\n\n`
|
||||
: `✅ Envio em massa concluído!\n\n`;
|
||||
|
||||
if (agendadaPara) {
|
||||
const dataFormatada = format(new Date(agendadaPara), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
||||
mensagem += `Será enviado em: ${dataFormatada}\n\n`;
|
||||
}
|
||||
|
||||
if (canal === "ambos") {
|
||||
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas\n`;
|
||||
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
|
||||
mensagem += `Chat: ${sucessosChat} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasChat} falhas\n`;
|
||||
mensagem += `Email: ${sucessosEmail} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasEmail} falhas`;
|
||||
} else if (canal === "chat") {
|
||||
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas`;
|
||||
mensagem += `Chat: ${sucessosChat} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasChat} falhas`;
|
||||
} else if (canal === "email") {
|
||||
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
|
||||
mensagem += `Email: ${sucessosEmail} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasEmail} falhas`;
|
||||
}
|
||||
|
||||
alert(mensagem);
|
||||
@@ -405,6 +494,9 @@
|
||||
enviarParaTodos = false;
|
||||
templateId = "";
|
||||
mensagemPersonalizada = "";
|
||||
agendarEnvio = false;
|
||||
dataAgendamento = "";
|
||||
horaAgendamento = "";
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao enviar notificação:", error);
|
||||
alert("Erro ao enviar notificação: " + (error.message || "Erro desconhecido"));
|
||||
@@ -592,6 +684,60 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Agendamento -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text font-medium">Agendar Envio</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={agendarEnvio}
|
||||
class="checkbox checkbox-primary"
|
||||
disabled={processando}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{#if agendarEnvio}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="data-agendamento">
|
||||
<span class="label-text">Data</span>
|
||||
</label>
|
||||
<input
|
||||
id="data-agendamento"
|
||||
type="date"
|
||||
class="input input-bordered"
|
||||
bind:value={dataAgendamento}
|
||||
min={minDate}
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="hora-agendamento">
|
||||
<span class="label-text">Hora</span>
|
||||
</label>
|
||||
<input
|
||||
id="hora-agendamento"
|
||||
type="time"
|
||||
class="input input-bordered"
|
||||
bind:value={horaAgendamento}
|
||||
min={horaMinima}
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if getPreviewAgendamento()}
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>{getPreviewAgendamento()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Botão Enviar -->
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
@@ -613,9 +759,15 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if enviarParaTodos && !processando}
|
||||
{#if processando}
|
||||
<!-- Texto já está no bloco acima -->
|
||||
{:else if agendarEnvio && enviarParaTodos}
|
||||
Agendar para Todos ({usuarios.length})
|
||||
{:else if agendarEnvio}
|
||||
Agendar Notificação
|
||||
{:else if enviarParaTodos}
|
||||
Enviar para Todos ({usuarios.length})
|
||||
{:else if !processando}
|
||||
{:else}
|
||||
Enviar Notificação
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -21,6 +21,7 @@ export const enfileirarEmail = mutation({
|
||||
corpo: v.string(),
|
||||
templateId: v.optional(v.id("templatesMensagens")),
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
@@ -33,6 +34,13 @@ export const enfileirarEmail = mutation({
|
||||
return { sucesso: false };
|
||||
}
|
||||
|
||||
// Validar agendamento se fornecido
|
||||
if (args.agendadaPara !== undefined) {
|
||||
if (args.agendadaPara <= Date.now()) {
|
||||
return { sucesso: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Adicionar à fila
|
||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||
destinatario: args.destinatario,
|
||||
@@ -44,12 +52,22 @@ export const enfileirarEmail = mutation({
|
||||
tentativas: 0,
|
||||
enviadoPor: args.enviadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
|
||||
// Agendar envio imediato via action
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
// Agendar envio
|
||||
if (args.agendadaPara !== undefined) {
|
||||
// Agendar para o momento especificado
|
||||
const delayMs = args.agendadaPara - Date.now();
|
||||
await ctx.scheduler.runAfter(delayMs, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
} else {
|
||||
// Envio imediato
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
}
|
||||
|
||||
return { sucesso: true, emailId };
|
||||
},
|
||||
@@ -65,6 +83,7 @@ export const enviarEmailComTemplate = mutation({
|
||||
templateCodigo: v.string(),
|
||||
variaveis: v.record(v.string(), v.string()),
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
@@ -82,6 +101,13 @@ export const enviarEmailComTemplate = mutation({
|
||||
return { sucesso: false };
|
||||
}
|
||||
|
||||
// Validar agendamento se fornecido
|
||||
if (args.agendadaPara !== undefined) {
|
||||
if (args.agendadaPara <= Date.now()) {
|
||||
return { sucesso: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Renderizar template
|
||||
const assunto = renderizarTemplate(template.titulo, args.variaveis);
|
||||
const corpo = renderizarTemplate(template.corpo, args.variaveis);
|
||||
@@ -97,12 +123,22 @@ export const enviarEmailComTemplate = mutation({
|
||||
tentativas: 0,
|
||||
enviadoPor: args.enviadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
|
||||
// Agendar envio imediato via action
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
// Agendar envio
|
||||
if (args.agendadaPara !== undefined) {
|
||||
// Agendar para o momento especificado
|
||||
const delayMs = args.agendadaPara - Date.now();
|
||||
await ctx.scheduler.runAfter(delayMs, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
} else {
|
||||
// Envio imediato
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId,
|
||||
});
|
||||
}
|
||||
|
||||
return { sucesso: true, emailId };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user