feat: update email notification handling to use scheduler for template sending, with improved error handling for fallback scenarios

This commit is contained in:
2025-12-01 05:45:19 -03:00
parent 4e3feca84d
commit d9e78079c8
4 changed files with 541 additions and 14 deletions

View File

@@ -0,0 +1,267 @@
<script lang="ts">
import { page } from '$app/stores';
import { useConvexClient, useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { FunctionReference } from 'convex/server';
import type { Doc } from '@sgse-app/backend/convex/_generated/dataModel';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {});
const params = $derived($page.params);
const templateIdParam = $derived(params.id ?? '');
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 template = $derived.by(() => {
const lista = templates as Doc<'templatesMensagens'>[];
return lista.find((t) => String(t._id) === templateIdParam) ?? null;
});
let nome = $state('');
let titulo = $state('');
let corpo = $state('');
let categoria = $state<'email' | 'chat' | 'ambos'>('email');
let variaveisTexto = $state('');
let tagsTexto = $state('');
let salvando = $state(false);
let mensagem = $state<{
tipo: 'success' | 'error' | 'info';
texto: string;
} | null>(null);
$effect(() => {
if (template) {
nome = template.nome ?? '';
titulo = template.titulo ?? '';
corpo = template.corpo ?? '';
categoria = (template.categoria as 'email' | 'chat' | 'ambos') ?? 'email';
variaveisTexto = (template.variaveis ?? []).join(', ');
tagsTexto = (template.tags ?? []).join(', ');
}
});
function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
function parseLista(input: string): string[] {
return input
.split(/[;,\n]/)
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
async function salvar() {
if (!template) {
mostrarMensagem('error', 'Template não encontrado.');
return;
}
if (!currentUser.data) {
mostrarMensagem('error', 'Usuário não autenticado.');
return;
}
if (!nome.trim() || !titulo.trim() || !corpo.trim()) {
mostrarMensagem('error', 'Preencha todos os campos obrigatórios.');
return;
}
const variaveis = parseLista(variaveisTexto);
const tags = parseLista(tagsTexto);
try {
salvando = true;
const resultado = await client.mutation(api.templatesMensagens.editarTemplate, {
templateId: template._id,
nome: nome.trim(),
titulo: titulo.trim(),
corpo: corpo.trim(),
variaveis,
categoria,
tags,
editadoPorId: currentUser.data._id
});
if (resultado.sucesso) {
mostrarMensagem('success', 'Template atualizado com sucesso!');
await goto(resolve('/ti/notificacoes/templates'));
} else {
mostrarMensagem('error', resultado.erro || 'Erro ao atualizar template.');
}
} catch (error) {
const erro = error instanceof Error ? error.message : 'Erro desconhecido';
mostrarMensagem('error', `Erro ao atualizar template: ${erro}`);
} finally {
salvando = false;
}
}
</script>
<div class="container mx-auto max-w-4xl px-4 py-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="bg-info/10 rounded-xl p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-info h-8 w-8"
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>
</div>
<div>
<h1 class="text-base-content text-3xl font-bold">Editar Template</h1>
<p class="text-base-content/60 mt-1">
Atualize as informações do template selecionado. Templates de sistema não podem ser
editados.
</p>
</div>
</div>
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Voltar </a>
</div>
{#if !template}
<div class="alert alert-error">
<span>Template não encontrado.</span>
</div>
{:else}
{#if mensagem}
<div
class="alert mb-6 shadow-lg"
class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === 'error'}
class:alert-info={mensagem.tipo === 'info'}
>
<span class="font-medium">{mensagem.texto}</span>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={() => {
mensagem = null;
}}
>
</button>
</div>
{/if}
<div class="card bg-base-100 shadow-xl">
<div class="card-body space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
</div>
<div class="form-control">
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
</label>
<input
id="titulo"
type="text"
bind:value={titulo}
class="input input-bordered"
maxlength="200"
/>
</div>
</div>
<div class="form-control">
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
<option value="email">Email</option>
<option value="chat">Chat</option>
<option value="ambos">Ambos</option>
</select>
</div>
<div class="form-control">
<label class="label" for="corpo">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
id="corpo"
bind:value={corpo}
class="textarea textarea-bordered h-40"
placeholder="Digite o conteúdo da mensagem. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e
vírgula).
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a>
<button class="btn btn-primary" onclick={salvar} disabled={salvando}>
{#if salvando}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
Salvar Alterações
{/if}
</button>
</div>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,243 @@
<script lang="ts">
import { useConvexClient, useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { FunctionReference } from 'convex/server';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
let codigo = $state('');
let nome = $state('');
let titulo = $state('');
let corpo = $state('');
let categoria = $state<'email' | 'chat' | 'ambos'>('email');
let variaveisTexto = $state('');
let tagsTexto = $state('');
let criando = $state(false);
let mensagem = $state<{
tipo: 'success' | 'error' | 'info';
texto: string;
} | null>(null);
function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
function parseLista(input: string): string[] {
return input
.split(/[;,\n]/)
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
async function salvar() {
if (!currentUser.data) {
mostrarMensagem('error', 'Usuário não autenticado.');
return;
}
if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) {
mostrarMensagem('error', 'Preencha todos os campos obrigatórios.');
return;
}
const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, '_');
const variaveis = parseLista(variaveisTexto);
const tags = parseLista(tagsTexto);
try {
criando = true;
const resultado = await client.mutation(api.templatesMensagens.criarTemplate, {
codigo: codigoNormalizado,
nome: nome.trim(),
titulo: titulo.trim(),
corpo: corpo.trim(),
variaveis,
categoria,
tags,
criadoPorId: currentUser.data._id as Id<'usuarios'>
});
if (resultado.sucesso) {
mostrarMensagem('success', 'Template criado com sucesso!');
await goto(resolve('/ti/notificacoes/templates'));
} else {
mostrarMensagem('error', resultado.erro || 'Erro ao criar template.');
}
} catch (error) {
const erro = error instanceof Error ? error.message : 'Erro desconhecido';
mostrarMensagem('error', `Erro ao criar template: ${erro}`);
} finally {
criando = false;
}
}
</script>
<div class="container mx-auto max-w-4xl px-4 py-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="bg-info/10 rounded-xl p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-info h-8 w-8"
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>
</div>
<div>
<h1 class="text-base-content text-3xl font-bold">Novo Template</h1>
<p class="text-base-content/60 mt-1">
Crie um template de email ou mensagem para reutilizar no sistema.
</p>
</div>
</div>
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Voltar </a>
</div>
{#if mensagem}
<div
class="alert mb-6 shadow-lg"
class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === 'error'}
class:alert-info={mensagem.tipo === 'info'}
>
<span class="font-medium">{mensagem.texto}</span>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={() => {
mensagem = null;
}}
>
</button>
</div>
{/if}
<div class="card bg-base-100 shadow-xl">
<div class="card-body space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="codigo">
<span class="label-text font-medium">Código *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
</label>
<input
id="codigo"
type="text"
bind:value={codigo}
class="input input-bordered"
maxlength="50"
/>
</div>
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
</div>
</div>
<div class="form-control">
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
</label>
<input
id="titulo"
type="text"
bind:value={titulo}
class="input input-bordered"
maxlength="200"
/>
</div>
<div class="form-control">
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
<option value="email">Email</option>
<option value="chat">Chat</option>
<option value="ambos">Ambos</option>
</select>
</div>
<div class="form-control">
<label class="label" for="corpo">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
id="corpo"
bind:value={corpo}
class="textarea textarea-bordered h-40"
placeholder="Digite o conteúdo da mensagem. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e
vírgula).
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a>
<button class="btn btn-primary" onclick={salvar} disabled={criando}>
{#if criando}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
Salvar Template
{/if}
</button>
</div>
</div>
</div>
</div>