- Added support for creating and managing group conversations and meeting rooms, allowing users to initiate discussions with multiple participants. - Implemented a modal for creating new conversations, including options for individual, group, and meeting room types. - Enhanced the chat list component to filter and display conversations based on type, improving user navigation. - Introduced admin functionalities for meeting rooms, enabling user management and role assignments within the chat interface. - Updated backend schema and API to accommodate new conversation types and related operations, ensuring robust data handling.
377 lines
13 KiB
Svelte
377 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from "convex-svelte";
|
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
|
import UserAvatar from "./UserAvatar.svelte";
|
|
import UserStatusBadge from "./UserStatusBadge.svelte";
|
|
|
|
interface Props {
|
|
conversaId: Id<"conversas">;
|
|
isAdmin: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { conversaId, isAdmin, onClose }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
const conversas = useQuery(api.chat.listarConversas, {});
|
|
const todosUsuarios = useQuery(api.chat.listarTodosUsuarios, {});
|
|
|
|
let activeTab = $state<"participantes" | "adicionar">("participantes");
|
|
let searchQuery = $state("");
|
|
let loading = $state<string | null>(null);
|
|
let error = $state<string | null>(null);
|
|
|
|
const conversa = $derived(() => {
|
|
if (!conversas?.data) return null;
|
|
return conversas.data.find((c: any) => c._id === conversaId);
|
|
});
|
|
|
|
const participantes = $derived(() => {
|
|
if (!conversa() || !todosUsuarios) return [];
|
|
const participantesIds = conversa()?.participantesInfo || [];
|
|
return participantesIds
|
|
.map((p: any) => {
|
|
const usuario = todosUsuarios.find((u: any) => u._id === p._id);
|
|
return usuario ? { ...usuario, ...p } : null;
|
|
})
|
|
.filter((p: any) => p !== null);
|
|
});
|
|
|
|
const administradoresIds = $derived(() => {
|
|
return conversa()?.administradores || [];
|
|
});
|
|
|
|
const usuariosDisponiveis = $derived(() => {
|
|
if (!todosUsuarios) return [];
|
|
const participantesIds = conversa()?.participantes || [];
|
|
return todosUsuarios.filter((u: any) => !participantesIds.includes(u._id));
|
|
});
|
|
|
|
const usuariosFiltrados = $derived(() => {
|
|
if (!searchQuery.trim()) return usuariosDisponiveis();
|
|
const query = searchQuery.toLowerCase();
|
|
return usuariosDisponiveis().filter((u: any) =>
|
|
u.nome.toLowerCase().includes(query) ||
|
|
u.email.toLowerCase().includes(query) ||
|
|
u.matricula.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
function isParticipanteAdmin(usuarioId: string): boolean {
|
|
return administradoresIds().includes(usuarioId as any);
|
|
}
|
|
|
|
function isCriador(usuarioId: string): boolean {
|
|
return conversa()?.criadoPor === usuarioId;
|
|
}
|
|
|
|
async function removerParticipante(participanteId: string) {
|
|
if (!confirm("Tem certeza que deseja remover este participante?")) return;
|
|
|
|
try {
|
|
loading = `remover-${participanteId}`;
|
|
error = null;
|
|
const resultado = await client.mutation(api.chat.removerParticipanteSala, {
|
|
conversaId,
|
|
participanteId: participanteId as any,
|
|
});
|
|
|
|
if (!resultado.sucesso) {
|
|
error = resultado.erro || "Erro ao remover participante";
|
|
}
|
|
} catch (err: any) {
|
|
error = err.message || "Erro ao remover participante";
|
|
} finally {
|
|
loading = null;
|
|
}
|
|
}
|
|
|
|
async function promoverAdmin(participanteId: string) {
|
|
if (!confirm("Promover este participante a administrador?")) return;
|
|
|
|
try {
|
|
loading = `promover-${participanteId}`;
|
|
error = null;
|
|
const resultado = await client.mutation(api.chat.promoverAdministrador, {
|
|
conversaId,
|
|
participanteId: participanteId as any,
|
|
});
|
|
|
|
if (!resultado.sucesso) {
|
|
error = resultado.erro || "Erro ao promover administrador";
|
|
}
|
|
} catch (err: any) {
|
|
error = err.message || "Erro ao promover administrador";
|
|
} finally {
|
|
loading = null;
|
|
}
|
|
}
|
|
|
|
async function rebaixarAdmin(participanteId: string) {
|
|
if (!confirm("Rebaixar este administrador a participante?")) return;
|
|
|
|
try {
|
|
loading = `rebaixar-${participanteId}`;
|
|
error = null;
|
|
const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
|
|
conversaId,
|
|
participanteId: participanteId as any,
|
|
});
|
|
|
|
if (!resultado.sucesso) {
|
|
error = resultado.erro || "Erro ao rebaixar administrador";
|
|
}
|
|
} catch (err: any) {
|
|
error = err.message || "Erro ao rebaixar administrador";
|
|
} finally {
|
|
loading = null;
|
|
}
|
|
}
|
|
|
|
async function adicionarParticipante(usuarioId: string) {
|
|
try {
|
|
loading = `adicionar-${usuarioId}`;
|
|
error = null;
|
|
const resultado = await client.mutation(api.chat.adicionarParticipanteSala, {
|
|
conversaId,
|
|
participanteId: usuarioId as any,
|
|
});
|
|
|
|
if (!resultado.sucesso) {
|
|
error = resultado.erro || "Erro ao adicionar participante";
|
|
} else {
|
|
searchQuery = "";
|
|
}
|
|
} catch (err: any) {
|
|
error = err.message || "Erro ao adicionar participante";
|
|
} finally {
|
|
loading = null;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50" onclick={onClose}>
|
|
<div
|
|
class="bg-base-100 rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col m-4"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Gerenciar Sala de Reunião</h2>
|
|
<p class="text-sm text-base-content/60">{conversa()?.nome || "Sem nome"}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-circle"
|
|
onclick={onClose}
|
|
aria-label="Fechar"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
{#if isAdmin}
|
|
<div class="tabs tabs-boxed p-4">
|
|
<button
|
|
type="button"
|
|
class={`tab ${activeTab === "participantes" ? "tab-active" : ""}`}
|
|
onclick={() => (activeTab = "participantes")}
|
|
>
|
|
Participantes
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={`tab ${activeTab === "adicionar" ? "tab-active" : ""}`}
|
|
onclick={() => (activeTab = "adicionar")}
|
|
>
|
|
Adicionar Participante
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Error Message -->
|
|
{#if error}
|
|
<div class="mx-6 mt-2 alert alert-error">
|
|
<span>{error}</span>
|
|
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (error = null)}>✕</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 overflow-y-auto px-6">
|
|
{#if activeTab === "participantes"}
|
|
<!-- Lista de Participantes -->
|
|
<div class="space-y-2 py-2">
|
|
{#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)}
|
|
<div
|
|
class="flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
|
|
>
|
|
<!-- Avatar -->
|
|
<div class="relative flex-shrink-0">
|
|
<UserAvatar
|
|
avatar={participante.avatar}
|
|
fotoPerfilUrl={participante.fotoPerfilUrl}
|
|
nome={participante.nome}
|
|
size="sm"
|
|
/>
|
|
<div class="absolute bottom-0 right-0">
|
|
<UserStatusBadge status={participante.statusPresenca} size="sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<p class="font-medium text-base-content truncate">{participante.nome}</p>
|
|
{#if ehAdmin}
|
|
<span class="badge badge-primary badge-sm">Admin</span>
|
|
{/if}
|
|
{#if ehCriador}
|
|
<span class="badge badge-secondary badge-sm">Criador</span>
|
|
{/if}
|
|
</div>
|
|
<p class="text-sm text-base-content/60 truncate">
|
|
{participante.setor || participante.email}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Ações (apenas para admins) -->
|
|
{#if isAdmin && !ehCriador}
|
|
<div class="flex items-center gap-1">
|
|
{#if ehAdmin}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={() => rebaixarAdmin(participante._id)}
|
|
disabled={isLoading}
|
|
title="Rebaixar administrador"
|
|
>
|
|
{#if isLoading && loading?.includes("rebaixar")}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
⬇️
|
|
{/if}
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={() => promoverAdmin(participante._id)}
|
|
disabled={isLoading}
|
|
title="Promover a administrador"
|
|
>
|
|
{#if isLoading && loading?.includes("promover")}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
⬆️
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-error btn-ghost"
|
|
onclick={() => removerParticipante(participante._id)}
|
|
disabled={isLoading}
|
|
title="Remover participante"
|
|
>
|
|
{#if isLoading && loading?.includes("remover")}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
✕
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-center py-8 text-base-content/50">
|
|
Nenhum participante encontrado
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else if activeTab === "adicionar" && isAdmin}
|
|
<!-- Adicionar Participante -->
|
|
<div class="mb-4">
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar usuários..."
|
|
class="input input-bordered w-full"
|
|
bind:value={searchQuery}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
{#if usuariosFiltrados().length > 0}
|
|
{#each usuariosFiltrados() as usuario (usuario._id)}
|
|
{@const isLoading = loading?.includes(usuario._id)}
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-4 py-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors flex items-center gap-3"
|
|
onclick={() => adicionarParticipante(usuario._id)}
|
|
disabled={isLoading}
|
|
>
|
|
<!-- Avatar -->
|
|
<div class="relative flex-shrink-0">
|
|
<UserAvatar
|
|
avatar={usuario.avatar}
|
|
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
|
nome={usuario.nome}
|
|
size="sm"
|
|
/>
|
|
<div class="absolute bottom-0 right-0">
|
|
<UserStatusBadge status={usuario.statusPresenca} size="sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<p class="font-medium text-base-content truncate">{usuario.nome}</p>
|
|
<p class="text-sm text-base-content/60 truncate">
|
|
{usuario.setor || usuario.email}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Botão Adicionar -->
|
|
{#if isLoading}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{:else}
|
|
<span class="text-primary">+</span>
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-center py-8 text-base-content/50">
|
|
{searchQuery.trim() ? "Nenhum usuário encontrado" : "Todos os usuários já são participantes"}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="px-6 py-4 border-t border-base-300">
|
|
<button type="button" class="btn btn-block" onclick={onClose}>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|