diff --git a/apps/web/src/lib/components/chat/ChatList.svelte b/apps/web/src/lib/components/chat/ChatList.svelte index 4ef925f..b5c9b81 100644 --- a/apps/web/src/lib/components/chat/ChatList.svelte +++ b/apps/web/src/lib/components/chat/ChatList.svelte @@ -6,6 +6,7 @@ import { ptBR } from "date-fns/locale"; import UserStatusBadge from "./UserStatusBadge.svelte"; import UserAvatar from "./UserAvatar.svelte"; + import NewConversationModal from "./NewConversationModal.svelte"; const client = useConvexClient(); @@ -15,7 +16,11 @@ // Buscar o perfil do usuário logado const meuPerfil = useQuery(api.usuarios.obterPerfil, {}); + // Buscar conversas (grupos e salas de reunião) + const conversas = useQuery(api.chat.listarConversas, {}); + let searchQuery = $state(""); + let activeTab = $state<"usuarios" | "conversas">("usuarios"); // Debug: monitorar carregamento de dados $effect(() => { @@ -85,6 +90,7 @@ } let processando = $state(false); + let showNewConversationModal = $state(false); async function handleClickUsuario(usuario: any) { if (processando) { @@ -132,6 +138,38 @@ }; return labels[status || "offline"] || "Offline"; } + + // Filtrar conversas por tipo e busca + const conversasFiltradas = $derived(() => { + if (!conversas?.data) return []; + + let lista = conversas.data.filter((c: any) => + c.tipo === "grupo" || c.tipo === "sala_reuniao" + ); + + // Aplicar busca + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + lista = lista.filter((c: any) => + c.nome?.toLowerCase().includes(query) + ); + } + + return lista; + }); + + function handleClickConversa(conversa: any) { + if (processando) return; + try { + processando = true; + abrirConversa(conversa._id); + } catch (error) { + console.error("Erro ao abrir conversa:", error); + alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`); + } finally { + processando = false; + } + }
@@ -161,104 +199,214 @@
- -
-

- Usuários do Sistema ({usuariosFiltrados.length}) -

-
- - -
- {#if usuarios?.data && usuariosFiltrados.length > 0} - {#each usuariosFiltrados as usuario (usuario._id)} - - {/each} - {:else if !usuarios?.data} - -
- -
- {:else} - -
+ +
+ +
+ + +
+ + +
+
+ Nova Conversa + +
+
+ + +
+ {#if activeTab === "usuarios"} + + {#if usuarios?.data && usuariosFiltrados.length > 0} + {#each usuariosFiltrados as usuario (usuario._id)} + + {/each} + {:else if !usuarios?.data} + +
+ +
+ {:else} + +
+ + + +

Nenhum usuário encontrado

+
+ {/if} + {:else} + + {#if conversas?.data && conversasFiltradas().length > 0} + {#each conversasFiltradas() as conversa (conversa._id)} + + {/each} + {:else if !conversas?.data} + +
+ +
+ {:else} + +
+ + + +

Nenhuma conversa encontrada

+

Crie um grupo ou sala de reunião para começar

+
+ {/if} {/if}
- + +{#if showNewConversationModal} + (showNewConversationModal = false)} /> +{/if} diff --git a/apps/web/src/lib/components/chat/ChatWidget.svelte b/apps/web/src/lib/components/chat/ChatWidget.svelte index e311d73..f014d77 100644 --- a/apps/web/src/lib/components/chat/ChatWidget.svelte +++ b/apps/web/src/lib/components/chat/ChatWidget.svelte @@ -43,6 +43,102 @@ let dragStart = $state({ x: 0, y: 0 }); let isAnimating = $state(false); + // Tamanho da janela (redimensionável) + const MIN_WIDTH = 300; + const MAX_WIDTH = 1200; + const MIN_HEIGHT = 400; + const MAX_HEIGHT = 900; + const DEFAULT_WIDTH = 440; + const DEFAULT_HEIGHT = 680; + + // Carregar tamanho salvo do localStorage ou usar padrão + function getSavedSize() { + if (typeof window === 'undefined') return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; + const saved = localStorage.getItem('chat-window-size'); + if (saved) { + try { + const parsed = JSON.parse(saved); + return { + width: Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH)), + height: Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT)) + }; + } catch { + return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; + } + } + return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; + } + + let windowSize = $state(getSavedSize()); + + // Salvar tamanho no localStorage + function saveSize() { + if (typeof window !== 'undefined') { + localStorage.setItem('chat-window-size', JSON.stringify(windowSize)); + } + } + + // Redimensionamento + let isResizing = $state(false); + let resizeStart = $state({ x: 0, y: 0, width: 0, height: 0 }); + let resizeDirection = $state(null); // 'n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw' + + function handleResizeStart(e: MouseEvent, direction: string) { + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + isResizing = true; + resizeDirection = direction; + resizeStart = { + x: e.clientX, + y: e.clientY, + width: windowSize.width, + height: windowSize.height + }; + document.body.classList.add('resizing'); + } + + function handleResizeMove(e: MouseEvent) { + if (!isResizing || !resizeDirection) return; + + const deltaX = e.clientX - resizeStart.x; + const deltaY = e.clientY - resizeStart.y; + + let newWidth = resizeStart.width; + let newHeight = resizeStart.height; + let newX = position.x; + let newY = position.y; + + // Redimensionar baseado na direção + if (resizeDirection.includes('e')) { + newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX)); + } + if (resizeDirection.includes('w')) { + newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX)); + newX = position.x + (resizeStart.width - newWidth); + } + if (resizeDirection.includes('s')) { + newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY)); + } + if (resizeDirection.includes('n')) { + newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY)); + newY = position.y + (resizeStart.height - newHeight); + } + + windowSize = { width: newWidth, height: newHeight }; + position = { x: newX, y: newY }; + } + + function handleResizeEnd() { + if (isResizing) { + isResizing = false; + resizeDirection = null; + document.body.classList.remove('resizing'); + saveSize(); + ajustarPosicao(); + } + } + // Sincronizar com stores $effect(() => { isOpen = $chatAberto; @@ -89,14 +185,19 @@ } function handleMouseMove(e: MouseEvent) { + if (isResizing) { + handleResizeMove(e); + return; + } + if (!isDragging) return; const newX = e.clientX - dragStart.x; const newY = e.clientY - dragStart.y; // Dimensões do widget - const widgetWidth = isOpen && !isMinimized ? 440 : 72; - const widgetHeight = isOpen && !isMinimized ? 680 : 72; + const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; + const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; // Limites da tela com margem de segurança const minX = -(widgetWidth - 100); // Permitir até 100px visíveis @@ -117,14 +218,15 @@ // Garantir que está dentro dos limites ao soltar ajustarPosicao(); } + handleResizeEnd(); } function ajustarPosicao() { isAnimating = true; // Dimensões do widget - const widgetWidth = isOpen && !isMinimized ? 440 : 72; - const widgetHeight = isOpen && !isMinimized ? 680 : 72; + const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; + const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; // Verificar se está fora dos limites let newX = position.x; @@ -243,10 +345,10 @@ class="fixed flex flex-col overflow-hidden backdrop-blur-2xl" style=" z-index: 99999 !important; - bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - 680}px`}; - right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - 440}px`}; - width: 440px; - height: 680px; + bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - windowSize.height}px`}; + right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - windowSize.width}px`}; + width: {windowSize.width}px; + height: {windowSize.height}px; max-width: calc(100vw - 3rem); max-height: calc(100vh - 3rem); position: fixed !important; @@ -335,6 +437,30 @@ + + + + {/if} + - + {#if isMinha} + + + + {:else if isAdmin?.data} + + + {/if} {/if} diff --git a/apps/web/src/lib/components/chat/NewConversationModal.svelte b/apps/web/src/lib/components/chat/NewConversationModal.svelte index f691ea3..965401a 100644 --- a/apps/web/src/lib/components/chat/NewConversationModal.svelte +++ b/apps/web/src/lib/components/chat/NewConversationModal.svelte @@ -2,6 +2,7 @@ import { useQuery, useConvexClient } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; import { abrirConversa } from "$lib/stores/chatStore"; + import { authStore } from "$lib/stores/auth.svelte"; import UserStatusBadge from "./UserStatusBadge.svelte"; import UserAvatar from "./UserAvatar.svelte"; @@ -12,24 +13,42 @@ let { onClose }: Props = $props(); const client = useConvexClient(); - const usuarios = useQuery(api.chat.listarTodosUsuarios, {}); + const usuarios = useQuery(api.usuarios.listarParaChat, {}); + const meuPerfil = useQuery(api.usuarios.obterPerfil, {}); - let activeTab = $state<"individual" | "grupo">("individual"); + let activeTab = $state<"individual" | "grupo" | "sala_reuniao">("individual"); let searchQuery = $state(""); let selectedUsers = $state([]); let groupName = $state(""); + let salaReuniaoName = $state(""); let loading = $state(false); const usuariosFiltrados = $derived(() => { - if (!usuarios) return []; - if (!searchQuery.trim()) return usuarios; - - const query = searchQuery.toLowerCase(); - return usuarios.filter((u: any) => - u.nome.toLowerCase().includes(query) || - u.email.toLowerCase().includes(query) || - u.matricula.toLowerCase().includes(query) - ); + if (!usuarios?.data) return []; + + // Filtrar o próprio usuário + const meuId = authStore.usuario?._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) { @@ -77,26 +96,63 @@ }); abrirConversa(conversaId); onClose(); - } catch (error) { + } catch (error: any) { console.error("Erro ao criar grupo:", error); - alert("Erro ao criar grupo"); + 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; } } -
+
e.stopPropagation()} + style="box-shadow: 0 20px 60px -15px rgba(0, 0, 0, 0.3);" > - -
-

Nova Conversa

+ +
+
+

+ + + + Nova Conversa +

- -
+ +
+
-
+
{#if activeTab === "grupo"}
-
-
-