feat: implement email notification system for 'Almoxarifado' alerts, enhancing user awareness of stock levels and alert statuses through automated email updates

This commit is contained in:
2025-12-21 08:02:14 -03:00
parent 500b7b362c
commit f0884a19a7
5 changed files with 419 additions and 15 deletions

View File

@@ -9,7 +9,7 @@
const client = useConvexClient();
let filtroTipo = $state<string>('');
let filtroStatus = $state<string>('ativo');
let filtroStatus = $state<string>('');
const alertasQuery = useQuery(api.almoxarifado.listarAlertas, {
status: filtroStatus ? (filtroStatus as AlertaStatus) : undefined,
@@ -17,6 +17,16 @@
});
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {});
// Criar mapa de materiais para lookup eficiente
const materiaisMap = $derived.by(() => {
if (!materiaisQuery.data) return new Map();
const map = new Map();
for (const material of materiaisQuery.data) {
map.set(material._id, material);
}
return map;
});
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
function mostrarMensagem(kind: 'success' | 'error', text: string) {
@@ -145,7 +155,11 @@
<!-- Lista de Alertas -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
{#if alertasQuery.data && alertasQuery.data.length > 0}
{#if alertasQuery === undefined}
<div class="flex items-center justify-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if alertasQuery.data && alertasQuery.data.length > 0}
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
@@ -162,7 +176,7 @@
</thead>
<tbody>
{#each alertasQuery.data as alerta}
{@const material = materiaisQuery.data?.find(m => m._id === alerta.materialId)}
{@const material = materiaisMap.get(alerta.materialId)}
{@const diferenca = alerta.quantidadeMinima - alerta.quantidadeAtual}
<tr class="hover:bg-base-200/50 transition-colors">
<td>
@@ -222,15 +236,29 @@
</div>
{:else}
<div class="text-center py-12">
<CheckCircle class="mx-auto mb-4 h-20 w-20 text-success" />
<AlertTriangle class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
<h3 class="text-2xl font-bold mb-2">Nenhum alerta encontrado</h3>
<p class="text-base-content/70 text-lg">
<p class="text-base-content/70 text-lg mb-4">
{#if filtroStatus === 'ativo'}
Não há alertas ativos no momento. Todos os materiais estão com estoque adequado!
{:else}
{:else if filtroStatus || filtroTipo}
Não há alertas com os filtros selecionados.
{:else}
Ainda não há alertas registrados no sistema.
{/if}
</p>
<div class="alert alert-info max-w-2xl mx-auto">
<AlertTriangle class="h-6 w-6" />
<div class="text-left">
<h4 class="font-bold mb-2">Como os alertas funcionam?</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<li>Os alertas são criados automaticamente quando o estoque de um material fica abaixo do mínimo configurado</li>
<li>O sistema permite apenas <strong>um alerta ativo por material</strong> para evitar duplicações</li>
<li>Quando o estoque volta ao normal, você pode resolver o alerta manualmente</li>
<li>Alertas são criados durante movimentações de estoque (entradas, saídas, ajustes)</li>
</ul>
</div>
</div>
</div>
{/if}
</div>

View File

@@ -4,6 +4,7 @@
import { useConvexClient, useQuery } from 'convex-svelte';
import { resolve } from '$app/paths';
import { ClipboardList, Plus, CheckCircle, XCircle, Package } from 'lucide-svelte';
import ErrorModal from '$lib/components/ErrorModal.svelte';
const client = useConvexClient();
@@ -34,12 +35,60 @@
const setoresQuery = useQuery(api.setores.list, {});
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
let showErrorModal = $state(false);
let errorModalTitle = $state('Erro ao processar requisição');
let errorModalMessage = $state('');
let errorModalDetails = $state('');
function mostrarMensagem(kind: 'success' | 'error', text: string) {
notice = { kind, text };
setTimeout(() => {
notice = null;
}, 5000);
if (kind === 'error') {
// Para erros, usar modal
errorModalTitle = 'Erro ao processar requisição';
// Extrair mensagem de erro amigável
let mensagemAmigavel = text;
let detalhesTecnicos = '';
// Remover prefixos técnicos do Convex se existirem
if (text.includes('[CONVEX') || text.includes('Request ID')) {
// Extrair apenas a parte da mensagem de erro após o último ']'
const partes = text.split(']');
if (partes.length > 1) {
mensagemAmigavel = partes[partes.length - 1].trim();
}
detalhesTecnicos = 'Detalhes técnicos:\n' + text;
}
// Melhorar mensagens específicas
if (mensagemAmigavel.includes('Funcionário não encontrado para o usuário')) {
errorModalTitle = 'Erro: Funcionário não encontrado';
mensagemAmigavel = 'Não foi possível encontrar o funcionário associado ao seu usuário.';
detalhesTecnicos = 'Por favor, entre em contato com o suporte técnico.\n\n' +
'1. Verifique se o seu usuário está associado a um funcionário no sistema\n' +
'2. Solicite a associação do seu usuário a um funcionário\n' +
'3. Entre em contato com a equipe de TI se o problema persistir\n\n' +
'Detalhes técnicos:\n' + text;
} else if (mensagemAmigavel.includes('não encontrado')) {
mensagemAmigavel = mensagemAmigavel.replace(/não encontrado/gi, 'não foi encontrado no sistema');
}
errorModalMessage = mensagemAmigavel;
errorModalDetails = detalhesTecnicos;
showErrorModal = true;
} else {
// Para sucesso, usar banner simples
notice = { kind, text };
setTimeout(() => {
notice = null;
}, 5000);
}
}
function fecharErrorModal() {
showErrorModal = false;
errorModalMessage = '';
errorModalDetails = '';
}
$effect(() => {
@@ -216,13 +265,22 @@
</div>
</div>
<!-- Notificações -->
<!-- Notificações (apenas sucesso) -->
{#if notice}
<div class="alert alert-{notice.kind} mb-6 shadow-lg">
<span>{notice.text}</span>
</div>
{/if}
<!-- Modal de Erro -->
<ErrorModal
open={showErrorModal}
title={errorModalTitle}
message={errorModalMessage}
details={errorModalDetails}
onClose={fecharErrorModal}
/>
<!-- Filtros -->
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
<div class="card-body">