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:
@@ -2,6 +2,7 @@ import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Doc } from "./_generated/dataModel";
|
||||
import { wrapEmailHTML, textToHTML } from "./utils/emailTemplateWrapper";
|
||||
|
||||
/**
|
||||
* Listar todos os templates
|
||||
@@ -40,7 +41,10 @@ export const criarTemplate = mutation({
|
||||
nome: v.string(),
|
||||
titulo: v.string(),
|
||||
corpo: v.string(),
|
||||
htmlCorpo: v.optional(v.string()),
|
||||
variaveis: v.optional(v.array(v.string())),
|
||||
categoria: v.optional(v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
criadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
@@ -58,6 +62,18 @@ export const criarTemplate = mutation({
|
||||
return { sucesso: false as const, erro: "Código de template já existe" };
|
||||
}
|
||||
|
||||
// Gerar HTML se não fornecido
|
||||
let htmlCorpo = args.htmlCorpo;
|
||||
if (!htmlCorpo) {
|
||||
// Se o corpo já for HTML, usar diretamente, senão converter
|
||||
if (args.corpo.includes("<") && args.corpo.includes(">")) {
|
||||
htmlCorpo = wrapEmailHTML(args.corpo, args.titulo);
|
||||
} else {
|
||||
const corpoHTML = textToHTML(args.corpo);
|
||||
htmlCorpo = wrapEmailHTML(corpoHTML, args.titulo);
|
||||
}
|
||||
}
|
||||
|
||||
// Criar template
|
||||
const templateId = await ctx.db.insert("templatesMensagens", {
|
||||
codigo: args.codigo,
|
||||
@@ -65,7 +81,10 @@ export const criarTemplate = mutation({
|
||||
tipo: "customizado",
|
||||
titulo: args.titulo,
|
||||
corpo: args.corpo,
|
||||
htmlCorpo,
|
||||
variaveis: args.variaveis,
|
||||
categoria: args.categoria || "email",
|
||||
tags: args.tags,
|
||||
criadoPor: args.criadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
@@ -93,7 +112,10 @@ export const editarTemplate = mutation({
|
||||
nome: v.optional(v.string()),
|
||||
titulo: v.optional(v.string()),
|
||||
corpo: v.optional(v.string()),
|
||||
htmlCorpo: v.optional(v.string()),
|
||||
variaveis: v.optional(v.array(v.string())),
|
||||
categoria: v.optional(v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
editadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
@@ -116,7 +138,21 @@ export const editarTemplate = mutation({
|
||||
if (args.nome !== undefined) updates.nome = args.nome;
|
||||
if (args.titulo !== undefined) updates.titulo = args.titulo;
|
||||
if (args.corpo !== undefined) updates.corpo = args.corpo;
|
||||
if (args.htmlCorpo !== undefined) {
|
||||
updates.htmlCorpo = args.htmlCorpo;
|
||||
} else if (args.corpo !== undefined) {
|
||||
// Se corpo foi atualizado mas htmlCorpo não, regenerar HTML
|
||||
const titulo = args.titulo || template.titulo;
|
||||
if (args.corpo.includes("<") && args.corpo.includes(">")) {
|
||||
updates.htmlCorpo = wrapEmailHTML(args.corpo, titulo);
|
||||
} else {
|
||||
const corpoHTML = textToHTML(args.corpo);
|
||||
updates.htmlCorpo = wrapEmailHTML(corpoHTML, titulo);
|
||||
}
|
||||
}
|
||||
if (args.variaveis !== undefined) updates.variaveis = args.variaveis;
|
||||
if (args.categoria !== undefined) updates.categoria = args.categoria;
|
||||
if (args.tags !== undefined) updates.tags = args.tags;
|
||||
|
||||
await ctx.db.patch(args.templateId, updates);
|
||||
|
||||
@@ -396,6 +432,33 @@ export const criarTemplatesPadrao = mutation({
|
||||
+ "</div></body></html>",
|
||||
variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"],
|
||||
},
|
||||
{
|
||||
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.",
|
||||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo", "urlSistema"],
|
||||
categoria: "email" as const,
|
||||
tags: ["ausencia", "solicitacao", "gestao"],
|
||||
},
|
||||
{
|
||||
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>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "urlSistema"],
|
||||
categoria: "email" as const,
|
||||
tags: ["ausencia", "aprovacao", "gestao"],
|
||||
},
|
||||
{
|
||||
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>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao", "urlSistema"],
|
||||
categoria: "email" as const,
|
||||
tags: ["ausencia", "reprovacao", "gestao"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const template of templatesPadrao) {
|
||||
@@ -418,4 +481,321 @@ export const criarTemplatesPadrao = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Atualizar HTML de um template
|
||||
*/
|
||||
export const atualizarTemplateHTML = mutation({
|
||||
args: {
|
||||
templateId: v.id("templatesMensagens"),
|
||||
htmlCorpo: v.string(),
|
||||
editadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const template = await ctx.db.get(args.templateId);
|
||||
if (!template) {
|
||||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||||
}
|
||||
|
||||
// Não permite editar templates do sistema
|
||||
if (template.tipo === "sistema") {
|
||||
return { sucesso: false as const, erro: "Templates do sistema não podem ser editados" };
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.templateId, {
|
||||
htmlCorpo: args.htmlCorpo,
|
||||
});
|
||||
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.editadoPorId,
|
||||
"editar",
|
||||
"templates",
|
||||
JSON.stringify({ templateId: args.templateId, campo: "htmlCorpo" }),
|
||||
args.templateId
|
||||
);
|
||||
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Preview de template renderizado com variáveis de teste
|
||||
*/
|
||||
export const previewTemplate = query({
|
||||
args: {
|
||||
templateId: v.id("templatesMensagens"),
|
||||
variaveisTeste: v.optional(v.record(v.string(), v.string())),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const template = await ctx.db.get(args.templateId);
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Variáveis padrão para teste
|
||||
const variaveisPadrao: Record<string, string> = {
|
||||
nome: "João Silva",
|
||||
matricula: "12345",
|
||||
senha: "Senha123!",
|
||||
motivo: "Exemplo de motivo",
|
||||
remetente: "Maria Santos",
|
||||
mensagem: "Esta é uma mensagem de exemplo para preview do template.",
|
||||
conversaId: "abc123",
|
||||
urlSistema: getBaseUrl(),
|
||||
solicitante: "João Silva",
|
||||
numeroTicket: "TKT-2024-001",
|
||||
prioridade: "Alta",
|
||||
categoria: "Suporte Técnico",
|
||||
responsavel: "Maria Santos",
|
||||
descricao: "Exemplo de descrição de chamado",
|
||||
destinario: "João Silva",
|
||||
tipoPrazo: "resolução",
|
||||
prazo: "24 horas",
|
||||
status: "Em andamento",
|
||||
rotaAcesso: "/ti/central-chamados",
|
||||
titulo: "Título de Exemplo",
|
||||
};
|
||||
|
||||
const variaveis = { ...variaveisPadrao, ...(args.variaveisTeste || {}) };
|
||||
|
||||
// Renderizar título e corpo
|
||||
const tituloRenderizado = renderizarTemplate(template.titulo, variaveis);
|
||||
const corpoRenderizado = renderizarTemplate(template.corpo, variaveis);
|
||||
|
||||
// Se tiver htmlCorpo, usar ele, senão gerar do corpo
|
||||
let htmlFinal = template.htmlCorpo;
|
||||
if (!htmlFinal) {
|
||||
if (corpoRenderizado.includes("<") && corpoRenderizado.includes(">")) {
|
||||
htmlFinal = wrapEmailHTML(corpoRenderizado, tituloRenderizado);
|
||||
} else {
|
||||
const corpoHTML = textToHTML(corpoRenderizado);
|
||||
htmlFinal = wrapEmailHTML(corpoHTML, tituloRenderizado);
|
||||
}
|
||||
} else {
|
||||
htmlFinal = renderizarTemplate(htmlFinal, variaveis);
|
||||
}
|
||||
|
||||
return {
|
||||
titulo: tituloRenderizado,
|
||||
corpo: corpoRenderizado,
|
||||
html: htmlFinal,
|
||||
variaveisUsadas: template.variaveis || [],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Função auxiliar para obter URL base
|
||||
*/
|
||||
function getBaseUrl(): string {
|
||||
const url = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
if (!url.match(/^https?:\/\//i)) {
|
||||
return `http://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportar templates (JSON)
|
||||
*/
|
||||
export const exportarTemplates = query({
|
||||
args: {
|
||||
templateIds: v.optional(v.array(v.id("templatesMensagens"))),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let templates;
|
||||
|
||||
if (args.templateIds && args.templateIds.length > 0) {
|
||||
templates = await Promise.all(
|
||||
args.templateIds.map((id) => ctx.db.get(id))
|
||||
);
|
||||
templates = templates.filter((t): t is Doc<"templatesMensagens"> => t !== null);
|
||||
} else {
|
||||
templates = await ctx.db.query("templatesMensagens").collect();
|
||||
}
|
||||
|
||||
// Remover campos internos e retornar apenas dados exportáveis
|
||||
return templates.map((t) => ({
|
||||
codigo: t.codigo,
|
||||
nome: t.nome,
|
||||
tipo: t.tipo,
|
||||
titulo: t.titulo,
|
||||
corpo: t.corpo,
|
||||
htmlCorpo: t.htmlCorpo,
|
||||
variaveis: t.variaveis,
|
||||
categoria: t.categoria,
|
||||
tags: t.tags,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Importar templates (JSON)
|
||||
*/
|
||||
export const importarTemplates = mutation({
|
||||
args: {
|
||||
templates: v.array(
|
||||
v.object({
|
||||
codigo: v.string(),
|
||||
nome: v.string(),
|
||||
tipo: v.optional(v.union(v.literal("sistema"), v.literal("customizado"))),
|
||||
titulo: v.string(),
|
||||
corpo: v.string(),
|
||||
htmlCorpo: v.optional(v.string()),
|
||||
variaveis: v.optional(v.array(v.string())),
|
||||
categoria: v.optional(v.union(v.literal("email"), v.literal("chat"), v.literal("ambos"))),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
})
|
||||
),
|
||||
importadoPorId: v.id("usuarios"),
|
||||
sobrescrever: v.optional(v.boolean()),
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
importados: v.number(),
|
||||
atualizados: v.number(),
|
||||
erros: v.array(v.string()),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
let importados = 0;
|
||||
let atualizados = 0;
|
||||
const erros: string[] = [];
|
||||
|
||||
for (const templateData of args.templates) {
|
||||
try {
|
||||
const existente = await ctx.db
|
||||
.query("templatesMensagens")
|
||||
.withIndex("by_codigo", (q) => q.eq("codigo", templateData.codigo))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
if (args.sobrescrever && existente.tipo === "customizado") {
|
||||
// Atualizar template existente
|
||||
await ctx.db.patch(existente._id, {
|
||||
nome: templateData.nome,
|
||||
titulo: templateData.titulo,
|
||||
corpo: templateData.corpo,
|
||||
htmlCorpo: templateData.htmlCorpo,
|
||||
variaveis: templateData.variaveis,
|
||||
categoria: templateData.categoria,
|
||||
tags: templateData.tags,
|
||||
});
|
||||
atualizados++;
|
||||
} else {
|
||||
erros.push(`Template ${templateData.codigo} já existe e sobrescrever está desabilitado`);
|
||||
}
|
||||
} else {
|
||||
// Criar novo template
|
||||
const tipo = templateData.tipo || "customizado";
|
||||
|
||||
// Gerar HTML se não fornecido
|
||||
let htmlCorpo = templateData.htmlCorpo;
|
||||
if (!htmlCorpo) {
|
||||
if (templateData.corpo.includes("<") && templateData.corpo.includes(">")) {
|
||||
htmlCorpo = wrapEmailHTML(templateData.corpo, templateData.titulo);
|
||||
} else {
|
||||
const corpoHTML = textToHTML(templateData.corpo);
|
||||
htmlCorpo = wrapEmailHTML(corpoHTML, templateData.titulo);
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.insert("templatesMensagens", {
|
||||
codigo: templateData.codigo,
|
||||
nome: templateData.nome,
|
||||
tipo,
|
||||
titulo: templateData.titulo,
|
||||
corpo: templateData.corpo,
|
||||
htmlCorpo,
|
||||
variaveis: templateData.variaveis,
|
||||
categoria: templateData.categoria || "email",
|
||||
tags: templateData.tags,
|
||||
criadoPor: args.importadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
importados++;
|
||||
}
|
||||
} catch (error) {
|
||||
const erroMsg = error instanceof Error ? error.message : String(error);
|
||||
erros.push(`Erro ao importar ${templateData.codigo}: ${erroMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.importadoPorId,
|
||||
"importar",
|
||||
"templates",
|
||||
JSON.stringify({ importados, atualizados, erros: erros.length }),
|
||||
undefined
|
||||
);
|
||||
|
||||
return {
|
||||
sucesso: erros.length === 0,
|
||||
importados,
|
||||
atualizados,
|
||||
erros,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Duplicar template
|
||||
*/
|
||||
export const duplicarTemplate = mutation({
|
||||
args: {
|
||||
templateId: v.id("templatesMensagens"),
|
||||
novoCodigo: v.string(),
|
||||
novoNome: v.optional(v.string()),
|
||||
criadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), templateId: v.id("templatesMensagens") }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const template = await ctx.db.get(args.templateId);
|
||||
if (!template) {
|
||||
return { sucesso: false as const, erro: "Template não encontrado" };
|
||||
}
|
||||
|
||||
// Verificar se novo código já existe
|
||||
const existente = await ctx.db
|
||||
.query("templatesMensagens")
|
||||
.withIndex("by_codigo", (q) => q.eq("codigo", args.novoCodigo))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Código de template já existe" };
|
||||
}
|
||||
|
||||
const templateId = await ctx.db.insert("templatesMensagens", {
|
||||
codigo: args.novoCodigo,
|
||||
nome: args.novoNome || `${template.nome} (Cópia)`,
|
||||
tipo: "customizado",
|
||||
titulo: template.titulo,
|
||||
corpo: template.corpo,
|
||||
htmlCorpo: template.htmlCorpo,
|
||||
variaveis: template.variaveis,
|
||||
categoria: template.categoria,
|
||||
tags: template.tags,
|
||||
criadoPor: args.criadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.criadoPorId,
|
||||
"duplicar",
|
||||
"templates",
|
||||
JSON.stringify({ templateId, codigo: args.novoCodigo, originalId: args.templateId }),
|
||||
templateId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, templateId };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user