feat: enhance email monitoring and management features

- Added a new section for monitoring email status, allowing users to track the email queue and identify sending issues.
- Updated the backend to support new internal queries for listing pending emails and retrieving email configurations.
- Refactored email-related mutations to improve error handling and streamline the email sending process.
- Enhanced the overall email management experience by providing clearer feedback and monitoring capabilities.
This commit is contained in:
2025-11-04 21:27:48 -03:00
parent 12db52a8a7
commit f6671e0f16
8 changed files with 505 additions and 772 deletions

View File

@@ -218,6 +218,19 @@
palette: "secondary",
icon: "envelope",
},
{
title: "Monitoramento de Emails",
description:
"Acompanhe o status da fila de emails, identifique problemas de envio e processe manualmente quando necessário.",
ctaLabel: "Monitorar Emails",
href: "/ti/monitoramento-emails",
palette: "info",
icon: "envelope",
highlightBadges: [
{ label: "Tempo Real", variant: "solid" },
{ label: "Debug", variant: "outline" },
],
},
{
title: "Gerenciar Usuários",
description:

View File

@@ -0,0 +1,265 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
let autoRefresh = $state(true);
let refreshInterval: ReturnType<typeof setInterval> | null = null;
let processando = $state(false);
const client = useConvexClient();
const estatisticas = useQuery(api.email.obterEstatisticasFilaEmails, {});
const filaEmails = useQuery(api.email.listarFilaEmails, { limite: 50 });
$effect(() => {
if (autoRefresh) {
refreshInterval = setInterval(() => {
// Forçar refresh das queries invalidando o cache
// As queries do Convex Svelte atualizam automaticamente
}, 5000); // Refresh a cada 5 segundos
} else {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
return () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
};
});
async function processarFilaManual() {
if (processando) return;
try {
processando = true;
const resultado = await client.action(api.email.processarFilaEmailsManual, { limite: 10 });
if (resultado.sucesso) {
alert(`✅ Processados: ${resultado.processados}, Falhas: ${resultado.falhas}`);
} else {
alert(`❌ Erro: ${resultado.erro || "Erro desconhecido"}`);
}
} catch (error) {
console.error("Erro ao processar fila:", error);
alert("Erro ao processar fila de emails");
} finally {
processando = false;
}
}
function formatarData(timestamp: number | undefined): string {
if (!timestamp) return "-";
return new Date(timestamp).toLocaleString("pt-BR");
}
function getStatusBadgeClass(status: string): string {
switch (status) {
case "pendente":
return "badge badge-warning";
case "enviando":
return "badge badge-info";
case "enviado":
return "badge badge-success";
case "falha":
return "badge badge-error";
default:
return "badge badge-ghost";
}
}
function getStatusLabel(status: string): string {
switch (status) {
case "pendente":
return "Pendente";
case "enviando":
return "Enviando";
case "enviado":
return "Enviado";
case "falha":
return "Falha";
default:
return status;
}
}
</script>
<div class="container mx-auto p-6">
<div class="mb-6">
<h1 class="text-3xl font-bold mb-2">📧 Monitoramento de Emails</h1>
<p class="text-base-content/70">
Acompanhe o status da fila de emails e identifique problemas de envio
</p>
</div>
<!-- Estatísticas -->
{#if estatisticas?.data}
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<div class="stat bg-base-200 rounded-lg shadow">
<div class="stat-title">Total</div>
<div class="stat-value text-2xl">{estatisticas.data.total}</div>
<div class="stat-desc">Emails na fila</div>
</div>
<div class="stat bg-warning/10 rounded-lg shadow">
<div class="stat-title">Pendentes</div>
<div class="stat-value text-2xl text-warning">{estatisticas.data.pendentes}</div>
<div class="stat-desc">Aguardando envio</div>
</div>
<div class="stat bg-info/10 rounded-lg shadow">
<div class="stat-title">Enviando</div>
<div class="stat-value text-2xl text-info">{estatisticas.data.enviando}</div>
<div class="stat-desc">Em processamento</div>
</div>
<div class="stat bg-success/10 rounded-lg shadow">
<div class="stat-title">Enviados</div>
<div class="stat-value text-2xl text-success">{estatisticas.data.enviados}</div>
<div class="stat-desc">Concluídos</div>
</div>
<div class="stat bg-error/10 rounded-lg shadow">
<div class="stat-title">Falhas</div>
<div class="stat-value text-2xl text-error">{estatisticas.data.falhas}</div>
<div class="stat-desc">Com erro</div>
</div>
</div>
{:else if estatisticas === undefined}
<div class="text-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
{/if}
<!-- Controles -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="form-control">
<label class="label cursor-pointer gap-4">
<span class="label-text">Atualização automática</span>
<input
type="checkbox"
class="toggle toggle-primary"
bind:checked={autoRefresh}
/>
</label>
</div>
<button
class="btn btn-primary"
onclick={processarFilaManual}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
Processando...
{:else}
🔄 Processar Fila Manualmente
{/if}
</button>
</div>
</div>
</div>
<!-- Lista de Emails -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Fila de Emails</h2>
{#if filaEmails?.data && filaEmails.data.length > 0}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Destinatário</th>
<th>Assunto</th>
<th>Status</th>
<th>Tentativas</th>
<th>Criado em</th>
<th>Última tentativa</th>
<th>Erro</th>
</tr>
</thead>
<tbody>
{#each filaEmails.data as email}
<tr>
<td>
<div class="font-medium">{email.destinatario}</div>
</td>
<td>
<div class="max-w-xs truncate" title={email.assunto}>
{email.assunto}
</div>
</td>
<td>
<span class={getStatusBadgeClass(email.status)}>
{getStatusLabel(email.status)}
</span>
</td>
<td>{email.tentativas || 0}</td>
<td class="text-sm">{formatarData(email.criadoEm)}</td>
<td class="text-sm">
{formatarData(email.ultimaTentativa)}
</td>
<td>
{#if email.erroDetalhes}
<div
class="tooltip tooltip-left"
data-tip={email.erroDetalhes}
>
<span class="text-error text-xs cursor-help">
⚠️ Ver erro
</span>
</div>
{:else}
<span class="text-base-content/50">-</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else if filaEmails?.data !== undefined}
<div class="text-center py-8 text-base-content/50">
<p>Nenhum email na fila</p>
</div>
{:else}
<div class="text-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
{/if}
</div>
</div>
<!-- Dicas de Troubleshooting -->
<div class="card bg-base-200 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title">🔍 Troubleshooting</h2>
<div class="space-y-2 text-sm">
<p>
<strong>Emails pendentes não estão sendo enviados?</strong>
</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>Verifique se a configuração SMTP está ativa em Configurações de Email</li>
<li>Confirme se o cron job está rodando (verifique logs do Convex)</li>
<li>Clique em "Processar Fila Manualmente" para forçar o processamento</li>
</ul>
<p class="mt-4">
<strong>Emails com status "Falha"?</strong>
</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>Verifique as credenciais SMTP em Configurações de Email</li>
<li>Confirme se o servidor SMTP está acessível</li>
<li>Verifique os logs do Convex para detalhes do erro</li>
<li>Teste a conexão SMTP na página de configurações</li>
</ul>
</div>
</div>
</div>
</div>