- 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.
423 lines
12 KiB
Svelte
423 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import { abrirConversa } from '$lib/stores/chatStore';
|
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
|
import UserAvatar from './UserAvatar.svelte';
|
|
import {
|
|
MessageSquare,
|
|
User,
|
|
Users,
|
|
Video,
|
|
X,
|
|
Search,
|
|
ChevronRight,
|
|
Plus,
|
|
UserX
|
|
} from 'lucide-svelte';
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { onClose }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
|
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
|
// Usuário atual
|
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
|
|
|
let activeTab = $state<'individual' | 'grupo' | 'sala_reuniao'>('individual');
|
|
let searchQuery = $state('');
|
|
let selectedUsers = $state<string[]>([]);
|
|
let groupName = $state('');
|
|
let salaReuniaoName = $state('');
|
|
let loading = $state(false);
|
|
|
|
const usuariosFiltrados = $derived(() => {
|
|
if (!usuarios?.data) return [];
|
|
|
|
// Filtrar o próprio usuário
|
|
const meuId = currentUser?.data?._id || meuPerfil?.data?._id;
|
|
let lista = usuarios.data.filter((u: any) => u._id !== meuId);
|
|
|
|
// Aplicar busca
|
|
if (searchQuery.trim()) {
|
|
const query = searchQuery.toLowerCase();
|
|
lista = lista.filter(
|
|
(u: any) =>
|
|
u.nome?.toLowerCase().includes(query) ||
|
|
u.email?.toLowerCase().includes(query) ||
|
|
u.matricula?.toLowerCase().includes(query)
|
|
);
|
|
}
|
|
|
|
// Ordenar: online primeiro, depois por nome
|
|
return lista.sort((a: any, b: any) => {
|
|
const statusOrder = {
|
|
online: 0,
|
|
ausente: 1,
|
|
externo: 2,
|
|
em_reuniao: 3,
|
|
offline: 4
|
|
};
|
|
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
|
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
|
|
|
if (statusA !== statusB) return statusA - statusB;
|
|
return (a.nome || '').localeCompare(b.nome || '');
|
|
});
|
|
});
|
|
|
|
function toggleUserSelection(userId: string) {
|
|
if (selectedUsers.includes(userId)) {
|
|
selectedUsers = selectedUsers.filter((id) => id !== userId);
|
|
} else {
|
|
selectedUsers = [...selectedUsers, userId];
|
|
}
|
|
}
|
|
|
|
async function handleCriarIndividual(userId: string) {
|
|
try {
|
|
loading = true;
|
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
|
tipo: 'individual',
|
|
participantes: [userId as any]
|
|
});
|
|
abrirConversa(conversaId);
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Erro ao criar conversa:', error);
|
|
alert('Erro ao criar conversa');
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function handleCriarGrupo() {
|
|
if (selectedUsers.length < 2) {
|
|
alert('Selecione pelo menos 2 participantes');
|
|
return;
|
|
}
|
|
|
|
if (!groupName.trim()) {
|
|
alert('Digite um nome para o grupo');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading = true;
|
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
|
tipo: 'grupo',
|
|
participantes: selectedUsers as any,
|
|
nome: groupName.trim()
|
|
});
|
|
abrirConversa(conversaId);
|
|
onClose();
|
|
} catch (error: any) {
|
|
console.error('Erro ao criar grupo:', error);
|
|
const mensagem = error?.message || error?.data || 'Erro desconhecido ao criar grupo';
|
|
alert(`Erro ao criar grupo: ${mensagem}`);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function handleCriarSalaReuniao() {
|
|
if (selectedUsers.length < 1) {
|
|
alert('Selecione pelo menos 1 participante');
|
|
return;
|
|
}
|
|
|
|
if (!salaReuniaoName.trim()) {
|
|
alert('Digite um nome para a sala de reunião');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading = true;
|
|
const conversaId = await client.mutation(api.chat.criarSalaReuniao, {
|
|
nome: salaReuniaoName.trim(),
|
|
participantes: selectedUsers as any
|
|
});
|
|
abrirConversa(conversaId);
|
|
onClose();
|
|
} catch (error: any) {
|
|
console.error('Erro ao criar sala de reunião:', error);
|
|
const mensagem =
|
|
error?.message || error?.data || 'Erro desconhecido ao criar sala de reunião';
|
|
alert(`Erro ao criar sala de reunião: ${mensagem}`);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
|
|
<div
|
|
class="modal-box flex max-h-[85vh] 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">
|
|
<h2 class="flex items-center gap-2 text-2xl font-bold">
|
|
<MessageSquare class="text-primary h-6 w-6" />
|
|
Nova Conversa
|
|
</h2>
|
|
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
|
<X class="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabs melhoradas -->
|
|
<div class="tabs tabs-boxed bg-base-200/50 p-4">
|
|
<button
|
|
type="button"
|
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
|
activeTab === 'individual'
|
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
|
: 'hover:bg-base-300'
|
|
}`}
|
|
onclick={() => {
|
|
activeTab = 'individual';
|
|
selectedUsers = [];
|
|
searchQuery = '';
|
|
}}
|
|
>
|
|
<User class="h-4 w-4" />
|
|
Individual
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
|
activeTab === 'grupo'
|
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
|
: 'hover:bg-base-300'
|
|
}`}
|
|
onclick={() => {
|
|
activeTab = 'grupo';
|
|
selectedUsers = [];
|
|
searchQuery = '';
|
|
}}
|
|
>
|
|
<Users class="h-4 w-4" />
|
|
Grupo
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
|
activeTab === 'sala_reuniao'
|
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
|
: 'hover:bg-base-300'
|
|
}`}
|
|
onclick={() => {
|
|
activeTab = 'sala_reuniao';
|
|
selectedUsers = [];
|
|
searchQuery = '';
|
|
}}
|
|
>
|
|
<Video class="h-4 w-4" />
|
|
Sala de Reunião
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 overflow-y-auto px-6 py-4">
|
|
{#if activeTab === 'grupo'}
|
|
<!-- Criar Grupo -->
|
|
<div class="mb-4">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-semibold">Nome do Grupo</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Digite o nome do grupo..."
|
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
|
bind:value={groupName}
|
|
maxlength="50"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-semibold">
|
|
Participantes {selectedUsers.length > 0
|
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
|
: ''}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
{:else if activeTab === 'sala_reuniao'}
|
|
<!-- Criar Sala de Reunião -->
|
|
<div class="mb-4">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-semibold">Nome da Sala de Reunião</span>
|
|
<span class="label-text-alt text-primary font-medium">👑 Você será o administrador</span
|
|
>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Digite o nome da sala de reunião..."
|
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
|
bind:value={salaReuniaoName}
|
|
maxlength="50"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-semibold">
|
|
Participantes {selectedUsers.length > 0
|
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
|
: ''}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Search melhorado -->
|
|
<div class="relative mb-4">
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar usuários por nome, email ou matrícula..."
|
|
class="input input-bordered focus:input-primary w-full pl-10 transition-colors"
|
|
bind:value={searchQuery}
|
|
/>
|
|
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
|
|
</div>
|
|
|
|
<!-- Lista de usuários -->
|
|
<div class="space-y-2">
|
|
{#if usuarios?.data && usuariosFiltrados().length > 0}
|
|
{#each usuariosFiltrados() as usuario (usuario._id)}
|
|
{@const isSelected = selectedUsers.includes(usuario._id)}
|
|
<button
|
|
type="button"
|
|
class={`flex w-full items-center gap-3 rounded-xl border-2 px-4 py-3 text-left transition-all duration-200 ${
|
|
isSelected
|
|
? 'border-primary bg-primary/10 scale-[1.02] shadow-md'
|
|
: 'border-base-300 hover:bg-base-200 hover:border-primary/30 hover:shadow-sm'
|
|
} ${loading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
|
onclick={() => {
|
|
if (loading) return;
|
|
if (activeTab === 'individual') {
|
|
handleCriarIndividual(usuario._id);
|
|
} else {
|
|
toggleUserSelection(usuario._id);
|
|
}
|
|
}}
|
|
disabled={loading}
|
|
>
|
|
<!-- Avatar -->
|
|
<div class="relative shrink-0">
|
|
<UserAvatar
|
|
avatar={usuario.avatar}
|
|
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
|
nome={usuario.nome}
|
|
size="md"
|
|
/>
|
|
<div class="absolute -right-1 -bottom-1">
|
|
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-base-content truncate font-semibold">
|
|
{usuario.nome}
|
|
</p>
|
|
<p class="text-base-content/60 truncate text-sm">
|
|
{usuario.setor || usuario.email || usuario.matricula || 'Sem informações'}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Checkbox melhorado (para grupo e sala de reunião) -->
|
|
{#if activeTab === 'grupo' || activeTab === 'sala_reuniao'}
|
|
<div class="shrink-0">
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-primary checkbox-lg"
|
|
checked={isSelected}
|
|
readonly
|
|
/>
|
|
</div>
|
|
{:else}
|
|
<!-- Ícone de seta para individual -->
|
|
<ChevronRight class="text-base-content/40 h-5 w-5" />
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
{:else if !usuarios?.data}
|
|
<div class="flex flex-col items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
<p class="text-base-content/60 mt-4">Carregando usuários...</p>
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
<UserX class="text-base-content/30 mb-4 h-16 w-16" />
|
|
<p class="text-base-content/70 font-medium">
|
|
{searchQuery.trim() ? 'Nenhum usuário encontrado' : 'Nenhum usuário disponível'}
|
|
</p>
|
|
{#if searchQuery.trim()}
|
|
<p class="text-base-content/50 mt-2 text-sm">
|
|
Tente buscar por nome, email ou matrícula
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer (para grupo e sala de reunião) -->
|
|
{#if activeTab === 'grupo'}
|
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
|
onclick={handleCriarGrupo}
|
|
disabled={loading || selectedUsers.length < 2 || !groupName.trim()}
|
|
>
|
|
{#if loading}
|
|
<span class="loading loading-spinner"></span>
|
|
Criando grupo...
|
|
{:else}
|
|
<Plus class="h-5 w-5" />
|
|
Criar Grupo
|
|
{/if}
|
|
</button>
|
|
{#if selectedUsers.length < 2 && activeTab === 'grupo'}
|
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
|
Selecione pelo menos 2 participantes
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{:else if activeTab === 'sala_reuniao'}
|
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
|
onclick={handleCriarSalaReuniao}
|
|
disabled={loading || selectedUsers.length < 1 || !salaReuniaoName.trim()}
|
|
>
|
|
{#if loading}
|
|
<span class="loading loading-spinner"></span>
|
|
Criando sala...
|
|
{:else}
|
|
<Plus class="h-5 w-5" />
|
|
Criar Sala de Reunião
|
|
{/if}
|
|
</button>
|
|
{#if selectedUsers.length < 1 && activeTab === 'sala_reuniao'}
|
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
|
Selecione pelo menos 1 participante
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button type="button" onclick={onClose}>fechar</button>
|
|
</form>
|
|
</dialog>
|