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:
@@ -218,6 +218,19 @@
|
|||||||
palette: "secondary",
|
palette: "secondary",
|
||||||
icon: "envelope",
|
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",
|
title: "Gerenciar Usuários",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
10
packages/backend/convex/_generated/api.d.ts
vendored
10
packages/backend/convex/_generated/api.d.ts
vendored
@@ -9,7 +9,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as actions_email from "../actions/email.js";
|
import type * as actions_email from "../actions/email.js";
|
||||||
|
import type * as actions_linkPreview from "../actions/linkPreview.js";
|
||||||
|
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
|
||||||
import type * as actions_smtp from "../actions/smtp.js";
|
import type * as actions_smtp from "../actions/smtp.js";
|
||||||
|
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
||||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||||
import type * as ausencias from "../ausencias.js";
|
import type * as ausencias from "../ausencias.js";
|
||||||
import type * as autenticacao from "../autenticacao.js";
|
import type * as autenticacao from "../autenticacao.js";
|
||||||
@@ -31,6 +34,8 @@ import type * as logsAtividades from "../logsAtividades.js";
|
|||||||
import type * as logsLogin from "../logsLogin.js";
|
import type * as logsLogin from "../logsLogin.js";
|
||||||
import type * as monitoramento from "../monitoramento.js";
|
import type * as monitoramento from "../monitoramento.js";
|
||||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||||
|
import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
|
||||||
|
import type * as pushNotifications from "../pushNotifications.js";
|
||||||
import type * as roles from "../roles.js";
|
import type * as roles from "../roles.js";
|
||||||
import type * as saldoFerias from "../saldoFerias.js";
|
import type * as saldoFerias from "../saldoFerias.js";
|
||||||
import type * as seed from "../seed.js";
|
import type * as seed from "../seed.js";
|
||||||
@@ -59,7 +64,10 @@ import type {
|
|||||||
*/
|
*/
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
"actions/email": typeof actions_email;
|
"actions/email": typeof actions_email;
|
||||||
|
"actions/linkPreview": typeof actions_linkPreview;
|
||||||
|
"actions/pushNotifications": typeof actions_pushNotifications;
|
||||||
"actions/smtp": typeof actions_smtp;
|
"actions/smtp": typeof actions_smtp;
|
||||||
|
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
||||||
atestadosLicencas: typeof atestadosLicencas;
|
atestadosLicencas: typeof atestadosLicencas;
|
||||||
ausencias: typeof ausencias;
|
ausencias: typeof ausencias;
|
||||||
autenticacao: typeof autenticacao;
|
autenticacao: typeof autenticacao;
|
||||||
@@ -81,6 +89,8 @@ declare const fullApi: ApiFromModules<{
|
|||||||
logsLogin: typeof logsLogin;
|
logsLogin: typeof logsLogin;
|
||||||
monitoramento: typeof monitoramento;
|
monitoramento: typeof monitoramento;
|
||||||
permissoesAcoes: typeof permissoesAcoes;
|
permissoesAcoes: typeof permissoesAcoes;
|
||||||
|
preferenciasNotificacao: typeof preferenciasNotificacao;
|
||||||
|
pushNotifications: typeof pushNotifications;
|
||||||
roles: typeof roles;
|
roles: typeof roles;
|
||||||
saldoFerias: typeof saldoFerias;
|
saldoFerias: typeof saldoFerias;
|
||||||
seed: typeof seed;
|
seed: typeof seed;
|
||||||
|
|||||||
@@ -28,12 +28,20 @@ export const enviar = action({
|
|||||||
const configRaw = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
|
const configRaw = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
|
||||||
|
|
||||||
if (!configRaw) {
|
if (!configRaw) {
|
||||||
|
console.error("❌ Configuração SMTP não encontrada ou inativa para email:", email.destinatario);
|
||||||
return {
|
return {
|
||||||
sucesso: false,
|
sucesso: false,
|
||||||
erro: "Configuração de email não encontrada ou inativa",
|
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("📧 Tentando enviar email:", {
|
||||||
|
para: email.destinatario,
|
||||||
|
assunto: email.assunto,
|
||||||
|
servidor: configRaw.servidor,
|
||||||
|
porta: configRaw.porta,
|
||||||
|
});
|
||||||
|
|
||||||
// Descriptografar senha usando função compatível com Node.js
|
// Descriptografar senha usando função compatível com Node.js
|
||||||
let senhaDescriptografada: string;
|
let senhaDescriptografada: string;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
"use node";
|
||||||
/**
|
/**
|
||||||
* Utilitários de criptografia compatíveis com Node.js
|
* Utilitários de criptografia compatíveis com Node.js
|
||||||
* Para uso em actions que rodam em ambiente Node.js
|
* Para uso em actions que rodam em ambiente Node.js
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export const criarSolicitacao = mutation({
|
|||||||
<li><strong>Motivo:</strong> ${args.motivo}</li>
|
<li><strong>Motivo:</strong> ${args.motivo}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.</p>`,
|
<p>Por favor, acesse o sistema para aprovar ou reprovar esta solicitação.</p>`,
|
||||||
enviadoPorId: funcionarioUsuario._id,
|
enviadoPor: funcionarioUsuario._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Criar ou obter conversa entre gestor e funcionário
|
// Criar ou obter conversa entre gestor e funcionário
|
||||||
@@ -486,7 +486,7 @@ export const aprovar = mutation({
|
|||||||
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}</li>
|
<li><strong>Período:</strong> ${new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")} até ${new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}</li>
|
||||||
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
||||||
</ul>`,
|
</ul>`,
|
||||||
enviadoPorId: args.gestorId,
|
enviadoPor: args.gestorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Criar ou obter conversa
|
// Criar ou obter conversa
|
||||||
@@ -605,7 +605,7 @@ export const reprovar = mutation({
|
|||||||
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
<li><strong>Motivo:</strong> ${solicitacao.motivo}</li>
|
||||||
<li><strong>Motivo da Reprovação:</strong> ${args.motivoReprovacao}</li>
|
<li><strong>Motivo da Reprovação:</strong> ${args.motivoReprovacao}</li>
|
||||||
</ul>`,
|
</ul>`,
|
||||||
enviadoPorId: args.gestorId,
|
enviadoPor: args.gestorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Criar ou obter conversa
|
// Criar ou obter conversa
|
||||||
|
|||||||
@@ -370,10 +370,10 @@ export const enviarMensagem = mutation({
|
|||||||
variaveis: {
|
variaveis: {
|
||||||
remetente: usuarioAtual.nome,
|
remetente: usuarioAtual.nome,
|
||||||
mensagem: descricao,
|
mensagem: descricao,
|
||||||
conversaId: args.conversaId,
|
conversaId: args.conversaId.toString(),
|
||||||
urlSistema,
|
urlSistema,
|
||||||
},
|
},
|
||||||
enviadoPorId: usuarioAtual._id,
|
enviadoPor: usuarioAtual._id,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(`Erro ao agendar email para usuário ${participanteId}:`, error);
|
console.error(`Erro ao agendar email para usuário ${participanteId}:`, error);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user