fix: foto perfil url
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();
|
||||||
|
|
||||||
@@ -34,8 +25,12 @@
|
|||||||
if (!currentUser.data) return null;
|
if (!currentUser.data) return null;
|
||||||
|
|
||||||
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
||||||
if (currentUser.data.fotoPerfil) {
|
if (currentUser.data.fotoPerfilUrl) {
|
||||||
return currentUser.data.fotoPerfil;
|
return currentUser.data.fotoPerfilUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser.data.avatar) {
|
||||||
|
return currentUser.data.avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: gerar avatar baseado no nome
|
// Fallback: gerar avatar baseado no nome
|
||||||
@@ -45,7 +40,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 +52,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 +62,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setores = [
|
const setores = [
|
||||||
{ nome: "Recursos Humanos", link: "/recursos-humanos" },
|
{ nome: 'Recursos Humanos', link: '/recursos-humanos' },
|
||||||
{ nome: "Financeiro", link: "/financeiro" },
|
{ nome: 'Financeiro', link: '/financeiro' },
|
||||||
{ nome: "Controladoria", link: "/controladoria" },
|
{ nome: 'Controladoria', link: '/controladoria' },
|
||||||
{ nome: "Licitações", link: "/licitacoes" },
|
{ nome: 'Licitações', link: '/licitacoes' },
|
||||||
{ nome: "Compras", link: "/compras" },
|
{ nome: 'Compras', link: '/compras' },
|
||||||
{ nome: "Jurídico", link: "/juridico" },
|
{ nome: 'Jurídico', link: '/juridico' },
|
||||||
{ nome: "Comunicação", link: "/comunicacao" },
|
{ nome: 'Comunicação', link: '/comunicacao' },
|
||||||
{ nome: "Programas Esportivos", link: "/programas-esportivos" },
|
{ nome: 'Programas Esportivos', link: '/programas-esportivos' },
|
||||||
{ nome: "Secretaria Executiva", link: "/secretaria-executiva" },
|
{ nome: 'Secretaria Executiva', link: '/secretaria-executiva' },
|
||||||
{
|
{
|
||||||
nome: "Secretaria de Gestão de Pessoas",
|
nome: 'Secretaria de Gestão de Pessoas',
|
||||||
link: "/gestao-pessoas",
|
link: '/gestao-pessoas'
|
||||||
},
|
},
|
||||||
{ nome: "Tecnologia da Informação", link: "/ti" },
|
{ nome: 'Tecnologia da Informação', link: '/ti' }
|
||||||
];
|
];
|
||||||
|
|
||||||
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 +117,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 +127,109 @@
|
|||||||
{
|
{
|
||||||
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +242,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 +304,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 +312,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,50 +321,44 @@
|
|||||||
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>
|
||||||
@@ -391,17 +367,17 @@
|
|||||||
<li class="rounded-xl">
|
<li class="rounded-xl">
|
||||||
<a
|
<a
|
||||||
href={s.link}
|
href={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 +391,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 +448,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 +458,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 +476,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 +500,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 +530,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 +544,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 +554,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 +567,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 +581,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}
|
||||||
|
|||||||
@@ -34,9 +34,10 @@
|
|||||||
if (!usuario) return null;
|
if (!usuario) return null;
|
||||||
|
|
||||||
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
||||||
if (usuario.fotoPerfil) {
|
if (usuario.fotoPerfilUrl) {
|
||||||
return usuario.fotoPerfil;
|
return usuario.fotoPerfilUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usuario.avatar) {
|
if (usuario.avatar) {
|
||||||
return getAvatarUrl(usuario.avatar);
|
return getAvatarUrl(usuario.avatar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
<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';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import {
|
import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from 'lucide-svelte';
|
||||||
X,
|
|
||||||
Users,
|
|
||||||
UserPlus,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
Trash2,
|
|
||||||
Search,
|
|
||||||
} from "lucide-svelte";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversaId: Id<"conversas">;
|
conversaId: Id<'conversas'>;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
@@ -26,8 +18,8 @@
|
|||||||
const conversas = useQuery(api.chat.listarConversas, {});
|
const conversas = useQuery(api.chat.listarConversas, {});
|
||||||
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
|
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
|
||||||
|
|
||||||
let activeTab = $state<"participantes" | "adicionar">("participantes");
|
let activeTab = $state<'participantes' | 'adicionar'>('participantes');
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state('');
|
||||||
let loading = $state<string | null>(null);
|
let loading = $state<string | null>(null);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
@@ -47,8 +39,7 @@
|
|||||||
if (!conv || !usuarios || usuarios.length === 0) return [];
|
if (!conv || !usuarios || usuarios.length === 0) return [];
|
||||||
|
|
||||||
const participantesInfo = conv.participantesInfo || [];
|
const participantesInfo = conv.participantesInfo || [];
|
||||||
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0)
|
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) return [];
|
||||||
return [];
|
|
||||||
|
|
||||||
return participantesInfo
|
return participantesInfo
|
||||||
.map((p: any) => {
|
.map((p: any) => {
|
||||||
@@ -69,20 +60,18 @@
|
|||||||
// Combinar dados do usuário com dados do participante (se p for objeto)
|
// Combinar dados do usuário com dados do participante (se p for objeto)
|
||||||
return {
|
return {
|
||||||
...usuario,
|
...usuario,
|
||||||
...(typeof p === "object" && p !== null && p !== undefined
|
...(typeof p === 'object' && p !== null && p !== undefined ? p : {}),
|
||||||
? p
|
|
||||||
: {}),
|
|
||||||
// Garantir que _id existe e priorizar o do usuario
|
// Garantir que _id existe e priorizar o do usuario
|
||||||
_id: usuario._id,
|
_id: usuario._id
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao processar participante:", err, p);
|
console.error('Erro ao processar participante:', err, p);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((p: any) => p !== null && p._id);
|
.filter((p: any) => p !== null && p._id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao calcular participantes:", err);
|
console.error('Erro ao calcular participantes:', err);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -96,8 +85,7 @@
|
|||||||
if (!usuarios || usuarios.length === 0) return [];
|
if (!usuarios || usuarios.length === 0) return [];
|
||||||
const participantesIds = conversa()?.participantes || [];
|
const participantesIds = conversa()?.participantes || [];
|
||||||
return usuarios.filter(
|
return usuarios.filter(
|
||||||
(u: any) =>
|
(u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id))
|
||||||
!participantesIds.some((pid: any) => String(pid) === String(u._id)),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,9 +95,9 @@
|
|||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
return disponiveis.filter(
|
return disponiveis.filter(
|
||||||
(u: any) =>
|
(u: any) =>
|
||||||
(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)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,66 +112,63 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removerParticipante(participanteId: string) {
|
async function removerParticipante(participanteId: string) {
|
||||||
if (!confirm("Tem certeza que deseja remover este participante?")) return;
|
if (!confirm('Tem certeza que deseja remover este participante?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = `remover-${participanteId}`;
|
loading = `remover-${participanteId}`;
|
||||||
error = null;
|
error = null;
|
||||||
const resultado = await client.mutation(
|
const resultado = await client.mutation(api.chat.removerParticipanteSala, {
|
||||||
api.chat.removerParticipanteSala,
|
|
||||||
{
|
|
||||||
conversaId,
|
conversaId,
|
||||||
participanteId: participanteId as any,
|
participanteId: participanteId as any
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!resultado.sucesso) {
|
if (!resultado.sucesso) {
|
||||||
error = resultado.erro || "Erro ao remover participante";
|
error = resultado.erro || 'Erro ao remover participante';
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || "Erro ao remover participante";
|
error = err.message || 'Erro ao remover participante';
|
||||||
} finally {
|
} finally {
|
||||||
loading = null;
|
loading = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promoverAdmin(participanteId: string) {
|
async function promoverAdmin(participanteId: string) {
|
||||||
if (!confirm("Promover este participante a administrador?")) return;
|
if (!confirm('Promover este participante a administrador?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = `promover-${participanteId}`;
|
loading = `promover-${participanteId}`;
|
||||||
error = null;
|
error = null;
|
||||||
const resultado = await client.mutation(api.chat.promoverAdministrador, {
|
const resultado = await client.mutation(api.chat.promoverAdministrador, {
|
||||||
conversaId,
|
conversaId,
|
||||||
participanteId: participanteId as any,
|
participanteId: participanteId as any
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resultado.sucesso) {
|
if (!resultado.sucesso) {
|
||||||
error = resultado.erro || "Erro ao promover administrador";
|
error = resultado.erro || 'Erro ao promover administrador';
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || "Erro ao promover administrador";
|
error = err.message || 'Erro ao promover administrador';
|
||||||
} finally {
|
} finally {
|
||||||
loading = null;
|
loading = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebaixarAdmin(participanteId: string) {
|
async function rebaixarAdmin(participanteId: string) {
|
||||||
if (!confirm("Rebaixar este administrador a participante?")) return;
|
if (!confirm('Rebaixar este administrador a participante?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = `rebaixar-${participanteId}`;
|
loading = `rebaixar-${participanteId}`;
|
||||||
error = null;
|
error = null;
|
||||||
const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
|
const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
|
||||||
conversaId,
|
conversaId,
|
||||||
participanteId: participanteId as any,
|
participanteId: participanteId as any
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resultado.sucesso) {
|
if (!resultado.sucesso) {
|
||||||
error = resultado.erro || "Erro ao rebaixar administrador";
|
error = resultado.erro || 'Erro ao rebaixar administrador';
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || "Erro ao rebaixar administrador";
|
error = err.message || 'Erro ao rebaixar administrador';
|
||||||
} finally {
|
} finally {
|
||||||
loading = null;
|
loading = null;
|
||||||
}
|
}
|
||||||
@@ -193,46 +178,38 @@
|
|||||||
try {
|
try {
|
||||||
loading = `adicionar-${usuarioId}`;
|
loading = `adicionar-${usuarioId}`;
|
||||||
error = null;
|
error = null;
|
||||||
const resultado = await client.mutation(
|
const resultado = await client.mutation(api.chat.adicionarParticipanteSala, {
|
||||||
api.chat.adicionarParticipanteSala,
|
|
||||||
{
|
|
||||||
conversaId,
|
conversaId,
|
||||||
participanteId: usuarioId as any,
|
participanteId: usuarioId as any
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!resultado.sucesso) {
|
if (!resultado.sucesso) {
|
||||||
error = resultado.erro || "Erro ao adicionar participante";
|
error = resultado.erro || 'Erro ao adicionar participante';
|
||||||
} else {
|
} else {
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || "Erro ao adicionar participante";
|
error = err.message || 'Erro ao adicionar participante';
|
||||||
} finally {
|
} finally {
|
||||||
loading = null;
|
loading = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog
|
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
|
||||||
class="modal modal-open"
|
|
||||||
onclick={(e) => e.target === e.currentTarget && onClose()}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="modal-box max-w-2xl max-h-[80vh] flex flex-col p-0"
|
class="modal-box flex max-h-[80vh] max-w-2xl flex-col p-0"
|
||||||
onclick={(e) => e.stopPropagation()}
|
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"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold flex items-center gap-2">
|
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
||||||
<Users class="w-5 h-5 text-primary" />
|
<Users class="text-primary h-5 w-5" />
|
||||||
Gerenciar Sala de Reunião
|
Gerenciar Sala de Reunião
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-base-content/60">
|
<p class="text-base-content/60 text-sm">
|
||||||
{conversa()?.nome || "Sem nome"}
|
{conversa()?.nome || 'Sem nome'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -241,7 +218,7 @@
|
|||||||
onclick={onClose}
|
onclick={onClose}
|
||||||
aria-label="Fechar"
|
aria-label="Fechar"
|
||||||
>
|
>
|
||||||
<X class="w-5 h-5" />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -250,18 +227,18 @@
|
|||||||
<div class="tabs tabs-boxed p-4">
|
<div class="tabs tabs-boxed p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 ${activeTab === "participantes" ? "tab-active" : ""}`}
|
class={`tab flex items-center gap-2 ${activeTab === 'participantes' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "participantes")}
|
onclick={() => (activeTab = 'participantes')}
|
||||||
>
|
>
|
||||||
<Users class="w-4 h-4" />
|
<Users class="h-4 w-4" />
|
||||||
Participantes
|
Participantes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 ${activeTab === "adicionar" ? "tab-active" : ""}`}
|
class={`tab flex items-center gap-2 ${activeTab === 'adicionar' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "adicionar")}
|
onclick={() => (activeTab = 'adicionar')}
|
||||||
>
|
>
|
||||||
<UserPlus class="w-4 h-4" />
|
<UserPlus class="h-4 w-4" />
|
||||||
Adicionar Participante
|
Adicionar Participante
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,14 +246,10 @@
|
|||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mx-6 mt-2 alert alert-error">
|
<div class="alert alert-error mx-6 mt-2">
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
<button
|
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (error = null)}>
|
||||||
type="button"
|
<X class="h-4 w-4" />
|
||||||
class="btn btn-sm btn-ghost"
|
|
||||||
onclick={() => (error = null)}
|
|
||||||
>
|
|
||||||
<X class="w-4 h-4" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -287,19 +260,15 @@
|
|||||||
<!-- Loading conversas -->
|
<!-- Loading conversas -->
|
||||||
<div class="flex items-center justify-center py-8">
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<span class="ml-2 text-sm text-base-content/60"
|
<span class="text-base-content/60 ml-2 text-sm">Carregando conversa...</span>
|
||||||
>Carregando conversa...</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{:else if !todosUsuariosQuery?.data}
|
{:else if !todosUsuariosQuery?.data}
|
||||||
<!-- Loading usuários -->
|
<!-- Loading usuários -->
|
||||||
<div class="flex items-center justify-center py-8">
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<span class="ml-2 text-sm text-base-content/60"
|
<span class="text-base-content/60 ml-2 text-sm">Carregando usuários...</span>
|
||||||
>Carregando usuários...</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "participantes"}
|
{:else if activeTab === 'participantes'}
|
||||||
<!-- Lista de Participantes -->
|
<!-- Lista de Participantes -->
|
||||||
<div class="space-y-2 py-2">
|
<div class="space-y-2 py-2">
|
||||||
{#if participantes().length > 0}
|
{#if participantes().length > 0}
|
||||||
@@ -309,41 +278,36 @@
|
|||||||
{@const ehCriador = isCriador(participanteId)}
|
{@const ehCriador = isCriador(participanteId)}
|
||||||
{@const isLoading = loading?.includes(participanteId)}
|
{@const isLoading = loading?.includes(participanteId)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
|
class="border-base-300 hover:bg-base-200 flex items-center gap-3 rounded-lg border p-3 transition-colors"
|
||||||
>
|
>
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={participante.avatar}
|
avatar={participante.avatar}
|
||||||
fotoPerfilUrl={participante.fotoPerfilUrl ||
|
fotoPerfilUrl={participante.fotoPerfilUrl || participante.avatar}
|
||||||
participante.fotoPerfil}
|
nome={participante.nome || 'Usuário'}
|
||||||
nome={participante.nome || "Usuário"}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<div class="absolute bottom-0 right-0">
|
|
||||||
<UserStatusBadge
|
|
||||||
status={participante.statusPresenca || "offline"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute right-0 bottom-0">
|
||||||
|
<UserStatusBadge status={participante.statusPresenca || 'offline'} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="font-medium text-base-content truncate">
|
<p class="text-base-content truncate font-medium">
|
||||||
{participante.nome || "Usuário"}
|
{participante.nome || 'Usuário'}
|
||||||
</p>
|
</p>
|
||||||
{#if ehAdmin}
|
{#if ehAdmin}
|
||||||
<span class="badge badge-primary badge-sm">Admin</span>
|
<span class="badge badge-primary badge-sm">Admin</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if ehCriador}
|
{#if ehCriador}
|
||||||
<span class="badge badge-secondary badge-sm">Criador</span
|
<span class="badge badge-secondary badge-sm">Criador</span>
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-base-content/60 truncate">
|
<p class="text-base-content/60 truncate text-sm">
|
||||||
{participante.setor || participante.email || ""}
|
{participante.setor || participante.email || ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -358,11 +322,10 @@
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
title="Rebaixar administrador"
|
title="Rebaixar administrador"
|
||||||
>
|
>
|
||||||
{#if isLoading && loading?.includes("rebaixar")}
|
{#if isLoading && loading?.includes('rebaixar')}
|
||||||
<span class="loading loading-spinner loading-xs"
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
></span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<ArrowDown class="w-4 h-4" />
|
<ArrowDown class="h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -373,11 +336,10 @@
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
title="Promover a administrador"
|
title="Promover a administrador"
|
||||||
>
|
>
|
||||||
{#if isLoading && loading?.includes("promover")}
|
{#if isLoading && loading?.includes('promover')}
|
||||||
<span class="loading loading-spinner loading-xs"
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
></span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<ArrowUp class="w-4 h-4" />
|
<ArrowUp class="h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -388,10 +350,10 @@
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
title="Remover participante"
|
title="Remover participante"
|
||||||
>
|
>
|
||||||
{#if isLoading && loading?.includes("remover")}
|
{#if isLoading && loading?.includes('remover')}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Trash2 class="w-4 h-4" />
|
<Trash2 class="h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,23 +361,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center py-8 text-base-content/50">
|
<div class="text-base-content/50 py-8 text-center">Nenhum participante encontrado</div>
|
||||||
Nenhum participante encontrado
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "adicionar" && isAdmin}
|
{:else if activeTab === 'adicionar' && isAdmin}
|
||||||
<!-- Adicionar Participante -->
|
<!-- Adicionar Participante -->
|
||||||
<div class="mb-4 relative">
|
<div class="relative mb-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar usuários..."
|
placeholder="Buscar usuários..."
|
||||||
class="input input-bordered w-full pl-10"
|
class="input input-bordered w-full pl-10"
|
||||||
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>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -425,7 +383,7 @@
|
|||||||
{@const isLoading = loading?.includes(usuarioId)}
|
{@const isLoading = loading?.includes(usuarioId)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors flex items-center gap-3"
|
class="border-base-300 hover:bg-base-200 flex w-full items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors"
|
||||||
onclick={() => adicionarParticipante(usuarioId)}
|
onclick={() => adicionarParticipante(usuarioId)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
@@ -433,25 +391,22 @@
|
|||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={usuario.avatar}
|
avatar={usuario.avatar}
|
||||||
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.fotoPerfil}
|
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.avatar}
|
||||||
nome={usuario.nome || "Usuário"}
|
nome={usuario.nome || 'Usuário'}
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<div class="absolute bottom-0 right-0">
|
|
||||||
<UserStatusBadge
|
|
||||||
status={usuario.statusPresenca || "offline"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute right-0 bottom-0">
|
||||||
|
<UserStatusBadge 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-medium text-base-content truncate">
|
<p class="text-base-content truncate font-medium">
|
||||||
{usuario.nome || "Usuário"}
|
{usuario.nome || 'Usuário'}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-base-content/60 truncate">
|
<p class="text-base-content/60 truncate text-sm">
|
||||||
{usuario.setor || usuario.email || ""}
|
{usuario.setor || usuario.email || ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -459,15 +414,15 @@
|
|||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<UserPlus class="w-5 h-5 text-primary" />
|
<UserPlus class="text-primary h-5 w-5" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center py-8 text-base-content/50">
|
<div class="text-base-content/50 py-8 text-center">
|
||||||
{searchQuery.trim()
|
{searchQuery.trim()
|
||||||
? "Nenhum usuário encontrado"
|
? 'Nenhum usuário encontrado'
|
||||||
: "Todos os usuários já são participantes"}
|
: 'Todos os usuários já são participantes'}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -475,10 +430,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-6 py-4 border-t border-base-300">
|
<div class="border-base-300 border-t px-6 py-4">
|
||||||
<button type="button" class="btn btn-block" onclick={onClose}>
|
<button type="button" class="btn btn-block" onclick={onClose}> Fechar </button>
|
||||||
Fechar
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { generateAvatarGallery } from '$lib/utils/avatars';
|
import { generateAvatarGallery } from '$lib/utils/avatars';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { X, Calendar } from 'lucide-svelte';
|
import { X, Calendar } from 'lucide-svelte';
|
||||||
|
import type { FunctionReturnType } from 'convex/server';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
@@ -28,11 +29,6 @@
|
|||||||
let avatarSelecionado = $state<string>('');
|
let avatarSelecionado = $state<string>('');
|
||||||
let mostrarBotaoCamera = $state(false);
|
let mostrarBotaoCamera = $state(false);
|
||||||
|
|
||||||
// Estados locais para atualização imediata
|
|
||||||
let fotoPerfilLocal = $state<string | null>(null);
|
|
||||||
let avatarLocal = $state<string | null>(null);
|
|
||||||
let perfilCarregado = $state(false);
|
|
||||||
|
|
||||||
// Estados para Minhas Férias
|
// Estados para Minhas Férias
|
||||||
let mostrarWizard = $state(false);
|
let mostrarWizard = $state(false);
|
||||||
let filtroStatusFerias = $state<string>('todos');
|
let filtroStatusFerias = $state<string>('todos');
|
||||||
@@ -47,29 +43,6 @@
|
|||||||
// Galeria de avatares (30 avatares profissionais 3D realistas)
|
// Galeria de avatares (30 avatares profissionais 3D realistas)
|
||||||
const avatarGallery = generateAvatarGallery(30);
|
const avatarGallery = generateAvatarGallery(30);
|
||||||
|
|
||||||
// Carregar perfil ao montar a página para garantir dados atualizados (apenas uma vez)
|
|
||||||
$effect(() => {
|
|
||||||
if (currentUser?.data && !perfilCarregado) {
|
|
||||||
perfilCarregado = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sincronizar com currentUser - atualiza automaticamente quando o usuário muda
|
|
||||||
$effect(() => {
|
|
||||||
const usuario = currentUser?.data;
|
|
||||||
if (usuario) {
|
|
||||||
// Atualizar foto de perfil (pode ser null ou string)
|
|
||||||
fotoPerfilLocal = usuario.fotoPerfil ?? null;
|
|
||||||
// Atualizar avatar (pode ser undefined ou string)
|
|
||||||
avatarLocal = usuario.avatar ?? null;
|
|
||||||
} else {
|
|
||||||
// Se não há usuário, limpar estados locais
|
|
||||||
fotoPerfilLocal = null;
|
|
||||||
avatarLocal = null;
|
|
||||||
perfilCarregado = false; // Reset para permitir recarregar quando houver usuário novamente
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FuncionarioId disponível diretamente do usuário atual
|
// FuncionarioId disponível diretamente do usuário atual
|
||||||
const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null);
|
const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null);
|
||||||
|
|
||||||
@@ -238,14 +211,6 @@
|
|||||||
erroUpload = '';
|
erroUpload = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Criar preview local IMEDIATAMENTE para feedback visual
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
fotoPerfilLocal = e.target?.result as string;
|
|
||||||
avatarLocal = null;
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
|
|
||||||
// 2. Gerar URL de upload
|
// 2. Gerar URL de upload
|
||||||
const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {});
|
const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {});
|
||||||
|
|
||||||
@@ -271,12 +236,6 @@
|
|||||||
// 5. Aguardar um pouco para garantir que o backend processou
|
// 5. Aguardar um pouco para garantir que o backend processou
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
// 6. Atualizar localmente com a URL retornada pelo backend (ou pelo currentUser)
|
|
||||||
if (currentUser?.data?.fotoPerfil) {
|
|
||||||
fotoPerfilLocal = currentUser.data.fotoPerfil;
|
|
||||||
avatarLocal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Limpar o input para permitir novo upload
|
// 8. Limpar o input para permitir novo upload
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
|
||||||
@@ -299,12 +258,8 @@
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
erroUpload = errorMessage || 'Erro ao fazer upload da foto';
|
erroUpload = errorMessage || 'Erro ao fazer upload da foto';
|
||||||
// Reverter mudança local se houver erro
|
|
||||||
fotoPerfilLocal = currentUser?.data?.fotoPerfil || null;
|
|
||||||
avatarLocal = currentUser?.data?.avatar || null;
|
|
||||||
} finally {
|
|
||||||
uploadandoFoto = false;
|
|
||||||
}
|
}
|
||||||
|
uploadandoFoto = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSelecionarAvatar(avatarUrl: string) {
|
async function handleSelecionarAvatar(avatarUrl: string) {
|
||||||
@@ -312,25 +267,12 @@
|
|||||||
erroUpload = '';
|
erroUpload = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Atualizar localmente IMEDIATAMENTE para feedback visual instantâneo
|
|
||||||
avatarLocal = avatarUrl;
|
|
||||||
fotoPerfilLocal = null;
|
|
||||||
|
|
||||||
// 2. Salvar avatar selecionado no backend
|
// 2. Salvar avatar selecionado no backend
|
||||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||||
avatar: avatarUrl,
|
avatar: avatarUrl,
|
||||||
fotoPerfil: undefined // Remove foto se colocar avatar
|
fotoPerfil: undefined // Remove foto se colocar avatar
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Aguardar um pouco para garantir que o backend processou
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
// 4. Garantir que os estados locais estão sincronizados com o usuário atual
|
|
||||||
if (currentUser?.data?.avatar) {
|
|
||||||
avatarLocal = currentUser.data.avatar;
|
|
||||||
fotoPerfilLocal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Fechar modal após sucesso
|
// 6. Fechar modal após sucesso
|
||||||
mostrarModalFoto = false;
|
mostrarModalFoto = false;
|
||||||
|
|
||||||
@@ -350,9 +292,6 @@
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
erroUpload = errorMessage || 'Erro ao salvar avatar';
|
erroUpload = errorMessage || 'Erro ao salvar avatar';
|
||||||
// Reverter mudança local se houver erro
|
|
||||||
avatarLocal = currentUser?.data?.avatar || null;
|
|
||||||
fotoPerfilLocal = currentUser?.data?.fotoPerfil || null;
|
|
||||||
} finally {
|
} finally {
|
||||||
uploadandoFoto = false;
|
uploadandoFoto = false;
|
||||||
}
|
}
|
||||||
@@ -391,6 +330,7 @@
|
|||||||
onmouseenter={() => (mostrarBotaoCamera = true)}
|
onmouseenter={() => (mostrarBotaoCamera = true)}
|
||||||
onmouseleave={() => (mostrarBotaoCamera = false)}
|
onmouseleave={() => (mostrarBotaoCamera = false)}
|
||||||
>
|
>
|
||||||
|
s
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="avatar cursor-pointer border-0 bg-transparent p-0"
|
class="avatar cursor-pointer border-0 bg-transparent p-0"
|
||||||
@@ -399,10 +339,14 @@
|
|||||||
<div
|
<div
|
||||||
class="animate-float h-40 w-40 rounded-full shadow-2xl ring-4 ring-white ring-offset-4 ring-offset-transparent transition-all duration-300 hover:scale-105 hover:ring-8"
|
class="animate-float h-40 w-40 rounded-full shadow-2xl ring-4 ring-white ring-offset-4 ring-offset-transparent transition-all duration-300 hover:scale-105 hover:ring-8"
|
||||||
>
|
>
|
||||||
{#if fotoPerfilLocal}
|
{#if currentUser.data?.fotoPerfilUrl}
|
||||||
<img src={fotoPerfilLocal} alt="Foto de perfil" class="object-cover" />
|
<img
|
||||||
{:else if avatarLocal}
|
src={currentUser.data.fotoPerfilUrl}
|
||||||
<img src={avatarLocal} alt="Avatar" class="object-cover" />
|
alt="Foto de perfil"
|
||||||
|
class="object-cover"
|
||||||
|
/>
|
||||||
|
{:else if currentUser.data?.avatar}
|
||||||
|
<img src={currentUser.data.avatar} alt="Avatar" class="object-cover" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center bg-white text-purple-700">
|
<div class="flex items-center justify-center bg-white text-purple-700">
|
||||||
<span class="text-5xl font-black"
|
<span class="text-5xl font-black"
|
||||||
@@ -2038,10 +1982,10 @@
|
|||||||
<div
|
<div
|
||||||
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
|
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
|
||||||
>
|
>
|
||||||
{#if fotoPerfilLocal}
|
{#if currentUser.data?.fotoPerfilUrl}
|
||||||
<img src={fotoPerfilLocal} alt="Foto atual" class="object-cover" />
|
<img src={currentUser.data.fotoPerfilUrl} alt="Foto atual" class="object-cover" />
|
||||||
{:else if avatarLocal}
|
{:else if currentUser.data?.avatar}
|
||||||
<img src={avatarLocal} alt="Avatar atual" class="object-cover" />
|
<img src={currentUser.data.avatar} alt="Avatar atual" class="object-cover" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bg-primary text-primary-content flex items-center justify-center">
|
<div class="bg-primary text-primary-content flex items-center justify-center">
|
||||||
<span class="text-4xl font-bold"
|
<span class="text-4xl font-bold"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const createAuth = (
|
|||||||
export const getCurrentUser = query({
|
export const getCurrentUser = query({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const authUser = await authComponent.safeGetAuthUser(ctx as any);
|
const authUser = await authComponent.safeGetAuthUser(ctx);
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export const getCurrentUser = query({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
|
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
|
||||||
const authUser = await authComponent.safeGetAuthUser(ctx as any);
|
const authUser = await authComponent.safeGetAuthUser(ctx);
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ export const createAuthUser = async (
|
|||||||
ctx: MutationCtx,
|
ctx: MutationCtx,
|
||||||
args: { nome: string; email: string; password: string }
|
args: { nome: string; email: string; password: string }
|
||||||
) => {
|
) => {
|
||||||
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
|
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
|
||||||
|
|
||||||
const result = await auth.api.signUpEmail({
|
const result = await auth.api.signUpEmail({
|
||||||
headers,
|
headers,
|
||||||
@@ -107,7 +107,7 @@ export const updatePassword = async (
|
|||||||
ctx: MutationCtx,
|
ctx: MutationCtx,
|
||||||
args: { newPassword: string; currentPassword: string }
|
args: { newPassword: string; currentPassword: string }
|
||||||
) => {
|
) => {
|
||||||
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
|
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
|
||||||
|
|
||||||
await auth.api.changePassword({
|
await auth.api.changePassword({
|
||||||
headers,
|
headers,
|
||||||
|
|||||||
Reference in New Issue
Block a user