feat: add tab navigation and content management for notifications page, allowing users to switch between Enviar Notificação, Gerenciar Templates, and Agendamentos for improved organization and usability
This commit is contained in:
46
packages/backend/convex/utils/chatTemplateWrapper.ts
Normal file
46
packages/backend/convex/utils/chatTemplateWrapper.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Wrapper para padronizar mensagens de chat do SGSE
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formata mensagem de chat com prefixo padronizado quando necessário
|
||||
* @param conteudo - Conteúdo da mensagem
|
||||
* @param tipo - Tipo da mensagem (opcional)
|
||||
* @returns Mensagem formatada
|
||||
*/
|
||||
export function wrapChatMessage(conteudo: string, tipo?: string): string {
|
||||
// Se já tiver formatação especial, retornar como está
|
||||
if (conteudo.includes('[SGSE]') || conteudo.includes('[Sistema]')) {
|
||||
return conteudo;
|
||||
}
|
||||
|
||||
// Para mensagens do sistema, adicionar prefixo
|
||||
if (tipo === 'sistema' || tipo === 'notificacao') {
|
||||
return `[SGSE] ${conteudo}`;
|
||||
}
|
||||
|
||||
return conteudo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata mensagem de chat com informações estruturadas
|
||||
* @param titulo - Título da notificação
|
||||
* @param conteudo - Conteúdo da mensagem
|
||||
* @param acao - Ação sugerida (opcional)
|
||||
* @returns Mensagem formatada
|
||||
*/
|
||||
export function formatChatNotification(
|
||||
titulo: string,
|
||||
conteudo: string,
|
||||
acao?: string
|
||||
): string {
|
||||
let mensagem = `🔔 ${titulo}\n\n${conteudo}`;
|
||||
|
||||
if (acao) {
|
||||
mensagem += `\n\n💡 ${acao}`;
|
||||
}
|
||||
|
||||
return mensagem;
|
||||
}
|
||||
|
||||
|
||||
185
packages/backend/convex/utils/emailTemplateWrapper.ts
Normal file
185
packages/backend/convex/utils/emailTemplateWrapper.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Wrapper HTML para templates de email do SGSE
|
||||
* Aplica estilo governamental profissional com logo e assinatura padronizada
|
||||
*/
|
||||
|
||||
/**
|
||||
* Obtém a URL base do sistema para uso em links de email
|
||||
*/
|
||||
function getBaseUrl(): string {
|
||||
// Em produção, usar variável de ambiente
|
||||
const url = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
// Garantir que tenha protocolo
|
||||
if (!url.match(/^https?:\/\//i)) {
|
||||
return `http://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera o HTML do header com logo do Governo de PE
|
||||
*/
|
||||
function generateHeader(): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
return `
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #1a3a52; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="text-align: center; padding: 20px 0;">
|
||||
<img src="${baseUrl}/logo_governo_PE.png" alt="Governo de Pernambuco" style="max-width: 200px; height: auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera o HTML do footer com assinatura SGSE
|
||||
*/
|
||||
function generateFooter(): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f5f5f5; border-top: 3px solid #1a3a52; margin-top: 30px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" style="padding: 30px 20px;">
|
||||
<tr>
|
||||
<td style="text-align: center; font-family: Arial, sans-serif; color: #333333; font-size: 14px; line-height: 1.6;">
|
||||
<p style="margin: 0 0 10px 0; font-weight: bold; color: #1a3a52; font-size: 16px;">
|
||||
SGSE - Sistema de Gerenciamento de Secretaria
|
||||
</p>
|
||||
<p style="margin: 0 0 10px 0; color: #666666;">
|
||||
Secretaria de Esportes do Estado de Pernambuco
|
||||
</p>
|
||||
<p style="margin: 0 0 15px 0; color: #666666; font-size: 12px;">
|
||||
Este é um email automático do sistema. Por favor, não responda diretamente a este email.
|
||||
</p>
|
||||
<hr style="border: none; border-top: 1px solid #dddddd; margin: 20px 0;" />
|
||||
<p style="margin: 0; color: #999999; font-size: 11px;">
|
||||
© ${currentYear} Secretaria de Esportes - Governo de Pernambuco. Todos os direitos reservados.
|
||||
</p>
|
||||
<p style="margin: 5px 0 0 0; color: #999999; font-size: 11px;">
|
||||
<a href="${baseUrl}" style="color: #1a3a52; text-decoration: none;">Acessar Sistema</a> |
|
||||
<a href="${baseUrl}/ti/notificacoes" style="color: #1a3a52; text-decoration: none;">Central de Notificações</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envolve o conteúdo HTML do email com template profissional governamental
|
||||
* @param conteudoHTML - Conteúdo HTML do corpo do email
|
||||
* @param titulo - Título do email (usado no meta)
|
||||
* @returns HTML completo do email pronto para envio
|
||||
*/
|
||||
export function wrapEmailHTML(conteudoHTML: string, titulo?: string): string {
|
||||
// Se o conteúdo já estiver dentro de um wrapper completo, retornar como está
|
||||
if (conteudoHTML.includes('<!DOCTYPE html>') || conteudoHTML.includes('<html')) {
|
||||
return conteudoHTML;
|
||||
}
|
||||
|
||||
// Garantir que o conteúdo tenha estrutura básica
|
||||
let conteudoProcessado = conteudoHTML.trim();
|
||||
|
||||
// Se não tiver tags HTML básicas, envolver em parágrafo
|
||||
if (!conteudoProcessado.match(/^<[a-z]/i)) {
|
||||
conteudoProcessado = `<p style="margin: 0 0 15px 0;">${conteudoProcessado}</p>`;
|
||||
}
|
||||
|
||||
const header = generateHeader();
|
||||
const footer = generateFooter();
|
||||
const emailTitle = titulo || "Notificação do SGSE";
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>${emailTitle}</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td {font-family: Arial, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f5f5f5; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
|
||||
<!-- Wrapper principal -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f5f5f5; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!-- Container do conteúdo -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden;">
|
||||
<!-- Header -->
|
||||
${header}
|
||||
|
||||
<!-- Corpo do email -->
|
||||
<tr>
|
||||
<td style="padding: 30px 20px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; color: #333333; font-size: 14px; line-height: 1.6;">
|
||||
${conteudoProcessado}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td>
|
||||
${footer}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Espaçamento inferior -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="padding: 20px 0; text-align: center; font-family: Arial, sans-serif; color: #999999; font-size: 11px;">
|
||||
<p style="margin: 0;">Se você não solicitou este email, pode ignorá-lo com segurança.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte texto plano em HTML básico
|
||||
* @param texto - Texto plano
|
||||
* @returns HTML formatado
|
||||
*/
|
||||
export function textToHTML(texto: string): string {
|
||||
return texto
|
||||
.split('\n')
|
||||
.map(linha => {
|
||||
const linhaTrim = linha.trim();
|
||||
if (!linhaTrim) return '<br />';
|
||||
// Detectar links
|
||||
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const linhaComLinks = linhaTrim.replace(linkRegex, '<a href="$1" style="color: #1a3a52; text-decoration: underline;">$1</a>');
|
||||
return `<p style="margin: 0 0 15px 0;">${linhaComLinks}</p>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
189
packages/backend/convex/utils/scanEmailSenders.ts
Normal file
189
packages/backend/convex/utils/scanEmailSenders.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Scanner automático de envios de email e mensagens no código
|
||||
* Identifica todos os locais onde emails são enviados para gerar templates
|
||||
*/
|
||||
|
||||
import { Doc } from "../_generated/dataModel";
|
||||
|
||||
export interface EmailSendLocation {
|
||||
arquivo: string;
|
||||
funcao: string;
|
||||
tipo: "enfileirarEmail" | "enviarEmailComTemplate" | "enviarMensagem" | "html_inline";
|
||||
linha?: number;
|
||||
contexto?: string;
|
||||
assunto?: string;
|
||||
corpo?: string;
|
||||
templateCodigo?: string;
|
||||
variaveis?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista de locais conhecidos onde emails são enviados
|
||||
* Este é um mapeamento manual baseado na análise do código
|
||||
*/
|
||||
export const LOCAIS_ENVIO_EMAIL: EmailSendLocation[] = [
|
||||
// Chamados
|
||||
{
|
||||
arquivo: "packages/backend/convex/chamados.ts",
|
||||
funcao: "registrarNotificacoes",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao solicitante quando chamado é criado/atualizado",
|
||||
assunto: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/chamados.ts",
|
||||
funcao: "registrarNotificacoes",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao responsável quando chamado é atualizado",
|
||||
assunto: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
},
|
||||
|
||||
// Ausências
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "solicitar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao gestor quando funcionário solicita ausência",
|
||||
assunto: "Nova Solicitação de Ausência - {{funcionarioNome}}",
|
||||
corpo: "Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.",
|
||||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "aprovar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao funcionário quando ausência é aprovada",
|
||||
assunto: "Solicitação de Ausência Aprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "reprovar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao funcionário quando ausência é reprovada",
|
||||
assunto: "Solicitação de Ausência Reprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao"],
|
||||
},
|
||||
|
||||
// Chat
|
||||
{
|
||||
arquivo: "packages/backend/convex/chat.ts",
|
||||
funcao: "enviarMensagem",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Email quando usuário recebe nova mensagem no chat (usuário offline)",
|
||||
templateCodigo: "chat_mensagem",
|
||||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/chat.ts",
|
||||
funcao: "enviarMensagem",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Email quando usuário é mencionado no chat (usuário offline)",
|
||||
templateCodigo: "chat_mencao",
|
||||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||||
},
|
||||
|
||||
// Painel de Notificações
|
||||
{
|
||||
arquivo: "apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte",
|
||||
funcao: "enviarNotificacao",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Envio manual de notificação via painel de TI",
|
||||
assunto: "Notificação do Sistema",
|
||||
corpo: "{{mensagemPersonalizada}}",
|
||||
variaveis: ["mensagemPersonalizada"],
|
||||
},
|
||||
{
|
||||
arquivo: "apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte",
|
||||
funcao: "enviarNotificacao",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Envio manual de notificação usando template via painel de TI",
|
||||
templateCodigo: "{{templateCodigo}}",
|
||||
variaveis: ["nome", "matricula"],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Sugestões de templates baseadas nos locais de envio encontrados
|
||||
*/
|
||||
export interface TemplateSuggestion {
|
||||
codigo: string;
|
||||
nome: string;
|
||||
titulo: string;
|
||||
corpo: string;
|
||||
categoria: "email" | "chat" | "ambos";
|
||||
variaveis: string[];
|
||||
tags: string[];
|
||||
origem: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar sugestões de templates baseadas nos locais de envio
|
||||
*/
|
||||
export function gerarSugestoesTemplates(): TemplateSuggestion[] {
|
||||
const sugestoes: TemplateSuggestion[] = [];
|
||||
|
||||
// Template para ausência solicitada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_solicitada",
|
||||
nome: "Ausência Solicitada",
|
||||
titulo: "Nova Solicitação de Ausência - {{funcionarioNome}}",
|
||||
corpo: "Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.",
|
||||
categoria: "email",
|
||||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo"],
|
||||
tags: ["ausencia", "solicitacao", "gestao"],
|
||||
origem: "ausencias.ts - solicitar",
|
||||
});
|
||||
|
||||
// Template para ausência aprovada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_aprovada",
|
||||
nome: "Ausência Aprovada",
|
||||
titulo: "Solicitação de Ausência Aprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>",
|
||||
categoria: "email",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo"],
|
||||
tags: ["ausencia", "aprovacao", "gestao"],
|
||||
origem: "ausencias.ts - aprovar",
|
||||
});
|
||||
|
||||
// Template para ausência reprovada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_reprovada",
|
||||
nome: "Ausência Reprovada",
|
||||
titulo: "Solicitação de Ausência Reprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>",
|
||||
categoria: "email",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao"],
|
||||
tags: ["ausencia", "reprovacao", "gestao"],
|
||||
origem: "ausencias.ts - reprovar",
|
||||
});
|
||||
|
||||
// Template genérico para notificações de chamados
|
||||
sugestoes.push({
|
||||
codigo: "chamado_notificacao",
|
||||
nome: "Notificação de Chamado",
|
||||
titulo: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
categoria: "email",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
tags: ["chamado", "notificacao", "suporte"],
|
||||
origem: "chamados.ts - registrarNotificacoes",
|
||||
});
|
||||
|
||||
return sugestoes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter todos os locais de envio de email
|
||||
*/
|
||||
export function obterLocaisEnvio(): EmailSendLocation[] {
|
||||
return LOCAIS_ENVIO_EMAIL;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user