feat: enhance ErrorModal and chat components with new features and improvements

- Refactored ErrorModal to utilize a dialog element for better accessibility and user experience, including a close button with an icon.
- Updated chat components to improve participant display and message read status, enhancing user engagement and clarity.
- Introduced loading indicators for user and conversation data in SalaReuniaoManager to improve responsiveness during data fetching.
- Enhanced message handling in MessageList to indicate whether messages have been read, providing users with better feedback on message status.
- Improved overall structure and styling across various components for consistency and maintainability.
This commit is contained in:
2025-11-05 14:05:52 -03:00
parent c459297968
commit 6166043735
9 changed files with 394 additions and 104 deletions

View File

@@ -16,7 +16,7 @@
const client = useConvexClient();
const conversas = useQuery(api.chat.listarConversas, {});
const todosUsuarios = useQuery(api.chat.listarTodosUsuarios, {});
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
let activeTab = $state<"participantes" | "adicionar">("participantes");
let searchQuery = $state("");
@@ -28,15 +28,52 @@
return conversas.data.find((c: any) => c._id === conversaId);
});
const todosUsuarios = $derived(() => {
return todosUsuariosQuery?.data || [];
});
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);
try {
const conv = conversa();
const usuarios = todosUsuarios();
if (!conv || !usuarios || usuarios.length === 0) return [];
const participantesInfo = conv.participantesInfo || [];
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) return [];
return participantesInfo
.map((p: any) => {
try {
// p pode ser um objeto com _id ou apenas um ID
const participanteId = p?._id || p;
if (!participanteId) return null;
const usuario = usuarios.find((u: any) => {
try {
return String(u?._id) === String(participanteId);
} catch {
return false;
}
});
if (!usuario) return null;
// Combinar dados do usuário com dados do participante (se p for objeto)
return {
...usuario,
...(typeof p === 'object' && p !== null && p !== undefined ? p : {}),
// Garantir que _id existe e priorizar o do usuario
_id: usuario._id
};
} catch (err) {
console.error("Erro ao processar participante:", err, p);
return null;
}
})
.filter((p: any) => p !== null && p._id);
} catch (err) {
console.error("Erro ao calcular participantes:", err);
return [];
}
});
const administradoresIds = $derived(() => {
@@ -44,27 +81,31 @@
});
const usuariosDisponiveis = $derived(() => {
if (!todosUsuarios) return [];
const usuarios = todosUsuarios();
if (!usuarios || usuarios.length === 0) return [];
const participantesIds = conversa()?.participantes || [];
return todosUsuarios.filter((u: any) => !participantesIds.includes(u._id));
return usuarios.filter((u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id)));
});
const usuariosFiltrados = $derived(() => {
if (!searchQuery.trim()) return usuariosDisponiveis();
const disponiveis = usuariosDisponiveis();
if (!searchQuery.trim()) return disponiveis;
const query = searchQuery.toLowerCase();
return usuariosDisponiveis().filter((u: any) =>
u.nome.toLowerCase().includes(query) ||
u.email.toLowerCase().includes(query) ||
u.matricula.toLowerCase().includes(query)
return disponiveis.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);
const admins = administradoresIds();
return admins.some((adminId: any) => String(adminId) === String(usuarioId));
}
function isCriador(usuarioId: string): boolean {
return conversa()?.criadoPor === usuarioId;
const criadoPor = conversa()?.criadoPor;
return criadoPor ? String(criadoPor) === String(usuarioId) : false;
}
async function removerParticipante(participanteId: string) {
@@ -207,14 +248,27 @@
<!-- Content -->
<div class="flex-1 overflow-y-auto px-6">
{#if activeTab === "participantes"}
{#if !conversas?.data}
<!-- Loading conversas -->
<div class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-2 text-sm text-base-content/60">Carregando conversa...</span>
</div>
{:else if !todosUsuariosQuery?.data}
<!-- Loading usuários -->
<div class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-2 text-sm text-base-content/60">Carregando usuários...</span>
</div>
{:else 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)}
{#each participantes() as participante (String(participante._id))}
{@const participanteId = String(participante._id)}
{@const ehAdmin = isParticipanteAdmin(participanteId)}
{@const ehCriador = isCriador(participanteId)}
{@const isLoading = loading?.includes(participanteId)}
<div
class="flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
>
@@ -222,19 +276,19 @@
<div class="relative flex-shrink-0">
<UserAvatar
avatar={participante.avatar}
fotoPerfilUrl={participante.fotoPerfilUrl}
nome={participante.nome}
fotoPerfilUrl={participante.fotoPerfilUrl || participante.fotoPerfil}
nome={participante.nome || "Usuário"}
size="sm"
/>
<div class="absolute bottom-0 right-0">
<UserStatusBadge status={participante.statusPresenca} size="sm" />
<UserStatusBadge status={participante.statusPresenca || "offline"} 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>
<p class="font-medium text-base-content truncate">{participante.nome || "Usuário"}</p>
{#if ehAdmin}
<span class="badge badge-primary badge-sm">Admin</span>
{/if}
@@ -243,7 +297,7 @@
{/if}
</div>
<p class="text-sm text-base-content/60 truncate">
{participante.setor || participante.email}
{participante.setor || participante.email || ""}
</p>
</div>
@@ -254,7 +308,7 @@
<button
type="button"
class="btn btn-xs btn-ghost"
onclick={() => rebaixarAdmin(participante._id)}
onclick={() => rebaixarAdmin(participanteId)}
disabled={isLoading}
title="Rebaixar administrador"
>
@@ -268,7 +322,7 @@
<button
type="button"
class="btn btn-xs btn-ghost"
onclick={() => promoverAdmin(participante._id)}
onclick={() => promoverAdmin(participanteId)}
disabled={isLoading}
title="Promover a administrador"
>
@@ -282,7 +336,7 @@
<button
type="button"
class="btn btn-xs btn-error btn-ghost"
onclick={() => removerParticipante(participante._id)}
onclick={() => removerParticipante(participanteId)}
disabled={isLoading}
title="Remover participante"
>
@@ -316,32 +370,33 @@
<div class="space-y-2">
{#if usuariosFiltrados().length > 0}
{#each usuariosFiltrados() as usuario (usuario._id)}
{@const isLoading = loading?.includes(usuario._id)}
{#each usuariosFiltrados() as usuario (String(usuario._id))}
{@const usuarioId = String(usuario._id)}
{@const isLoading = loading?.includes(usuarioId)}
<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)}
onclick={() => adicionarParticipante(usuarioId)}
disabled={isLoading}
>
<!-- Avatar -->
<div class="relative flex-shrink-0">
<UserAvatar
avatar={usuario.avatar}
fotoPerfilUrl={usuario.fotoPerfilUrl}
nome={usuario.nome}
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.fotoPerfil}
nome={usuario.nome || "Usuário"}
size="sm"
/>
<div class="absolute bottom-0 right-0">
<UserStatusBadge status={usuario.statusPresenca} size="sm" />
<UserStatusBadge status={usuario.statusPresenca || "offline"} 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="font-medium text-base-content truncate">{usuario.nome || "Usuário"}</p>
<p class="text-sm text-base-content/60 truncate">
{usuario.setor || usuario.email}
{usuario.setor || usuario.email || ""}
</p>
</div>