refactor: enhance chat components with type safety and response functionality

- Updated type definitions in ChatWindow and MessageList components for better type safety.
- Improved MessageInput to handle message responses, including a preview feature for replying to messages.
- Enhanced the chat message handling logic to support message references and improve user interaction.
- Refactored notification utility functions to support push notifications and rate limiting for email sending.
- Updated backend schema to accommodate new features related to message responses and notifications.
This commit is contained in:
2025-11-04 20:36:01 -03:00
parent 15374276d5
commit 12db52a8a7
23 changed files with 3195 additions and 503 deletions

View File

@@ -1,262 +1,312 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { Doc } from "./_generated/dataModel";
/**
* Listar todos os templates
*/
export const listarTemplates = query({
args: {},
handler: async (ctx) => {
const templates = await ctx.db.query("templatesMensagens").collect();
return templates;
},
});
/**
* Obter template por código
*/
export const obterTemplatePorCodigo = query({
args: {
codigo: v.string(),
},
handler: async (ctx, args) => {
const template = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
return template;
},
});
/**
* Criar template customizado (apenas TI_MASTER)
*/
export const criarTemplate = mutation({
args: {
codigo: v.string(),
nome: v.string(),
titulo: v.string(),
corpo: v.string(),
variaveis: v.optional(v.array(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) => {
// Verificar se código já existe
const existente = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
if (existente) {
return { sucesso: false as const, erro: "Código de template já existe" };
}
// Criar template
const templateId = await ctx.db.insert("templatesMensagens", {
codigo: args.codigo,
nome: args.nome,
tipo: "customizado",
titulo: args.titulo,
corpo: args.corpo,
variaveis: args.variaveis,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"templates",
JSON.stringify({ templateId, codigo: args.codigo, nome: args.nome }),
templateId
);
return { sucesso: true as const, templateId };
},
});
/**
* Editar template customizado (apenas TI_MASTER, não edita templates do sistema)
*/
export const editarTemplate = mutation({
args: {
templateId: v.id("templatesMensagens"),
nome: v.optional(v.string()),
titulo: v.optional(v.string()),
corpo: v.optional(v.string()),
variaveis: v.optional(v.array(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" };
}
// Atualizar template
const updates: Partial<Doc<"templatesMensagens">> = {};
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.variaveis !== undefined) updates.variaveis = args.variaveis;
await ctx.db.patch(args.templateId, updates);
// Log de atividade
await registrarAtividade(
ctx,
args.editadoPorId,
"editar",
"templates",
JSON.stringify(updates),
args.templateId
);
return { sucesso: true as const };
},
});
/**
* Excluir template customizado (apenas TI_MASTER, não exclui templates do sistema)
*/
export const excluirTemplate = mutation({
args: {
templateId: v.id("templatesMensagens"),
excluidoPorId: 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 excluir templates do sistema
if (template.tipo === "sistema") {
return { sucesso: false as const, erro: "Templates do sistema não podem ser excluídos" };
}
// Excluir template
await ctx.db.delete(args.templateId);
// Log de atividade
await registrarAtividade(
ctx,
args.excluidoPorId,
"excluir",
"templates",
JSON.stringify({ templateId: args.templateId, codigo: template.codigo }),
args.templateId
);
return { sucesso: true as const };
},
});
/**
* Renderizar template com variáveis
*/
export function renderizarTemplate(template: string, variaveis: Record<string, string>): string {
let resultado = template;
for (const [chave, valor] of Object.entries(variaveis)) {
const placeholder = `{{${chave}}}`;
resultado = resultado.replace(new RegExp(placeholder, "g"), valor);
}
return resultado;
}
/**
* Criar templates padrão do sistema (chamado no seed)
*/
export const criarTemplatesPadrao = mutation({
args: {},
handler: async (ctx) => {
const templatesPadrao = [
{
codigo: "USUARIO_BLOQUEADO",
nome: "Usuário Bloqueado",
titulo: "Sua conta foi bloqueada",
corpo: "Sua conta no SGSE foi bloqueada.\n\nMotivo: {{motivo}}\n\nPara mais informações, entre em contato com a TI.",
variaveis: ["motivo"],
},
{
codigo: "USUARIO_DESBLOQUEADO",
nome: "Usuário Desbloqueado",
titulo: "Sua conta foi desbloqueada",
corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
variaveis: [],
},
{
codigo: "SENHA_RESETADA",
nome: "Senha Resetada",
titulo: "Sua senha foi resetada",
corpo: "Sua senha foi resetada pela equipe de TI.\n\nNova senha temporária: {{senha}}\n\nPor favor, altere sua senha no próximo login.",
variaveis: ["senha"],
},
{
codigo: "PERMISSAO_ALTERADA",
nome: "Permissão Alterada",
titulo: "Suas permissões foram atualizadas",
corpo: "Suas permissões de acesso ao sistema foram atualizadas.\n\nPara verificar suas novas permissões, acesse o menu de perfil.",
variaveis: [],
},
{
codigo: "AVISO_GERAL",
nome: "Aviso Geral",
titulo: "{{titulo}}",
corpo: "{{mensagem}}",
variaveis: ["titulo", "mensagem"],
},
{
codigo: "BEM_VINDO",
nome: "Boas-vindas",
titulo: "Bem-vindo ao SGSE",
corpo: "Olá {{nome}},\n\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI",
variaveis: ["nome", "matricula", "senha"],
},
];
for (const template of templatesPadrao) {
// Verificar se já existe
const existente = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", template.codigo))
.first();
if (!existente) {
await ctx.db.insert("templatesMensagens", {
...template,
tipo: "sistema",
criadoEm: Date.now(),
});
}
}
return { sucesso: true };
},
});
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { Doc } from "./_generated/dataModel";
/**
* Listar todos os templates
*/
export const listarTemplates = query({
args: {},
handler: async (ctx) => {
const templates = await ctx.db.query("templatesMensagens").collect();
return templates;
},
});
/**
* Obter template por código
*/
export const obterTemplatePorCodigo = query({
args: {
codigo: v.string(),
},
handler: async (ctx, args) => {
const template = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
return template;
},
});
/**
* Criar template customizado (apenas TI_MASTER)
*/
export const criarTemplate = mutation({
args: {
codigo: v.string(),
nome: v.string(),
titulo: v.string(),
corpo: v.string(),
variaveis: v.optional(v.array(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) => {
// Verificar se código já existe
const existente = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.codigo))
.first();
if (existente) {
return { sucesso: false as const, erro: "Código de template já existe" };
}
// Criar template
const templateId = await ctx.db.insert("templatesMensagens", {
codigo: args.codigo,
nome: args.nome,
tipo: "customizado",
titulo: args.titulo,
corpo: args.corpo,
variaveis: args.variaveis,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"templates",
JSON.stringify({ templateId, codigo: args.codigo, nome: args.nome }),
templateId
);
return { sucesso: true as const, templateId };
},
});
/**
* Editar template customizado (apenas TI_MASTER, não edita templates do sistema)
*/
export const editarTemplate = mutation({
args: {
templateId: v.id("templatesMensagens"),
nome: v.optional(v.string()),
titulo: v.optional(v.string()),
corpo: v.optional(v.string()),
variaveis: v.optional(v.array(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" };
}
// Atualizar template
const updates: Partial<Doc<"templatesMensagens">> = {};
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.variaveis !== undefined) updates.variaveis = args.variaveis;
await ctx.db.patch(args.templateId, updates);
// Log de atividade
await registrarAtividade(
ctx,
args.editadoPorId,
"editar",
"templates",
JSON.stringify(updates),
args.templateId
);
return { sucesso: true as const };
},
});
/**
* Excluir template customizado (apenas TI_MASTER, não exclui templates do sistema)
*/
export const excluirTemplate = mutation({
args: {
templateId: v.id("templatesMensagens"),
excluidoPorId: 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 excluir templates do sistema
if (template.tipo === "sistema") {
return { sucesso: false as const, erro: "Templates do sistema não podem ser excluídos" };
}
// Excluir template
await ctx.db.delete(args.templateId);
// Log de atividade
await registrarAtividade(
ctx,
args.excluidoPorId,
"excluir",
"templates",
JSON.stringify({ templateId: args.templateId, codigo: template.codigo }),
args.templateId
);
return { sucesso: true as const };
},
});
/**
* Renderizar template com variáveis
*/
export function renderizarTemplate(template: string, variaveis: Record<string, string>): string {
let resultado = template;
for (const [chave, valor] of Object.entries(variaveis)) {
const placeholder = `{{${chave}}}`;
resultado = resultado.replace(new RegExp(placeholder, "g"), valor);
}
return resultado;
}
/**
* Criar templates padrão do sistema (chamado no seed)
*/
export const criarTemplatesPadrao = mutation({
args: {},
handler: async (ctx) => {
const templatesPadrao = [
{
codigo: "USUARIO_BLOQUEADO",
nome: "Usuário Bloqueado",
titulo: "Sua conta foi bloqueada",
corpo: "Sua conta no SGSE foi bloqueada.\n\nMotivo: {{motivo}}\n\nPara mais informações, entre em contato com a TI.",
variaveis: ["motivo"],
},
{
codigo: "USUARIO_DESBLOQUEADO",
nome: "Usuário Desbloqueado",
titulo: "Sua conta foi desbloqueada",
corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
variaveis: [],
},
{
codigo: "SENHA_RESETADA",
nome: "Senha Resetada",
titulo: "Sua senha foi resetada",
corpo: "Sua senha foi resetada pela equipe de TI.\n\nNova senha temporária: {{senha}}\n\nPor favor, altere sua senha no próximo login.",
variaveis: ["senha"],
},
{
codigo: "PERMISSAO_ALTERADA",
nome: "Permissão Alterada",
titulo: "Suas permissões foram atualizadas",
corpo: "Suas permissões de acesso ao sistema foram atualizadas.\n\nPara verificar suas novas permissões, acesse o menu de perfil.",
variaveis: [],
},
{
codigo: "AVISO_GERAL",
nome: "Aviso Geral",
titulo: "{{titulo}}",
corpo: "{{mensagem}}",
variaveis: ["titulo", "mensagem"],
},
{
codigo: "BEM_VINDO",
nome: "Boas-vindas",
titulo: "Bem-vindo ao SGSE",
corpo: "Olá {{nome}},\n\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI",
variaveis: ["nome", "matricula", "senha"],
},
{
codigo: "chat_mensagem",
nome: "Nova Mensagem no Chat",
titulo: "Nova mensagem de {{remetente}}",
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
+ "<h2 style='color: #4F46E5;'>Nova mensagem no chat</h2>"
+ "<p><strong>{{remetente}}</strong> enviou uma nova mensagem:</p>"
+ "<div style='background-color: #F3F4F6; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
+ "<p style='margin: 0;'>{{mensagem}}</p>"
+ "</div>"
+ "<p style='margin-top: 30px;'>"
+ "<a href='{{urlSistema}}/chat?conversa={{conversaId}}' "
+ "style='background-color: #4F46E5; color: white; padding: 12px 24px; "
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
+ "Ver conversa"
+ "</a>"
+ "</p>"
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
+ "Você está recebendo este email porque não estava online quando a mensagem foi enviada. "
+ "Você pode desativar essas notificações nas configurações da conversa."
+ "</p>"
+ "</div></body></html>",
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
},
{
codigo: "chat_mencao",
nome: "Menção no Chat",
titulo: "{{remetente}} mencionou você",
corpo: "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
+ "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
+ "<h2 style='color: #DC2626;'>Você foi mencionado!</h2>"
+ "<p><strong>{{remetente}}</strong> mencionou você em uma mensagem:</p>"
+ "<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>"
+ "<p style='margin: 0;'>{{mensagem}}</p>"
+ "</div>"
+ "<p style='margin-top: 30px;'>"
+ "<a href='{{urlSistema}}/chat?conversa={{conversaId}}' "
+ "style='background-color: #DC2626; color: white; padding: 12px 24px; "
+ "text-decoration: none; border-radius: 6px; display: inline-block;'>"
+ "Ver mensagem"
+ "</a>"
+ "</p>"
+ "<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>"
+ "Você está recebendo este email porque foi mencionado em uma conversa. "
+ "Você pode desativar essas notificações nas configurações da conversa."
+ "</p>"
+ "</div></body></html>",
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
},
];
for (const template of templatesPadrao) {
// Verificar se já existe
const existente = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", template.codigo))
.first();
if (!existente) {
await ctx.db.insert("templatesMensagens", {
...template,
tipo: "sistema",
criadoEm: Date.now(),
});
}
}
return { sucesso: true };
},
});