Ajustes final etapa1 #69
@@ -9,7 +9,7 @@
|
|||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
let filtroTipo = $state<string>('');
|
let filtroTipo = $state<string>('');
|
||||||
let filtroStatus = $state<string>('ativo');
|
let filtroStatus = $state<string>('');
|
||||||
|
|
||||||
const alertasQuery = useQuery(api.almoxarifado.listarAlertas, {
|
const alertasQuery = useQuery(api.almoxarifado.listarAlertas, {
|
||||||
status: filtroStatus ? (filtroStatus as AlertaStatus) : undefined,
|
status: filtroStatus ? (filtroStatus as AlertaStatus) : undefined,
|
||||||
@@ -17,6 +17,16 @@
|
|||||||
});
|
});
|
||||||
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {});
|
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);
|
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
||||||
|
|
||||||
function mostrarMensagem(kind: 'success' | 'error', text: string) {
|
function mostrarMensagem(kind: 'success' | 'error', text: string) {
|
||||||
@@ -145,7 +155,11 @@
|
|||||||
<!-- Lista de Alertas -->
|
<!-- Lista de Alertas -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
||||||
<div class="card-body">
|
<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">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -162,7 +176,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each alertasQuery.data as alerta}
|
{#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}
|
{@const diferenca = alerta.quantidadeMinima - alerta.quantidadeAtual}
|
||||||
<tr class="hover:bg-base-200/50 transition-colors">
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
<td>
|
<td>
|
||||||
@@ -222,15 +236,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center py-12">
|
<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>
|
<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'}
|
{#if filtroStatus === 'ativo'}
|
||||||
Não há alertas ativos no momento. Todos os materiais estão com estoque adequado!
|
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.
|
Não há alertas com os filtros selecionados.
|
||||||
|
{:else}
|
||||||
|
Ainda não há alertas registrados no sistema.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { ClipboardList, Plus, CheckCircle, XCircle, Package } from 'lucide-svelte';
|
import { ClipboardList, Plus, CheckCircle, XCircle, Package } from 'lucide-svelte';
|
||||||
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -34,12 +35,60 @@
|
|||||||
const setoresQuery = useQuery(api.setores.list, {});
|
const setoresQuery = useQuery(api.setores.list, {});
|
||||||
|
|
||||||
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
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) {
|
function mostrarMensagem(kind: 'success' | 'error', text: string) {
|
||||||
notice = { kind, text };
|
if (kind === 'error') {
|
||||||
setTimeout(() => {
|
// Para erros, usar modal
|
||||||
notice = null;
|
errorModalTitle = 'Erro ao processar requisição';
|
||||||
}, 5000);
|
|
||||||
|
// 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(() => {
|
$effect(() => {
|
||||||
@@ -216,13 +265,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notificações -->
|
<!-- Notificações (apenas sucesso) -->
|
||||||
{#if notice}
|
{#if notice}
|
||||||
<div class="alert alert-{notice.kind} mb-6 shadow-lg">
|
<div class="alert alert-{notice.kind} mb-6 shadow-lg">
|
||||||
<span>{notice.text}</span>
|
<span>{notice.text}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal de Erro -->
|
||||||
|
<ErrorModal
|
||||||
|
open={showErrorModal}
|
||||||
|
title={errorModalTitle}
|
||||||
|
message={errorModalMessage}
|
||||||
|
details={errorModalDetails}
|
||||||
|
onClose={fecharErrorModal}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Doc, Id } from './_generated/dataModel';
|
|||||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||||
import { internalMutation, internalAction, internalQuery, mutation, query } from './_generated/server';
|
import { internalMutation, internalAction, internalQuery, mutation, query } from './_generated/server';
|
||||||
import { internal } from './_generated/api';
|
import { internal } from './_generated/api';
|
||||||
|
import { api } from './_generated/api';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
import {
|
import {
|
||||||
alertaStatus,
|
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'>) {
|
async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais'>) {
|
||||||
const material = await ctx.db.get(materialId);
|
const material = await ctx.db.get(materialId);
|
||||||
if (!material || !material.ativo) return;
|
if (!material || !material.ativo) return;
|
||||||
@@ -401,7 +512,7 @@ async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Criar alerta
|
// Criar alerta
|
||||||
await ctx.db.insert('alertasEstoque', {
|
const alertaId = await ctx.db.insert('alertasEstoque', {
|
||||||
materialId,
|
materialId,
|
||||||
tipo,
|
tipo,
|
||||||
quantidadeAtual: material.estoqueAtual,
|
quantidadeAtual: material.estoqueAtual,
|
||||||
@@ -409,6 +520,13 @@ async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais
|
|||||||
status: 'ativo',
|
status: 'ativo',
|
||||||
criadoEm: Date.now()
|
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'>) {
|
async function resolverAlertasMaterial(ctx: MutationCtx, materialId: Id<'materiais'>) {
|
||||||
@@ -979,12 +1097,23 @@ export const resolverAlerta = mutation({
|
|||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario) throw new Error('Usuário não autenticado');
|
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, {
|
await ctx.db.patch(args.id, {
|
||||||
status: 'resolvido',
|
status: 'resolvido',
|
||||||
resolvidoEm: Date.now(),
|
resolvidoEm: Date.now(),
|
||||||
resolvidoPor: usuario._id
|
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
|
// Registrar histórico
|
||||||
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
||||||
status: 'resolvido'
|
status: 'resolvido'
|
||||||
@@ -1003,10 +1132,21 @@ export const ignorarAlerta = mutation({
|
|||||||
throw new Error('Apenas alertas ativos podem ser ignorados');
|
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, {
|
await ctx.db.patch(args.id, {
|
||||||
status: 'ignorado'
|
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
|
// Registrar histórico
|
||||||
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
|
||||||
status: 'ignorado'
|
status: 'ignorado'
|
||||||
|
|||||||
@@ -71,10 +71,17 @@ export const atualizarConfiguracao = mutation({
|
|||||||
// Desativar configuração antiga
|
// Desativar configuração antiga
|
||||||
await ctx.db.patch(config._id, { ativo: false });
|
await ctx.db.patch(config._id, { ativo: false });
|
||||||
|
|
||||||
// Criar nova configuração
|
// Criar nova configuração (sem incluir _id e campos de sistema)
|
||||||
const dadosNovos = {
|
const dadosNovos = {
|
||||||
...config,
|
estoqueMinimoPadrao: args.estoqueMinimoPadrao ?? config.estoqueMinimoPadrao,
|
||||||
...args,
|
diasAntecedenciaAlerta: args.diasAntecedenciaAlerta ?? config.diasAntecedenciaAlerta,
|
||||||
|
permitirEstoqueNegativo: args.permitirEstoqueNegativo ?? config.permitirEstoqueNegativo,
|
||||||
|
requerAprovacaoRequisicao: args.requerAprovacaoRequisicao ?? config.requerAprovacaoRequisicao,
|
||||||
|
rolesAprovacao: args.rolesAprovacao ?? config.rolesAprovacao,
|
||||||
|
emailAlertasAtivo: args.emailAlertasAtivo ?? config.emailAlertasAtivo,
|
||||||
|
emailsDestinatarios: args.emailsDestinatarios ?? config.emailsDestinatarios,
|
||||||
|
periodicidadeInventario: args.periodicidadeInventario ?? config.periodicidadeInventario,
|
||||||
|
ultimoInventario: args.ultimoInventario ?? config.ultimoInventario,
|
||||||
ativo: true,
|
ativo: true,
|
||||||
atualizadoPor: usuario._id,
|
atualizadoPor: usuario._id,
|
||||||
atualizadoEm: Date.now()
|
atualizadoEm: Date.now()
|
||||||
|
|||||||
@@ -1005,6 +1005,177 @@ export const criarTemplatesPadrao = mutation({
|
|||||||
],
|
],
|
||||||
categoria: 'email' as const,
|
categoria: 'email' as const,
|
||||||
tags: ['erro', '500', 'servidor', 'critico', 'notificacao', 'ti']
|
tags: ['erro', '500', 'servidor', 'critico', 'notificacao', 'ti']
|
||||||
|
},
|
||||||
|
// ===================== ALMOXARIFADO - ALERTAS =====================
|
||||||
|
{
|
||||||
|
codigo: 'almoxarifado_alerta_criado',
|
||||||
|
nome: 'Almoxarifado - Alerta de Estoque Criado',
|
||||||
|
titulo: '⚠️ Alerta de Estoque: {{materialNome}}',
|
||||||
|
corpo:
|
||||||
|
'Olá,\n\n' +
|
||||||
|
'Um novo alerta de estoque foi criado no sistema:\n\n' +
|
||||||
|
'Material: {{materialNome}}\n' +
|
||||||
|
'Código: {{materialCodigo}}\n' +
|
||||||
|
'Tipo de Alerta: {{tipoAlerta}}\n' +
|
||||||
|
'Quantidade Atual: {{quantidadeAtual}} {{unidadeMedida}}\n' +
|
||||||
|
'Quantidade Mínima: {{quantidadeMinima}} {{unidadeMedida}}\n' +
|
||||||
|
'Diferença: {{diferenca}} {{unidadeMedida}}\n\n' +
|
||||||
|
'Por favor, verifique o estoque e realize a reposição necessária.',
|
||||||
|
htmlCorpo:
|
||||||
|
'<div style="max-width: 600px; margin: 0 auto; padding: 20px;">' +
|
||||||
|
'<div style="background: linear-gradient(135deg, #F59E0B 0%, #EF4444 100%); border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' +
|
||||||
|
'<h2 style="color: #FFFFFF; margin: 0 0 10px 0; font-size: 24px; font-weight: bold;">⚠️ Alerta de Estoque</h2>' +
|
||||||
|
'<p style="color: #FFFFFF; margin: 0; font-size: 16px; font-weight: 500;">Material: <strong>{{materialNome}}</strong></p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Olá,</p>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Um novo alerta de estoque foi criado no sistema:</p>' +
|
||||||
|
'<div style="background-color: #FEF2F2; border-left: 4px solid #EF4444; padding: 20px; border-radius: 8px; margin: 20px 0;">' +
|
||||||
|
'<p style="margin: 0 0 15px 0; color: #991B1B; font-weight: bold; font-size: 16px;">📦 Informações do Material:</p>' +
|
||||||
|
'<table style="width: 100%; border-collapse: collapse;">' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold; width: 160px;">Material:</td><td style="padding: 8px 0; color: #333;">{{materialNome}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Código:</td><td style="padding: 8px 0; color: #333; font-family: monospace;">{{materialCodigo}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Tipo de Alerta:</td><td style="padding: 8px 0;"><span style="background-color: #EF4444; color: white; padding: 4px 12px; border-radius: 4px; font-weight: bold;">{{tipoAlerta}}</span></td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Atual:</td><td style="padding: 8px 0; color: #DC2626; font-size: 18px; font-weight: bold;">{{quantidadeAtual}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Mínima:</td><td style="padding: 8px 0; color: #333;">{{quantidadeMinima}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Diferença:</td><td style="padding: 8px 0; color: #F59E0B; font-weight: bold;">{{diferenca}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'</table>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="background-color: #EFF6FF; border-left: 4px solid #2563EB; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||||||
|
'<p style="margin: 0; color: #1E40AF; font-size: 14px; line-height: 1.6;">' +
|
||||||
|
'<strong>💡 Ação Necessária:</strong> Por favor, verifique o estoque e realize a reposição necessária.' +
|
||||||
|
'</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="margin-top: 30px;">' +
|
||||||
|
'<a href="{{urlSistema}}/almoxarifado/alertas" style="background-color: #EF4444; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Ver Alertas</a>' +
|
||||||
|
'</p>' +
|
||||||
|
'<p style="color: #666666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #E0E0E0;">' +
|
||||||
|
'Almoxarifado SGSE - Sistema de Gerenciamento de Secretaria' +
|
||||||
|
'</p>' +
|
||||||
|
'</div>',
|
||||||
|
variaveis: [
|
||||||
|
'materialNome',
|
||||||
|
'materialCodigo',
|
||||||
|
'tipoAlerta',
|
||||||
|
'quantidadeAtual',
|
||||||
|
'quantidadeMinima',
|
||||||
|
'unidadeMedida',
|
||||||
|
'diferenca',
|
||||||
|
'urlSistema'
|
||||||
|
],
|
||||||
|
categoria: 'email' as const,
|
||||||
|
tags: ['almoxarifado', 'alerta', 'estoque', 'reposicao']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codigo: 'almoxarifado_alerta_resolvido',
|
||||||
|
nome: 'Almoxarifado - Alerta de Estoque Resolvido',
|
||||||
|
titulo: '✅ Alerta de Estoque Resolvido: {{materialNome}}',
|
||||||
|
corpo:
|
||||||
|
'Olá,\n\n' +
|
||||||
|
'O alerta de estoque abaixo foi resolvido:\n\n' +
|
||||||
|
'Material: {{materialNome}}\n' +
|
||||||
|
'Código: {{materialCodigo}}\n' +
|
||||||
|
'Quantidade Atual: {{quantidadeAtual}} {{unidadeMedida}}\n' +
|
||||||
|
'Quantidade Mínima: {{quantidadeMinima}} {{unidadeMedida}}\n' +
|
||||||
|
'Resolvido por: {{resolvidoPor}}\n' +
|
||||||
|
'Data: {{dataResolucao}}\n\n' +
|
||||||
|
'O estoque está agora acima do mínimo configurado.',
|
||||||
|
htmlCorpo:
|
||||||
|
'<div style="max-width: 600px; margin: 0 auto; padding: 20px;">' +
|
||||||
|
'<div style="background: linear-gradient(135deg, #10B981 0%, #059669 100%); border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' +
|
||||||
|
'<h2 style="color: #FFFFFF; margin: 0 0 10px 0; font-size: 24px; font-weight: bold;">✅ Alerta Resolvido</h2>' +
|
||||||
|
'<p style="color: #FFFFFF; margin: 0; font-size: 16px; font-weight: 500;">Material: <strong>{{materialNome}}</strong></p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Olá,</p>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">O alerta de estoque abaixo foi resolvido:</p>' +
|
||||||
|
'<div style="background-color: #ECFDF5; border-left: 4px solid #10B981; padding: 20px; border-radius: 8px; margin: 20px 0;">' +
|
||||||
|
'<p style="margin: 0 0 15px 0; color: #065F46; font-weight: bold; font-size: 16px;">📦 Informações do Material:</p>' +
|
||||||
|
'<table style="width: 100%; border-collapse: collapse;">' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold; width: 160px;">Material:</td><td style="padding: 8px 0; color: #333;">{{materialNome}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Código:</td><td style="padding: 8px 0; color: #333; font-family: monospace;">{{materialCodigo}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Atual:</td><td style="padding: 8px 0; color: #059669; font-size: 18px; font-weight: bold;">{{quantidadeAtual}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Mínima:</td><td style="padding: 8px 0; color: #333;">{{quantidadeMinima}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Resolvido por:</td><td style="padding: 8px 0; color: #333;">{{resolvidoPor}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Data:</td><td style="padding: 8px 0; color: #333;">{{dataResolucao}}</td></tr>' +
|
||||||
|
'</table>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="background-color: #F0FDF4; padding: 15px; border-radius: 4px; margin: 20px 0; text-align: center;">' +
|
||||||
|
'<p style="margin: 0; color: #166534; font-size: 14px; line-height: 1.6; font-weight: bold;">✅ O estoque está agora acima do mínimo configurado.</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="margin-top: 30px;">' +
|
||||||
|
'<a href="{{urlSistema}}/almoxarifado/alertas" style="background-color: #10B981; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Ver Alertas</a>' +
|
||||||
|
'</p>' +
|
||||||
|
'<p style="color: #666666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #E0E0E0;">' +
|
||||||
|
'Almoxarifado SGSE - Sistema de Gerenciamento de Secretaria' +
|
||||||
|
'</p>' +
|
||||||
|
'</div>',
|
||||||
|
variaveis: [
|
||||||
|
'materialNome',
|
||||||
|
'materialCodigo',
|
||||||
|
'quantidadeAtual',
|
||||||
|
'quantidadeMinima',
|
||||||
|
'unidadeMedida',
|
||||||
|
'resolvidoPor',
|
||||||
|
'dataResolucao',
|
||||||
|
'urlSistema'
|
||||||
|
],
|
||||||
|
categoria: 'email' as const,
|
||||||
|
tags: ['almoxarifado', 'alerta', 'estoque', 'resolvido']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codigo: 'almoxarifado_alerta_ignorado',
|
||||||
|
nome: 'Almoxarifado - Alerta de Estoque Ignorado',
|
||||||
|
titulo: '⚠️ Alerta de Estoque Ignorado: {{materialNome}}',
|
||||||
|
corpo:
|
||||||
|
'Olá,\n\n' +
|
||||||
|
'O alerta de estoque abaixo foi ignorado:\n\n' +
|
||||||
|
'Material: {{materialNome}}\n' +
|
||||||
|
'Código: {{materialCodigo}}\n' +
|
||||||
|
'Tipo de Alerta: {{tipoAlerta}}\n' +
|
||||||
|
'Quantidade Atual: {{quantidadeAtual}} {{unidadeMedida}}\n' +
|
||||||
|
'Quantidade Mínima: {{quantidadeMinima}} {{unidadeMedida}}\n' +
|
||||||
|
'Data: {{dataIgnorado}}\n\n' +
|
||||||
|
'⚠️ Atenção: O estoque ainda está abaixo do mínimo configurado.',
|
||||||
|
htmlCorpo:
|
||||||
|
'<div style="max-width: 600px; margin: 0 auto; padding: 20px;">' +
|
||||||
|
'<div style="background: linear-gradient(135deg, #6B7280 0%, #4B5563 100%); border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' +
|
||||||
|
'<h2 style="color: #FFFFFF; margin: 0 0 10px 0; font-size: 24px; font-weight: bold;">⚠️ Alerta Ignorado</h2>' +
|
||||||
|
'<p style="color: #FFFFFF; margin: 0; font-size: 16px; font-weight: 500;">Material: <strong>{{materialNome}}</strong></p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">Olá,</p>' +
|
||||||
|
'<p style="color: #333333; font-size: 16px; line-height: 1.6; margin: 0 0 20px 0;">O alerta de estoque abaixo foi ignorado:</p>' +
|
||||||
|
'<div style="background-color: #F3F4F6; border-left: 4px solid #6B7280; padding: 20px; border-radius: 8px; margin: 20px 0;">' +
|
||||||
|
'<p style="margin: 0 0 15px 0; color: #374151; font-weight: bold; font-size: 16px;">📦 Informações do Material:</p>' +
|
||||||
|
'<table style="width: 100%; border-collapse: collapse;">' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold; width: 160px;">Material:</td><td style="padding: 8px 0; color: #333;">{{materialNome}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Código:</td><td style="padding: 8px 0; color: #333; font-family: monospace;">{{materialCodigo}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Tipo de Alerta:</td><td style="padding: 8px 0;"><span style="background-color: #6B7280; color: white; padding: 4px 12px; border-radius: 4px; font-weight: bold;">{{tipoAlerta}}</span></td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Atual:</td><td style="padding: 8px 0; color: #DC2626; font-size: 18px; font-weight: bold;">{{quantidadeAtual}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Quantidade Mínima:</td><td style="padding: 8px 0; color: #333;">{{quantidadeMinima}} {{unidadeMedida}}</td></tr>' +
|
||||||
|
'<tr><td style="padding: 8px 0; color: #333; font-weight: bold;">Data:</td><td style="padding: 8px 0; color: #333;">{{dataIgnorado}}</td></tr>' +
|
||||||
|
'</table>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="background-color: #FEF2F2; border-left: 4px solid #EF4444; padding: 15px; border-radius: 4px; margin: 20px 0;">' +
|
||||||
|
'<p style="margin: 0; color: #991B1B; font-size: 14px; line-height: 1.6; font-weight: bold;">⚠️ Atenção: O estoque ainda está abaixo do mínimo configurado.</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p style="margin-top: 30px;">' +
|
||||||
|
'<a href="{{urlSistema}}/almoxarifado/alertas" style="background-color: #6B7280; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Ver Alertas</a>' +
|
||||||
|
'</p>' +
|
||||||
|
'<p style="color: #666666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #E0E0E0;">' +
|
||||||
|
'Almoxarifado SGSE - Sistema de Gerenciamento de Secretaria' +
|
||||||
|
'</p>' +
|
||||||
|
'</div>',
|
||||||
|
variaveis: [
|
||||||
|
'materialNome',
|
||||||
|
'materialCodigo',
|
||||||
|
'tipoAlerta',
|
||||||
|
'quantidadeAtual',
|
||||||
|
'quantidadeMinima',
|
||||||
|
'unidadeMedida',
|
||||||
|
'dataIgnorado',
|
||||||
|
'urlSistema'
|
||||||
|
],
|
||||||
|
categoria: 'email' as const,
|
||||||
|
tags: ['almoxarifado', 'alerta', 'estoque', 'ignorado']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user