- Updated various Svelte components to improve code readability and maintainability. - Standardized button classes across components for a consistent user interface. - Enhanced error handling and user feedback in modals and forms. - Cleaned up unnecessary imports and optimized component structure for better performance.
436 lines
14 KiB
Svelte
436 lines
14 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 todosUsuariosQuery = 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 todosUsuarios = $derived(() => {
|
|
return todosUsuariosQuery?.data || [];
|
|
});
|
|
|
|
const participantes = $derived(() => {
|
|
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(() => {
|
|
return conversa()?.administradores || [];
|
|
});
|
|
|
|
const usuariosDisponiveis = $derived(() => {
|
|
const usuarios = todosUsuarios();
|
|
if (!usuarios || usuarios.length === 0) return [];
|
|
const participantesIds = conversa()?.participantes || [];
|
|
return usuarios.filter(
|
|
(u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id))
|
|
);
|
|
});
|
|
|
|
const usuariosFiltrados = $derived(() => {
|
|
const disponiveis = usuariosDisponiveis();
|
|
if (!searchQuery.trim()) return disponiveis;
|
|
const query = searchQuery.toLowerCase();
|
|
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 {
|
|
const admins = administradoresIds();
|
|
return admins.some((adminId: any) => String(adminId) === String(usuarioId));
|
|
}
|
|
|
|
function isCriador(usuarioId: string): boolean {
|
|
const criadoPor = conversa()?.criadoPor;
|
|
return criadoPor ? String(criadoPor) === String(usuarioId) : false;
|
|
}
|
|
|
|
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 flex max-h-[80vh] max-w-2xl flex-col p-0"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<!-- Header -->
|
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
|
<div>
|
|
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
|
<Users class="text-primary h-5 w-5" />
|
|
Gerenciar Sala de Reunião
|
|
</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
{conversa()?.nome || 'Sem nome'}
|
|
</p>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
|
<X class="h-5 w-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="h-4 w-4" />
|
|
Participantes
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={`tab flex items-center gap-2 ${activeTab === 'adicionar' ? 'tab-active' : ''}`}
|
|
onclick={() => (activeTab = 'adicionar')}
|
|
>
|
|
<UserPlus class="h-4 w-4" />
|
|
Adicionar Participante
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Error Message -->
|
|
{#if error}
|
|
<div class="alert alert-error mx-6 mt-2">
|
|
<span>{error}</span>
|
|
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (error = null)}>
|
|
<X class="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 overflow-y-auto px-6">
|
|
{#if !conversas?.data}
|
|
<!-- Loading conversas -->
|
|
<div class="flex items-center justify-center py-8">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
<span class="text-base-content/60 ml-2 text-sm">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="text-base-content/60 ml-2 text-sm">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 (String(participante._id))}
|
|
{@const participanteId = String(participante._id)}
|
|
{@const ehAdmin = isParticipanteAdmin(participanteId)}
|
|
{@const ehCriador = isCriador(participanteId)}
|
|
{@const isLoading = loading?.includes(participanteId)}
|
|
<div
|
|
class="border-base-300 hover:bg-base-200 flex items-center gap-3 rounded-lg border p-3 transition-colors"
|
|
>
|
|
<!-- Avatar -->
|
|
<div class="relative shrink-0">
|
|
<UserAvatar
|
|
avatar={participante.avatar}
|
|
fotoPerfilUrl={participante.fotoPerfilUrl || participante.avatar}
|
|
nome={participante.nome || 'Usuário'}
|
|
size="sm"
|
|
/>
|
|
<div class="absolute right-0 bottom-0">
|
|
<UserStatusBadge status={participante.statusPresenca || 'offline'} size="sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<p class="text-base-content truncate font-medium">
|
|
{participante.nome || 'Usuário'}
|
|
</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-base-content/60 truncate text-sm">
|
|
{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(participanteId)}
|
|
disabled={isLoading}
|
|
title="Rebaixar administrador"
|
|
>
|
|
{#if isLoading && loading?.includes('rebaixar')}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
<ArrowDown class="h-4 w-4" />
|
|
{/if}
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={() => promoverAdmin(participanteId)}
|
|
disabled={isLoading}
|
|
title="Promover a administrador"
|
|
>
|
|
{#if isLoading && loading?.includes('promover')}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
<ArrowUp class="h-4 w-4" />
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-error btn-ghost"
|
|
onclick={() => removerParticipante(participanteId)}
|
|
disabled={isLoading}
|
|
title="Remover participante"
|
|
>
|
|
{#if isLoading && loading?.includes('remover')}
|
|
<span class="loading loading-spinner loading-xs"></span>
|
|
{:else}
|
|
<Trash2 class="h-4 w-4" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-base-content/50 py-8 text-center">Nenhum participante encontrado</div>
|
|
{/if}
|
|
</div>
|
|
{:else if activeTab === 'adicionar' && isAdmin}
|
|
<!-- Adicionar Participante -->
|
|
<div class="relative mb-4">
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar usuários..."
|
|
class="input input-bordered w-full pl-10"
|
|
bind:value={searchQuery}
|
|
/>
|
|
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
{#if usuariosFiltrados().length > 0}
|
|
{#each usuariosFiltrados() as usuario (String(usuario._id))}
|
|
{@const usuarioId = String(usuario._id)}
|
|
{@const isLoading = loading?.includes(usuarioId)}
|
|
<button
|
|
type="button"
|
|
class="border-base-300 hover:bg-base-200 flex w-full items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors"
|
|
onclick={() => adicionarParticipante(usuarioId)}
|
|
disabled={isLoading}
|
|
>
|
|
<!-- Avatar -->
|
|
<div class="relative shrink-0">
|
|
<UserAvatar
|
|
avatar={usuario.avatar}
|
|
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.avatar}
|
|
nome={usuario.nome || 'Usuário'}
|
|
size="sm"
|
|
/>
|
|
<div class="absolute right-0 bottom-0">
|
|
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-base-content truncate font-medium">
|
|
{usuario.nome || 'Usuário'}
|
|
</p>
|
|
<p class="text-base-content/60 truncate text-sm">
|
|
{usuario.setor || usuario.email || ''}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Botão Adicionar -->
|
|
{#if isLoading}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{:else}
|
|
<UserPlus class="text-primary h-5 w-5" />
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-base-content/50 py-8 text-center">
|
|
{searchQuery.trim()
|
|
? 'Nenhum usuário encontrado'
|
|
: 'Todos os usuários já são participantes'}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="border-base-300 border-t px-6 py-4">
|
|
<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>
|