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:
@@ -3,6 +3,7 @@ import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, internalAction, internalQuery, mutation, query } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { api } from './_generated/api';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import {
|
||||
alertaStatus,
|
||||
@@ -376,6 +377,116 @@ async function registrarHistorico(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Função helper para enviar emails de alertas de almoxarifado
|
||||
*/
|
||||
async function enviarEmailAlerta(
|
||||
ctx: MutationCtx,
|
||||
alerta: Doc<'alertasEstoque'>,
|
||||
material: Doc<'materiais'>,
|
||||
tipoEmail: 'criado' | 'resolvido' | 'ignorado',
|
||||
usuarioResolucao?: Doc<'usuarios'>
|
||||
) {
|
||||
try {
|
||||
// Buscar configuração de almoxarifado
|
||||
const config = await ctx.db
|
||||
.query('configuracoesAlmoxarifado')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
// Verificar se emails de alerta estão ativados
|
||||
if (!config || !config.emailAlertasAtivo || !config.emailsDestinatarios || config.emailsDestinatarios.length === 0) {
|
||||
return; // Emails desativados ou sem destinatários
|
||||
}
|
||||
|
||||
// Determinar template e variáveis baseado no tipo
|
||||
let templateCodigo: string;
|
||||
// URL do sistema (buscar de configuração ou usar padrão)
|
||||
const urlSistema = process.env.NEXT_PUBLIC_SITE_URL || process.env.SITE_URL || 'http://localhost:5173';
|
||||
|
||||
const variaveis: Record<string, string> = {
|
||||
materialNome: material.nome,
|
||||
materialCodigo: material.codigo,
|
||||
quantidadeAtual: material.estoqueAtual.toString(),
|
||||
quantidadeMinima: material.estoqueMinimo.toString(),
|
||||
unidadeMedida: material.unidadeMedida,
|
||||
urlSistema
|
||||
};
|
||||
|
||||
if (tipoEmail === 'criado') {
|
||||
templateCodigo = 'almoxarifado_alerta_criado';
|
||||
const tipoAlertaLabel =
|
||||
alerta.tipo === 'estoque_zerado'
|
||||
? 'Estoque Zerado'
|
||||
: alerta.tipo === 'estoque_minimo'
|
||||
? 'Estoque Mínimo'
|
||||
: 'Reposição Necessária';
|
||||
const diferenca = alerta.quantidadeMinima - alerta.quantidadeAtual;
|
||||
variaveis.tipoAlerta = tipoAlertaLabel;
|
||||
variaveis.diferenca = diferenca.toString();
|
||||
} else if (tipoEmail === 'resolvido') {
|
||||
templateCodigo = 'almoxarifado_alerta_resolvido';
|
||||
const usuarioNome = usuarioResolucao?.nome || 'Sistema';
|
||||
const dataResolucao = alerta.resolvidoEm
|
||||
? new Date(alerta.resolvidoEm).toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: new Date().toLocaleDateString('pt-BR');
|
||||
variaveis.resolvidoPor = usuarioNome;
|
||||
variaveis.dataResolucao = dataResolucao;
|
||||
} else {
|
||||
// ignorado
|
||||
templateCodigo = 'almoxarifado_alerta_ignorado';
|
||||
const tipoAlertaLabel =
|
||||
alerta.tipo === 'estoque_zerado'
|
||||
? 'Estoque Zerado'
|
||||
: alerta.tipo === 'estoque_minimo'
|
||||
? 'Estoque Mínimo'
|
||||
: 'Reposição Necessária';
|
||||
const dataIgnorado = new Date().toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
variaveis.tipoAlerta = tipoAlertaLabel;
|
||||
variaveis.dataIgnorado = dataIgnorado;
|
||||
}
|
||||
|
||||
// Buscar usuário atual para enviar o email
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
if (!usuarioAtual) return;
|
||||
|
||||
// Enviar email para cada destinatário configurado
|
||||
for (const emailDestinatario of config.emailsDestinatarios) {
|
||||
try {
|
||||
// Agendar action para enviar email com template (assíncrono, não bloqueia)
|
||||
ctx.scheduler
|
||||
.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: emailDestinatario,
|
||||
templateCodigo,
|
||||
variaveis,
|
||||
enviadoPor: usuarioAtual._id
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Erro ao agendar email de alerta para ${emailDestinatario}:`, error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erro ao agendar email de alerta para ${emailDestinatario}:`, error);
|
||||
// Continua para o próximo destinatário mesmo se falhar
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar emails de alerta:', error);
|
||||
// Não falha a operação principal se houver erro no email
|
||||
}
|
||||
}
|
||||
|
||||
async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais'>) {
|
||||
const material = await ctx.db.get(materialId);
|
||||
if (!material || !material.ativo) return;
|
||||
@@ -401,7 +512,7 @@ async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais
|
||||
}
|
||||
|
||||
// Criar alerta
|
||||
await ctx.db.insert('alertasEstoque', {
|
||||
const alertaId = await ctx.db.insert('alertasEstoque', {
|
||||
materialId,
|
||||
tipo,
|
||||
quantidadeAtual: material.estoqueAtual,
|
||||
@@ -409,6 +520,13 @@ async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais
|
||||
status: 'ativo',
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Buscar alerta criado para enviar email
|
||||
const alertaCriado = await ctx.db.get(alertaId);
|
||||
if (alertaCriado) {
|
||||
// Enviar email de notificação (assíncrono, não bloqueia)
|
||||
await enviarEmailAlerta(ctx, alertaCriado, material, 'criado');
|
||||
}
|
||||
}
|
||||
|
||||
async function resolverAlertasMaterial(ctx: MutationCtx, materialId: Id<'materiais'>) {
|
||||
@@ -979,12 +1097,23 @@ export const resolverAlerta = mutation({
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) throw new Error('Usuário não autenticado');
|
||||
|
||||
// Buscar material antes de atualizar o alerta
|
||||
const material = await ctx.db.get(alerta.materialId);
|
||||
if (!material) throw new Error('Material não encontrado');
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
status: 'resolvido',
|
||||
resolvidoEm: Date.now(),
|
||||
resolvidoPor: usuario._id
|
||||
});
|
||||
|
||||
// Buscar alerta atualizado para enviar email
|
||||
const alertaResolvido = await ctx.db.get(args.id);
|
||||
if (alertaResolvido) {
|
||||
// Enviar email de notificação (assíncrono, não bloqueia)
|
||||
await enviarEmailAlerta(ctx, alertaResolvido, material, 'resolvido', usuario);
|
||||
}
|
||||
|
||||
// Registrar histórico
|
||||
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
||||
status: 'resolvido'
|
||||
@@ -1003,10 +1132,21 @@ export const ignorarAlerta = mutation({
|
||||
throw new Error('Apenas alertas ativos podem ser ignorados');
|
||||
}
|
||||
|
||||
// Buscar material antes de atualizar o alerta
|
||||
const material = await ctx.db.get(alerta.materialId);
|
||||
if (!material) throw new Error('Material não encontrado');
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
status: 'ignorado'
|
||||
});
|
||||
|
||||
// Buscar alerta atualizado para enviar email
|
||||
const alertaIgnorado = await ctx.db.get(args.id);
|
||||
if (alertaIgnorado) {
|
||||
// Enviar email de notificação (assíncrono, não bloqueia)
|
||||
await enviarEmailAlerta(ctx, alertaIgnorado, material, 'ignorado');
|
||||
}
|
||||
|
||||
// Registrar histórico
|
||||
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
||||
status: 'ignorado'
|
||||
|
||||
Reference in New Issue
Block a user