Fix usuarios page #6

Merged
killer-cf merged 28 commits from fix-usuarios-page into master 2025-11-04 17:42:21 +00:00
62 changed files with 5853 additions and 5997 deletions
Showing only changes of commit 3d8f907fa5 - Show all commits

View File

@@ -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>

View File

@@ -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 };
},