-
-
{usuario.nome}
+
{usuario.nome}
- {usuario.setor || usuario.email}
+ {usuario.setor || usuario.email || usuario.matricula || "Sem informações"}
-
- {#if activeTab === "grupo"}
-
+
+ {#if activeTab === "grupo" || activeTab === "sala_reuniao"}
+
+
+
+ {:else}
+
+
{/if}
{/each}
- {:else if !usuarios}
-
-
+ {:else if !usuarios?.data}
+
+
+
Carregando usuários...
{:else}
-
- Nenhum usuário encontrado
+
+
+
+ {searchQuery.trim() ? "Nenhum usuário encontrado" : "Nenhum usuário disponível"}
+
+ {#if searchQuery.trim()}
+
Tente buscar por nome, email ou matrícula
+ {/if}
{/if}
-
+
{#if activeTab === "grupo"}
-
+
+ {#if selectedUsers.length < 2 && activeTab === "grupo"}
+
Selecione pelo menos 2 participantes
+ {/if}
+
+ {:else if activeTab === "sala_reuniao"}
+
+
+ {#if selectedUsers.length < 1 && activeTab === "sala_reuniao"}
+
Selecione pelo menos 1 participante
+ {/if}
{/if}
diff --git a/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
new file mode 100644
index 0000000..0413796
--- /dev/null
+++ b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
@@ -0,0 +1,376 @@
+
+
+
+
e.stopPropagation()}
+ >
+
+
+
+
Gerenciar Sala de Reunião
+
{conversa()?.nome || "Sem nome"}
+
+
+
+
+
+ {#if isAdmin}
+
+
+
+
+ {/if}
+
+
+ {#if error}
+
+ {error}
+
+
+ {/if}
+
+
+
+ {#if activeTab === "participantes"}
+
+
+ {#if participantes().length > 0}
+ {#each participantes() as participante (participante._id)}
+ {@const ehAdmin = isParticipanteAdmin(participante._id)}
+ {@const ehCriador = isCriador(participante._id)}
+ {@const isLoading = loading?.includes(participante._id)}
+
+
+
+
+
+
+
+
{participante.nome}
+ {#if ehAdmin}
+
Admin
+ {/if}
+ {#if ehCriador}
+
Criador
+ {/if}
+
+
+ {participante.setor || participante.email}
+
+
+
+
+ {#if isAdmin && !ehCriador}
+
+ {#if ehAdmin}
+
+ {:else}
+
+ {/if}
+
+
+ {/if}
+
+ {/each}
+ {:else}
+
+ Nenhum participante encontrado
+
+ {/if}
+
+ {:else if activeTab === "adicionar" && isAdmin}
+
+
+
+
+
+
+ {#if usuariosFiltrados().length > 0}
+ {#each usuariosFiltrados() as usuario (usuario._id)}
+ {@const isLoading = loading?.includes(usuario._id)}
+
+ {/each}
+ {:else}
+
+ {searchQuery.trim() ? "Nenhum usuário encontrado" : "Todos os usuários já são participantes"}
+
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
index 7ddaa09..ff7e182 100644
--- a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
+++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte
@@ -727,12 +727,12 @@
"enviando",
"Criando/buscando conversa...",
);
- const conversaResult = await client.mutation(
+ const conversaId = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as Id<"usuarios"> },
);
- if (conversaResult.conversaId) {
+ if (conversaId) {
const mensagem = usarTemplate
? templateSelecionado?.corpo || ""
: mensagemPersonalizada;
@@ -748,7 +748,7 @@
resultadoChat = await client.mutation(
api.chat.agendarMensagem,
{
- conversaId: conversaResult.conversaId,
+ conversaId: conversaId,
conteudo: mensagem,
agendadaPara: agendadaPara,
},
@@ -773,7 +773,7 @@
"Enviando mensagem...",
);
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
- conversaId: conversaResult.conversaId,
+ conversaId: conversaId,
conteudo: mensagem,
tipo: "texto",
permitirNotificacaoParaSiMesmo: true,
@@ -982,12 +982,12 @@
"enviando",
"Processando...",
);
- const conversaResult = await client.mutation(
+ const conversaId = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{ outroUsuarioId: destinatario._id as Id<"usuarios"> },
);
- if (conversaResult.conversaId) {
+ if (conversaId) {
// Para templates, usar corpo direto (o backend já faz substituição via email)
// Para mensagem personalizada, usar diretamente
const mensagem = usarTemplate
@@ -996,7 +996,7 @@
if (agendadaPara) {
await client.mutation(api.chat.agendarMensagem, {
- conversaId: conversaResult.conversaId,
+ conversaId: conversaId,
conteudo: mensagem,
agendadaPara: agendadaPara,
});
@@ -1013,7 +1013,7 @@
);
} else {
await client.mutation(api.chat.enviarMensagem, {
- conversaId: conversaResult.conversaId,
+ conversaId: conversaId,
conteudo: mensagem,
tipo: "texto",
permitirNotificacaoParaSiMesmo: true,
diff --git a/packages/backend/convex/chat.ts b/packages/backend/convex/chat.ts
index 5494989..b36b449 100644
--- a/packages/backend/convex/chat.ts
+++ b/packages/backend/convex/chat.ts
@@ -48,6 +48,30 @@ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
return usuarioAtual;
}
+/**
+ * Helper function para verificar se usuário é administrador de uma sala de reunião
+ */
+async function verificarPermissaoAdmin(
+ ctx: QueryCtx | MutationCtx,
+ conversaId: Id<"conversas">,
+ usuarioId: Id<"usuarios">
+): Promise
{
+ const conversa = await ctx.db.get(conversaId);
+ if (!conversa) return false;
+
+ // Verificar se é sala de reunião
+ if (conversa.tipo !== "sala_reuniao") return false;
+
+ // Verificar se tem array de administradores
+ if (!conversa.administradores || conversa.administradores.length === 0) {
+ // Se não tem administradores definidos, o criador é admin por padrão
+ return conversa.criadoPor === usuarioId;
+ }
+
+ // Verificar se está na lista de administradores
+ return conversa.administradores.includes(usuarioId);
+}
+
// ========== MUTATIONS ==========
/**
@@ -55,7 +79,7 @@ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
*/
export const criarConversa = mutation({
args: {
- tipo: v.union(v.literal("individual"), v.literal("grupo")),
+ tipo: v.union(v.literal("individual"), v.literal("grupo"), v.literal("sala_reuniao")),
participantes: v.array(v.id("usuarios")),
nome: v.optional(v.string()),
avatar: v.optional(v.string()),
@@ -86,27 +110,38 @@ export const criarConversa = mutation({
}
}
- // Criar nova conversa
- const conversaId = await ctx.db.insert("conversas", {
+ // Preparar dados da conversa
+ const dadosConversa: any = {
tipo: args.tipo,
nome: args.nome,
avatar: args.avatar,
participantes: args.participantes,
criadoPor: usuarioAtual._id,
criadoEm: Date.now(),
- });
+ };
+
+ // Se for sala de reunião, adicionar administradores (criador sempre é admin)
+ if (args.tipo === "sala_reuniao") {
+ dadosConversa.administradores = [usuarioAtual._id];
+ }
+
+ // Criar nova conversa
+ const conversaId = await ctx.db.insert("conversas", dadosConversa);
// Criar notificações para outros participantes
- if (args.tipo === "grupo") {
+ if (args.tipo === "grupo" || args.tipo === "sala_reuniao") {
+ const tipoNotificacao = args.tipo === "sala_reuniao" ? "adicionado_grupo" : "adicionado_grupo";
+ const tipoTexto = args.tipo === "sala_reuniao" ? "sala de reunião" : "grupo";
+
for (const participanteId of args.participantes) {
if (participanteId !== usuarioAtual._id) {
await ctx.db.insert("notificacoes", {
usuarioId: participanteId,
- tipo: "adicionado_grupo",
+ tipo: tipoNotificacao,
conversaId,
remetenteId: usuarioAtual._id,
- titulo: "Adicionado a grupo",
- descricao: `Você foi adicionado ao grupo "${
+ titulo: args.tipo === "sala_reuniao" ? "Adicionado a sala de reunião" : "Adicionado a grupo",
+ descricao: `Você foi adicionado à ${tipoTexto} "${
args.nome || "Sem nome"
}" por ${usuarioAtual.nome}`,
lida: false,
@@ -120,6 +155,69 @@ export const criarConversa = mutation({
},
});
+/**
+ * Cria uma nova sala de reunião (wrapper específico para facilitar uso)
+ */
+export const criarSalaReuniao = mutation({
+ args: {
+ nome: v.string(),
+ participantes: v.array(v.id("usuarios")),
+ avatar: v.optional(v.string()),
+ },
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) throw new Error("Não autenticado");
+
+ // Validar nome
+ if (!args.nome || args.nome.trim().length === 0) {
+ throw new Error("O nome da sala de reunião é obrigatório");
+ }
+
+ // Validar participantes
+ const participantesUnicos = [...new Set(args.participantes)];
+ if (!participantesUnicos.includes(usuarioAtual._id)) {
+ participantesUnicos.push(usuarioAtual._id);
+ }
+
+ // Preparar dados da conversa
+ const dadosConversa: any = {
+ tipo: "sala_reuniao" as const,
+ nome: args.nome.trim(),
+ avatar: args.avatar,
+ participantes: participantesUnicos,
+ criadoPor: usuarioAtual._id,
+ criadoEm: Date.now(),
+ administradores: [usuarioAtual._id], // Criador sempre é admin
+ };
+
+ // Criar nova conversa
+ const conversaId = await ctx.db.insert("conversas", dadosConversa);
+
+ // Criar notificações para outros participantes
+ const tipoNotificacao = "adicionado_grupo";
+ const tipoTexto = "sala de reunião";
+
+ for (const participanteId of participantesUnicos) {
+ if (participanteId !== usuarioAtual._id) {
+ await ctx.db.insert("notificacoes", {
+ usuarioId: participanteId,
+ tipo: tipoNotificacao,
+ conversaId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Adicionado a sala de reunião",
+ descricao: `Você foi adicionado à ${tipoTexto} "${
+ args.nome || "Sem nome"
+ }" por ${usuarioAtual.nome}`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+ }
+ }
+
+ return conversaId;
+ },
+});
+
/**
* Cria ou busca uma conversa individual com outro usuário
*/
@@ -840,7 +938,10 @@ export const deletarMensagem = mutation({
throw new Error("Mensagem inválida");
}
- if (mensagem.remetenteId !== usuarioAtual._id) {
+ // Verificar se é admin de sala de reunião ou se é o próprio remetente
+ const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id);
+
+ if (mensagem.remetenteId !== usuarioAtual._id && !isAdmin) {
throw new Error("Você só pode deletar suas próprias mensagens");
}
@@ -853,8 +954,370 @@ export const deletarMensagem = mutation({
},
});
+/**
+ * Deleta uma mensagem como administrador (com notificação ao remetente)
+ */
+export const deletarMensagemComoAdmin = mutation({
+ args: {
+ mensagemId: v.id("mensagens"),
+ },
+ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) {
+ return { sucesso: false, erro: "Não autenticado" };
+ }
+
+ const mensagem = await ctx.db.get(args.mensagemId);
+ if (!mensagem) {
+ return { sucesso: false, erro: "Mensagem não encontrada" };
+ }
+
+ // SEGURANÇA: Verificar se a mensagem pertence a uma conversa onde o usuário é participante
+ const conversa = await ctx.db.get(mensagem.conversaId);
+ if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) {
+ return { sucesso: false, erro: "Você não tem acesso a esta mensagem" };
+ }
+
+ // SEGURANÇA: Verificar se o remetente da mensagem é participante da conversa
+ if (!conversa.participantes.includes(mensagem.remetenteId)) {
+ return { sucesso: false, erro: "Mensagem inválida" };
+ }
+
+ // Verificar se usuário é administrador da sala
+ const isAdmin = await verificarPermissaoAdmin(ctx, mensagem.conversaId, usuarioAtual._id);
+ if (!isAdmin) {
+ return { sucesso: false, erro: "Apenas administradores podem deletar mensagens de outros usuários" };
+ }
+
+ // Não permitir deletar mensagem já deletada
+ if (mensagem.deletada) {
+ return { sucesso: false, erro: "Mensagem já foi deletada" };
+ }
+
+ // Deletar mensagem
+ await ctx.db.patch(args.mensagemId, {
+ deletada: true,
+ conteudo: "Mensagem deletada por administrador",
+ });
+
+ // Criar notificação para o remetente original (se não for o próprio admin)
+ if (mensagem.remetenteId !== usuarioAtual._id) {
+ const remetente = await ctx.db.get(mensagem.remetenteId);
+ if (remetente) {
+ await ctx.db.insert("notificacoes", {
+ usuarioId: mensagem.remetenteId,
+ tipo: "nova_mensagem",
+ conversaId: mensagem.conversaId,
+ mensagemId: args.mensagemId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Mensagem deletada",
+ descricao: `Sua mensagem foi deletada por um administrador da sala "${conversa.nome || "Sem nome"}"`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+ }
+ }
+
+ return { sucesso: true };
+ },
+});
+
+/**
+ * Adiciona um participante à sala de reunião (apenas administradores)
+ */
+export const adicionarParticipanteSala = mutation({
+ args: {
+ conversaId: v.id("conversas"),
+ participanteId: v.id("usuarios"),
+ },
+ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) {
+ return { sucesso: false, erro: "Não autenticado" };
+ }
+
+ const conversa = await ctx.db.get(args.conversaId);
+ if (!conversa) {
+ return { sucesso: false, erro: "Sala de reunião não encontrada" };
+ }
+
+ // Verificar se é sala de reunião
+ if (conversa.tipo !== "sala_reuniao") {
+ return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" };
+ }
+
+ // Verificar se usuário é administrador
+ const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
+ if (!isAdmin) {
+ return { sucesso: false, erro: "Apenas administradores podem adicionar participantes" };
+ }
+
+ // Verificar se participante já está na sala
+ if (conversa.participantes.includes(args.participanteId)) {
+ return { sucesso: false, erro: "Usuário já é participante desta sala" };
+ }
+
+ // Verificar se participante existe
+ const participante = await ctx.db.get(args.participanteId);
+ if (!participante) {
+ return { sucesso: false, erro: "Usuário não encontrado" };
+ }
+
+ // Adicionar participante
+ const novosParticipantes = [...conversa.participantes, args.participanteId];
+ await ctx.db.patch(args.conversaId, {
+ participantes: novosParticipantes,
+ });
+
+ // Criar notificação para o novo participante
+ await ctx.db.insert("notificacoes", {
+ usuarioId: args.participanteId,
+ tipo: "adicionado_grupo",
+ conversaId: args.conversaId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Adicionado a sala de reunião",
+ descricao: `Você foi adicionado à sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+
+ return { sucesso: true };
+ },
+});
+
+/**
+ * Remove um participante da sala de reunião (apenas administradores, não pode remover outros admins)
+ */
+export const removerParticipanteSala = mutation({
+ args: {
+ conversaId: v.id("conversas"),
+ participanteId: v.id("usuarios"),
+ },
+ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) {
+ return { sucesso: false, erro: "Não autenticado" };
+ }
+
+ const conversa = await ctx.db.get(args.conversaId);
+ if (!conversa) {
+ return { sucesso: false, erro: "Sala de reunião não encontrada" };
+ }
+
+ // Verificar se é sala de reunião
+ if (conversa.tipo !== "sala_reuniao") {
+ return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" };
+ }
+
+ // Verificar se usuário é administrador
+ const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
+ if (!isAdmin) {
+ return { sucesso: false, erro: "Apenas administradores podem remover participantes" };
+ }
+
+ // Verificar se participante está na sala
+ if (!conversa.participantes.includes(args.participanteId)) {
+ return { sucesso: false, erro: "Usuário não é participante desta sala" };
+ }
+
+ // Verificar se está tentando remover outro administrador
+ const isParticipanteAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId);
+ if (isParticipanteAdmin) {
+ return { sucesso: false, erro: "Não é possível remover outros administradores" };
+ }
+
+ // Remover participante
+ const novosParticipantes = conversa.participantes.filter((p) => p !== args.participanteId);
+ await ctx.db.patch(args.conversaId, {
+ participantes: novosParticipantes,
+ });
+
+ // Criar notificação para o participante removido
+ const participanteRemovido = await ctx.db.get(args.participanteId);
+ if (participanteRemovido) {
+ await ctx.db.insert("notificacoes", {
+ usuarioId: args.participanteId,
+ tipo: "nova_mensagem",
+ conversaId: args.conversaId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Removido da sala de reunião",
+ descricao: `Você foi removido da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+ }
+
+ return { sucesso: true };
+ },
+});
+
+/**
+ * Promove um participante a administrador (apenas administradores)
+ */
+export const promoverAdministrador = mutation({
+ args: {
+ conversaId: v.id("conversas"),
+ participanteId: v.id("usuarios"),
+ },
+ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) {
+ return { sucesso: false, erro: "Não autenticado" };
+ }
+
+ const conversa = await ctx.db.get(args.conversaId);
+ if (!conversa) {
+ return { sucesso: false, erro: "Sala de reunião não encontrada" };
+ }
+
+ // Verificar se é sala de reunião
+ if (conversa.tipo !== "sala_reuniao") {
+ return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" };
+ }
+
+ // Verificar se usuário é administrador
+ const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
+ if (!isAdmin) {
+ return { sucesso: false, erro: "Apenas administradores podem promover outros administradores" };
+ }
+
+ // Verificar se participante está na sala
+ if (!conversa.participantes.includes(args.participanteId)) {
+ return { sucesso: false, erro: "Usuário não é participante desta sala" };
+ }
+
+ // Verificar se já é administrador
+ const jaEhAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId);
+ if (jaEhAdmin) {
+ return { sucesso: false, erro: "Usuário já é administrador desta sala" };
+ }
+
+ // Obter lista atual de administradores ou criar nova
+ const administradoresAtuais = conversa.administradores || [];
+
+ // Se não está na lista, adicionar
+ if (!administradoresAtuais.includes(args.participanteId)) {
+ const novosAdministradores = [...administradoresAtuais, args.participanteId];
+ await ctx.db.patch(args.conversaId, {
+ administradores: novosAdministradores,
+ });
+
+ // Criar notificação para o novo administrador
+ const novoAdmin = await ctx.db.get(args.participanteId);
+ if (novoAdmin) {
+ await ctx.db.insert("notificacoes", {
+ usuarioId: args.participanteId,
+ tipo: "nova_mensagem",
+ conversaId: args.conversaId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Promovido a administrador",
+ descricao: `Você foi promovido a administrador da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+ }
+ }
+
+ return { sucesso: true };
+ },
+});
+
+/**
+ * Rebaixa um administrador a participante (apenas administradores, não pode rebaixar a si mesmo)
+ */
+export const rebaixarAdministrador = mutation({
+ args: {
+ conversaId: v.id("conversas"),
+ participanteId: v.id("usuarios"),
+ },
+ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) {
+ return { sucesso: false, erro: "Não autenticado" };
+ }
+
+ const conversa = await ctx.db.get(args.conversaId);
+ if (!conversa) {
+ return { sucesso: false, erro: "Sala de reunião não encontrada" };
+ }
+
+ // Verificar se é sala de reunião
+ if (conversa.tipo !== "sala_reuniao") {
+ return { sucesso: false, erro: "Esta funcionalidade é apenas para salas de reunião" };
+ }
+
+ // Verificar se usuário é administrador
+ const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
+ if (!isAdmin) {
+ return { sucesso: false, erro: "Apenas administradores podem rebaixar outros administradores" };
+ }
+
+ // Não permitir rebaixar a si mesmo
+ if (args.participanteId === usuarioAtual._id) {
+ return { sucesso: false, erro: "Você não pode rebaixar a si mesmo" };
+ }
+
+ // Verificar se é administrador
+ const isParticipanteAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, args.participanteId);
+ if (!isParticipanteAdmin) {
+ return { sucesso: false, erro: "Usuário não é administrador desta sala" };
+ }
+
+ // Não permitir rebaixar o criador da sala
+ if (conversa.criadoPor === args.participanteId) {
+ return { sucesso: false, erro: "Não é possível rebaixar o criador da sala" };
+ }
+
+ // Remover da lista de administradores
+ const administradoresAtuais = conversa.administradores || [];
+ const novosAdministradores = administradoresAtuais.filter((adminId) => adminId !== args.participanteId);
+
+ await ctx.db.patch(args.conversaId, {
+ administradores: novosAdministradores.length > 0 ? novosAdministradores : undefined,
+ });
+
+ // Criar notificação para o administrador rebaixado
+ const adminRebaixado = await ctx.db.get(args.participanteId);
+ if (adminRebaixado) {
+ await ctx.db.insert("notificacoes", {
+ usuarioId: args.participanteId,
+ tipo: "nova_mensagem",
+ conversaId: args.conversaId,
+ remetenteId: usuarioAtual._id,
+ titulo: "Rebaixado de administrador",
+ descricao: `Você foi rebaixado de administrador da sala de reunião "${conversa.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
+ lida: false,
+ criadaEm: Date.now(),
+ });
+ }
+
+ return { sucesso: true };
+ },
+});
+
// ========== QUERIES ==========
+/**
+ * Verifica se o usuário atual é administrador de uma sala de reunião
+ */
+export const verificarSeEhAdmin = query({
+ args: {
+ conversaId: v.id("conversas"),
+ },
+ returns: v.boolean(),
+ handler: async (ctx, args) => {
+ const usuarioAtual = await getUsuarioAutenticado(ctx);
+ if (!usuarioAtual) return false;
+
+ return await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
+ },
+});
+
/**
* Lista todas as conversas do usuário logado
* SEGURANÇA: Usuário só vê conversas onde é participante
@@ -947,11 +1410,36 @@ export const listarConversas = query({
).length;
}
+ // Verificar se usuário é administrador (apenas para salas de reunião)
+ const isAdmin = conversa.tipo === "sala_reuniao"
+ ? await verificarPermissaoAdmin(ctx, conversa._id, usuarioAtual._id)
+ : false;
+
+ // Enriquecer participantes com fotoPerfilUrl (para grupos e salas)
+ const participantesInfo = await Promise.all(
+ participantes
+ .filter((p) => p !== null)
+ .map(async (participante) => {
+ if (!participante) return null;
+
+ let fotoPerfilUrl = null;
+ if (participante.fotoPerfil) {
+ fotoPerfilUrl = await ctx.storage.getUrl(participante.fotoPerfil);
+ }
+
+ return {
+ ...participante,
+ fotoPerfilUrl,
+ };
+ })
+ );
+
return {
...conversa,
outroUsuario,
- participantesInfo: participantes.filter((p) => p !== null),
+ participantesInfo: participantesInfo.filter((p) => p !== null),
naoLidas,
+ isAdmin, // Adicionar flag de admin
};
})
);
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts
index f9b75e2..28bfdae 100644
--- a/packages/backend/convex/schema.ts
+++ b/packages/backend/convex/schema.ts
@@ -619,10 +619,11 @@ export default defineSchema({
// 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
+ tipo: v.union(v.literal("individual"), v.literal("grupo"), v.literal("sala_reuniao")),
+ nome: v.optional(v.string()), // nome do grupo/sala
+ avatar: v.optional(v.string()), // avatar do grupo/sala
participantes: v.array(v.id("usuarios")), // IDs dos participantes
+ administradores: v.optional(v.array(v.id("usuarios"))), // IDs dos administradores (apenas para sala_reuniao)
ultimaMensagem: v.optional(v.string()),
ultimaMensagemTimestamp: v.optional(v.number()),
criadoPor: v.id("usuarios"),