feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -9,17 +9,17 @@
|
||||
* @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;
|
||||
}
|
||||
// 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}`;
|
||||
}
|
||||
// Para mensagens do sistema, adicionar prefixo
|
||||
if (tipo === 'sistema' || tipo === 'notificacao') {
|
||||
return `[SGSE] ${conteudo}`;
|
||||
}
|
||||
|
||||
return conteudo;
|
||||
return conteudo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,18 +29,12 @@ export function wrapChatMessage(conteudo: string, tipo?: string): string {
|
||||
* @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;
|
||||
export function formatChatNotification(titulo: string, conteudo: string, acao?: string): string {
|
||||
let mensagem = `🔔 ${titulo}\n\n${conteudo}`;
|
||||
|
||||
if (acao) {
|
||||
mensagem += `\n\n💡 ${acao}`;
|
||||
}
|
||||
|
||||
return mensagem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
* 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;
|
||||
// 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 `
|
||||
const baseUrl = getBaseUrl();
|
||||
return `
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #1a3a52; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
@@ -42,10 +42,10 @@ function generateHeader(): string {
|
||||
* Gera o HTML do footer com assinatura SGSE
|
||||
*/
|
||||
function generateFooter(): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `
|
||||
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">
|
||||
@@ -85,24 +85,24 @@ function generateFooter(): string {
|
||||
* @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;
|
||||
}
|
||||
// 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>`;
|
||||
}
|
||||
// Garantir que o conteúdo tenha estrutura básica
|
||||
let conteudoProcessado = conteudoHTML.trim();
|
||||
|
||||
const header = generateHeader();
|
||||
const footer = generateFooter();
|
||||
const emailTitle = titulo || "Notificação do SGSE";
|
||||
// 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>`;
|
||||
}
|
||||
|
||||
return `
|
||||
const header = generateHeader();
|
||||
const footer = generateFooter();
|
||||
const emailTitle = titulo || 'Notificação do SGSE';
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
@@ -169,17 +169,18 @@ export function wrapEmailHTML(conteudoHTML: string, titulo?: string): string {
|
||||
* @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('');
|
||||
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('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,144 +8,143 @@
|
||||
* Considera headers como X-Forwarded-For, X-Real-IP, etc.
|
||||
*/
|
||||
export function getClientIP(request: Request): string | undefined {
|
||||
// Headers que podem conter o IP do cliente (case-insensitive)
|
||||
const getHeader = (name: string): string | null => {
|
||||
// Tentar diferentes variações de case
|
||||
const variations = [
|
||||
name,
|
||||
name.toLowerCase(),
|
||||
name.toUpperCase(),
|
||||
name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(),
|
||||
];
|
||||
|
||||
for (const variation of variations) {
|
||||
const value = request.headers.get(variation);
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
// As variações de case já cobrem a maioria dos casos
|
||||
// Se não encontrou, retorna null
|
||||
return null;
|
||||
};
|
||||
|
||||
const forwardedFor = getHeader("x-forwarded-for");
|
||||
const realIP = getHeader("x-real-ip");
|
||||
const cfConnectingIP = getHeader("cf-connecting-ip"); // Cloudflare
|
||||
const trueClientIP = getHeader("true-client-ip"); // Cloudflare Enterprise
|
||||
const xClientIP = getHeader("x-client-ip");
|
||||
const forwarded = getHeader("forwarded");
|
||||
const remoteAddr = getHeader("remote-addr");
|
||||
|
||||
// Log para debug
|
||||
console.log("Procurando IP nos headers:", {
|
||||
"x-forwarded-for": forwardedFor,
|
||||
"x-real-ip": realIP,
|
||||
"cf-connecting-ip": cfConnectingIP,
|
||||
"true-client-ip": trueClientIP,
|
||||
"x-client-ip": xClientIP,
|
||||
"forwarded": forwarded,
|
||||
"remote-addr": remoteAddr,
|
||||
});
|
||||
|
||||
// Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain)
|
||||
// O primeiro IP é geralmente o IP original do cliente
|
||||
if (forwardedFor) {
|
||||
const ips = forwardedFor.split(",").map((ip) => ip.trim());
|
||||
// Pegar o primeiro IP válido
|
||||
for (const ip of ips) {
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em X-Forwarded-For:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarded header (RFC 7239)
|
||||
if (forwarded) {
|
||||
// Formato: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
const forMatch = forwarded.match(/for=([^;,\s]+)/i);
|
||||
if (forMatch && forMatch[1]) {
|
||||
const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em Forwarded:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outros headers com IP único
|
||||
if (realIP && isValidIP(realIP)) {
|
||||
console.log("IP encontrado em X-Real-IP:", realIP);
|
||||
return realIP;
|
||||
}
|
||||
|
||||
if (cfConnectingIP && isValidIP(cfConnectingIP)) {
|
||||
console.log("IP encontrado em CF-Connecting-IP:", cfConnectingIP);
|
||||
return cfConnectingIP;
|
||||
}
|
||||
|
||||
if (trueClientIP && isValidIP(trueClientIP)) {
|
||||
console.log("IP encontrado em True-Client-IP:", trueClientIP);
|
||||
return trueClientIP;
|
||||
}
|
||||
|
||||
if (xClientIP && isValidIP(xClientIP)) {
|
||||
console.log("IP encontrado em X-Client-IP:", xClientIP);
|
||||
return xClientIP;
|
||||
}
|
||||
|
||||
if (remoteAddr && isValidIP(remoteAddr)) {
|
||||
console.log("IP encontrado em Remote-Addr:", remoteAddr);
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
// Tentar extrair do URL (último recurso)
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Se o servidor estiver configurado para passar IP via query param
|
||||
const ipFromQuery = url.searchParams.get("ip");
|
||||
if (ipFromQuery && isValidIP(ipFromQuery)) {
|
||||
console.log("IP encontrado em query param:", ipFromQuery);
|
||||
return ipFromQuery;
|
||||
}
|
||||
} catch {
|
||||
// Ignorar erro de parsing do URL
|
||||
}
|
||||
|
||||
console.log("Nenhum IP válido encontrado nos headers");
|
||||
return undefined;
|
||||
// Headers que podem conter o IP do cliente (case-insensitive)
|
||||
const getHeader = (name: string): string | null => {
|
||||
// Tentar diferentes variações de case
|
||||
const variations = [
|
||||
name,
|
||||
name.toLowerCase(),
|
||||
name.toUpperCase(),
|
||||
name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
||||
];
|
||||
|
||||
for (const variation of variations) {
|
||||
const value = request.headers.get(variation);
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
// As variações de case já cobrem a maioria dos casos
|
||||
// Se não encontrou, retorna null
|
||||
return null;
|
||||
};
|
||||
|
||||
const forwardedFor = getHeader('x-forwarded-for');
|
||||
const realIP = getHeader('x-real-ip');
|
||||
const cfConnectingIP = getHeader('cf-connecting-ip'); // Cloudflare
|
||||
const trueClientIP = getHeader('true-client-ip'); // Cloudflare Enterprise
|
||||
const xClientIP = getHeader('x-client-ip');
|
||||
const forwarded = getHeader('forwarded');
|
||||
const remoteAddr = getHeader('remote-addr');
|
||||
|
||||
// Log para debug
|
||||
console.log('Procurando IP nos headers:', {
|
||||
'x-forwarded-for': forwardedFor,
|
||||
'x-real-ip': realIP,
|
||||
'cf-connecting-ip': cfConnectingIP,
|
||||
'true-client-ip': trueClientIP,
|
||||
'x-client-ip': xClientIP,
|
||||
forwarded: forwarded,
|
||||
'remote-addr': remoteAddr
|
||||
});
|
||||
|
||||
// Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain)
|
||||
// O primeiro IP é geralmente o IP original do cliente
|
||||
if (forwardedFor) {
|
||||
const ips = forwardedFor.split(',').map((ip) => ip.trim());
|
||||
// Pegar o primeiro IP válido
|
||||
for (const ip of ips) {
|
||||
if (isValidIP(ip)) {
|
||||
console.log('IP encontrado em X-Forwarded-For:', ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarded header (RFC 7239)
|
||||
if (forwarded) {
|
||||
// Formato: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
const forMatch = forwarded.match(/for=([^;,\s]+)/i);
|
||||
if (forMatch && forMatch[1]) {
|
||||
const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6
|
||||
if (isValidIP(ip)) {
|
||||
console.log('IP encontrado em Forwarded:', ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outros headers com IP único
|
||||
if (realIP && isValidIP(realIP)) {
|
||||
console.log('IP encontrado em X-Real-IP:', realIP);
|
||||
return realIP;
|
||||
}
|
||||
|
||||
if (cfConnectingIP && isValidIP(cfConnectingIP)) {
|
||||
console.log('IP encontrado em CF-Connecting-IP:', cfConnectingIP);
|
||||
return cfConnectingIP;
|
||||
}
|
||||
|
||||
if (trueClientIP && isValidIP(trueClientIP)) {
|
||||
console.log('IP encontrado em True-Client-IP:', trueClientIP);
|
||||
return trueClientIP;
|
||||
}
|
||||
|
||||
if (xClientIP && isValidIP(xClientIP)) {
|
||||
console.log('IP encontrado em X-Client-IP:', xClientIP);
|
||||
return xClientIP;
|
||||
}
|
||||
|
||||
if (remoteAddr && isValidIP(remoteAddr)) {
|
||||
console.log('IP encontrado em Remote-Addr:', remoteAddr);
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
// Tentar extrair do URL (último recurso)
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Se o servidor estiver configurado para passar IP via query param
|
||||
const ipFromQuery = url.searchParams.get('ip');
|
||||
if (ipFromQuery && isValidIP(ipFromQuery)) {
|
||||
console.log('IP encontrado em query param:', ipFromQuery);
|
||||
return ipFromQuery;
|
||||
}
|
||||
} catch {
|
||||
// Ignorar erro de parsing do URL
|
||||
}
|
||||
|
||||
console.log('Nenhum IP válido encontrado nos headers');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se uma string é um endereço IP válido (IPv4 ou IPv6)
|
||||
*/
|
||||
function isValidIP(ip: string): boolean {
|
||||
if (!ip || ip.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split(".");
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// Validar IPv6 (formato simplificado)
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar IPv6 comprimido (com ::)
|
||||
const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6CompressedRegex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!ip || ip.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split('.');
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// Validar IPv6 (formato simplificado)
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar IPv6 comprimido (com ::)
|
||||
const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6CompressedRegex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
* Identifica todos os locais onde emails são enviados para gerar templates
|
||||
*/
|
||||
|
||||
import { Doc } from "../_generated/dataModel";
|
||||
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[];
|
||||
arquivo: string;
|
||||
funcao: string;
|
||||
tipo: 'enfileirarEmail' | 'enviarEmailComTemplate' | 'enviarMensagem' | 'html_inline';
|
||||
linha?: number;
|
||||
contexto?: string;
|
||||
assunto?: string;
|
||||
corpo?: string;
|
||||
templateCodigo?: string;
|
||||
variaveis?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,168 +22,187 @@ export interface EmailSendLocation {
|
||||
* 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"],
|
||||
},
|
||||
// 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;
|
||||
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[] = [];
|
||||
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 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 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 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",
|
||||
});
|
||||
// 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;
|
||||
return sugestoes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter todos os locais de envio de email
|
||||
*/
|
||||
export function obterLocaisEnvio(): EmailSendLocation[] {
|
||||
return LOCAIS_ENVIO_EMAIL;
|
||||
return LOCAIS_ENVIO_EMAIL;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user