feat: update email notification handling to use scheduler for template sending, with improved error handling for fallback scenarios
This commit is contained in:
@@ -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 {{variavel}} 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>
|
||||
@@ -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 {{variavel}} 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>
|
||||
Reference in New Issue
Block a user