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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user