feat: implement comprehensive chat system with user presence management, notification handling, and avatar integration; enhance UI components for improved user experience
This commit is contained in:
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -15,6 +15,8 @@ import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js
|
||||
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
|
||||
import type * as betterAuth_adapter from "../betterAuth/adapter.js";
|
||||
import type * as betterAuth_auth from "../betterAuth/auth.js";
|
||||
import type * as chat from "../chat.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as dashboard from "../dashboard.js";
|
||||
import type * as documentos from "../documentos.js";
|
||||
import type * as funcionarios from "../funcionarios.js";
|
||||
@@ -52,6 +54,8 @@ declare const fullApi: ApiFromModules<{
|
||||
"betterAuth/_generated/server": typeof betterAuth__generated_server;
|
||||
"betterAuth/adapter": typeof betterAuth_adapter;
|
||||
"betterAuth/auth": typeof betterAuth_auth;
|
||||
chat: typeof chat;
|
||||
crons: typeof crons;
|
||||
dashboard: typeof dashboard;
|
||||
documentos: typeof documentos;
|
||||
funcionarios: typeof funcionarios;
|
||||
|
||||
1146
packages/backend/convex/chat.ts
Normal file
1146
packages/backend/convex/chat.ts
Normal file
File diff suppressed because it is too large
Load Diff
21
packages/backend/convex/crons.ts
Normal file
21
packages/backend/convex/crons.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cronJobs } from "convex/server";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
const crons = cronJobs();
|
||||
|
||||
// Enviar mensagens agendadas a cada minuto
|
||||
crons.interval(
|
||||
"enviar-mensagens-agendadas",
|
||||
{ minutes: 1 },
|
||||
internal.chat.enviarMensagensAgendadas
|
||||
);
|
||||
|
||||
// Limpar indicadores de digitação antigos (>10s) a cada minuto
|
||||
crons.interval(
|
||||
"limpar-indicadores-digitacao",
|
||||
{ minutes: 1 },
|
||||
internal.chat.limparIndicadoresDigitacao
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
@@ -198,11 +198,28 @@ export default defineSchema({
|
||||
ultimoAcesso: v.optional(v.number()),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
|
||||
// Campos de Chat e Perfil
|
||||
avatar: v.optional(v.string()), // "avatar-1" até "avatar-15" ou storageId
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
setor: v.optional(v.string()),
|
||||
statusMensagem: v.optional(v.string()), // max 100 chars
|
||||
statusPresenca: v.optional(v.union(
|
||||
v.literal("online"),
|
||||
v.literal("offline"),
|
||||
v.literal("ausente"),
|
||||
v.literal("externo"),
|
||||
v.literal("em_reuniao")
|
||||
)),
|
||||
ultimaAtividade: v.optional(v.number()), // timestamp
|
||||
notificacoesAtivadas: v.optional(v.boolean()),
|
||||
somNotificacao: v.optional(v.boolean()),
|
||||
})
|
||||
.index("by_matricula", ["matricula"])
|
||||
.index("by_email", ["email"])
|
||||
.index("by_role", ["roleId"])
|
||||
.index("by_ativo", ["ativo"]),
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_status_presenca", ["statusPresenca"]),
|
||||
|
||||
roles: defineTable({
|
||||
nome: v.string(), // "admin", "ti", "usuario_avancado", "usuario"
|
||||
@@ -294,4 +311,82 @@ export default defineSchema({
|
||||
descricao: v.string(),
|
||||
})
|
||||
.index("by_chave", ["chave"]),
|
||||
|
||||
// Sistema de Chat
|
||||
conversas: defineTable({
|
||||
tipo: v.union(v.literal("individual"), v.literal("grupo")),
|
||||
nome: v.optional(v.string()), // nome do grupo
|
||||
avatar: v.optional(v.string()), // avatar do grupo
|
||||
participantes: v.array(v.id("usuarios")), // IDs dos participantes
|
||||
ultimaMensagem: v.optional(v.string()),
|
||||
ultimaMensagemTimestamp: v.optional(v.number()),
|
||||
criadoPor: v.id("usuarios"),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_criado_por", ["criadoPor"])
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_ultima_mensagem", ["ultimaMensagemTimestamp"]),
|
||||
|
||||
mensagens: defineTable({
|
||||
conversaId: v.id("conversas"),
|
||||
remetenteId: v.id("usuarios"),
|
||||
tipo: v.union(
|
||||
v.literal("texto"),
|
||||
v.literal("arquivo"),
|
||||
v.literal("imagem")
|
||||
),
|
||||
conteudo: v.string(), // texto ou nome do arquivo
|
||||
arquivoId: v.optional(v.id("_storage")),
|
||||
arquivoNome: v.optional(v.string()),
|
||||
arquivoTamanho: v.optional(v.number()),
|
||||
arquivoTipo: v.optional(v.string()),
|
||||
reagiuPor: v.optional(v.array(v.object({
|
||||
usuarioId: v.id("usuarios"),
|
||||
emoji: v.string()
|
||||
}))),
|
||||
mencoes: v.optional(v.array(v.id("usuarios"))),
|
||||
agendadaPara: v.optional(v.number()), // timestamp
|
||||
enviadaEm: v.number(),
|
||||
editadaEm: v.optional(v.number()),
|
||||
deletada: v.optional(v.boolean()),
|
||||
})
|
||||
.index("by_conversa", ["conversaId", "enviadaEm"])
|
||||
.index("by_remetente", ["remetenteId"])
|
||||
.index("by_agendamento", ["agendadaPara"]),
|
||||
|
||||
leituras: defineTable({
|
||||
conversaId: v.id("conversas"),
|
||||
usuarioId: v.id("usuarios"),
|
||||
ultimaMensagemLida: v.id("mensagens"),
|
||||
lidaEm: v.number(),
|
||||
})
|
||||
.index("by_conversa_usuario", ["conversaId", "usuarioId"])
|
||||
.index("by_usuario", ["usuarioId"]),
|
||||
|
||||
notificacoes: defineTable({
|
||||
usuarioId: v.id("usuarios"),
|
||||
tipo: v.union(
|
||||
v.literal("nova_mensagem"),
|
||||
v.literal("mencao"),
|
||||
v.literal("grupo_criado"),
|
||||
v.literal("adicionado_grupo")
|
||||
),
|
||||
conversaId: v.optional(v.id("conversas")),
|
||||
mensagemId: v.optional(v.id("mensagens")),
|
||||
remetenteId: v.optional(v.id("usuarios")),
|
||||
titulo: v.string(),
|
||||
descricao: v.string(),
|
||||
lida: v.boolean(),
|
||||
criadaEm: v.number(),
|
||||
})
|
||||
.index("by_usuario", ["usuarioId", "lida", "criadaEm"])
|
||||
.index("by_usuario_lida", ["usuarioId", "lida"]),
|
||||
|
||||
digitando: defineTable({
|
||||
conversaId: v.id("conversas"),
|
||||
usuarioId: v.id("usuarios"),
|
||||
iniciouEm: v.number(),
|
||||
})
|
||||
.index("by_conversa", ["conversaId", "iniciouEm"])
|
||||
.index("by_usuario", ["usuarioId"]),
|
||||
});
|
||||
|
||||
@@ -318,3 +318,254 @@ export const alterarRole = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Atualizar perfil do usuário (foto, avatar, setor, status, preferências)
|
||||
*/
|
||||
export const atualizarPerfil = mutation({
|
||||
args: {
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
setor: v.optional(v.string()),
|
||||
statusMensagem: v.optional(v.string()),
|
||||
statusPresenca: v.optional(
|
||||
v.union(
|
||||
v.literal("online"),
|
||||
v.literal("offline"),
|
||||
v.literal("ausente"),
|
||||
v.literal("externo"),
|
||||
v.literal("em_reuniao")
|
||||
)
|
||||
),
|
||||
notificacoesAtivadas: v.optional(v.boolean()),
|
||||
somNotificacao: v.optional(v.boolean()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// TENTAR BETTER AUTH PRIMEIRO
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
|
||||
let usuarioAtual = null;
|
||||
|
||||
if (identity && identity.email) {
|
||||
// Buscar por email (Better Auth)
|
||||
usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
}
|
||||
|
||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||
if (!usuarioAtual) {
|
||||
const sessaoAtiva = await ctx.db
|
||||
.query("sessoes")
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.order("desc")
|
||||
.first();
|
||||
|
||||
if (sessaoAtiva) {
|
||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||
|
||||
// Validar statusMensagem (max 100 chars)
|
||||
if (args.statusMensagem && args.statusMensagem.length > 100) {
|
||||
throw new Error("Mensagem de status deve ter no máximo 100 caracteres");
|
||||
}
|
||||
|
||||
// Atualizar apenas os campos fornecidos
|
||||
const updates: any = { atualizadoEm: Date.now() };
|
||||
|
||||
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||
if (args.setor !== undefined) updates.setor = args.setor;
|
||||
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
|
||||
if (args.statusPresenca !== undefined) {
|
||||
updates.statusPresenca = args.statusPresenca;
|
||||
updates.ultimaAtividade = Date.now();
|
||||
}
|
||||
if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas;
|
||||
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
|
||||
|
||||
await ctx.db.patch(usuarioAtual._id, updates);
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter perfil do usuário atual
|
||||
*/
|
||||
export const obterPerfil = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
console.log("=== DEBUG obterPerfil ===");
|
||||
|
||||
// TENTAR BETTER AUTH PRIMEIRO
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
console.log("Identity:", identity ? "encontrado" : "null");
|
||||
|
||||
let usuarioAtual = null;
|
||||
|
||||
if (identity && identity.email) {
|
||||
console.log("Tentando buscar por email:", identity.email);
|
||||
// Buscar por email (Better Auth)
|
||||
usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
|
||||
console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO");
|
||||
}
|
||||
|
||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||
if (!usuarioAtual) {
|
||||
console.log("Buscando por sessão ativa...");
|
||||
const sessaoAtiva = await ctx.db
|
||||
.query("sessoes")
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.order("desc")
|
||||
.first();
|
||||
|
||||
console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO");
|
||||
|
||||
if (sessaoAtiva) {
|
||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||
console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO");
|
||||
}
|
||||
}
|
||||
|
||||
if (!usuarioAtual) {
|
||||
console.log("❌ Nenhum usuário encontrado");
|
||||
// Listar todos os usuários para debug
|
||||
const todosUsuarios = await ctx.db.query("usuarios").collect();
|
||||
console.log("Total de usuários no banco:", todosUsuarios.length);
|
||||
console.log("Emails cadastrados:", todosUsuarios.map(u => u.email));
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("✅ Usuário encontrado:", usuarioAtual.nome);
|
||||
|
||||
// Buscar fotoPerfil URL se existir
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuarioAtual.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
|
||||
}
|
||||
|
||||
return {
|
||||
_id: usuarioAtual._id,
|
||||
nome: usuarioAtual.nome,
|
||||
email: usuarioAtual.email,
|
||||
matricula: usuarioAtual.matricula,
|
||||
avatar: usuarioAtual.avatar,
|
||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
setor: usuarioAtual.setor,
|
||||
statusMensagem: usuarioAtual.statusMensagem,
|
||||
statusPresenca: usuarioAtual.statusPresenca,
|
||||
notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true,
|
||||
somNotificacao: usuarioAtual.somNotificacao ?? true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar todos usuários para o chat (com avatar, foto e status)
|
||||
*/
|
||||
export const listarParaChat = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
fotoPerfilUrl: v.union(v.string(), v.null()),
|
||||
statusPresenca: v.optional(
|
||||
v.union(
|
||||
v.literal("online"),
|
||||
v.literal("offline"),
|
||||
v.literal("ausente"),
|
||||
v.literal("externo"),
|
||||
v.literal("em_reuniao")
|
||||
)
|
||||
),
|
||||
statusMensagem: v.optional(v.string()),
|
||||
ultimaAtividade: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
// Buscar todos os usuários ativos
|
||||
const usuarios = await ctx.db
|
||||
.query("usuarios")
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
// Buscar foto de perfil URL para cada usuário
|
||||
const usuariosComFoto = await Promise.all(
|
||||
usuarios.map(async (usuario) => {
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuario.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
||||
}
|
||||
|
||||
return {
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula,
|
||||
avatar: usuario.avatar,
|
||||
fotoPerfil: usuario.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
statusPresenca: usuario.statusPresenca || "offline",
|
||||
statusMensagem: usuario.statusMensagem,
|
||||
ultimaAtividade: usuario.ultimaAtividade,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return usuariosComFoto;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Gera URL para upload de foto de perfil
|
||||
*/
|
||||
export const uploadFotoPerfil = mutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// TENTAR BETTER AUTH PRIMEIRO
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
|
||||
let usuarioAtual = null;
|
||||
|
||||
if (identity && identity.email) {
|
||||
// Buscar por email (Better Auth)
|
||||
usuarioAtual = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.first();
|
||||
}
|
||||
|
||||
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
|
||||
if (!usuarioAtual) {
|
||||
const sessaoAtiva = await ctx.db
|
||||
.query("sessoes")
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.order("desc")
|
||||
.first();
|
||||
|
||||
if (sessaoAtiva) {
|
||||
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!usuarioAtual) throw new Error("Usuário não autenticado");
|
||||
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user