refactor: enhance notification management UI and improve data handling

- Refactored the notification management component to improve data extraction from queries, ensuring robust handling of loading states and errors.
- Introduced a modal for creating new templates, including validation and user authentication checks.
- Enhanced the notification sending logic to support bulk sending and provide detailed feedback on the sending process.
- Improved UI elements for better user experience, including loading indicators and dynamic user selection options.
This commit is contained in:
2025-11-03 23:04:31 -03:00
parent 35ff55822d
commit e59d96735a

View File

@@ -1,25 +1,193 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { authStore } from "$lib/stores/auth.svelte";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
const client = useConvexClient();
const templates = useQuery(api.templatesMensagens.listarTemplates, {});
const usuarios = useQuery(api.usuarios.listar, {});
// Queries
const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {});
const usuariosQuery = useQuery(api.usuarios.listar, {});
// Extrair dados das queries de forma robusta
const templates = $derived.by(() => {
if (templatesQuery === undefined || templatesQuery === null) {
return [];
}
if ("data" in templatesQuery && templatesQuery.data !== undefined) {
return Array.isArray(templatesQuery.data) ? templatesQuery.data : [];
}
if (Array.isArray(templatesQuery)) {
return templatesQuery;
}
return [];
});
const usuarios = $derived.by(() => {
if (usuariosQuery === undefined || usuariosQuery === null) {
return [];
}
if ("data" in usuariosQuery && usuariosQuery.data !== undefined) {
return Array.isArray(usuariosQuery.data) ? usuariosQuery.data : [];
}
if (Array.isArray(usuariosQuery)) {
return usuariosQuery;
}
return [];
});
// Estados de carregamento e erro
const carregandoTemplates = $derived(
templatesQuery === undefined || templatesQuery === null
);
const carregandoUsuarios = $derived(
usuariosQuery === undefined || usuariosQuery === null
);
// Verificar erros de forma mais robusta
const erroTemplates = $derived.by(() => {
if (templatesQuery === undefined || templatesQuery === null) return false;
// Verificar se é um objeto com propriedade error
if (typeof templatesQuery === 'object' && 'error' in templatesQuery) {
return templatesQuery.error !== undefined && templatesQuery.error !== null;
}
return false;
});
const erroUsuarios = $derived.by(() => {
if (usuariosQuery === undefined || usuariosQuery === null) return false;
if (typeof usuariosQuery === 'object' && 'error' in usuariosQuery) {
return usuariosQuery.error !== undefined && usuariosQuery.error !== null;
}
return false;
});
// Log para debug (remover depois se necessário)
$effect(() => {
if (templatesQuery !== undefined && templatesQuery !== null) {
console.log("Templates Query:", templatesQuery);
console.log("Templates Extraídos:", templates);
}
});
let destinatarioId = $state("");
let enviarParaTodos = $state(false);
let canal = $state<"chat" | "email" | "ambos">("chat");
let templateId = $state("");
let mensagemPersonalizada = $state("");
let usarTemplate = $state(true);
let processando = $state(false);
let criandoTemplates = $state(false);
let progressoEnvio = $state({ total: 0, enviados: 0, falhas: 0 });
// Estados para modal de novo template
let modalNovoTemplateAberto = $state(false);
let codigoTemplate = $state("");
let nomeTemplate = $state("");
let tituloTemplate = $state("");
let corpoTemplate = $state("");
let variaveisTemplate = $state("");
let criandoNovoTemplate = $state(false);
const templateSelecionado = $derived(
templates?.data?.find(t => t._id === templateId)
templates.find(t => t._id === templateId)
);
async function criarTemplatesPadrao() {
if (criandoTemplates) return;
criandoTemplates = true;
try {
const resultado = await client.mutation(api.templatesMensagens.criarTemplatesPadrao, {});
if (resultado.sucesso) {
alert("✅ Templates padrão criados com sucesso! A página será recarregada.");
window.location.reload();
} else {
alert("❌ Erro ao criar templates padrão.");
}
} catch (error: any) {
console.error("Erro ao criar templates:", error);
alert("❌ Erro ao criar templates: " + (error.message || "Erro desconhecido"));
} finally {
criandoTemplates = false;
}
}
function abrirModalNovoTemplate() {
modalNovoTemplateAberto = true;
// Limpar campos
codigoTemplate = "";
nomeTemplate = "";
tituloTemplate = "";
corpoTemplate = "";
variaveisTemplate = "";
}
function fecharModalNovoTemplate() {
modalNovoTemplateAberto = false;
}
async function salvarNovoTemplate() {
if (!authStore.usuario) {
alert("❌ Você precisa estar autenticado para criar templates.");
return;
}
// Validações
if (!codigoTemplate.trim()) {
alert("❌ O código do template é obrigatório.");
return;
}
if (!nomeTemplate.trim()) {
alert("❌ O nome do template é obrigatório.");
return;
}
if (!tituloTemplate.trim()) {
alert("❌ O título do template é obrigatório.");
return;
}
if (!corpoTemplate.trim()) {
alert("❌ O corpo do template é obrigatório.");
return;
}
// Processar variáveis (separadas por vírgula ou espaço)
const variaveis = variaveisTemplate
.split(/[,;\s]+/)
.map(v => v.trim())
.filter(v => v.length > 0);
criandoNovoTemplate = true;
try {
const resultado = await client.mutation(api.templatesMensagens.criarTemplate, {
codigo: codigoTemplate.trim().toUpperCase().replace(/\s+/g, "_"),
nome: nomeTemplate.trim(),
titulo: tituloTemplate.trim(),
corpo: corpoTemplate.trim(),
variaveis: variaveis.length > 0 ? variaveis : undefined,
criadoPorId: authStore.usuario._id as Id<"usuarios">,
});
if (resultado.sucesso) {
alert("✅ Template criado com sucesso!");
fecharModalNovoTemplate();
// Recarregar a página para atualizar a lista
window.location.reload();
} else {
alert("❌ Erro ao criar template: " + (resultado.erro || "Erro desconhecido"));
}
} catch (error: any) {
console.error("Erro ao criar template:", error);
alert("❌ Erro ao criar template: " + (error.message || "Erro desconhecido"));
} finally {
criandoNovoTemplate = false;
}
}
async function enviarNotificacao() {
if (!destinatarioId) {
alert("Selecione um destinatário");
if (!enviarParaTodos && !destinatarioId) {
alert("Selecione um destinatário ou marque 'Enviar para todos'");
return;
}
@@ -34,93 +202,207 @@
}
processando = true;
try {
const destinatario = usuarios?.data?.find(u => u._id === destinatarioId);
progressoEnvio = { total: 0, enviados: 0, falhas: 0 };
if (!destinatario) {
alert("Destinatário não encontrado");
try {
// Obter lista de destinatários
const destinatarios: typeof usuarios = enviarParaTodos
? usuarios
: usuarios.filter(u => u._id === destinatarioId);
if (destinatarios.length === 0) {
alert("Nenhum destinatário encontrado");
return;
}
let resultadoChat = null;
let resultadoEmail = null;
progressoEnvio.total = destinatarios.length;
// ENVIAR PARA CHAT
if (canal === "chat" || canal === "ambos") {
const conversaResult = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatarioId as any }
);
// Se for envio para um único usuário
if (destinatarios.length === 1) {
const destinatario = destinatarios[0];
let resultadoChat = null;
let resultadoEmail = null;
if (conversaResult.conversaId) {
const mensagem = usarTemplate
? templateSelecionado?.corpo || ""
: mensagemPersonalizada;
// ENVIAR PARA CHAT
if (canal === "chat" || canal === "ambos") {
try {
const conversaResult = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as any }
);
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
conversaId: conversaResult.conversaId,
conteudo: mensagem,
tipo: "texto", // Tipo de mensagem
permitirNotificacaoParaSiMesmo: true, // ✅ Permite notificação para si mesmo via painel admin
});
}
}
if (conversaResult.conversaId) {
const mensagem = usarTemplate
? templateSelecionado?.corpo || ""
: mensagemPersonalizada;
// ENVIAR PARA EMAIL
if (canal === "email" || canal === "ambos") {
if (!destinatario.email) {
alert("Destinatário não possui email cadastrado");
processando = false;
return;
}
if (usarTemplate && templateId) {
// Usar template
const template = templateSelecionado;
if (template) {
resultadoEmail = await client.mutation(api.email.enviarEmailComTemplate, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
templateCodigo: template.codigo,
variaveis: {
nome: destinatario.nome,
matricula: destinatario.matricula,
},
enviadoPorId: destinatario._id as any, // TODO: Pegar usuário logado
});
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);
}
} else {
// Mensagem personalizada
resultadoEmail = await client.mutation(api.email.enfileirarEmail, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
assunto: "Notificação do Sistema",
corpo: mensagemPersonalizada,
enviadoPorId: destinatario._id as any, // TODO: Pegar usuário logado
});
}
}
// 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.";
// ENVIAR PARA EMAIL
if (canal === "email" || canal === "ambos") {
if (destinatario.email) {
try {
if (usarTemplate && templateId) {
const template = templateSelecionado;
if (template) {
resultadoEmail = await client.mutation(api.email.enviarEmailComTemplate, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
templateCodigo: template.codigo,
variaveis: {
nome: destinatario.nome,
matricula: destinatario.matricula,
},
enviadoPorId: destinatario._id as any,
});
}
} else {
resultadoEmail = await client.mutation(api.email.enfileirarEmail, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
assunto: "Notificação do Sistema",
corpo: mensagemPersonalizada,
enviadoPorId: destinatario._id as any,
});
}
} catch (error) {
console.error("Erro ao enviar email:", error);
}
}
}
} else if (canal === "chat" && resultadoChat) {
mensagem = "✅ Mensagem enviada no Chat!";
} else if (canal === "email" && resultadoEmail) {
mensagem = "✅ Email enfileirado para envio!";
}
alert(mensagem);
// 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.";
}
} else if (canal === "chat" && resultadoChat) {
mensagem = "✅ Mensagem enviada no Chat!";
} else if (canal === "email" && resultadoEmail) {
mensagem = "✅ Email enfileirado para envio!";
}
alert(mensagem);
progressoEnvio.enviados = 1;
} else {
// ENVIO EM MASSA
let sucessosChat = 0;
let sucessosEmail = 0;
let falhasChat = 0;
let falhasEmail = 0;
for (const destinatario of destinatarios) {
try {
// ENVIAR PARA CHAT
if (canal === "chat" || canal === "ambos") {
try {
const conversaResult = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as any }
);
if (conversaResult.conversaId) {
// Para templates, usar corpo direto (o backend já faz substituição via email)
// Para mensagem personalizada, usar diretamente
const mensagem = usarTemplate
? templateSelecionado?.corpo || ""
: mensagemPersonalizada;
await client.mutation(api.chat.enviarMensagem, {
conversaId: conversaResult.conversaId,
conteudo: mensagem,
tipo: "texto",
permitirNotificacaoParaSiMesmo: true,
});
sucessosChat++;
} else {
falhasChat++;
}
} catch (error) {
console.error(`Erro ao enviar chat para ${destinatario.nome}:`, error);
falhasChat++;
}
}
// ENVIAR PARA EMAIL
if (canal === "email" || canal === "ambos") {
if (destinatario.email) {
try {
if (usarTemplate && templateId) {
const template = templateSelecionado;
if (template) {
await client.mutation(api.email.enviarEmailComTemplate, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
templateCodigo: template.codigo,
variaveis: {
nome: destinatario.nome,
matricula: destinatario.matricula || "",
},
enviadoPorId: destinatario._id as any,
});
sucessosEmail++;
} else {
falhasEmail++;
}
} else {
await client.mutation(api.email.enfileirarEmail, {
destinatario: destinatario.email,
destinatarioId: destinatario._id as any,
assunto: "Notificação do Sistema",
corpo: mensagemPersonalizada,
enviadoPorId: destinatario._id as any,
});
sucessosEmail++;
}
} catch (error) {
console.error(`Erro ao enviar email para ${destinatario.nome}:`, error);
falhasEmail++;
}
} else {
falhasEmail++;
}
}
progressoEnvio.enviados++;
} catch (error) {
console.error(`Erro geral ao enviar para ${destinatario.nome}:`, error);
progressoEnvio.falhas++;
}
}
// Feedback de envio em massa
let mensagem = `✅ Envio em massa concluído!\n\n`;
if (canal === "ambos") {
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas\n`;
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
} else if (canal === "chat") {
mensagem += `Chat: ${sucessosChat} enviados, ${falhasChat} falhas`;
} else if (canal === "email") {
mensagem += `Email: ${sucessosEmail} enviados, ${falhasEmail} falhas`;
}
alert(mensagem);
}
// Limpar form
destinatarioId = "";
enviarParaTodos = false;
templateId = "";
mensagemPersonalizada = "";
} catch (error: any) {
@@ -128,6 +410,7 @@
alert("Erro ao enviar notificação: " + (error.message || "Erro desconhecido"));
} finally {
processando = false;
progressoEnvio = { total: 0, enviados: 0, falhas: 0 };
}
}
</script>
@@ -156,19 +439,46 @@
<!-- Destinatário -->
<div class="form-control mb-4">
<label class="label" for="destinatario-select">
<span class="label-text font-medium">Destinatário *</span>
</label>
<select id="destinatario-select" bind:value={destinatarioId} class="select select-bordered">
<div class="flex items-center justify-between mb-2">
<label class="label" for="destinatario-select">
<span class="label-text font-medium">Destinatário *</span>
</label>
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
bind:checked={enviarParaTodos}
class="checkbox checkbox-primary checkbox-sm"
disabled={processando}
/>
<span class="label-text text-sm">Enviar para todos ({usuarios.length} usuários)</span>
</label>
</div>
<select
id="destinatario-select"
bind:value={destinatarioId}
class="select select-bordered"
disabled={enviarParaTodos || processando}
>
<option value="">Selecione um usuário</option>
{#if usuarios?.data}
{#each usuarios.data as usuario}
{#if carregandoUsuarios}
<option disabled>Carregando usuários...</option>
{:else if usuarios.length > 0}
{#each usuarios as usuario}
<option value={usuario._id}>
{usuario.nome} ({usuario.matricula})
</option>
{/each}
{:else}
<option disabled>Nenhum usuário disponível</option>
{/if}
</select>
{#if enviarParaTodos}
<label class="label">
<span class="label-text-alt text-warning">
⚠️ A notificação será enviada para todos os {usuarios.length} usuários do sistema
</span>
</label>
{/if}
</div>
<!-- Canal -->
@@ -242,12 +552,16 @@
</label>
<select id="template-select" bind:value={templateId} class="select select-bordered">
<option value="">Selecione um template</option>
{#if templates?.data}
{#each templates.data as template}
{#if carregandoTemplates}
<option disabled>Carregando templates...</option>
{:else if templates.length > 0}
{#each templates as template}
<option value={template._id}>
{template.nome}
</option>
{/each}
{:else}
<option disabled>Nenhum template disponível</option>
{/if}
</select>
</div>
@@ -282,17 +596,28 @@
<div class="card-actions justify-end mt-4">
<button
class="btn btn-primary btn-block"
onclick={enviarNotificacao}
onclick={() => enviarNotificacao()}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{#if progressoEnvio.total > 1}
<span class="ml-2">
Enviando... ({progressoEnvio.enviados}/{progressoEnvio.total})
</span>
{:else}
<span class="ml-2">Enviando...</span>
{/if}
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
{/if}
Enviar Notificação
{#if enviarParaTodos && !processando}
Enviar para Todos ({usuarios.length})
{:else if !processando}
Enviar Notificação
{/if}
</button>
</div>
</div>
@@ -303,7 +628,11 @@
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title">Templates Disponíveis</h2>
<button class="btn btn-sm btn-outline btn-primary">
<button
type="button"
class="btn btn-sm btn-outline btn-primary"
onclick={() => abrirModalNovoTemplate()}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
@@ -311,17 +640,15 @@
</button>
</div>
{#if !templates?.data}
<div class="flex justify-center py-10">
{#if carregandoTemplates}
<div class="flex flex-col items-center justify-center py-10 gap-4">
<span class="loading loading-spinner loading-lg"></span>
<p class="text-sm text-base-content/60">Carregando templates...</p>
</div>
{:else if templates.data.length === 0}
<div class="text-center py-10 text-base-content/60">
Nenhum template disponível
</div>
{:else}
{:else if templates.length > 0}
<!-- Mostrar templates se existirem -->
<div class="space-y-3 max-h-[600px] overflow-y-auto">
{#each templates.data as template}
{#each templates as template}
<div class="card bg-base-200 compact">
<div class="card-body">
<div class="flex items-start justify-between">
@@ -359,6 +686,30 @@
</div>
{/each}
</div>
{:else}
<!-- Nenhum template disponível - mostrar botão para criar -->
<div class="text-center py-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="font-medium text-base-content mb-2">Nenhum template disponível</p>
<p class="text-sm text-base-content/60 mb-4">Clique no botão abaixo para criar os templates padrão do sistema.</p>
<button
class="btn btn-primary btn-sm"
onclick={criarTemplatesPadrao}
disabled={criandoTemplates}
>
{#if criandoTemplates}
<span class="loading loading-spinner loading-xs"></span>
Criando templates...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Criar Templates Padrão
{/if}
</button>
</div>
{/if}
</div>
</div>
@@ -373,3 +724,124 @@
</div>
</div>
<!-- Modal Novo Template -->
{#if modalNovoTemplateAberto}
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4">Criar Novo Template</h3>
<div class="space-y-4">
<!-- Código -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Código *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
</label>
<input
type="text"
bind:value={codigoTemplate}
placeholder="CODIGO_TEMPLATE"
class="input input-bordered"
maxlength="50"
/>
<label class="label">
<span class="label-text-alt">Código único para identificar o template (será convertido para MAIÚSCULAS)</span>
</label>
</div>
<!-- Nome -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Nome *</span>
</label>
<input
type="text"
bind:value={nomeTemplate}
placeholder="Nome do Template"
class="input input-bordered"
maxlength="100"
/>
<label class="label">
<span class="label-text-alt">Nome exibido na lista de templates</span>
</label>
</div>
<!-- Título -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Título *</span>
</label>
<input
type="text"
bind:value={tituloTemplate}
placeholder="Título da Mensagem"
class="input input-bordered"
maxlength="200"
/>
<label class="label">
<span class="label-text-alt">Título usado no assunto do email ou cabeçalho da mensagem</span>
</label>
</div>
<!-- Corpo -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
bind:value={corpoTemplate}
placeholder="Digite o conteúdo da mensagem..."
class="textarea textarea-bordered h-32"
maxlength="2000"
></textarea>
<label class="label">
<span class="label-text-alt">Use {'{{'}variavel{'}}'} para variáveis dinâmicas (ex: {'{{'}nome{'}}'}, {'{{'}data{'}}'})</span>
</label>
</div>
<!-- Variáveis -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
type="text"
bind:value={variaveisTemplate}
placeholder="nome, data, valor (separadas por vírgula)"
class="input input-bordered"
/>
<label class="label">
<span class="label-text-alt">Liste as variáveis que podem ser substituídas no template (separadas por vírgula ou espaço)</span>
</label>
</div>
</div>
<div class="modal-action">
<button
class="btn btn-ghost"
onclick={fecharModalNovoTemplate}
disabled={criandoNovoTemplate}
>
Cancelar
</button>
<button
class="btn btn-primary"
onclick={salvarNovoTemplate}
disabled={criandoNovoTemplate}
>
{#if criandoNovoTemplate}
<span class="loading loading-spinner loading-sm"></span>
Criando...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Criar Template
{/if}
</button>
</div>
</div>
<div class="modal-backdrop" onclick={fecharModalNovoTemplate}></div>
</div>
{/if}