refactor: enhance chat components and improve user interaction
- Refactored Sidebar, ChatList, ChatWindow, and NewConversationModal components for better readability and maintainability. - Updated user data handling to utilize the latest API responses, ensuring accurate display of user statuses and notifications. - Improved modal interactions and user feedback mechanisms across chat components. - Cleaned up unused code and optimized state management for a smoother user experience.
This commit is contained in:
@@ -1,27 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from '$app/state';
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from '$app/navigation';
|
||||||
import logo from "$lib/assets/logo_governo_PE.png";
|
import logo from '$lib/assets/logo_governo_PE.png';
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte';
|
||||||
import { loginModalStore } from "$lib/stores/loginModal.svelte";
|
import { loginModalStore } from '$lib/stores/loginModal.svelte';
|
||||||
import { useQuery } from "convex-svelte";
|
import { useQuery } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import NotificationBell from "$lib/components/chat/NotificationBell.svelte";
|
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
||||||
import ChatWidget from "$lib/components/chat/ChatWidget.svelte";
|
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
||||||
import PresenceManager from "$lib/components/chat/PresenceManager.svelte";
|
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
||||||
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
|
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||||
import {
|
import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte';
|
||||||
Menu,
|
import { authClient } from '$lib/auth';
|
||||||
User,
|
import { resolve } from '$app/paths';
|
||||||
Home,
|
|
||||||
UserPlus,
|
|
||||||
XCircle,
|
|
||||||
LogIn,
|
|
||||||
Tag,
|
|
||||||
Plus,
|
|
||||||
Check,
|
|
||||||
} from "lucide-svelte";
|
|
||||||
import { authClient } from "$lib/auth";
|
|
||||||
|
|
||||||
let { children }: { children: Snippet } = $props();
|
let { children }: { children: Snippet } = $props();
|
||||||
|
|
||||||
@@ -45,7 +36,7 @@
|
|||||||
// Função para gerar classes do menu ativo
|
// Função para gerar classes do menu ativo
|
||||||
function getMenuClasses(isActive: boolean) {
|
function getMenuClasses(isActive: boolean) {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"group font-semibold flex items-center justify-center gap-2 text-center p-3.5 rounded-xl border-2 transition-all duration-300 shadow-md hover:shadow-lg hover:scale-105";
|
'group font-semibold flex items-center justify-center gap-2 text-center p-3.5 rounded-xl border-2 transition-all duration-300 shadow-md hover:shadow-lg hover:scale-105';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return `${baseClasses} border-primary bg-primary text-white shadow-lg scale-105`;
|
return `${baseClasses} border-primary bg-primary text-white shadow-lg scale-105`;
|
||||||
@@ -57,7 +48,7 @@
|
|||||||
// Função para gerar classes do botão "Solicitar Acesso"
|
// Função para gerar classes do botão "Solicitar Acesso"
|
||||||
function getSolicitarClasses(isActive: boolean) {
|
function getSolicitarClasses(isActive: boolean) {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"group font-semibold flex items-center justify-center gap-2 text-center p-3.5 rounded-xl border-2 transition-all duration-300 shadow-md hover:shadow-lg hover:scale-105";
|
'group font-semibold flex items-center justify-center gap-2 text-center p-3.5 rounded-xl border-2 transition-all duration-300 shadow-md hover:shadow-lg hover:scale-105';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return `${baseClasses} border-success bg-success text-white shadow-lg scale-105`;
|
return `${baseClasses} border-success bg-success text-white shadow-lg scale-105`;
|
||||||
@@ -67,49 +58,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setores = [
|
const setores = [
|
||||||
{ nome: "Recursos Humanos", link: "/recursos-humanos" },
|
{ nome: 'Recursos Humanos', link: '/recursos-humanos' as const },
|
||||||
{ nome: "Financeiro", link: "/financeiro" },
|
{ nome: 'Financeiro', link: '/financeiro' as const },
|
||||||
{ nome: "Controladoria", link: "/controladoria" },
|
{ nome: 'Controladoria', link: '/controladoria' as const },
|
||||||
{ nome: "Licitações", link: "/licitacoes" },
|
{ nome: 'Licitações', link: '/licitacoes' as const },
|
||||||
{ nome: "Compras", link: "/compras" },
|
{ nome: 'Compras', link: '/compras' as const },
|
||||||
{ nome: "Jurídico", link: "/juridico" },
|
{ nome: 'Jurídico', link: '/juridico' as const },
|
||||||
{ nome: "Comunicação", link: "/comunicacao" },
|
{ nome: 'Comunicação', link: '/comunicacao' as const },
|
||||||
{ nome: "Programas Esportivos", link: "/programas-esportivos" },
|
{ nome: 'Programas Esportivos', link: '/programas-esportivos' as const },
|
||||||
{ nome: "Secretaria Executiva", link: "/secretaria-executiva" },
|
{ nome: 'Secretaria Executiva', link: '/secretaria-executiva' as const },
|
||||||
{
|
{
|
||||||
nome: "Secretaria de Gestão de Pessoas",
|
nome: 'Secretaria de Gestão de Pessoas',
|
||||||
link: "/gestao-pessoas",
|
link: '/gestao-pessoas' as const
|
||||||
},
|
},
|
||||||
{ nome: "Tecnologia da Informação", link: "/ti" },
|
{ nome: 'Tecnologia da Informação', link: '/ti' as const }
|
||||||
];
|
];
|
||||||
|
|
||||||
let showAboutModal = $state(false);
|
let showAboutModal = $state(false);
|
||||||
let matricula = $state("");
|
let matricula = $state('');
|
||||||
let senha = $state("");
|
let senha = $state('');
|
||||||
let erroLogin = $state("");
|
let erroLogin = $state('');
|
||||||
let carregandoLogin = $state(false);
|
let carregandoLogin = $state(false);
|
||||||
|
|
||||||
// Sincronizar com o store global
|
// Sincronizar com o store global
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (loginModalStore.showModal && !matricula && !senha) {
|
if (loginModalStore.showModal && !matricula && !senha) {
|
||||||
matricula = "";
|
matricula = '';
|
||||||
senha = "";
|
senha = '';
|
||||||
erroLogin = "";
|
erroLogin = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function openLoginModal() {
|
function openLoginModal() {
|
||||||
loginModalStore.open();
|
loginModalStore.open();
|
||||||
matricula = "";
|
matricula = '';
|
||||||
senha = "";
|
senha = '';
|
||||||
erroLogin = "";
|
erroLogin = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLoginModal() {
|
function closeLoginModal() {
|
||||||
loginModalStore.close();
|
loginModalStore.close();
|
||||||
matricula = "";
|
matricula = '';
|
||||||
senha = "";
|
senha = '';
|
||||||
erroLogin = "";
|
erroLogin = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAboutModal() {
|
function openAboutModal() {
|
||||||
@@ -122,7 +113,7 @@
|
|||||||
|
|
||||||
async function handleLogin(e: Event) {
|
async function handleLogin(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
erroLogin = "";
|
erroLogin = '';
|
||||||
carregandoLogin = true;
|
carregandoLogin = true;
|
||||||
|
|
||||||
// const browserInfo = await getBrowserInfo();
|
// const browserInfo = await getBrowserInfo();
|
||||||
@@ -132,115 +123,110 @@
|
|||||||
{
|
{
|
||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
alert(ctx.error.message);
|
alert(ctx.error.message);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
closeLoginModal();
|
closeLoginModal();
|
||||||
goto("/");
|
goto(resolve('/'));
|
||||||
} else {
|
} else {
|
||||||
erroLogin = "Erro ao fazer login";
|
erroLogin = 'Erro ao fazer login';
|
||||||
}
|
}
|
||||||
|
carregandoLogin = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
const result = await authClient.signOut();
|
const result = await authClient.signOut();
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.error("Sign out error:", result.error);
|
console.error('Sign out error:', result.error);
|
||||||
}
|
}
|
||||||
goto("/");
|
goto(resolve('/'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Header Fixo acima de tudo -->
|
<!-- Header Fixo acima de tudo -->
|
||||||
<div
|
<div
|
||||||
class="navbar bg-linear-to-r from-primary/30 via-primary/20 to-primary/30 backdrop-blur-sm shadow-lg border-b border-primary/10 px-6 lg:px-8 fixed top-0 left-0 right-0 z-50 min-h-24"
|
class="navbar from-primary/30 via-primary/20 to-primary/30 border-primary/10 fixed top-0 right-0 left-0 z-50 min-h-24 border-b bg-linear-to-r px-6 shadow-lg backdrop-blur-sm lg:px-8"
|
||||||
>
|
>
|
||||||
<div class="flex-none lg:hidden">
|
<div class="flex-none lg:hidden">
|
||||||
<label
|
<label
|
||||||
for="my-drawer-3"
|
for="my-drawer-3"
|
||||||
class="relative flex items-center justify-center w-14 h-14 rounded-2xl overflow-hidden cursor-pointer group transition-all duration-300 hover:scale-105"
|
class="group relative flex h-14 w-14 cursor-pointer items-center justify-center overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105"
|
||||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
||||||
aria-label="Abrir menu"
|
aria-label="Abrir menu"
|
||||||
>
|
>
|
||||||
<!-- Efeito de brilho no hover -->
|
<!-- Efeito de brilho no hover -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Ícone de menu hambúrguer -->
|
<!-- Ícone de menu hambúrguer -->
|
||||||
<Menu
|
<Menu
|
||||||
class="w-7 h-7 text-white relative z-10 group-hover:scale-110 transition-transform duration-300"
|
class="relative z-10 h-7 w-7 text-white transition-transform duration-300 group-hover:scale-110"
|
||||||
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex items-center gap-4 lg:gap-6">
|
<div class="flex flex-1 items-center gap-4 lg:gap-6">
|
||||||
<!-- Logo MODERNO do Governo -->
|
<!-- Logo MODERNO do Governo -->
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<div
|
<div
|
||||||
class="w-16 lg:w-20 rounded-2xl shadow-xl p-2 relative overflow-hidden group transition-all duration-300 hover:scale-105"
|
class="group relative w-16 overflow-hidden rounded-2xl p-2 shadow-xl transition-all duration-300 hover:scale-105 lg:w-20"
|
||||||
style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border: 2px solid rgba(102, 126, 234, 0.1);"
|
style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border: 2px solid rgba(102, 126, 234, 0.1);"
|
||||||
>
|
>
|
||||||
<!-- Efeito de brilho no hover -->
|
<!-- Efeito de brilho no hover -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
class="from-primary/5 absolute inset-0 bg-linear-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo do Governo de PE"
|
alt="Logo do Governo de PE"
|
||||||
class="w-full h-full object-contain relative z-10 transition-transform duration-300 group-hover:scale-105"
|
class="relative z-10 h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
|
||||||
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.08));"
|
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.08));"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Brilho sutil no canto -->
|
<!-- Brilho sutil no canto -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 right-0 w-8 h-8 bg-linear-to-br from-white/40 to-transparent rounded-bl-full opacity-70"
|
class="absolute top-0 right-0 h-8 w-8 rounded-bl-full bg-linear-to-br from-white/40 to-transparent opacity-70"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1 class="text-xl lg:text-3xl font-bold text-primary tracking-tight">
|
<h1 class="text-primary text-xl font-bold tracking-tight lg:text-3xl">SGSE</h1>
|
||||||
SGSE
|
|
||||||
</h1>
|
|
||||||
<p
|
<p
|
||||||
class="text-xs lg:text-base text-base-content/80 hidden sm:block font-medium leading-tight"
|
class="text-base-content/80 hidden text-xs leading-tight font-medium sm:block lg:text-base"
|
||||||
>
|
>
|
||||||
Sistema de Gerenciamento da<br class="lg:hidden" /> Secretaria de Esportes
|
Sistema de Gerenciamento da<br class="lg:hidden" /> Secretaria de Esportes
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none flex items-center gap-4 ml-auto">
|
<div class="ml-auto flex flex-none items-center gap-4">
|
||||||
{#if currentUser.data}
|
{#if currentUser.data}
|
||||||
<!-- Sino de notificações no canto superior direito -->
|
<!-- Sino de notificações no canto superior direito -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden lg:flex flex-col items-end mr-2">
|
<div class="mr-2 hidden flex-col items-end lg:flex">
|
||||||
<span class="text-sm font-semibold text-primary"
|
<span class="text-primary text-sm font-semibold">{currentUser.data.nome}</span>
|
||||||
>{currentUser.data.nome}</span
|
<span class="text-base-content/60 text-xs">{currentUser.data.role?.nome}</span>
|
||||||
>
|
|
||||||
<span class="text-xs text-base-content/60"
|
|
||||||
>{currentUser.data.role?.nome}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<!-- Botão de Perfil ULTRA MODERNO -->
|
<!-- Botão de Perfil ULTRA MODERNO -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="relative flex items-center justify-center w-14 h-14 rounded-2xl overflow-hidden group transition-all duration-300 hover:scale-105"
|
class="group relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105"
|
||||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
||||||
aria-label="Menu do usuário"
|
aria-label="Menu do usuário"
|
||||||
>
|
>
|
||||||
<!-- Efeito de brilho no hover -->
|
<!-- Efeito de brilho no hover -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Anel de pulso sutil -->
|
<!-- Anel de pulso sutil -->
|
||||||
@@ -253,62 +239,58 @@
|
|||||||
{#if avatarUrlDoUsuario()}
|
{#if avatarUrlDoUsuario()}
|
||||||
<img
|
<img
|
||||||
src={avatarUrlDoUsuario()}
|
src={avatarUrlDoUsuario()}
|
||||||
alt={currentUser.data?.nome || "Usuário"}
|
alt={currentUser.data?.nome || 'Usuário'}
|
||||||
class="w-full h-full object-cover relative z-10"
|
class="relative z-10 h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Ícone de usuário moderno (fallback) -->
|
<!-- Ícone de usuário moderno (fallback) -->
|
||||||
<User
|
<User
|
||||||
class="w-7 h-7 text-white relative z-10 group-hover:scale-110 transition-transform duration-300"
|
class="relative z-10 h-7 w-7 text-white transition-transform duration-300 group-hover:scale-110"
|
||||||
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Badge de status online -->
|
<!-- Badge de status online -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-1 right-1 w-3 h-3 bg-success rounded-full border-2 border-white shadow-lg z-20"
|
class="bg-success absolute top-1 right-1 z-20 h-3 w-3 rounded-full border-2 border-white shadow-lg"
|
||||||
style="animation: pulse-dot 2s ease-in-out infinite;"
|
style="animation: pulse-dot 2s ease-in-out infinite;"
|
||||||
></div>
|
></div>
|
||||||
</button>
|
</button>
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content z-1 menu p-2 shadow-xl bg-base-100 rounded-box w-52 mt-4 border border-primary/20"
|
class="dropdown-content menu bg-base-100 rounded-box border-primary/20 z-1 mt-4 w-52 border p-2 shadow-xl"
|
||||||
>
|
>
|
||||||
<li class="menu-title">
|
<li class="menu-title">
|
||||||
<span class="text-primary font-bold">{currentUser.data?.nome}</span>
|
<span class="text-primary font-bold">{currentUser.data?.nome}</span>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/perfil">Meu Perfil</a></li>
|
<li><a href={resolve('/perfil')}>Meu Perfil</a></li>
|
||||||
<li><a href="/alterar-senha">Alterar Senha</a></li>
|
<li><a href={resolve('/alterar-senha')}>Alterar Senha</a></li>
|
||||||
<div class="divider my-0"></div>
|
<div class="divider my-0"></div>
|
||||||
<li>
|
<li>
|
||||||
<button type="button" onclick={handleLogout} class="text-error"
|
<button type="button" onclick={handleLogout} class="text-error">Sair</button>
|
||||||
>Sair</button
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-lg shadow-2xl hover:shadow-primary/30 transition-all duration-500 hover:scale-110 group relative overflow-hidden border-0 bg-linear-to-br from-primary via-primary to-primary/80 hover:from-primary/90 hover:via-primary/80 hover:to-primary/70"
|
class="btn btn-lg hover:shadow-primary/30 group from-primary via-primary to-primary/80 hover:from-primary/90 hover:via-primary/80 hover:to-primary/70 relative overflow-hidden border-0 bg-linear-to-br shadow-2xl transition-all duration-500 hover:scale-110"
|
||||||
style="width: 4rem; height: 4rem; border-radius: 9999px;"
|
style="width: 4rem; height: 4rem; border-radius: 9999px;"
|
||||||
onclick={() => openLoginModal()}
|
onclick={() => openLoginModal()}
|
||||||
aria-label="Login"
|
aria-label="Login"
|
||||||
>
|
>
|
||||||
<!-- Efeito de brilho animado -->
|
<!-- Efeito de brilho animado -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-r from-transparent via-white/30 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"
|
class="absolute inset-0 -translate-x-full bg-linear-to-r from-transparent via-white/30 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Anel pulsante de fundo -->
|
<!-- Anel pulsante de fundo -->
|
||||||
<div
|
<div class="absolute inset-0 rounded-full bg-white/10 group-hover:animate-ping"></div>
|
||||||
class="absolute inset-0 rounded-full bg-white/10 group-hover:animate-ping"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Ícone de login premium -->
|
<!-- Ícone de login premium -->
|
||||||
<User
|
<User
|
||||||
class="h-8 w-8 relative z-10 text-white group-hover:scale-110 transition-all duration-500"
|
class="relative z-10 h-8 w-8 text-white transition-all duration-500 group-hover:scale-110"
|
||||||
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
@@ -319,10 +301,7 @@
|
|||||||
|
|
||||||
<div class="drawer lg:drawer-open" style="margin-top: 96px;">
|
<div class="drawer lg:drawer-open" style="margin-top: 96px;">
|
||||||
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
|
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
|
||||||
<div
|
<div class="drawer-content flex flex-col lg:ml-72" style="min-height: calc(100vh - 96px);">
|
||||||
class="drawer-content flex flex-col lg:ml-72"
|
|
||||||
style="min-height: calc(100vh - 96px);"
|
|
||||||
>
|
|
||||||
<!-- Page content -->
|
<!-- Page content -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
@@ -330,7 +309,7 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer
|
<footer
|
||||||
class="footer footer-center bg-linear-to-r from-primary/30 via-primary/20 to-primary/30 backdrop-blur-sm text-base-content p-6 border-t-2 border-primary/20 shadow-inner shrink-0"
|
class="footer footer-center from-primary/30 via-primary/20 to-primary/30 text-base-content border-primary/20 shrink-0 border-t-2 bg-linear-to-r p-6 shadow-inner backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div class="grid grid-flow-col gap-6 text-sm font-medium">
|
<div class="grid grid-flow-col gap-6 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
@@ -339,69 +318,63 @@
|
|||||||
onclick={() => openAboutModal()}>Sobre</button
|
onclick={() => openAboutModal()}>Sobre</button
|
||||||
>
|
>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/" class="link link-hover hover:text-primary transition-colors"
|
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
||||||
>Contato</a
|
>Contato</a
|
||||||
>
|
>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/" class="link link-hover hover:text-primary transition-colors"
|
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
||||||
>Suporte</a
|
>Suporte</a
|
||||||
>
|
>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/" class="link link-hover hover:text-primary transition-colors"
|
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
||||||
>Privacidade</a
|
>Privacidade</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 mt-2">
|
<div class="mt-2 flex items-center gap-3">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<div class="w-10 rounded-lg bg-white p-1.5 shadow-md">
|
<div class="w-10 rounded-lg bg-white p-1.5 shadow-md">
|
||||||
<img src={logo} alt="Logo" class="w-full h-full object-contain" />
|
<img src={logo} alt="Logo" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<p class="text-xs font-bold text-primary">
|
<p class="text-primary text-xs font-bold">Governo do Estado de Pernambuco</p>
|
||||||
Governo do Estado de Pernambuco
|
<p class="text-base-content/70 text-xs">Secretaria de Esportes</p>
|
||||||
</p>
|
|
||||||
<p class="text-xs text-base-content/70">Secretaria de Esportes</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-base-content/60 mt-2">
|
<p class="text-base-content/60 mt-2 text-xs">
|
||||||
© {new Date().getFullYear()} - Todos os direitos reservados
|
© {new Date().getFullYear()} - Todos os direitos reservados
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-side z-40 fixed" style="margin-top: 96px;">
|
<div class="drawer-side fixed z-40" style="margin-top: 96px;">
|
||||||
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"
|
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
></label>
|
|
||||||
<div
|
<div
|
||||||
class="menu bg-linear-to-b from-primary/25 to-primary/15 backdrop-blur-sm w-72 p-4 flex flex-col gap-2 h-[calc(100vh-96px)] overflow-y-auto border-r-2 border-primary/20 shadow-xl"
|
class="menu from-primary/25 to-primary/15 border-primary/20 flex h-[calc(100vh-96px)] w-72 flex-col gap-2 overflow-y-auto border-r-2 bg-linear-to-b p-4 shadow-xl backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<!-- Sidebar menu items -->
|
<!-- Sidebar menu items -->
|
||||||
<ul class="flex flex-col gap-2">
|
<ul class="flex flex-col gap-2">
|
||||||
<li class="rounded-xl">
|
<li class="rounded-xl">
|
||||||
<a href="/" class={getMenuClasses(currentPath === "/")}>
|
<a href={resolve('/')} class={getMenuClasses(currentPath === '/')}>
|
||||||
<Home
|
<Home class="h-5 w-5 transition-transform group-hover:scale-110" strokeWidth={2} />
|
||||||
class="h-5 w-5 group-hover:scale-110 transition-transform"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{#each setores as s}
|
{#each setores as s (s.link)}
|
||||||
{@const isActive = currentPath.startsWith(s.link)}
|
{@const isActive = currentPath.startsWith(s.link)}
|
||||||
<li class="rounded-xl">
|
<li class="rounded-xl">
|
||||||
<a
|
<a
|
||||||
href={s.link}
|
href={resolve(s.link)}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? 'page' : undefined}
|
||||||
class={getMenuClasses(isActive)}
|
class={getMenuClasses(isActive)}
|
||||||
>
|
>
|
||||||
<span>{s.nome}</span>
|
<span>{s.nome}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
<li class="rounded-xl mt-auto">
|
<li class="mt-auto rounded-xl">
|
||||||
<a
|
<a
|
||||||
href="/solicitar-acesso"
|
href={resolve('/solicitar-acesso')}
|
||||||
class={getSolicitarClasses(currentPath === "/solicitar-acesso")}
|
class={getSolicitarClasses(currentPath === '/solicitar-acesso')}
|
||||||
>
|
>
|
||||||
<UserPlus class="h-5 w-5" strokeWidth={2} />
|
<UserPlus class="h-5 w-5" strokeWidth={2} />
|
||||||
<span>Solicitar acesso</span>
|
<span>Solicitar acesso</span>
|
||||||
@@ -415,31 +388,29 @@
|
|||||||
<!-- Modal de Login -->
|
<!-- Modal de Login -->
|
||||||
{#if loginModalStore.showModal}
|
{#if loginModalStore.showModal}
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box relative overflow-hidden bg-base-100 max-w-md">
|
<div class="modal-box bg-base-100 relative max-w-md overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
onclick={closeLoginModal}
|
onclick={closeLoginModal}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="text-center mb-6">
|
<div class="mb-6 text-center">
|
||||||
<div class="avatar mb-4">
|
<div class="avatar mb-4">
|
||||||
<div class="w-20 rounded-lg bg-primary/10 p-3">
|
<div class="bg-primary/10 w-20 rounded-lg p-3">
|
||||||
<img src={logo} alt="Logo" class="w-full h-full object-contain" />
|
<img src={logo} alt="Logo" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold text-3xl text-primary">Login</h3>
|
<h3 class="text-primary text-3xl font-bold">Login</h3>
|
||||||
<p class="text-sm text-base-content/60 mt-2">
|
<p class="text-base-content/60 mt-2 text-sm">Acesse o sistema com suas credenciais</p>
|
||||||
Acesse o sistema com suas credenciais
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if erroLogin}
|
{#if erroLogin}
|
||||||
<div class="alert alert-error mb-4">
|
<div class="alert alert-error mb-4">
|
||||||
<XCircle class="stroke-current shrink-0 h-6 w-6" strokeWidth={2} />
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||||
<span>{erroLogin}</span>
|
<span>{erroLogin}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -474,11 +445,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<button
|
<button type="submit" class="btn btn-primary w-full" disabled={carregandoLogin}>
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary w-full"
|
|
||||||
disabled={carregandoLogin}
|
|
||||||
>
|
|
||||||
{#if carregandoLogin}
|
{#if carregandoLogin}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Entrando...
|
Entrando...
|
||||||
@@ -488,17 +455,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-4 space-y-2">
|
<div class="mt-4 space-y-2 text-center">
|
||||||
<a
|
<a
|
||||||
href="/solicitar-acesso"
|
href={resolve('/solicitar-acesso')}
|
||||||
class="link link-primary text-sm block"
|
class="link link-primary block text-sm"
|
||||||
onclick={closeLoginModal}
|
onclick={closeLoginModal}
|
||||||
>
|
>
|
||||||
Não tem acesso? Solicite aqui
|
Não tem acesso? Solicite aqui
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/esqueci-senha"
|
href={resolve('/esqueci-senha')}
|
||||||
class="link link-secondary text-sm block"
|
class="link link-secondary block text-sm"
|
||||||
onclick={closeLoginModal}
|
onclick={closeLoginModal}
|
||||||
>
|
>
|
||||||
Esqueceu sua senha?
|
Esqueceu sua senha?
|
||||||
@@ -506,16 +473,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="divider text-xs text-base-content/40">
|
<div class="divider text-base-content/40 text-xs">Credenciais de teste</div>
|
||||||
Credenciais de teste
|
<div class="bg-base-200 rounded-lg p-3 text-xs">
|
||||||
</div>
|
<p class="mb-1 font-semibold">Admin:</p>
|
||||||
<div class="bg-base-200 p-3 rounded-lg text-xs">
|
|
||||||
<p class="font-semibold mb-1">Admin:</p>
|
|
||||||
<p>
|
<p>
|
||||||
Matrícula: <code class="bg-base-300 px-2 py-1 rounded">0000</code>
|
Matrícula: <code class="bg-base-300 rounded px-2 py-1">0000</code>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Senha: <code class="bg-base-300 px-2 py-1 rounded">Admin@123</code>
|
Senha: <code class="bg-base-300 rounded px-2 py-1">Admin@123</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,31 +497,27 @@
|
|||||||
{#if showAboutModal}
|
{#if showAboutModal}
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div
|
<div
|
||||||
class="modal-box max-w-2xl relative overflow-hidden bg-linear-to-br from-base-100 to-base-200"
|
class="modal-box from-base-100 to-base-200 relative max-w-2xl overflow-hidden bg-linear-to-br"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
onclick={closeAboutModal}
|
onclick={closeAboutModal}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="text-center space-y-6 py-4">
|
<div class="space-y-6 py-4 text-center">
|
||||||
<!-- Logo e Título -->
|
<!-- Logo e Título -->
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<div class="w-24 rounded-xl bg-white p-3 shadow-lg">
|
<div class="w-24 rounded-xl bg-white p-3 shadow-lg">
|
||||||
<img
|
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
|
||||||
src={logo}
|
|
||||||
alt="Logo SGSE"
|
|
||||||
class="w-full h-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-3xl font-bold text-primary mb-2">SGSE</h3>
|
<h3 class="text-primary mb-2 text-3xl font-bold">SGSE</h3>
|
||||||
<p class="text-lg font-semibold text-base-content/80">
|
<p class="text-base-content/80 text-lg font-semibold">
|
||||||
Sistema de Gerenciamento da<br />Secretaria de Esportes
|
Sistema de Gerenciamento da<br />Secretaria de Esportes
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -566,12 +527,12 @@
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<!-- Informações de Versão -->
|
<!-- Informações de Versão -->
|
||||||
<div class="bg-primary/10 rounded-xl p-6 space-y-3">
|
<div class="bg-primary/10 space-y-3 rounded-xl p-6">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
<Tag class="h-5 w-5 text-primary" strokeWidth={2} />
|
<Tag class="text-primary h-5 w-5" strokeWidth={2} />
|
||||||
<p class="text-sm font-medium text-base-content/70">Versão</p>
|
<p class="text-base-content/70 text-sm font-medium">Versão</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold text-primary">1.0 26_2025</p>
|
<p class="text-primary text-2xl font-bold">1.0 26_2025</p>
|
||||||
<div class="badge badge-warning badge-lg gap-2">
|
<div class="badge badge-warning badge-lg gap-2">
|
||||||
<Plus class="h-4 w-4" strokeWidth={2} />
|
<Plus class="h-4 w-4" strokeWidth={2} />
|
||||||
Em Desenvolvimento
|
Em Desenvolvimento
|
||||||
@@ -580,12 +541,8 @@
|
|||||||
|
|
||||||
<!-- Desenvolvido por -->
|
<!-- Desenvolvido por -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-medium text-base-content/60">
|
<p class="text-base-content/60 text-sm font-medium">Desenvolvido por</p>
|
||||||
Desenvolvido por
|
<p class="text-primary text-lg font-bold">Secretaria de Esportes de Pernambuco</p>
|
||||||
</p>
|
|
||||||
<p class="text-lg font-bold text-primary">
|
|
||||||
Secretaria de Esportes de Pernambuco
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
@@ -594,12 +551,12 @@
|
|||||||
<!-- Informações Adicionais -->
|
<!-- Informações Adicionais -->
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div class="bg-base-200 rounded-lg p-3">
|
<div class="bg-base-200 rounded-lg p-3">
|
||||||
<p class="font-semibold text-primary">Governo</p>
|
<p class="text-primary font-semibold">Governo</p>
|
||||||
<p class="text-xs text-base-content/70">Estado de Pernambuco</p>
|
<p class="text-base-content/70 text-xs">Estado de Pernambuco</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-base-200 rounded-lg p-3">
|
<div class="bg-base-200 rounded-lg p-3">
|
||||||
<p class="font-semibold text-primary">Ano</p>
|
<p class="text-primary font-semibold">Ano</p>
|
||||||
<p class="text-xs text-base-content/70">2025</p>
|
<p class="text-base-content/70 text-xs">2025</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -607,7 +564,7 @@
|
|||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-lg w-full max-w-xs mx-auto shadow-lg hover:shadow-xl transition-all duration-300"
|
class="btn btn-primary btn-lg mx-auto w-full max-w-xs shadow-lg transition-all duration-300 hover:shadow-xl"
|
||||||
onclick={closeAboutModal}
|
onclick={closeAboutModal}
|
||||||
>
|
>
|
||||||
<Check class="h-6 w-6" strokeWidth={2} />
|
<Check class="h-6 w-6" strokeWidth={2} />
|
||||||
@@ -621,7 +578,7 @@
|
|||||||
onclick={closeAboutModal}
|
onclick={closeAboutModal}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onkeydown={(e) => e.key === "Escape" && closeAboutModal()}
|
onkeydown={(e) => e.key === 'Escape' && closeAboutModal()}
|
||||||
></div>
|
></div>
|
||||||
</dialog>
|
</dialog>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { abrirConversa } from "$lib/stores/chatStore";
|
import { abrirConversa } from '$lib/stores/chatStore';
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import { ptBR } from "date-fns/locale";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import NewConversationModal from './NewConversationModal.svelte';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import NewConversationModal from "./NewConversationModal.svelte";
|
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -19,138 +18,102 @@
|
|||||||
// Buscar conversas (grupos e salas de reunião)
|
// Buscar conversas (grupos e salas de reunião)
|
||||||
const conversas = useQuery(api.chat.listarConversas, {});
|
const conversas = useQuery(api.chat.listarConversas, {});
|
||||||
|
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state('');
|
||||||
let activeTab = $state<"usuarios" | "conversas">("usuarios");
|
let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
|
||||||
|
|
||||||
// Debug: monitorar carregamento de dados
|
|
||||||
$effect(() => {
|
|
||||||
console.log(
|
|
||||||
"📊 [ChatList] Usuários carregados:",
|
|
||||||
usuarios?.data?.length || 0,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"👤 [ChatList] Meu perfil:",
|
|
||||||
meuPerfil?.data?.nome || "Carregando...",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"🆔 [ChatList] Meu ID:",
|
|
||||||
meuPerfil?.data?._id || "Não encontrado",
|
|
||||||
);
|
|
||||||
if (usuarios?.data) {
|
|
||||||
const meuId = meuPerfil?.data?._id;
|
|
||||||
const meusDadosNaLista = usuarios.data.find((u: any) => u._id === meuId);
|
|
||||||
if (meusDadosNaLista) {
|
|
||||||
console.warn(
|
|
||||||
"⚠️ [ChatList] ATENÇÃO: Meu usuário está na lista do backend!",
|
|
||||||
meusDadosNaLista.nome,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const usuariosFiltrados = $derived.by(() => {
|
const usuariosFiltrados = $derived.by(() => {
|
||||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
||||||
|
|
||||||
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
|
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
|
||||||
if (!meuPerfil?.data) {
|
if (!meuPerfil?.data) {
|
||||||
console.log("⏳ [ChatList] Aguardando perfil do usuário...");
|
console.log('⏳ [ChatList] Aguardando perfil do usuário...');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const meuId = meuPerfil.data._id;
|
const meuId = meuPerfil.data._id;
|
||||||
|
|
||||||
// Filtrar o próprio usuário da lista (filtro de segurança no frontend)
|
// Filtrar o próprio usuário da lista (filtro de segurança no frontend)
|
||||||
let listaFiltrada = usuarios.data.filter((u: any) => u._id !== meuId);
|
let listaFiltrada = usuarios.data.filter((u) => u._id !== meuId);
|
||||||
|
|
||||||
// Log se ainda estiver na lista após filtro (não deveria acontecer)
|
// Log se ainda estiver na lista após filtro (não deveria acontecer)
|
||||||
const aindaNaLista = listaFiltrada.find((u: any) => u._id === meuId);
|
const aindaNaLista = listaFiltrada.find((u) => u._id === meuId);
|
||||||
if (aindaNaLista) {
|
if (aindaNaLista) {
|
||||||
console.error(
|
console.error('❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!');
|
||||||
"❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar busca por nome/email/matrícula
|
// Aplicar busca por nome/email/matrícula
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
listaFiltrada = listaFiltrada.filter(
|
listaFiltrada = listaFiltrada.filter(
|
||||||
(u: any) =>
|
(u) =>
|
||||||
u.nome?.toLowerCase().includes(query) ||
|
u.nome?.toLowerCase().includes(query) ||
|
||||||
u.email?.toLowerCase().includes(query) ||
|
u.email?.toLowerCase().includes(query) ||
|
||||||
u.matricula?.toLowerCase().includes(query),
|
u.matricula?.toLowerCase().includes(query)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordenar: Online primeiro, depois por nome
|
// Ordenar: Online primeiro, depois por nome
|
||||||
return listaFiltrada.sort((a: any, b: any) => {
|
return listaFiltrada.sort((a, b) => {
|
||||||
const statusOrder = {
|
const statusOrder = {
|
||||||
online: 0,
|
online: 0,
|
||||||
ausente: 1,
|
ausente: 1,
|
||||||
externo: 2,
|
externo: 2,
|
||||||
em_reuniao: 3,
|
em_reuniao: 3,
|
||||||
offline: 4,
|
offline: 4
|
||||||
};
|
};
|
||||||
const statusA =
|
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
const statusB =
|
|
||||||
statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
|
||||||
|
|
||||||
if (statusA !== statusB) return statusA - statusB;
|
if (statusA !== statusB) return statusA - statusB;
|
||||||
return a.nome.localeCompare(b.nome);
|
return a.nome.localeCompare(b.nome);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatarTempo(timestamp: number | undefined): string {
|
|
||||||
if (!timestamp) return "";
|
|
||||||
try {
|
|
||||||
return formatDistanceToNow(new Date(timestamp), {
|
|
||||||
addSuffix: true,
|
|
||||||
locale: ptBR,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
let showNewConversationModal = $state(false);
|
let showNewConversationModal = $state(false);
|
||||||
|
|
||||||
async function handleClickUsuario(usuario: any) {
|
async function handleClickUsuario(usuario: {
|
||||||
|
_id: Id<'usuarios'>;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
matricula: string | undefined;
|
||||||
|
avatar: string | undefined;
|
||||||
|
fotoPerfil: Id<'_storage'> | undefined;
|
||||||
|
fotoPerfilUrl: string | null;
|
||||||
|
statusPresenca: 'online' | 'offline' | 'ausente' | 'externo' | 'em_reuniao';
|
||||||
|
statusMensagem: string | undefined;
|
||||||
|
ultimaAtividade: number | undefined;
|
||||||
|
}) {
|
||||||
if (processando) {
|
if (processando) {
|
||||||
console.log("⏳ Já está processando uma ação, aguarde...");
|
console.log('⏳ Já está processando uma ação, aguarde...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processando = true;
|
processando = true;
|
||||||
console.log("🔄 Clicou no usuário:", usuario.nome, "ID:", usuario._id);
|
console.log('🔄 Clicou no usuário:', usuario.nome, 'ID:', usuario._id);
|
||||||
|
|
||||||
// Criar ou buscar conversa individual com este usuário
|
// Criar ou buscar conversa individual com este usuário
|
||||||
console.log("📞 Chamando mutation criarOuBuscarConversaIndividual...");
|
console.log('📞 Chamando mutation criarOuBuscarConversaIndividual...');
|
||||||
const conversaId = await client.mutation(
|
const conversaId = await client.mutation(api.chat.criarOuBuscarConversaIndividual, {
|
||||||
api.chat.criarOuBuscarConversaIndividual,
|
outroUsuarioId: usuario._id
|
||||||
{
|
});
|
||||||
outroUsuarioId: usuario._id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Conversa criada/encontrada. ID:", conversaId);
|
console.log('✅ Conversa criada/encontrada. ID:', conversaId);
|
||||||
|
|
||||||
// Abrir a conversa
|
// Abrir a conversa
|
||||||
console.log("📂 Abrindo conversa...");
|
console.log('📂 Abrindo conversa...');
|
||||||
abrirConversa(conversaId as any);
|
abrirConversa(conversaId);
|
||||||
|
|
||||||
console.log("✅ Conversa aberta com sucesso!");
|
console.log('✅ Conversa aberta com sucesso!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erro ao abrir conversa:", error);
|
console.error('❌ Erro ao abrir conversa:', error);
|
||||||
console.error("Detalhes do erro:", {
|
console.error('Detalhes do erro:', {
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error),
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
usuario: usuario,
|
usuario: usuario
|
||||||
});
|
});
|
||||||
alert(
|
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
@@ -158,51 +121,47 @@
|
|||||||
|
|
||||||
function getStatusLabel(status: string | undefined): string {
|
function getStatusLabel(status: string | undefined): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
online: "Online",
|
online: 'Online',
|
||||||
offline: "Offline",
|
offline: 'Offline',
|
||||||
ausente: "Ausente",
|
ausente: 'Ausente',
|
||||||
externo: "Externo",
|
externo: 'Externo',
|
||||||
em_reuniao: "Em Reunião",
|
em_reuniao: 'Em Reunião'
|
||||||
};
|
};
|
||||||
return labels[status || "offline"] || "Offline";
|
return labels[status || 'offline'] || 'Offline';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar conversas por tipo e busca
|
// Filtrar conversas por tipo e busca
|
||||||
const conversasFiltradas = $derived(() => {
|
const conversasFiltradas = $derived(() => {
|
||||||
if (!conversas?.data) return [];
|
if (!conversas?.data) return [];
|
||||||
|
|
||||||
let lista = conversas.data.filter(
|
let lista = conversas.data.filter((c) => c.tipo === 'grupo' || c.tipo === 'sala_reuniao');
|
||||||
(c: any) => c.tipo === "grupo" || c.tipo === "sala_reuniao",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Aplicar busca
|
// Aplicar busca
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
lista = lista.filter((c: any) => c.nome?.toLowerCase().includes(query));
|
lista = lista.filter((c) => c.nome?.toLowerCase().includes(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lista;
|
return lista;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClickConversa(conversa: any) {
|
function handleClickConversa(conversa: Doc<'conversas'>) {
|
||||||
if (processando) return;
|
if (processando) return;
|
||||||
try {
|
try {
|
||||||
processando = true;
|
processando = true;
|
||||||
abrirConversa(conversa._id);
|
abrirConversa(conversa._id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao abrir conversa:", error);
|
console.error('Erro ao abrir conversa:', error);
|
||||||
alert(
|
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex h-full flex-col">
|
||||||
<!-- Search bar -->
|
<!-- Search bar -->
|
||||||
<div class="p-4 border-b border-base-300">
|
<div class="border-base-300 border-b p-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -216,7 +175,7 @@
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50"
|
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -228,27 +187,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs e Título -->
|
<!-- Tabs e Título -->
|
||||||
<div class="border-b border-base-300 bg-base-200">
|
<div class="border-base-300 bg-base-200 border-b">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-boxed p-2">
|
<div class="tabs tabs-boxed p-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex-1 ${activeTab === "usuarios" ? "tab-active" : ""}`}
|
class={`tab flex-1 ${activeTab === 'usuarios' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "usuarios")}
|
onclick={() => (activeTab = 'usuarios')}
|
||||||
>
|
>
|
||||||
👥 Usuários ({usuariosFiltrados.length})
|
👥 Usuários ({usuariosFiltrados.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex-1 ${activeTab === "conversas" ? "tab-active" : ""}`}
|
class={`tab flex-1 ${activeTab === 'conversas' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "conversas")}
|
onclick={() => (activeTab = 'conversas')}
|
||||||
>
|
>
|
||||||
💬 Conversas ({conversasFiltradas().length})
|
💬 Conversas ({conversasFiltradas().length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botão Nova Conversa -->
|
<!-- Botão Nova Conversa -->
|
||||||
<div class="px-4 pb-2 flex justify-end">
|
<div class="flex justify-end px-4 pb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
@@ -262,13 +221,9 @@
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4 mr-1"
|
class="mr-1 h-4 w-4"
|
||||||
>
|
>
|
||||||
<path
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 4.5v15m7.5-7.5h-15"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
Nova Conversa
|
Nova Conversa
|
||||||
</button>
|
</button>
|
||||||
@@ -277,21 +232,21 @@
|
|||||||
|
|
||||||
<!-- Lista de conteúdo -->
|
<!-- Lista de conteúdo -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{#if activeTab === "usuarios"}
|
{#if activeTab === 'usuarios'}
|
||||||
<!-- Lista de usuários -->
|
<!-- Lista de usuários -->
|
||||||
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
||||||
{#each usuariosFiltrados as usuario (usuario._id)}
|
{#each usuariosFiltrados as usuario (usuario._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
|
class="hover:bg-base-200 border-base-300 flex w-full items-center gap-3 border-b px-4 py-3 text-left transition-colors {processando
|
||||||
? 'opacity-50 cursor-wait'
|
? 'cursor-wait opacity-50'
|
||||||
: 'cursor-pointer'}"
|
: 'cursor-pointer'}"
|
||||||
onclick={() => handleClickUsuario(usuario)}
|
onclick={() => handleClickUsuario(usuario)}
|
||||||
disabled={processando}
|
disabled={processando}
|
||||||
>
|
>
|
||||||
<!-- Ícone de mensagem -->
|
<!-- Ícone de mensagem -->
|
||||||
<div
|
<div
|
||||||
class="shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110"
|
||||||
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);"
|
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -302,11 +257,9 @@
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="w-5 h-5 text-primary"
|
class="text-primary h-5 w-5"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
||||||
/>
|
|
||||||
<path d="M9 10h.01M15 10h.01" />
|
<path d="M9 10h.01M15 10h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,23 +273,19 @@
|
|||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<!-- Status badge -->
|
<!-- Status badge -->
|
||||||
<div class="absolute bottom-0 right-0">
|
<div class="absolute right-0 bottom-0">
|
||||||
<UserStatusBadge
|
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
|
||||||
status={usuario.statusPresenca || "offline"}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="mb-1 flex items-center justify-between">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{usuario.nome}
|
{usuario.nome}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
class="text-xs px-2 py-0.5 rounded-full {usuario.statusPresenca ===
|
class="rounded-full px-2 py-0.5 text-xs {usuario.statusPresenca === 'online'
|
||||||
'online'
|
|
||||||
? 'bg-success/20 text-success'
|
? 'bg-success/20 text-success'
|
||||||
: usuario.statusPresenca === 'ausente'
|
: usuario.statusPresenca === 'ausente'
|
||||||
? 'bg-warning/20 text-warning'
|
? 'bg-warning/20 text-warning'
|
||||||
@@ -348,7 +297,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm text-base-content/70 truncate">
|
<p class="text-base-content/70 truncate text-sm">
|
||||||
{usuario.statusMensagem || usuario.email}
|
{usuario.statusMensagem || usuario.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,21 +306,19 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{:else if !usuarios?.data}
|
{:else if !usuarios?.data}
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Nenhum usuário encontrado -->
|
<!-- Nenhum usuário encontrado -->
|
||||||
<div
|
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-16 h-16 text-base-content/30 mb-4"
|
class="text-base-content/30 mb-4 h-16 w-16"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -388,27 +335,27 @@
|
|||||||
{#each conversasFiltradas() as conversa (conversa._id)}
|
{#each conversasFiltradas() as conversa (conversa._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
|
class="hover:bg-base-200 border-base-300 flex w-full items-center gap-3 border-b px-4 py-3 text-left transition-colors {processando
|
||||||
? 'opacity-50 cursor-wait'
|
? 'cursor-wait opacity-50'
|
||||||
: 'cursor-pointer'}"
|
: 'cursor-pointer'}"
|
||||||
onclick={() => handleClickConversa(conversa)}
|
onclick={() => handleClickConversa(conversa)}
|
||||||
disabled={processando}
|
disabled={processando}
|
||||||
>
|
>
|
||||||
<!-- Ícone de grupo/sala -->
|
<!-- Ícone de grupo/sala -->
|
||||||
<div
|
<div
|
||||||
class="shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {conversa.tipo ===
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110 {conversa.tipo ===
|
||||||
'sala_reuniao'
|
'sala_reuniao'
|
||||||
? 'bg-linear-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
|
? 'border border-blue-300/30 bg-linear-to-br from-blue-500/20 to-purple-500/20'
|
||||||
: 'bg-linear-to-br from-primary/20 to-secondary/20 border border-primary/30'}"
|
: 'from-primary/20 to-secondary/20 border-primary/30 border bg-linear-to-br'}"
|
||||||
>
|
>
|
||||||
{#if conversa.tipo === "sala_reuniao"}
|
{#if conversa.tipo === 'sala_reuniao'}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-5 h-5 text-blue-500"
|
class="h-5 w-5 text-blue-500"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -423,7 +370,7 @@
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-5 h-5 text-primary"
|
class="text-primary h-5 w-5"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -435,37 +382,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="mb-1 flex items-center justify-between">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{conversa.nome ||
|
{conversa.nome ||
|
||||||
(conversa.tipo === "sala_reuniao"
|
(conversa.tipo === 'sala_reuniao' ? 'Sala sem nome' : 'Grupo sem nome')}
|
||||||
? "Sala sem nome"
|
|
||||||
: "Grupo sem nome")}
|
|
||||||
</p>
|
</p>
|
||||||
{#if conversa.naoLidas > 0}
|
{#if conversa.naoLidas > 0}
|
||||||
<span class="badge badge-primary badge-sm"
|
<span class="badge badge-primary badge-sm">{conversa.naoLidas}</span>
|
||||||
>{conversa.naoLidas}</span
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="text-xs px-2 py-0.5 rounded-full {conversa.tipo ===
|
class="rounded-full px-2 py-0.5 text-xs {conversa.tipo === 'sala_reuniao'
|
||||||
'sala_reuniao'
|
|
||||||
? 'bg-blue-500/20 text-blue-500'
|
? 'bg-blue-500/20 text-blue-500'
|
||||||
: 'bg-primary/20 text-primary'}"
|
: 'bg-primary/20 text-primary'}"
|
||||||
>
|
>
|
||||||
{conversa.tipo === "sala_reuniao"
|
{conversa.tipo === 'sala_reuniao' ? '👑 Sala de Reunião' : '👥 Grupo'}
|
||||||
? "👑 Sala de Reunião"
|
|
||||||
: "👥 Grupo"}
|
|
||||||
</span>
|
</span>
|
||||||
{#if conversa.participantesInfo}
|
{#if conversa.participantesInfo}
|
||||||
<span class="text-xs text-base-content/50">
|
<span class="text-base-content/50 text-xs">
|
||||||
{conversa.participantesInfo.length} participante{conversa
|
{conversa.participantesInfo.length} participante{conversa.participantesInfo
|
||||||
.participantesInfo.length !== 1
|
.length !== 1
|
||||||
? "s"
|
? 's'
|
||||||
: ""}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -474,21 +414,19 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{:else if !conversas?.data}
|
{:else if !conversas?.data}
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Nenhuma conversa encontrada -->
|
<!-- Nenhuma conversa encontrada -->
|
||||||
<div
|
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-16 h-16 text-base-content/30 mb-4"
|
class="text-base-content/30 mb-4 h-16 w-16"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -496,12 +434,8 @@
|
|||||||
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-base-content/70 font-medium mb-2">
|
<p class="text-base-content/70 mb-2 font-medium">Nenhuma conversa encontrada</p>
|
||||||
Nenhuma conversa encontrada
|
<p class="text-base-content/50 text-sm">Crie um grupo ou sala de reunião para começar</p>
|
||||||
</p>
|
|
||||||
<p class="text-sm text-base-content/50">
|
|
||||||
Crie um grupo ou sala de reunião para começar
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { voltarParaLista } from "$lib/stores/chatStore";
|
import { voltarParaLista } from '$lib/stores/chatStore';
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import MessageList from "./MessageList.svelte";
|
import MessageList from './MessageList.svelte';
|
||||||
import MessageInput from "./MessageInput.svelte";
|
import MessageInput from './MessageInput.svelte';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import ScheduleMessageModal from "./ScheduleMessageModal.svelte";
|
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
|
||||||
import SalaReuniaoManager from "./SalaReuniaoManager.svelte";
|
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
|
||||||
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
|
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||||
import {
|
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
|
||||||
Bell,
|
|
||||||
X,
|
|
||||||
ArrowLeft,
|
|
||||||
LogOut,
|
|
||||||
MoreVertical,
|
|
||||||
Users,
|
|
||||||
Clock,
|
|
||||||
XCircle,
|
|
||||||
} from "lucide-svelte";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversaId: string;
|
conversaId: string;
|
||||||
@@ -38,67 +29,54 @@
|
|||||||
|
|
||||||
const conversas = useQuery(api.chat.listarConversas, {});
|
const conversas = useQuery(api.chat.listarConversas, {});
|
||||||
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
|
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
|
||||||
conversaId: conversaId as Id<"conversas">,
|
conversaId: conversaId as Id<'conversas'>
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversa = $derived(() => {
|
const conversa = $derived(() => {
|
||||||
console.log("🔍 [ChatWindow] Buscando conversa ID:", conversaId);
|
console.log('🔍 [ChatWindow] Buscando conversa ID:', conversaId);
|
||||||
console.log("📋 [ChatWindow] Conversas disponíveis:", conversas?.data);
|
console.log('📋 [ChatWindow] Conversas disponíveis:', conversas?.data);
|
||||||
|
|
||||||
if (!conversas?.data || !Array.isArray(conversas.data)) {
|
if (!conversas?.data || !Array.isArray(conversas.data)) {
|
||||||
console.log(
|
console.log('⚠️ [ChatWindow] conversas.data não é um array ou está vazio');
|
||||||
"⚠️ [ChatWindow] conversas.data não é um array ou está vazio",
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encontrada = conversas.data.find(
|
const encontrada = conversas.data.find((c: { _id: string }) => c._id === conversaId);
|
||||||
(c: { _id: string }) => c._id === conversaId,
|
console.log('✅ [ChatWindow] Conversa encontrada:', encontrada);
|
||||||
);
|
|
||||||
console.log("✅ [ChatWindow] Conversa encontrada:", encontrada);
|
|
||||||
return encontrada;
|
return encontrada;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getNomeConversa(): string {
|
function getNomeConversa(): string {
|
||||||
const c = conversa();
|
const c = conversa();
|
||||||
if (!c) return "Carregando...";
|
if (!c) return 'Carregando...';
|
||||||
if (c.tipo === "grupo" || c.tipo === "sala_reuniao") {
|
if (c.tipo === 'grupo' || c.tipo === 'sala_reuniao') {
|
||||||
return (
|
return c.nome || (c.tipo === 'sala_reuniao' ? 'Sala sem nome' : 'Grupo sem nome');
|
||||||
c.nome ||
|
|
||||||
(c.tipo === "sala_reuniao" ? "Sala sem nome" : "Grupo sem nome")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return c.outroUsuario?.nome || "Usuário";
|
return c.outroUsuario?.nome || 'Usuário';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvatarConversa(): string {
|
function getAvatarConversa(): string {
|
||||||
const c = conversa();
|
const c = conversa();
|
||||||
if (!c) return "💬";
|
if (!c) return '💬';
|
||||||
if (c.tipo === "grupo") {
|
if (c.tipo === 'grupo') {
|
||||||
return c.avatar || "👥";
|
return c.avatar || '👥';
|
||||||
}
|
}
|
||||||
if (c.outroUsuario?.avatar) {
|
if (c.outroUsuario?.avatar) {
|
||||||
return c.outroUsuario.avatar;
|
return c.outroUsuario.avatar;
|
||||||
}
|
}
|
||||||
return "👤";
|
return '👤';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusConversa():
|
function getStatusConversa(): 'online' | 'offline' | 'ausente' | 'externo' | 'em_reuniao' | null {
|
||||||
| "online"
|
|
||||||
| "offline"
|
|
||||||
| "ausente"
|
|
||||||
| "externo"
|
|
||||||
| "em_reuniao"
|
|
||||||
| null {
|
|
||||||
const c = conversa();
|
const c = conversa();
|
||||||
if (c && c.tipo === "individual" && c.outroUsuario) {
|
if (c && c.tipo === 'individual' && c.outroUsuario) {
|
||||||
return (
|
return (
|
||||||
(c.outroUsuario.statusPresenca as
|
(c.outroUsuario.statusPresenca as
|
||||||
| "online"
|
| 'online'
|
||||||
| "offline"
|
| 'offline'
|
||||||
| "ausente"
|
| 'ausente'
|
||||||
| "externo"
|
| 'externo'
|
||||||
| "em_reuniao") || "offline"
|
| 'em_reuniao') || 'offline'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -106,7 +84,7 @@
|
|||||||
|
|
||||||
function getStatusMensagem(): string | null {
|
function getStatusMensagem(): string | null {
|
||||||
const c = conversa();
|
const c = conversa();
|
||||||
if (c && c.tipo === "individual" && c.outroUsuario) {
|
if (c && c.tipo === 'individual' && c.outroUsuario) {
|
||||||
return c.outroUsuario.statusMensagem || null;
|
return c.outroUsuario.statusMensagem || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -114,145 +92,139 @@
|
|||||||
|
|
||||||
async function handleSairGrupoOuSala() {
|
async function handleSairGrupoOuSala() {
|
||||||
const c = conversa();
|
const c = conversa();
|
||||||
if (!c || (c.tipo !== "grupo" && c.tipo !== "sala_reuniao")) return;
|
if (!c || (c.tipo !== 'grupo' && c.tipo !== 'sala_reuniao')) return;
|
||||||
|
|
||||||
const tipoTexto = c.tipo === "sala_reuniao" ? "sala de reunião" : "grupo";
|
const tipoTexto = c.tipo === 'sala_reuniao' ? 'sala de reunião' : 'grupo';
|
||||||
if (
|
if (!confirm(`Tem certeza que deseja sair da ${tipoTexto} "${c.nome || 'Sem nome'}"?`)) {
|
||||||
!confirm(
|
|
||||||
`Tem certeza que deseja sair da ${tipoTexto} "${c.nome || "Sem nome"}"?`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resultado = await client.mutation(api.chat.sairGrupoOuSala, {
|
const resultado = await client.mutation(api.chat.sairGrupoOuSala, {
|
||||||
conversaId: conversaId as Id<"conversas">,
|
conversaId: conversaId as Id<'conversas'>
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resultado.sucesso) {
|
if (resultado.sucesso) {
|
||||||
voltarParaLista();
|
voltarParaLista();
|
||||||
} else {
|
} else {
|
||||||
alert(resultado.erro || "Erro ao sair da conversa");
|
alert(resultado.erro || 'Erro ao sair da conversa');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao sair da conversa:", error);
|
console.error('Erro ao sair da conversa:', error);
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : 'Erro ao sair da conversa';
|
||||||
error instanceof Error ? error.message : "Erro ao sair da conversa";
|
|
||||||
alert(errorMessage);
|
alert(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDocumentClick() {
|
||||||
|
if (showAdminMenu) showAdminMenu = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full" onclick={() => (showAdminMenu = false)}>
|
<svelte:window onclick={handleDocumentClick} />
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div class="border-base-300 bg-base-200 flex items-center gap-3 border-b px-4 py-3">
|
||||||
class="flex items-center gap-3 px-4 py-3 border-b border-base-300 bg-base-200"
|
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<!-- Botão Voltar -->
|
<!-- Botão Voltar -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm btn-circle hover:bg-primary/20 transition-all duration-200 hover:scale-110"
|
class="btn btn-circle hover:bg-primary/20 transition-all duration-200 hover:scale-110"
|
||||||
onclick={voltarParaLista}
|
onclick={voltarParaLista}
|
||||||
aria-label="Voltar"
|
aria-label="Voltar"
|
||||||
title="Voltar para lista de conversas"
|
title="Voltar para lista de conversas"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="w-6 h-6 text-primary" strokeWidth={2.5} />
|
<ArrowLeft class="text-primary h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Avatar e Info -->
|
<!-- Avatar e Info -->
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
{#if conversa() && conversa()?.tipo === "individual" && conversa()?.outroUsuario}
|
{#if conversa() && conversa()?.tipo === 'individual' && conversa()?.outroUsuario}
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={conversa()?.outroUsuario?.avatar}
|
avatar={conversa()?.outroUsuario?.avatar}
|
||||||
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
|
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
|
||||||
nome={conversa()?.outroUsuario?.nome || "Usuário"}
|
nome={conversa()?.outroUsuario?.nome || 'Usuário'}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-full text-xl">
|
||||||
class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-xl"
|
|
||||||
>
|
|
||||||
{getAvatarConversa()}
|
{getAvatarConversa()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if getStatusConversa()}
|
{#if getStatusConversa()}
|
||||||
<div class="absolute bottom-0 right-0">
|
<div class="absolute right-0 bottom-0">
|
||||||
<UserStatusBadge status={getStatusConversa()} size="sm" />
|
<UserStatusBadge status={getStatusConversa()} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{getNomeConversa()}
|
{getNomeConversa()}
|
||||||
</p>
|
</p>
|
||||||
{#if getStatusMensagem()}
|
{#if getStatusMensagem()}
|
||||||
<p class="text-xs text-base-content/60 truncate">
|
<p class="text-base-content/60 truncate text-xs">
|
||||||
{getStatusMensagem()}
|
{getStatusMensagem()}
|
||||||
</p>
|
</p>
|
||||||
{:else if getStatusConversa()}
|
{:else if getStatusConversa()}
|
||||||
<p class="text-xs text-base-content/60">
|
<p class="text-base-content/60 text-xs">
|
||||||
{getStatusConversa() === "online"
|
{getStatusConversa() === 'online'
|
||||||
? "Online"
|
? 'Online'
|
||||||
: getStatusConversa() === "ausente"
|
: getStatusConversa() === 'ausente'
|
||||||
? "Ausente"
|
? 'Ausente'
|
||||||
: getStatusConversa() === "em_reuniao"
|
: getStatusConversa() === 'em_reuniao'
|
||||||
? "Em reunião"
|
? 'Em reunião'
|
||||||
: getStatusConversa() === "externo"
|
: getStatusConversa() === 'externo'
|
||||||
? "Externo"
|
? 'Externo'
|
||||||
: "Offline"}
|
: 'Offline'}
|
||||||
</p>
|
</p>
|
||||||
{:else if conversa() && (conversa()?.tipo === "grupo" || conversa()?.tipo === "sala_reuniao")}
|
{:else if conversa() && (conversa()?.tipo === 'grupo' || conversa()?.tipo === 'sala_reuniao')}
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<div class="mt-1 flex items-center gap-2">
|
||||||
<p class="text-xs text-base-content/60">
|
<p class="text-base-content/60 text-xs">
|
||||||
{conversa()?.participantesInfo?.length || 0}
|
{conversa()?.participantesInfo?.length || 0}
|
||||||
{conversa()?.participantesInfo?.length === 1
|
{conversa()?.participantesInfo?.length === 1 ? 'participante' : 'participantes'}
|
||||||
? "participante"
|
|
||||||
: "participantes"}
|
|
||||||
</p>
|
</p>
|
||||||
{#if conversa()?.participantesInfo && conversa()?.participantesInfo.length > 0}
|
{#if conversa()?.participantesInfo && conversa()?.participantesInfo.length > 0}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex -space-x-2">
|
<div class="flex -space-x-2">
|
||||||
{#each conversa()?.participantesInfo.slice(0, 5) as participante (participante._id)}
|
{#each conversa()?.participantesInfo.slice(0, 5) as participante (participante._id)}
|
||||||
<div
|
<div
|
||||||
class="relative w-5 h-5 rounded-full border-2 border-base-200 overflow-hidden bg-base-200"
|
class="border-base-200 bg-base-200 relative h-5 w-5 overflow-hidden rounded-full border-2"
|
||||||
title={participante.nome}
|
title={participante.nome}
|
||||||
>
|
>
|
||||||
{#if participante.fotoPerfilUrl}
|
{#if participante.fotoPerfilUrl}
|
||||||
<img
|
<img
|
||||||
src={participante.fotoPerfilUrl}
|
src={participante.fotoPerfilUrl}
|
||||||
alt={participante.nome}
|
alt={participante.nome}
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else if participante.avatar}
|
{:else if participante.avatar}
|
||||||
<img
|
<img
|
||||||
src={getAvatarUrl(participante.avatar)}
|
src={getAvatarUrl(participante.avatar)}
|
||||||
alt={participante.nome}
|
alt={participante.nome}
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
<img
|
||||||
src={getAvatarUrl(participante.nome)}
|
src={getAvatarUrl(participante.nome)}
|
||||||
alt={participante.nome}
|
alt={participante.nome}
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if conversa()?.participantesInfo.length > 5}
|
{#if conversa()?.participantesInfo.length > 5}
|
||||||
<div
|
<div
|
||||||
class="w-5 h-5 rounded-full border-2 border-base-200 bg-base-300 flex items-center justify-center text-[8px] font-semibold text-base-content/70"
|
class="border-base-200 bg-base-300 text-base-content/70 flex h-5 w-5 items-center justify-center rounded-full border-2 text-[8px] font-semibold"
|
||||||
title={`+${conversa()?.participantesInfo.length - 5} mais`}
|
title={`+${conversa()?.participantesInfo.length - 5} mais`}
|
||||||
>
|
>
|
||||||
+{conversa()?.participantesInfo.length - 5}
|
+{conversa()?.participantesInfo.length - 5}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
|
||||||
<span
|
<span
|
||||||
class="text-[10px] text-primary font-semibold ml-1 whitespace-nowrap"
|
class="text-primary ml-1 text-[10px] font-semibold whitespace-nowrap"
|
||||||
title="Você é administrador desta sala">• Admin</span
|
title="Você é administrador desta sala">• Admin</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -265,10 +237,10 @@
|
|||||||
<!-- Botões de ação -->
|
<!-- Botões de ação -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<!-- Botão Sair (apenas para grupos e salas de reunião) -->
|
<!-- Botão Sair (apenas para grupos e salas de reunião) -->
|
||||||
{#if conversa() && (conversa()?.tipo === "grupo" || conversa()?.tipo === "sala_reuniao")}
|
{#if conversa() && (conversa()?.tipo === 'grupo' || conversa()?.tipo === 'sala_reuniao')}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
|
||||||
style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
|
style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -278,21 +250,21 @@
|
|||||||
title="Sair da conversa"
|
title="Sair da conversa"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-red-500/0 group-hover:bg-red-500/10 transition-colors duration-300"
|
class="absolute inset-0 bg-red-500/0 transition-colors duration-300 group-hover:bg-red-500/10"
|
||||||
></div>
|
></div>
|
||||||
<LogOut
|
<LogOut
|
||||||
class="w-5 h-5 text-red-500 relative z-10 group-hover:scale-110 transition-transform"
|
class="relative z-10 h-5 w-5 text-red-500 transition-transform group-hover:scale-110"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Botão Menu Administrativo (apenas para salas de reunião e apenas para admins) -->
|
<!-- Botão Menu Administrativo (apenas para salas de reunião e apenas para admins) -->
|
||||||
{#if conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
|
||||||
<div class="relative admin-menu-container">
|
<div class="admin-menu-container relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
|
||||||
style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);"
|
style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -302,84 +274,78 @@
|
|||||||
title="Recursos administrativos"
|
title="Recursos administrativos"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-blue-500/0 group-hover:bg-blue-500/10 transition-colors duration-300"
|
class="absolute inset-0 bg-blue-500/0 transition-colors duration-300 group-hover:bg-blue-500/10"
|
||||||
></div>
|
></div>
|
||||||
<MoreVertical
|
<MoreVertical
|
||||||
class="w-5 h-5 text-blue-500 relative z-10 group-hover:scale-110 transition-transform"
|
class="relative z-10 h-5 w-5 text-blue-500 transition-transform group-hover:scale-110"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{#if showAdminMenu}
|
{#if showAdminMenu}
|
||||||
<ul
|
<ul
|
||||||
class="absolute right-0 top-full mt-2 bg-base-100 rounded-lg shadow-xl border border-base-300 w-56 z-[100] overflow-hidden"
|
class="bg-base-100 border-base-300 absolute top-full right-0 z-[100] mt-2 w-56 overflow-hidden rounded-lg border shadow-xl"
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
class="hover:bg-base-200 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showSalaManager = true;
|
showSalaManager = true;
|
||||||
showAdminMenu = false;
|
showAdminMenu = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Users class="w-4 h-4" strokeWidth={2} />
|
<Users class="h-4 w-4" strokeWidth={2} />
|
||||||
Gerenciar Participantes
|
Gerenciar Participantes
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
class="hover:bg-base-200 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showNotificacaoModal = true;
|
showNotificacaoModal = true;
|
||||||
showAdminMenu = false;
|
showAdminMenu = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Bell class="w-4 h-4" strokeWidth={2} />
|
<Bell class="h-4 w-4" strokeWidth={2} />
|
||||||
Enviar Notificação
|
Enviar Notificação
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 hover:bg-error/10 transition-colors flex items-center gap-2 text-error"
|
class="hover:bg-error/10 text-error flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
(async () => {
|
(async () => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
"Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.",
|
'Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
const resultado = await client.mutation(
|
const resultado = await client.mutation(api.chat.encerrarReuniao, {
|
||||||
api.chat.encerrarReuniao,
|
conversaId: conversaId as Id<'conversas'>
|
||||||
{
|
});
|
||||||
conversaId: conversaId as Id<"conversas">,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (resultado.sucesso) {
|
if (resultado.sucesso) {
|
||||||
alert("Reunião encerrada com sucesso!");
|
alert('Reunião encerrada com sucesso!');
|
||||||
voltarParaLista();
|
voltarParaLista();
|
||||||
} else {
|
} else {
|
||||||
alert(resultado.erro || "Erro ao encerrar reunião");
|
alert(resultado.erro || 'Erro ao encerrar reunião');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : 'Erro ao encerrar reunião';
|
||||||
? error.message
|
|
||||||
: "Erro ao encerrar reunião";
|
|
||||||
alert(errorMessage);
|
alert(errorMessage);
|
||||||
}
|
}
|
||||||
showAdminMenu = false;
|
showAdminMenu = false;
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XCircle class="w-4 h-4" strokeWidth={2} />
|
<XCircle class="h-4 w-4" strokeWidth={2} />
|
||||||
Encerrar Reunião
|
Encerrar Reunião
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -391,17 +357,17 @@
|
|||||||
<!-- Botão Agendar MODERNO -->
|
<!-- Botão Agendar MODERNO -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
|
||||||
style="background: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.2);"
|
style="background: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.2);"
|
||||||
onclick={() => (showScheduleModal = true)}
|
onclick={() => (showScheduleModal = true)}
|
||||||
aria-label="Agendar mensagem"
|
aria-label="Agendar mensagem"
|
||||||
title="Agendar mensagem"
|
title="Agendar mensagem"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-purple-500/0 group-hover:bg-purple-500/10 transition-colors duration-300"
|
class="absolute inset-0 bg-purple-500/0 transition-colors duration-300 group-hover:bg-purple-500/10"
|
||||||
></div>
|
></div>
|
||||||
<Clock
|
<Clock
|
||||||
class="w-5 h-5 text-purple-500 relative z-10 group-hover:scale-110 transition-transform"
|
class="relative z-10 h-5 w-5 text-purple-500 transition-transform group-hover:scale-110"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -409,46 +375,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mensagens -->
|
<!-- Mensagens -->
|
||||||
<div class="flex-1 overflow-hidden min-h-0">
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
<MessageList conversaId={conversaId as Id<"conversas">} />
|
<MessageList conversaId={conversaId as Id<'conversas'>} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input -->
|
<!-- Input -->
|
||||||
<div class="border-t border-base-300 shrink-0">
|
<div class="border-base-300 shrink-0 border-t">
|
||||||
<MessageInput conversaId={conversaId as Id<"conversas">} />
|
<MessageInput conversaId={conversaId as Id<'conversas'>} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de Agendamento -->
|
<!-- Modal de Agendamento -->
|
||||||
{#if showScheduleModal}
|
{#if showScheduleModal}
|
||||||
<ScheduleMessageModal
|
<ScheduleMessageModal
|
||||||
conversaId={conversaId as Id<"conversas">}
|
conversaId={conversaId as Id<'conversas'>}
|
||||||
onClose={() => (showScheduleModal = false)}
|
onClose={() => (showScheduleModal = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Modal de Gerenciamento de Sala -->
|
<!-- Modal de Gerenciamento de Sala -->
|
||||||
{#if showSalaManager && conversa()?.tipo === "sala_reuniao"}
|
{#if showSalaManager && conversa()?.tipo === 'sala_reuniao'}
|
||||||
<SalaReuniaoManager
|
<SalaReuniaoManager
|
||||||
conversaId={conversaId as Id<"conversas">}
|
conversaId={conversaId as Id<'conversas'>}
|
||||||
isAdmin={isAdmin?.data ?? false}
|
isAdmin={isAdmin?.data ?? false}
|
||||||
onClose={() => (showSalaManager = false)}
|
onClose={() => (showSalaManager = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Modal de Enviar Notificação -->
|
<!-- Modal de Enviar Notificação -->
|
||||||
{#if showNotificacaoModal && conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
{#if showNotificacaoModal && conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
|
||||||
<dialog
|
<dialog
|
||||||
class="modal modal-open"
|
class="modal modal-open"
|
||||||
onclick={(e) =>
|
onclick={(e) => e.target === e.currentTarget && (showNotificacaoModal = false)}
|
||||||
e.target === e.currentTarget && (showNotificacaoModal = false)}
|
|
||||||
>
|
>
|
||||||
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
|
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
|
||||||
<div
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300"
|
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
||||||
>
|
<Bell class="text-primary h-5 w-5" />
|
||||||
<h2 class="text-xl font-semibold flex items-center gap-2">
|
|
||||||
<Bell class="w-5 h-5 text-primary" />
|
|
||||||
Enviar Notificação
|
Enviar Notificação
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
@@ -456,7 +419,7 @@
|
|||||||
class="btn btn-ghost btn-sm btn-circle"
|
class="btn btn-ghost btn-sm btn-circle"
|
||||||
onclick={() => (showNotificacaoModal = false)}
|
onclick={() => (showNotificacaoModal = false)}
|
||||||
>
|
>
|
||||||
<X class="w-5 h-5" />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
@@ -464,35 +427,30 @@
|
|||||||
onsubmit={async (e) => {
|
onsubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const titulo = formData.get("titulo") as string;
|
const titulo = formData.get('titulo') as string;
|
||||||
const mensagem = formData.get("mensagem") as string;
|
const mensagem = formData.get('mensagem') as string;
|
||||||
|
|
||||||
if (!titulo.trim() || !mensagem.trim()) {
|
if (!titulo.trim() || !mensagem.trim()) {
|
||||||
alert("Preencha todos os campos");
|
alert('Preencha todos os campos');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resultado = await client.mutation(
|
const resultado = await client.mutation(api.chat.enviarNotificacaoReuniao, {
|
||||||
api.chat.enviarNotificacaoReuniao,
|
conversaId: conversaId as Id<'conversas'>,
|
||||||
{
|
|
||||||
conversaId: conversaId as Id<"conversas">,
|
|
||||||
titulo: titulo.trim(),
|
titulo: titulo.trim(),
|
||||||
mensagem: mensagem.trim(),
|
mensagem: mensagem.trim()
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (resultado.sucesso) {
|
if (resultado.sucesso) {
|
||||||
alert("Notificação enviada com sucesso!");
|
alert('Notificação enviada com sucesso!');
|
||||||
showNotificacaoModal = false;
|
showNotificacaoModal = false;
|
||||||
} else {
|
} else {
|
||||||
alert(resultado.erro || "Erro ao enviar notificação");
|
alert(resultado.erro || 'Erro ao enviar notificação');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : 'Erro ao enviar notificação';
|
||||||
? error.message
|
|
||||||
: "Erro ao enviar notificação";
|
|
||||||
alert(errorMessage);
|
alert(errorMessage);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -529,17 +487,13 @@
|
|||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary flex-1">
|
<button type="submit" class="btn btn-primary flex-1"> Enviar </button>
|
||||||
Enviar
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<form method="dialog" class="modal-backdrop">
|
||||||
<button type="button" onclick={() => (showNotificacaoModal = false)}
|
<button type="button" onclick={() => (showNotificacaoModal = false)}>fechar</button>
|
||||||
>fechar</button
|
|
||||||
>
|
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { abrirConversa } from "$lib/stores/chatStore";
|
import { abrirConversa } from '$lib/stores/chatStore';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
User,
|
User,
|
||||||
@@ -13,8 +13,9 @@
|
|||||||
Search,
|
Search,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Plus,
|
Plus,
|
||||||
UserX,
|
UserX
|
||||||
} from "lucide-svelte";
|
} from 'lucide-svelte';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -28,11 +29,11 @@
|
|||||||
// Usuário atual
|
// Usuário atual
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
|
|
||||||
let activeTab = $state<"individual" | "grupo" | "sala_reuniao">("individual");
|
let activeTab = $state<'individual' | 'grupo' | 'sala_reuniao'>('individual');
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state('');
|
||||||
let selectedUsers = $state<string[]>([]);
|
let selectedUsers = $state<Id<'usuarios'>[]>([]);
|
||||||
let groupName = $state("");
|
let groupName = $state('');
|
||||||
let salaReuniaoName = $state("");
|
let salaReuniaoName = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
const usuariosFiltrados = $derived(() => {
|
const usuariosFiltrados = $derived(() => {
|
||||||
@@ -40,39 +41,37 @@
|
|||||||
|
|
||||||
// Filtrar o próprio usuário
|
// Filtrar o próprio usuário
|
||||||
const meuId = currentUser?.data?._id || meuPerfil?.data?._id;
|
const meuId = currentUser?.data?._id || meuPerfil?.data?._id;
|
||||||
let lista = usuarios.data.filter((u: any) => u._id !== meuId);
|
let lista = usuarios.data.filter((u) => u._id !== meuId);
|
||||||
|
|
||||||
// Aplicar busca
|
// Aplicar busca
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
lista = lista.filter(
|
lista = lista.filter(
|
||||||
(u: any) =>
|
(u) =>
|
||||||
u.nome?.toLowerCase().includes(query) ||
|
u.nome?.toLowerCase().includes(query) ||
|
||||||
u.email?.toLowerCase().includes(query) ||
|
u.email?.toLowerCase().includes(query) ||
|
||||||
u.matricula?.toLowerCase().includes(query),
|
u.matricula?.toLowerCase().includes(query)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordenar: online primeiro, depois por nome
|
// Ordenar: online primeiro, depois por nome
|
||||||
return lista.sort((a: any, b: any) => {
|
return lista.sort((a, b) => {
|
||||||
const statusOrder = {
|
const statusOrder = {
|
||||||
online: 0,
|
online: 0,
|
||||||
ausente: 1,
|
ausente: 1,
|
||||||
externo: 2,
|
externo: 2,
|
||||||
em_reuniao: 3,
|
em_reuniao: 3,
|
||||||
offline: 4,
|
offline: 4
|
||||||
};
|
};
|
||||||
const statusA =
|
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
const statusB =
|
|
||||||
statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
|
||||||
|
|
||||||
if (statusA !== statusB) return statusA - statusB;
|
if (statusA !== statusB) return statusA - statusB;
|
||||||
return (a.nome || "").localeCompare(b.nome || "");
|
return (a.nome || '').localeCompare(b.nome || '');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleUserSelection(userId: string) {
|
function toggleUserSelection(userId: Id<'usuarios'>) {
|
||||||
if (selectedUsers.includes(userId)) {
|
if (selectedUsers.includes(userId)) {
|
||||||
selectedUsers = selectedUsers.filter((id) => id !== userId);
|
selectedUsers = selectedUsers.filter((id) => id !== userId);
|
||||||
} else {
|
} else {
|
||||||
@@ -80,18 +79,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCriarIndividual(userId: string) {
|
async function handleCriarIndividual(userId: Id<'usuarios'>) {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarConversa, {
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
||||||
tipo: "individual",
|
tipo: 'individual',
|
||||||
participantes: [userId as any],
|
participantes: [userId]
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar conversa:", error);
|
console.error('Erro ao criar conversa:', error);
|
||||||
alert("Erro ao criar conversa");
|
alert('Erro ao criar conversa');
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -99,29 +98,27 @@
|
|||||||
|
|
||||||
async function handleCriarGrupo() {
|
async function handleCriarGrupo() {
|
||||||
if (selectedUsers.length < 2) {
|
if (selectedUsers.length < 2) {
|
||||||
alert("Selecione pelo menos 2 participantes");
|
alert('Selecione pelo menos 2 participantes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupName.trim()) {
|
if (!groupName.trim()) {
|
||||||
alert("Digite um nome para o grupo");
|
alert('Digite um nome para o grupo');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarConversa, {
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
||||||
tipo: "grupo",
|
tipo: 'grupo',
|
||||||
participantes: selectedUsers as any,
|
participantes: selectedUsers,
|
||||||
nome: groupName.trim(),
|
nome: groupName.trim()
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar grupo:", error);
|
console.error('Erro ao criar grupo:', error);
|
||||||
const mensagem =
|
alert('Erro ao criar grupo');
|
||||||
error?.message || error?.data || "Erro desconhecido ao criar grupo";
|
|
||||||
alert(`Erro ao criar grupo: ${mensagem}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -129,12 +126,12 @@
|
|||||||
|
|
||||||
async function handleCriarSalaReuniao() {
|
async function handleCriarSalaReuniao() {
|
||||||
if (selectedUsers.length < 1) {
|
if (selectedUsers.length < 1) {
|
||||||
alert("Selecione pelo menos 1 participante");
|
alert('Selecione pelo menos 1 participante');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!salaReuniaoName.trim()) {
|
if (!salaReuniaoName.trim()) {
|
||||||
alert("Digite um nome para a sala de reunião");
|
alert('Digite um nome para a sala de reunião');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,168 +139,147 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarSalaReuniao, {
|
const conversaId = await client.mutation(api.chat.criarSalaReuniao, {
|
||||||
nome: salaReuniaoName.trim(),
|
nome: salaReuniaoName.trim(),
|
||||||
participantes: selectedUsers as any,
|
participantes: selectedUsers
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar sala de reunião:", error);
|
console.error('Erro ao criar sala de reunião:', error);
|
||||||
const mensagem =
|
alert('Erro ao criar sala de reunião');
|
||||||
error?.message ||
|
|
||||||
error?.data ||
|
|
||||||
"Erro desconhecido ao criar sala de reunião";
|
|
||||||
alert(`Erro ao criar sala de reunião: ${mensagem}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog
|
<dialog class="modal modal-open">
|
||||||
class="modal modal-open"
|
<div class="modal-box flex max-h-[85vh] max-w-2xl flex-col p-0">
|
||||||
onclick={(e) => e.target === e.currentTarget && onClose()}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-box max-w-2xl max-h-[85vh] flex flex-col p-0"
|
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300"
|
<h2 class="flex items-center gap-2 text-2xl font-bold">
|
||||||
>
|
<MessageSquare class="text-primary h-6 w-6" />
|
||||||
<h2 class="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<MessageSquare class="w-6 h-6 text-primary" />
|
|
||||||
Nova Conversa
|
Nova Conversa
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
||||||
type="button"
|
<X class="h-5 w-5" />
|
||||||
class="btn btn-ghost btn-sm btn-circle"
|
|
||||||
onclick={onClose}
|
|
||||||
aria-label="Fechar"
|
|
||||||
>
|
|
||||||
<X class="w-5 h-5" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs melhoradas -->
|
<!-- Tabs melhoradas -->
|
||||||
<div class="tabs tabs-boxed p-4 bg-base-200/50">
|
<div class="tabs tabs-boxed bg-base-200/50 p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "individual"
|
activeTab === 'individual'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "individual";
|
activeTab = 'individual';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<User class="w-4 h-4" />
|
<User class="h-4 w-4" />
|
||||||
Individual
|
Individual
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "grupo"
|
activeTab === 'grupo'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "grupo";
|
activeTab = 'grupo';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Users class="w-4 h-4" />
|
<Users class="h-4 w-4" />
|
||||||
Grupo
|
Grupo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "sala_reuniao"
|
activeTab === 'sala_reuniao'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "sala_reuniao";
|
activeTab = 'sala_reuniao';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Video class="w-4 h-4" />
|
<Video class="h-4 w-4" />
|
||||||
Sala de Reunião
|
Sala de Reunião
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||||
{#if activeTab === "grupo"}
|
{#if activeTab === 'grupo'}
|
||||||
<!-- Criar Grupo -->
|
<!-- Criar Grupo -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">Nome do Grupo</span>
|
<span class="label-text font-semibold">Nome do Grupo</span>
|
||||||
</label>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Digite o nome do grupo..."
|
placeholder="Digite o nome do grupo..."
|
||||||
class="input input-bordered w-full focus:input-primary transition-colors"
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
||||||
bind:value={groupName}
|
bind:value={groupName}
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">
|
<span class="label-text font-semibold">
|
||||||
Participantes {selectedUsers.length > 0
|
Participantes {selectedUsers.length > 0
|
||||||
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? "s" : ""})`
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
||||||
: ""}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "sala_reuniao"}
|
</div>
|
||||||
|
{:else if activeTab === 'sala_reuniao'}
|
||||||
<!-- Criar Sala de Reunião -->
|
<!-- Criar Sala de Reunião -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">Nome da Sala de Reunião</span
|
<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
|
||||||
>
|
>
|
||||||
<span class="label-text-alt text-primary font-medium"
|
</div>
|
||||||
>👑 Você será o administrador</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Digite o nome da sala de reunião..."
|
placeholder="Digite o nome da sala de reunião..."
|
||||||
class="input input-bordered w-full focus:input-primary transition-colors"
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
||||||
bind:value={salaReuniaoName}
|
bind:value={salaReuniaoName}
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">
|
<span class="label-text font-semibold">
|
||||||
Participantes {selectedUsers.length > 0
|
Participantes {selectedUsers.length > 0
|
||||||
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? "s" : ""})`
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
||||||
: ""}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Search melhorado -->
|
<!-- Search melhorado -->
|
||||||
<div class="mb-4 relative">
|
<div class="relative mb-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar usuários por nome, email ou matrícula..."
|
placeholder="Buscar usuários por nome, email ou matrícula..."
|
||||||
class="input input-bordered w-full pl-10 focus:input-primary transition-colors"
|
class="input input-bordered focus:input-primary w-full pl-10 transition-colors"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
<Search
|
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
|
||||||
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de usuários -->
|
<!-- Lista de usuários -->
|
||||||
@@ -313,14 +289,14 @@
|
|||||||
{@const isSelected = selectedUsers.includes(usuario._id)}
|
{@const isSelected = selectedUsers.includes(usuario._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`w-full text-left px-4 py-3 rounded-xl border-2 transition-all duration-200 flex items-center gap-3 ${
|
class={`flex w-full items-center gap-3 rounded-xl border-2 px-4 py-3 text-left transition-all duration-200 ${
|
||||||
isSelected
|
isSelected
|
||||||
? "border-primary bg-primary/10 shadow-md scale-[1.02]"
|
? 'border-primary bg-primary/10 scale-[1.02] shadow-md'
|
||||||
: "border-base-300 hover:bg-base-200 hover:border-primary/30 hover:shadow-sm"
|
: 'border-base-300 hover:bg-base-200 hover:border-primary/30 hover:shadow-sm'
|
||||||
} ${loading ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
} ${loading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (activeTab === "individual") {
|
if (activeTab === 'individual') {
|
||||||
handleCriarIndividual(usuario._id);
|
handleCriarIndividual(usuario._id);
|
||||||
} else {
|
} else {
|
||||||
toggleUserSelection(usuario._id);
|
toggleUserSelection(usuario._id);
|
||||||
@@ -336,29 +312,23 @@
|
|||||||
nome={usuario.nome}
|
nome={usuario.nome}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<div class="absolute -bottom-1 -right-1">
|
<div class="absolute -right-1 -bottom-1">
|
||||||
<UserStatusBadge
|
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
|
||||||
status={usuario.statusPresenca || "offline"}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{usuario.nome}
|
{usuario.nome}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-base-content/60 truncate">
|
<p class="text-base-content/60 truncate text-sm">
|
||||||
{usuario.setor ||
|
{usuario.setor || usuario.email || usuario.matricula || 'Sem informações'}
|
||||||
usuario.email ||
|
|
||||||
usuario.matricula ||
|
|
||||||
"Sem informações"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Checkbox melhorado (para grupo e sala de reunião) -->
|
<!-- Checkbox melhorado (para grupo e sala de reunião) -->
|
||||||
{#if activeTab === "grupo" || activeTab === "sala_reuniao"}
|
{#if activeTab === 'grupo' || activeTab === 'sala_reuniao'}
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -369,28 +339,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Ícone de seta para individual -->
|
<!-- Ícone de seta para individual -->
|
||||||
<ChevronRight class="w-5 h-5 text-base-content/40" />
|
<ChevronRight class="text-base-content/40 h-5 w-5" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if !usuarios?.data}
|
{:else if !usuarios?.data}
|
||||||
<div class="flex flex-col items-center justify-center py-12">
|
<div class="flex flex-col items-center justify-center py-12">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
></span>
|
<p class="text-base-content/60 mt-4">Carregando usuários...</p>
|
||||||
<p class="mt-4 text-base-content/60">Carregando usuários...</p>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
class="flex flex-col items-center justify-center py-12 text-center"
|
<UserX class="text-base-content/30 mb-4 h-16 w-16" />
|
||||||
>
|
|
||||||
<UserX class="w-16 h-16 text-base-content/30 mb-4" />
|
|
||||||
<p class="text-base-content/70 font-medium">
|
<p class="text-base-content/70 font-medium">
|
||||||
{searchQuery.trim()
|
{searchQuery.trim() ? 'Nenhum usuário encontrado' : 'Nenhum usuário disponível'}
|
||||||
? "Nenhum usuário encontrado"
|
|
||||||
: "Nenhum usuário disponível"}
|
|
||||||
</p>
|
</p>
|
||||||
{#if searchQuery.trim()}
|
{#if searchQuery.trim()}
|
||||||
<p class="text-sm text-base-content/50 mt-2">
|
<p class="text-base-content/50 mt-2 text-sm">
|
||||||
Tente buscar por nome, email ou matrícula
|
Tente buscar por nome, email ou matrícula
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -400,11 +365,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer (para grupo e sala de reunião) -->
|
<!-- Footer (para grupo e sala de reunião) -->
|
||||||
{#if activeTab === "grupo"}
|
{#if activeTab === 'grupo'}
|
||||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
||||||
onclick={handleCriarGrupo}
|
onclick={handleCriarGrupo}
|
||||||
disabled={loading || selectedUsers.length < 2 || !groupName.trim()}
|
disabled={loading || selectedUsers.length < 2 || !groupName.trim()}
|
||||||
>
|
>
|
||||||
@@ -412,36 +377,34 @@
|
|||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
Criando grupo...
|
Criando grupo...
|
||||||
{:else}
|
{:else}
|
||||||
<Plus class="w-5 h-5" />
|
<Plus class="h-5 w-5" />
|
||||||
Criar Grupo
|
Criar Grupo
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if selectedUsers.length < 2 && activeTab === "grupo"}
|
{#if selectedUsers.length < 2 && activeTab === 'grupo'}
|
||||||
<p class="text-xs text-base-content/50 text-center mt-2">
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
||||||
Selecione pelo menos 2 participantes
|
Selecione pelo menos 2 participantes
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "sala_reuniao"}
|
{:else if activeTab === 'sala_reuniao'}
|
||||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
||||||
onclick={handleCriarSalaReuniao}
|
onclick={handleCriarSalaReuniao}
|
||||||
disabled={loading ||
|
disabled={loading || selectedUsers.length < 1 || !salaReuniaoName.trim()}
|
||||||
selectedUsers.length < 1 ||
|
|
||||||
!salaReuniaoName.trim()}
|
|
||||||
>
|
>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
Criando sala...
|
Criando sala...
|
||||||
{:else}
|
{:else}
|
||||||
<Plus class="w-5 h-5" />
|
<Plus class="h-5 w-5" />
|
||||||
Criar Sala de Reunião
|
Criar Sala de Reunião
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if selectedUsers.length < 1 && activeTab === "sala_reuniao"}
|
{#if selectedUsers.length < 1 && activeTab === 'sala_reuniao'}
|
||||||
<p class="text-xs text-base-content/50 text-center mt-2">
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
||||||
Selecione pelo menos 1 participante
|
Selecione pelo menos 1 participante
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
|
||||||
let { onClose }: { onClose: () => void } = $props();
|
let { onClose }: { onClose: () => void } = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const alertasQuery = useQuery(api.monitoramento.listarAlertas, {});
|
const alertas = useQuery(api.monitoramento.listarAlertas, {});
|
||||||
const alertas = $derived.by(() => {
|
|
||||||
if (!alertasQuery) return [];
|
$inspect(alertas);
|
||||||
// O useQuery pode retornar o array diretamente ou em .data
|
|
||||||
if (Array.isArray(alertasQuery)) return alertasQuery;
|
|
||||||
return alertasQuery.data ?? [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Estado para novo alerta
|
// Estado para novo alerta
|
||||||
let editingAlertId = $state<Id<"alertConfigurations"> | null>(null);
|
let editingAlertId = $state<Id<'alertConfigurations'> | null>(null);
|
||||||
let metricName = $state("cpuUsage");
|
let metricName = $state('cpuUsage');
|
||||||
let threshold = $state(80);
|
let threshold = $state(80);
|
||||||
let operator = $state<">" | "<" | ">=" | "<=" | "==">(">");
|
let operator = $state<'>' | '<' | '>=' | '<=' | '=='>('>');
|
||||||
let enabled = $state(true);
|
let enabled = $state(true);
|
||||||
let notifyByEmail = $state(false);
|
let notifyByEmail = $state(false);
|
||||||
let notifyByChat = $state(true);
|
let notifyByChat = $state(true);
|
||||||
@@ -26,29 +22,29 @@
|
|||||||
let showForm = $state(false);
|
let showForm = $state(false);
|
||||||
|
|
||||||
const metricOptions = [
|
const metricOptions = [
|
||||||
{ value: "cpuUsage", label: "Uso de CPU (%)" },
|
{ value: 'cpuUsage', label: 'Uso de CPU (%)' },
|
||||||
{ value: "memoryUsage", label: "Uso de Memória (%)" },
|
{ value: 'memoryUsage', label: 'Uso de Memória (%)' },
|
||||||
{ value: "networkLatency", label: "Latência de Rede (ms)" },
|
{ value: 'networkLatency', label: 'Latência de Rede (ms)' },
|
||||||
{ value: "storageUsed", label: "Armazenamento Usado (%)" },
|
{ value: 'storageUsed', label: 'Armazenamento Usado (%)' },
|
||||||
{ value: "usuariosOnline", label: "Usuários Online" },
|
{ value: 'usuariosOnline', label: 'Usuários Online' },
|
||||||
{ value: "mensagensPorMinuto", label: "Mensagens por Minuto" },
|
{ value: 'mensagensPorMinuto', label: 'Mensagens por Minuto' },
|
||||||
{ value: "tempoRespostaMedio", label: "Tempo de Resposta (ms)" },
|
{ value: 'tempoRespostaMedio', label: 'Tempo de Resposta (ms)' },
|
||||||
{ value: "errosCount", label: "Contagem de Erros" },
|
{ value: 'errosCount', label: 'Contagem de Erros' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const operatorOptions = [
|
const operatorOptions = [
|
||||||
{ value: ">", label: "Maior que (>)" },
|
{ value: '>', label: 'Maior que (>)' },
|
||||||
{ value: ">=", label: "Maior ou igual (≥)" },
|
{ value: '>=', label: 'Maior ou igual (≥)' },
|
||||||
{ value: "<", label: "Menor que (<)" },
|
{ value: '<', label: 'Menor que (<)' },
|
||||||
{ value: "<=", label: "Menor ou igual (≤)" },
|
{ value: '<=', label: 'Menor ou igual (≤)' },
|
||||||
{ value: "==", label: "Igual a (=)" },
|
{ value: '==', label: 'Igual a (=)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
editingAlertId = null;
|
editingAlertId = null;
|
||||||
metricName = "cpuUsage";
|
metricName = 'cpuUsage';
|
||||||
threshold = 80;
|
threshold = 80;
|
||||||
operator = ">";
|
operator = '>';
|
||||||
enabled = true;
|
enabled = true;
|
||||||
notifyByEmail = false;
|
notifyByEmail = false;
|
||||||
notifyByChat = true;
|
notifyByChat = true;
|
||||||
@@ -76,33 +72,31 @@
|
|||||||
operator,
|
operator,
|
||||||
enabled,
|
enabled,
|
||||||
notifyByEmail,
|
notifyByEmail,
|
||||||
notifyByChat,
|
notifyByChat
|
||||||
});
|
});
|
||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao salvar alerta:", error);
|
console.error('Erro ao salvar alerta:', error);
|
||||||
alert("Erro ao salvar alerta. Tente novamente.");
|
alert('Erro ao salvar alerta. Tente novamente.');
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAlert(alertId: Id<"alertConfigurations">) {
|
async function deleteAlert(alertId: Id<'alertConfigurations'>) {
|
||||||
if (!confirm("Tem certeza que deseja deletar este alerta?")) return;
|
if (!confirm('Tem certeza que deseja deletar este alerta?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao deletar alerta:", error);
|
console.error('Erro ao deletar alerta:', error);
|
||||||
alert("Erro ao deletar alerta. Tente novamente.");
|
alert('Erro ao deletar alerta. Tente novamente.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMetricLabel(metricName: string): string {
|
function getMetricLabel(metricName: string): string {
|
||||||
return (
|
return metricOptions.find((m) => m.value === metricName)?.label || metricName;
|
||||||
metricOptions.find((m) => m.value === metricName)?.label || metricName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOperatorLabel(op: string): string {
|
function getOperatorLabel(op: string): string {
|
||||||
@@ -111,29 +105,23 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box max-w-4xl bg-linear-to-br from-base-100 to-base-200">
|
<div class="modal-box from-base-100 to-base-200 max-w-4xl bg-linear-to-br">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
onclick={onClose}
|
onclick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
<h3 class="text-primary mb-2 text-3xl font-bold">⚙️ Configuração de Alertas</h3>
|
||||||
⚙️ Configuração de Alertas
|
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/60 mb-6">
|
<p class="text-base-content/60 mb-6">
|
||||||
Configure alertas personalizados para monitoramento do sistema
|
Configure alertas personalizados para monitoramento do sistema
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Botão Novo Alerta -->
|
<!-- Botão Novo Alerta -->
|
||||||
{#if !showForm}
|
{#if !showForm}
|
||||||
<button
|
<button type="button" class="btn btn-primary mb-6" onclick={() => (showForm = true)}>
|
||||||
type="button"
|
|
||||||
class="btn btn-primary mb-6"
|
|
||||||
onclick={() => (showForm = true)}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -154,13 +142,13 @@
|
|||||||
|
|
||||||
<!-- Formulário de Alerta -->
|
<!-- Formulário de Alerta -->
|
||||||
{#if showForm}
|
{#if showForm}
|
||||||
<div class="card bg-base-100 shadow-xl mb-6 border-2 border-primary/20">
|
<div class="card bg-base-100 border-primary/20 mb-6 border-2 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title text-xl">
|
<h4 class="card-title text-xl">
|
||||||
{editingAlertId ? "Editar Alerta" : "Novo Alerta"}
|
{editingAlertId ? 'Editar Alerta' : 'Novo Alerta'}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<!-- Métrica -->
|
<!-- Métrica -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="metric">
|
<label class="label" for="metric">
|
||||||
@@ -171,7 +159,7 @@
|
|||||||
class="select select-bordered select-primary"
|
class="select select-bordered select-primary"
|
||||||
bind:value={metricName}
|
bind:value={metricName}
|
||||||
>
|
>
|
||||||
{#each metricOptions as option}
|
{#each metricOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -187,7 +175,7 @@
|
|||||||
class="select select-bordered select-primary"
|
class="select select-bordered select-primary"
|
||||||
bind:value={operator}
|
bind:value={operator}
|
||||||
>
|
>
|
||||||
{#each operatorOptions as option}
|
{#each operatorOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -212,11 +200,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label cursor-pointer justify-start gap-4">
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
<span class="label-text font-semibold">Alerta Ativo</span>
|
<span class="label-text font-semibold">Alerta Ativo</span>
|
||||||
<input
|
<input type="checkbox" class="toggle toggle-primary" bind:checked={enabled} />
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-primary"
|
|
||||||
bind:checked={enabled}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +217,7 @@
|
|||||||
<span class="label-text">
|
<span class="label-text">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 inline mr-2"
|
class="mr-2 inline h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -258,7 +242,7 @@
|
|||||||
<span class="label-text">
|
<span class="label-text">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 inline mr-2"
|
class="mr-2 inline h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -281,7 +265,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="stroke-current shrink-0 w-6 h-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -301,13 +285,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botões -->
|
<!-- Botões -->
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions mt-4 justify-end">
|
||||||
<button
|
<button type="button" class="btn btn-ghost" onclick={resetForm} disabled={saving}>
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost"
|
|
||||||
onclick={resetForm}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -345,9 +324,9 @@
|
|||||||
<!-- Lista de Alertas -->
|
<!-- Lista de Alertas -->
|
||||||
<div class="divider">Alertas Configurados</div>
|
<div class="divider">Alertas Configurados</div>
|
||||||
|
|
||||||
{#if alertas.length > 0}
|
{#if alertas.data && alertas.data.length > 0}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra">
|
<table class="table-zebra table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Métrica</th>
|
<th>Métrica</th>
|
||||||
@@ -358,8 +337,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each alertas as alerta}
|
{#each alertas.data as alerta (alerta._id)}
|
||||||
<tr class={!alerta.enabled ? "opacity-50" : ""}>
|
<tr class={!alerta.enabled ? 'opacity-50' : ''}>
|
||||||
<td>
|
<td>
|
||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
{getMetricLabel(alerta.metricName)}
|
{getMetricLabel(alerta.metricName)}
|
||||||
@@ -423,6 +402,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
title="Editar Alerta"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
onclick={() => editAlert(alerta)}
|
onclick={() => editAlert(alerta)}
|
||||||
@@ -443,6 +423,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Deletar Alerta"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs text-error"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
onclick={() => deleteAlert(alerta._id)}
|
onclick={() => deleteAlert(alerta._id)}
|
||||||
@@ -475,7 +456,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="stroke-info shrink-0 w-6 h-6"
|
class="stroke-info h-6 w-6 shrink-0"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -484,9 +465,7 @@
|
|||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span
|
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
|
||||||
>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -139,25 +139,6 @@ export const configurarAlerta = mutation({
|
|||||||
*/
|
*/
|
||||||
export const listarAlertas = query({
|
export const listarAlertas = query({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id('alertConfigurations'),
|
|
||||||
metricName: v.string(),
|
|
||||||
threshold: v.number(),
|
|
||||||
operator: v.union(
|
|
||||||
v.literal('>'),
|
|
||||||
v.literal('<'),
|
|
||||||
v.literal('>='),
|
|
||||||
v.literal('<='),
|
|
||||||
v.literal('==')
|
|
||||||
),
|
|
||||||
enabled: v.boolean(),
|
|
||||||
notifyByEmail: v.boolean(),
|
|
||||||
notifyByChat: v.boolean(),
|
|
||||||
createdBy: v.id('usuarios'),
|
|
||||||
lastModified: v.number()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const alertas = await ctx.db.query('alertConfigurations').collect();
|
const alertas = await ctx.db.query('alertConfigurations').collect();
|
||||||
return alertas;
|
return alertas;
|
||||||
|
|||||||
Reference in New Issue
Block a user