Files
sgse-app/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte
deyvisonwanderley 6cb7414dcc fix: update dependencies and improve chat component structure
- Updated `lucide-svelte` dependency to version 0.552.0 across multiple files for consistency.
- Refactored chat components to enhance structure and readability, including adjustments to the Sidebar, ChatList, and MessageInput components.
- Improved notification handling in chat components to ensure better user experience and responsiveness.
- Added type safety enhancements in various components to ensure better integration with backend data models.
2025-11-05 11:52:01 -03:00

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";
import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from "lucide-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>
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
<div class="modal-box max-w-2xl max-h-[80vh] flex flex-col p-0" 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 flex items-center gap-2">
<Users class="w-5 h-5 text-primary" />
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"
>
<X class="w-5 h-5" />
</button>
</div>
<!-- Tabs -->
{#if isAdmin}
<div class="tabs tabs-boxed p-4">
<button
type="button"
class={`tab flex items-center gap-2 ${activeTab === "participantes" ? "tab-active" : ""}`}
onclick={() => (activeTab = "participantes")}
>
<Users class="w-4 h-4" />
Participantes
</button>
<button
type="button"
class={`tab flex items-center gap-2 ${activeTab === "adicionar" ? "tab-active" : ""}`}
onclick={() => (activeTab = "adicionar")}
>
<UserPlus class="w-4 h-4" />
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)}>
<X class="w-4 h-4" />
</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}
<ArrowDown class="w-4 h-4" />
{/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}
<ArrowUp class="w-4 h-4" />
{/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}
<Trash2 class="w-4 h-4" />
{/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 relative">
<input
type="text"
placeholder="Buscar usuários..."
class="input input-bordered w-full pl-10"
bind:value={searchQuery}
/>
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40" />
</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}
<UserPlus class="w-5 h-5 text-primary" />
{/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>
<form method="dialog" class="modal-backdrop">
<button type="button" onclick={onClose}>fechar</button>
</form>
</dialog>