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">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
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 { authStore } from "$lib/stores/auth.svelte";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
|
|
||||||
@@ -81,6 +83,35 @@
|
|||||||
let criandoTemplates = $state(false);
|
let criandoTemplates = $state(false);
|
||||||
let progressoEnvio = $state({ total: 0, enviados: 0, falhas: 0 });
|
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
|
// Estados para modal de novo template
|
||||||
let modalNovoTemplateAberto = $state(false);
|
let modalNovoTemplateAberto = $state(false);
|
||||||
let codigoTemplate = $state("");
|
let codigoTemplate = $state("");
|
||||||
@@ -201,6 +232,27 @@
|
|||||||
return;
|
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;
|
processando = true;
|
||||||
progressoEnvio = { total: 0, enviados: 0, falhas: 0 };
|
progressoEnvio = { total: 0, enviados: 0, falhas: 0 };
|
||||||
|
|
||||||
@@ -236,12 +288,22 @@
|
|||||||
? templateSelecionado?.corpo || ""
|
? templateSelecionado?.corpo || ""
|
||||||
: mensagemPersonalizada;
|
: mensagemPersonalizada;
|
||||||
|
|
||||||
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
|
if (agendadaPara) {
|
||||||
conversaId: conversaResult.conversaId,
|
// Agendar mensagem
|
||||||
conteudo: mensagem,
|
resultadoChat = await client.mutation(api.chat.agendarMensagem, {
|
||||||
tipo: "texto",
|
conversaId: conversaResult.conversaId,
|
||||||
permitirNotificacaoParaSiMesmo: true,
|
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) {
|
} catch (error) {
|
||||||
console.error("Erro ao enviar chat:", error);
|
console.error("Erro ao enviar chat:", error);
|
||||||
@@ -264,6 +326,7 @@
|
|||||||
matricula: destinatario.matricula,
|
matricula: destinatario.matricula,
|
||||||
},
|
},
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: destinatario._id as any,
|
||||||
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -273,6 +336,7 @@
|
|||||||
assunto: "Notificação do Sistema",
|
assunto: "Notificação do Sistema",
|
||||||
corpo: mensagemPersonalizada,
|
corpo: mensagemPersonalizada,
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: destinatario._id as any,
|
||||||
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -282,19 +346,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Feedback de sucesso
|
// Feedback de sucesso
|
||||||
let mensagem = "Notificação enviada com sucesso!";
|
let mensagem = agendadaPara
|
||||||
if (canal === "ambos") {
|
? `✅ Notificação agendada com sucesso!`
|
||||||
if (resultadoChat && resultadoEmail) {
|
: "Notificação enviada com sucesso!";
|
||||||
mensagem = "✅ Notificação enviada para Chat e Email!";
|
if (agendadaPara) {
|
||||||
} else if (resultadoChat) {
|
const dataFormatada = format(new Date(agendadaPara), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
||||||
mensagem = "✅ Notificação enviada para Chat. Email falhou.";
|
mensagem += `\n\nSerá enviada em: ${dataFormatada}`;
|
||||||
} else if (resultadoEmail) {
|
} else {
|
||||||
mensagem = "✅ Notificação enviada para Email. Chat falhou.";
|
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);
|
alert(mensagem);
|
||||||
@@ -323,12 +394,20 @@
|
|||||||
? templateSelecionado?.corpo || ""
|
? templateSelecionado?.corpo || ""
|
||||||
: mensagemPersonalizada;
|
: mensagemPersonalizada;
|
||||||
|
|
||||||
await client.mutation(api.chat.enviarMensagem, {
|
if (agendadaPara) {
|
||||||
conversaId: conversaResult.conversaId,
|
await client.mutation(api.chat.agendarMensagem, {
|
||||||
conteudo: mensagem,
|
conversaId: conversaResult.conversaId,
|
||||||
tipo: "texto",
|
conteudo: mensagem,
|
||||||
permitirNotificacaoParaSiMesmo: true,
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await client.mutation(api.chat.enviarMensagem, {
|
||||||
|
conversaId: conversaResult.conversaId,
|
||||||
|
conteudo: mensagem,
|
||||||
|
tipo: "texto",
|
||||||
|
permitirNotificacaoParaSiMesmo: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
sucessosChat++;
|
sucessosChat++;
|
||||||
} else {
|
} else {
|
||||||
falhasChat++;
|
falhasChat++;
|
||||||
@@ -355,6 +434,7 @@
|
|||||||
matricula: destinatario.matricula || "",
|
matricula: destinatario.matricula || "",
|
||||||
},
|
},
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: destinatario._id as any,
|
||||||
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
sucessosEmail++;
|
sucessosEmail++;
|
||||||
} else {
|
} else {
|
||||||
@@ -367,6 +447,7 @@
|
|||||||
assunto: "Notificação do Sistema",
|
assunto: "Notificação do Sistema",
|
||||||
corpo: mensagemPersonalizada,
|
corpo: mensagemPersonalizada,
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: destinatario._id as any,
|
||||||
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
sucessosEmail++;
|
sucessosEmail++;
|
||||||
}
|
}
|
||||||
@@ -387,14 +468,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Feedback de envio em massa
|
// 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") {
|
if (canal === "ambos") {
|
||||||
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas\n`;
|
mensagem += `Chat: ${sucessosChat} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasChat} falhas\n`;
|
||||||
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
|
mensagem += `Email: ${sucessosEmail} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasEmail} falhas`;
|
||||||
} else if (canal === "chat") {
|
} else if (canal === "chat") {
|
||||||
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas`;
|
mensagem += `Chat: ${sucessosChat} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasChat} falhas`;
|
||||||
} else if (canal === "email") {
|
} else if (canal === "email") {
|
||||||
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
|
mensagem += `Email: ${sucessosEmail} ${agendadaPara ? 'agendados' : 'enviados'}, ${falhasEmail} falhas`;
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(mensagem);
|
alert(mensagem);
|
||||||
@@ -405,6 +494,9 @@
|
|||||||
enviarParaTodos = false;
|
enviarParaTodos = false;
|
||||||
templateId = "";
|
templateId = "";
|
||||||
mensagemPersonalizada = "";
|
mensagemPersonalizada = "";
|
||||||
|
agendarEnvio = false;
|
||||||
|
dataAgendamento = "";
|
||||||
|
horaAgendamento = "";
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao enviar notificação:", error);
|
console.error("Erro ao enviar notificação:", error);
|
||||||
alert("Erro ao enviar notificação: " + (error.message || "Erro desconhecido"));
|
alert("Erro ao enviar notificação: " + (error.message || "Erro desconhecido"));
|
||||||
@@ -592,6 +684,60 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 -->
|
<!-- Botão Enviar -->
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions justify-end mt-4">
|
||||||
<button
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/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})
|
Enviar para Todos ({usuarios.length})
|
||||||
{:else if !processando}
|
{:else}
|
||||||
Enviar Notificação
|
Enviar Notificação
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const enfileirarEmail = mutation({
|
|||||||
corpo: v.string(),
|
corpo: v.string(),
|
||||||
templateId: v.optional(v.id("templatesMensagens")),
|
templateId: v.optional(v.id("templatesMensagens")),
|
||||||
enviadoPorId: v.id("usuarios"),
|
enviadoPorId: v.id("usuarios"),
|
||||||
|
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||||
},
|
},
|
||||||
returns: v.object({
|
returns: v.object({
|
||||||
sucesso: v.boolean(),
|
sucesso: v.boolean(),
|
||||||
@@ -33,6 +34,13 @@ export const enfileirarEmail = mutation({
|
|||||||
return { sucesso: false };
|
return { sucesso: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validar agendamento se fornecido
|
||||||
|
if (args.agendadaPara !== undefined) {
|
||||||
|
if (args.agendadaPara <= Date.now()) {
|
||||||
|
return { sucesso: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Adicionar à fila
|
// Adicionar à fila
|
||||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||||
destinatario: args.destinatario,
|
destinatario: args.destinatario,
|
||||||
@@ -44,12 +52,22 @@ export const enfileirarEmail = mutation({
|
|||||||
tentativas: 0,
|
tentativas: 0,
|
||||||
enviadoPor: args.enviadoPorId,
|
enviadoPor: args.enviadoPorId,
|
||||||
criadoEm: Date.now(),
|
criadoEm: Date.now(),
|
||||||
|
agendadaPara: args.agendadaPara,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agendar envio imediato via action
|
// Agendar envio
|
||||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
if (args.agendadaPara !== undefined) {
|
||||||
emailId,
|
// 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 };
|
return { sucesso: true, emailId };
|
||||||
},
|
},
|
||||||
@@ -65,6 +83,7 @@ export const enviarEmailComTemplate = mutation({
|
|||||||
templateCodigo: v.string(),
|
templateCodigo: v.string(),
|
||||||
variaveis: v.record(v.string(), v.string()),
|
variaveis: v.record(v.string(), v.string()),
|
||||||
enviadoPorId: v.id("usuarios"),
|
enviadoPorId: v.id("usuarios"),
|
||||||
|
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||||
},
|
},
|
||||||
returns: v.object({
|
returns: v.object({
|
||||||
sucesso: v.boolean(),
|
sucesso: v.boolean(),
|
||||||
@@ -82,6 +101,13 @@ export const enviarEmailComTemplate = mutation({
|
|||||||
return { sucesso: false };
|
return { sucesso: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validar agendamento se fornecido
|
||||||
|
if (args.agendadaPara !== undefined) {
|
||||||
|
if (args.agendadaPara <= Date.now()) {
|
||||||
|
return { sucesso: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Renderizar template
|
// Renderizar template
|
||||||
const assunto = renderizarTemplate(template.titulo, args.variaveis);
|
const assunto = renderizarTemplate(template.titulo, args.variaveis);
|
||||||
const corpo = renderizarTemplate(template.corpo, args.variaveis);
|
const corpo = renderizarTemplate(template.corpo, args.variaveis);
|
||||||
@@ -97,12 +123,22 @@ export const enviarEmailComTemplate = mutation({
|
|||||||
tentativas: 0,
|
tentativas: 0,
|
||||||
enviadoPor: args.enviadoPorId,
|
enviadoPor: args.enviadoPorId,
|
||||||
criadoEm: Date.now(),
|
criadoEm: Date.now(),
|
||||||
|
agendadaPara: args.agendadaPara,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agendar envio imediato via action
|
// Agendar envio
|
||||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
if (args.agendadaPara !== undefined) {
|
||||||
emailId,
|
// 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 };
|
return { sucesso: true, emailId };
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user