786 lines
26 KiB
Svelte
786 lines
26 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/state';
|
|
import { goto } from '$app/navigation';
|
|
import logo from '$lib/assets/logo_governo_PE.png';
|
|
import type { Snippet } from 'svelte';
|
|
import { loginModalStore } from '$lib/stores/loginModal.svelte';
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
|
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
|
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
|
|
|
import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte';
|
|
import { authClient } from '$lib/auth';
|
|
import { resolve } from '$app/paths';
|
|
import { obterIPPublico } from '$lib/utils/deviceInfo';
|
|
|
|
let { children }: { children: Snippet } = $props();
|
|
|
|
const currentPath = $derived(page.url.pathname);
|
|
|
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
|
const convexClient = useConvexClient();
|
|
|
|
// Função para obter a URL do avatar/foto do usuário
|
|
const avatarUrlDoUsuario = $derived(() => {
|
|
if (!currentUser.data) return null;
|
|
|
|
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
|
if (currentUser.data.fotoPerfilUrl) {
|
|
return currentUser.data.fotoPerfilUrl;
|
|
}
|
|
|
|
if (currentUser.data.avatar) {
|
|
return currentUser.data.avatar;
|
|
}
|
|
|
|
// Fallback: retornar null para usar o ícone User do Lucide
|
|
return null;
|
|
});
|
|
|
|
// Função para gerar classes do menu ativo
|
|
function getMenuClasses(isActive: boolean) {
|
|
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';
|
|
|
|
if (isActive) {
|
|
return `${baseClasses} border-primary bg-primary text-white shadow-lg scale-105`;
|
|
}
|
|
|
|
return `${baseClasses} border-primary/30 bg-linear-to-br from-base-100 to-base-200 text-base-content hover:from-primary hover:to-primary/80 hover:text-white`;
|
|
}
|
|
|
|
// Função para gerar classes do botão "Solicitar Acesso"
|
|
function getSolicitarClasses(isActive: boolean) {
|
|
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';
|
|
|
|
if (isActive) {
|
|
return `${baseClasses} border-success bg-success text-white shadow-lg scale-105`;
|
|
}
|
|
|
|
return `${baseClasses} border-success/30 bg-linear-to-br from-success/10 to-success/20 text-base-content hover:from-success hover:to-success/80 hover:text-white`;
|
|
}
|
|
|
|
const setores = [
|
|
{ nome: 'Recursos Humanos', link: '/recursos-humanos' },
|
|
{ nome: 'Financeiro', link: '/financeiro' },
|
|
{ nome: 'Controladoria', link: '/controladoria' },
|
|
{ nome: 'Licitações', link: '/licitacoes' },
|
|
{ nome: 'Compras', link: '/compras' },
|
|
{ nome: 'Jurídico', link: '/juridico' },
|
|
{ nome: 'Comunicação', link: '/comunicacao' },
|
|
{ nome: 'Programas Esportivos', link: '/programas-esportivos' },
|
|
{ nome: 'Secretaria Executiva', link: '/secretaria-executiva' },
|
|
{
|
|
nome: 'Secretaria de Gestão de Pessoas',
|
|
link: '/gestao-pessoas'
|
|
},
|
|
{ nome: 'Tecnologia da Informação', link: '/ti' }
|
|
];
|
|
|
|
let showAboutModal = $state(false);
|
|
let matricula = $state('');
|
|
let senha = $state('');
|
|
let erroLogin = $state('');
|
|
let carregandoLogin = $state(false);
|
|
|
|
// Sincronizar com o store global
|
|
$effect(() => {
|
|
if (loginModalStore.showModal && !matricula && !senha) {
|
|
matricula = '';
|
|
senha = '';
|
|
erroLogin = '';
|
|
}
|
|
});
|
|
|
|
function openLoginModal() {
|
|
loginModalStore.open();
|
|
matricula = '';
|
|
senha = '';
|
|
erroLogin = '';
|
|
carregandoLogin = false;
|
|
}
|
|
|
|
function closeLoginModal() {
|
|
loginModalStore.close();
|
|
matricula = '';
|
|
senha = '';
|
|
erroLogin = '';
|
|
carregandoLogin = false;
|
|
}
|
|
|
|
function openAboutModal() {
|
|
showAboutModal = true;
|
|
}
|
|
|
|
function closeAboutModal() {
|
|
showAboutModal = false;
|
|
}
|
|
|
|
async function handleLogin(e: Event) {
|
|
e.preventDefault();
|
|
erroLogin = '';
|
|
carregandoLogin = true;
|
|
|
|
// Obter IP público e userAgent (rápido, não bloqueia)
|
|
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : undefined;
|
|
|
|
// Obter IP público com timeout curto (não bloquear login)
|
|
const ipPublicoPromise = obterIPPublico().catch(() => undefined);
|
|
const ipPublicoTimeout = new Promise<undefined>(
|
|
(resolve) => setTimeout(() => resolve(undefined), 2000) // Timeout de 2 segundos
|
|
);
|
|
const ipPublico = await Promise.race([ipPublicoPromise, ipPublicoTimeout]);
|
|
|
|
// Função para coletar GPS em background (não bloqueia login)
|
|
async function coletarGPS(): Promise<any> {
|
|
try {
|
|
const { obterLocalizacaoRapida } = await import('$lib/utils/deviceInfo');
|
|
// Usar versão rápida com timeout curto (3 segundos máximo)
|
|
const gpsPromise = obterLocalizacaoRapida();
|
|
const gpsTimeout = new Promise<{}>((resolve) => setTimeout(() => resolve({}), 3000));
|
|
return await Promise.race([gpsPromise, gpsTimeout]);
|
|
} catch (err) {
|
|
console.warn('Erro ao obter GPS (não bloqueia login):', err);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Iniciar coleta de GPS em background (não esperar)
|
|
const gpsPromise = coletarGPS();
|
|
|
|
const result = await authClient.signIn.email(
|
|
{ email: matricula.trim(), password: senha },
|
|
{
|
|
onError: async (ctx) => {
|
|
// Registrar tentativa de login falha
|
|
try {
|
|
// Tentar obter GPS se já estiver disponível (não esperar)
|
|
let localizacaoGPS: any = {};
|
|
try {
|
|
localizacaoGPS = await Promise.race([
|
|
gpsPromise,
|
|
new Promise<{}>((resolve) => setTimeout(() => resolve({}), 100))
|
|
]);
|
|
} catch {
|
|
// Ignorar se GPS não estiver pronto
|
|
}
|
|
|
|
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
|
matriculaOuEmail: matricula.trim(),
|
|
sucesso: false,
|
|
motivoFalha: ctx.error?.message || 'Erro desconhecido',
|
|
userAgent: userAgent,
|
|
ipAddress: ipPublico,
|
|
latitudeGPS: localizacaoGPS.latitude,
|
|
longitudeGPS: localizacaoGPS.longitude,
|
|
precisaoGPS: localizacaoGPS.precisao,
|
|
enderecoGPS: localizacaoGPS.endereco,
|
|
cidadeGPS: localizacaoGPS.cidade,
|
|
estadoGPS: localizacaoGPS.estado,
|
|
paisGPS: localizacaoGPS.pais
|
|
});
|
|
} catch (err) {
|
|
console.error('Erro ao registrar tentativa de login falha:', err);
|
|
}
|
|
alert(ctx.error.message);
|
|
}
|
|
}
|
|
);
|
|
|
|
if (result.data) {
|
|
// Registrar tentativa de login bem-sucedida
|
|
// Fazer de forma assíncrona para não bloquear o login
|
|
(async () => {
|
|
try {
|
|
// Aguardar um pouco para o usuário ser sincronizado no Convex
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
// Tentar obter GPS se já estiver disponível (não esperar)
|
|
let localizacaoGPS: any = {};
|
|
try {
|
|
localizacaoGPS = await Promise.race([
|
|
gpsPromise,
|
|
new Promise<{}>((resolve) => setTimeout(() => resolve({}), 100))
|
|
]);
|
|
} catch {
|
|
// Ignorar se GPS não estiver pronto
|
|
}
|
|
|
|
// Buscar o usuário no Convex usando getCurrentUser
|
|
const usuario = await convexClient.query(api.auth.getCurrentUser, {});
|
|
|
|
if (usuario && usuario._id) {
|
|
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
|
usuarioId: usuario._id,
|
|
matriculaOuEmail: matricula.trim(),
|
|
sucesso: true,
|
|
userAgent: userAgent,
|
|
ipAddress: ipPublico,
|
|
latitudeGPS: localizacaoGPS.latitude,
|
|
longitudeGPS: localizacaoGPS.longitude,
|
|
precisaoGPS: localizacaoGPS.precisao,
|
|
enderecoGPS: localizacaoGPS.endereco,
|
|
cidadeGPS: localizacaoGPS.cidade,
|
|
estadoGPS: localizacaoGPS.estado,
|
|
paisGPS: localizacaoGPS.pais
|
|
});
|
|
} else {
|
|
// Se não encontrou o usuário, registrar sem usuarioId (será atualizado depois)
|
|
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
|
matriculaOuEmail: matricula.trim(),
|
|
sucesso: true,
|
|
userAgent: userAgent,
|
|
ipAddress: ipPublico,
|
|
latitudeGPS: localizacaoGPS.latitude,
|
|
longitudeGPS: localizacaoGPS.longitude,
|
|
precisaoGPS: localizacaoGPS.precisao,
|
|
enderecoGPS: localizacaoGPS.endereco,
|
|
cidadeGPS: localizacaoGPS.cidade,
|
|
estadoGPS: localizacaoGPS.estado,
|
|
paisGPS: localizacaoGPS.pais
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Erro ao registrar tentativa de login:', err);
|
|
// Não bloquear o login se houver erro ao registrar
|
|
}
|
|
})();
|
|
|
|
closeLoginModal();
|
|
goto(resolve('/'));
|
|
} else {
|
|
erroLogin = 'Erro ao fazer login';
|
|
}
|
|
carregandoLogin = false;
|
|
}
|
|
|
|
async function handleLogout() {
|
|
const result = await authClient.signOut();
|
|
if (result.error) {
|
|
console.error('Sign out error:', result.error);
|
|
}
|
|
// Resetar tema para padrão ao fazer logout
|
|
const { aplicarTemaPadrao } = await import('$lib/utils/temas');
|
|
aplicarTemaPadrao();
|
|
goto(resolve('/'));
|
|
}
|
|
</script>
|
|
|
|
<!-- Header Fixo acima de tudo -->
|
|
<div
|
|
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">
|
|
<label
|
|
for="my-drawer-3"
|
|
class="group bg-primary relative flex h-14 w-14 cursor-pointer items-center justify-center overflow-hidden rounded-2xl shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
|
aria-label="Abrir menu"
|
|
>
|
|
<!-- Efeito de brilho no hover -->
|
|
<div
|
|
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>
|
|
|
|
<!-- Ícone de menu hambúrguer -->
|
|
<Menu
|
|
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));"
|
|
strokeWidth={2.5}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div class="flex flex-1 items-center gap-4 lg:gap-6">
|
|
<!-- Logo MODERNO do Governo -->
|
|
<div class="avatar">
|
|
<div
|
|
class="group bg-base-100 border-primary/20 relative w-16 overflow-hidden rounded-2xl border-2 p-2 shadow-xl transition-all duration-300 hover:scale-105 lg:w-20"
|
|
>
|
|
<!-- Efeito de brilho no hover -->
|
|
<div
|
|
class="from-primary/5 absolute inset-0 bg-linear-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
|
></div>
|
|
|
|
<!-- Logo -->
|
|
<img
|
|
src={logo}
|
|
alt="Logo do Governo de PE"
|
|
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));"
|
|
/>
|
|
|
|
<!-- Brilho sutil no canto -->
|
|
<div
|
|
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 class="flex flex-col">
|
|
<h1 class="text-primary text-xl font-bold tracking-tight lg:text-3xl">SGSE</h1>
|
|
<p
|
|
class="text-base-content/80 hidden text-xs leading-tight font-medium sm:block lg:text-base"
|
|
>
|
|
Sistema de Gerenciamento de Secretaria
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="ml-auto flex flex-none items-center gap-4">
|
|
{#if currentUser.data}
|
|
<!-- Nome e Perfil à esquerda do avatar -->
|
|
<div class="hidden flex-col items-end lg:flex">
|
|
<span class="text-primary text-sm font-semibold">{currentUser.data.nome}</span>
|
|
<span class="text-base-content/60 text-xs">{currentUser.data.role?.nome}</span>
|
|
</div>
|
|
|
|
<div class="dropdown dropdown-end">
|
|
<!-- Botão de Perfil ULTRA MODERNO -->
|
|
<button
|
|
type="button"
|
|
tabindex="0"
|
|
class="group bg-primary relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
|
aria-label="Menu do usuário"
|
|
>
|
|
<!-- Efeito de brilho no hover -->
|
|
<div
|
|
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>
|
|
|
|
<!-- Avatar/Foto do usuário ou ícone padrão -->
|
|
{#if avatarUrlDoUsuario()}
|
|
<img
|
|
src={avatarUrlDoUsuario()}
|
|
alt={currentUser.data?.nome || 'Usuário'}
|
|
class="relative z-10 h-full w-full object-cover"
|
|
/>
|
|
{:else}
|
|
<!-- Ícone de usuário moderno (fallback) -->
|
|
<User
|
|
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));"
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Anel de pulso sutil -->
|
|
<div
|
|
class="absolute inset-0 rounded-2xl"
|
|
style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
|
|
></div>
|
|
|
|
<!-- Badge de status online -->
|
|
<div
|
|
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;"
|
|
></div>
|
|
</button>
|
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
<ul
|
|
tabindex="0"
|
|
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">
|
|
<span class="text-primary font-bold">{currentUser.data?.nome}</span>
|
|
</li>
|
|
<li><a href={resolve('/perfil')}>Meu Perfil</a></li>
|
|
<li><a href={resolve('/alterar-senha')}>Alterar Senha</a></li>
|
|
<div class="divider my-0"></div>
|
|
<li>
|
|
<button type="button" onclick={handleLogout} class="text-error">Sair</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Sino de notificações no canto superior direito -->
|
|
<div class="relative">
|
|
<NotificationBell />
|
|
</div>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
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;"
|
|
onclick={() => openLoginModal()}
|
|
aria-label="Login"
|
|
>
|
|
<!-- Efeito de brilho animado -->
|
|
<div
|
|
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>
|
|
|
|
<!-- Anel pulsante de fundo -->
|
|
<div class="absolute inset-0 rounded-full bg-white/10 group-hover:animate-ping"></div>
|
|
|
|
<!-- Ícone de login premium -->
|
|
<User
|
|
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));"
|
|
strokeWidth={2.5}
|
|
/>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="drawer lg:drawer-open" style="margin-top: 96px;">
|
|
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
|
|
<div class="drawer-content flex flex-col lg:ml-72" style="min-height: calc(100vh - 96px);">
|
|
<!-- Page content -->
|
|
<div class="flex-1 overflow-y-auto">
|
|
{@render children?.()}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer
|
|
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">
|
|
<button
|
|
type="button"
|
|
class="link link-hover hover:text-primary transition-colors"
|
|
onclick={() => openAboutModal()}>Sobre</button
|
|
>
|
|
<span class="text-base-content/30">•</span>
|
|
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
|
|
>Contato</a
|
|
>
|
|
<span class="text-base-content/30">•</span>
|
|
<a
|
|
href={resolve('/abrir-chamado')}
|
|
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
|
>
|
|
<span class="text-base-content/30">•</span>
|
|
<a
|
|
href={resolve('/privacidade')}
|
|
class="link link-hover hover:text-primary transition-colors">Privacidade</a
|
|
>
|
|
</div>
|
|
<div class="mt-2 flex items-center gap-3">
|
|
<div class="avatar">
|
|
<div class="w-10 rounded-lg bg-white p-1.5 shadow-md">
|
|
<img src={logo} alt="Logo" class="h-full w-full object-contain" />
|
|
</div>
|
|
</div>
|
|
<div class="text-left">
|
|
<p class="text-primary text-xs font-bold">Governo do Estado de Pernambuco</p>
|
|
<p class="text-base-content/70 text-xs">Secretaria de Esportes</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-base-content/60 mt-2 text-xs">
|
|
© {new Date().getFullYear()} - Todos os direitos reservados
|
|
</p>
|
|
</footer>
|
|
</div>
|
|
<div class="drawer-side fixed z-40" style="margin-top: 96px;">
|
|
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"></label>
|
|
<div
|
|
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 -->
|
|
<ul class="flex flex-col gap-2">
|
|
<li class="rounded-xl">
|
|
<a href={resolve('/')} class={getMenuClasses(currentPath === '/')}>
|
|
<Home class="h-5 w-5 transition-transform group-hover:scale-110" strokeWidth={2} />
|
|
<span>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
{#each setores as s (s.link)}
|
|
{@const isActive = currentPath.startsWith(s.link)}
|
|
<li class="rounded-xl">
|
|
<a
|
|
href={resolve(s.link)}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
class={getMenuClasses(isActive)}
|
|
>
|
|
<span>{s.nome}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
<li class="mt-auto rounded-xl">
|
|
<a
|
|
href={resolve('/abrir-chamado')}
|
|
class={getSolicitarClasses(currentPath === '/abrir-chamado')}
|
|
>
|
|
<UserPlus class="h-5 w-5" strokeWidth={2} />
|
|
<span>Abrir Chamado</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal de Login -->
|
|
{#if loginModalStore.showModal}
|
|
<dialog class="modal modal-open">
|
|
<div
|
|
class="modal-box from-base-100 via-base-100 to-primary/5 relative max-w-md overflow-hidden bg-gradient-to-br shadow-2xl backdrop-blur-sm"
|
|
>
|
|
<!-- Botão de fechar moderno -->
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle btn-ghost hover:bg-error/20 hover:text-error absolute top-4 right-4 z-10 transition-all duration-200"
|
|
onclick={closeLoginModal}
|
|
aria-label="Fechar modal"
|
|
>
|
|
<XCircle class="h-5 w-5" strokeWidth={2.5} />
|
|
</button>
|
|
|
|
<!-- Decoração de fundo -->
|
|
<div class="bg-primary/10 absolute -top-20 -right-20 h-40 w-40 rounded-full blur-3xl"></div>
|
|
<div class="bg-primary/5 absolute -bottom-20 -left-20 h-40 w-40 rounded-full blur-3xl"></div>
|
|
|
|
<div class="relative z-10 p-8">
|
|
<!-- Header com logo e título -->
|
|
<div class="mb-8 text-center">
|
|
<div class="avatar mx-auto mb-5">
|
|
<div
|
|
class="group ring-primary/20 relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
|
>
|
|
<div
|
|
class="from-primary/10 absolute inset-0 bg-gradient-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
|
></div>
|
|
<img src={logo} alt="Logo SGSE" class="relative z-10 h-full w-full object-contain" />
|
|
</div>
|
|
</div>
|
|
<h3 class="text-primary mb-2 text-4xl font-bold tracking-tight">Login</h3>
|
|
<p class="text-base-content/70 text-sm font-medium">
|
|
Acesse o sistema com suas credenciais
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Mensagem de erro -->
|
|
{#if erroLogin}
|
|
<div
|
|
class="alert alert-error border-error/30 bg-error/10 mb-6 shadow-lg backdrop-blur-sm"
|
|
>
|
|
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2.5} />
|
|
<span class="font-medium">{erroLogin}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Formulário -->
|
|
<form class="space-y-5" onsubmit={handleLogin}>
|
|
<!-- Campo Matrícula/E-mail -->
|
|
<div class="form-control">
|
|
<label class="label pb-2" for="login-matricula">
|
|
<span class="text-primary label-text text-sm font-semibold">Matrícula ou E-mail</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
id="login-matricula"
|
|
type="text"
|
|
placeholder="Digite sua matrícula ou e-mail"
|
|
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
|
bind:value={matricula}
|
|
required
|
|
disabled={carregandoLogin}
|
|
autocomplete="username"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Campo Senha -->
|
|
<div class="form-control">
|
|
<label class="label pb-2" for="login-password">
|
|
<span class="text-primary label-text text-sm font-semibold">Senha</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
id="login-password"
|
|
type="password"
|
|
placeholder="Digite sua senha"
|
|
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
|
bind:value={senha}
|
|
required
|
|
disabled={carregandoLogin}
|
|
autocomplete="current-password"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botão de submit -->
|
|
<div class="form-control pt-2">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary btn-lg group from-primary via-primary to-primary/90 relative w-full overflow-hidden border-0 bg-gradient-to-r shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
|
|
disabled={carregandoLogin}
|
|
>
|
|
<!-- Efeito de brilho animado -->
|
|
<div
|
|
class="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
|
|
></div>
|
|
|
|
{#if carregandoLogin}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
<span class="font-semibold">Entrando...</span>
|
|
{:else}
|
|
<LogIn
|
|
class="h-5 w-5 transition-transform duration-300 group-hover:scale-110"
|
|
strokeWidth={2.5}
|
|
/>
|
|
<span class="font-semibold">Entrar</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Links auxiliares -->
|
|
<div class="space-y-3 pt-4 text-center">
|
|
<a
|
|
href={resolve('/abrir-chamado')}
|
|
class="link link-primary block text-sm font-medium transition-all duration-200 hover:scale-105"
|
|
onclick={closeLoginModal}
|
|
>
|
|
Abrir Chamado
|
|
</a>
|
|
<a
|
|
href={resolve('/esqueci-senha')}
|
|
class="link link-secondary block text-sm font-medium transition-all duration-200 hover:scale-105"
|
|
onclick={closeLoginModal}
|
|
>
|
|
Esqueceu sua senha?
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<form method="dialog" class="modal-backdrop" onclick={closeLoginModal}>
|
|
<button type="button">close</button>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|
|
|
|
<!-- Modal Sobre -->
|
|
{#if showAboutModal}
|
|
<dialog class="modal modal-open">
|
|
<div
|
|
class="modal-box from-base-100 to-base-200 relative max-w-md overflow-hidden bg-gradient-to-br shadow-xl"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 absolute top-2 right-2 z-10"
|
|
onclick={closeAboutModal}
|
|
>
|
|
✕
|
|
</button>
|
|
|
|
<div class="space-y-5 px-6 py-6 text-center">
|
|
<!-- Logo e Título -->
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="avatar">
|
|
<div class="ring-primary/20 w-20 rounded-xl bg-white p-3 shadow-lg ring-2">
|
|
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<h3 class="text-primary text-2xl font-bold tracking-tight">SGSE</h3>
|
|
<p class="text-base-content/70 text-sm font-medium">
|
|
Sistema de Gerenciamento de Secretaria
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="divider my-1"></div>
|
|
|
|
<!-- Informações de Versão -->
|
|
<div
|
|
class="from-primary/10 to-primary/5 border-primary/10 space-y-2 rounded-xl border bg-gradient-to-br p-4 shadow-sm"
|
|
>
|
|
<div class="flex items-center justify-center gap-2">
|
|
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
|
|
<p class="text-base-content/60 text-xs font-medium tracking-wide uppercase">Versão</p>
|
|
</div>
|
|
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
|
|
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
|
|
<Plus class="h-3.5 w-3.5" strokeWidth={2} />
|
|
Em Desenvolvimento
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desenvolvido por -->
|
|
<div class="space-y-1.5">
|
|
<p class="text-base-content/50 text-xs font-medium tracking-wide uppercase">
|
|
Desenvolvido por
|
|
</p>
|
|
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="divider my-1"></div>
|
|
|
|
<!-- Informações Adicionais -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div
|
|
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
|
>
|
|
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Governo</p>
|
|
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
|
|
</div>
|
|
<div
|
|
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
|
>
|
|
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Ano</p>
|
|
<p class="text-base-content/60 text-xs font-medium">2025</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botão OK -->
|
|
<div class="pt-3">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm mx-auto w-full max-w-xs shadow-md transition-all duration-200 hover:shadow-lg"
|
|
onclick={closeAboutModal}
|
|
>
|
|
<Check class="h-4 w-4" strokeWidth={2} />
|
|
OK
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="modal-backdrop"
|
|
onclick={closeAboutModal}
|
|
role="button"
|
|
tabindex="0"
|
|
onkeydown={(e) => e.key === 'Escape' && closeAboutModal()}
|
|
></div>
|
|
</dialog>
|
|
{/if}
|
|
|
|
<!-- Componentes de Chat (apenas se autenticado) -->
|
|
{#if currentUser.data}
|
|
<PresenceManager />
|
|
<ChatWidget />
|
|
{/if}
|
|
|
|
<style>
|
|
/* Animação de pulso sutil para o anel do botão de perfil */
|
|
@keyframes pulse-ring-subtle {
|
|
0%,
|
|
100% {
|
|
opacity: 0.1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.3;
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
|
|
/* Animação de pulso para o badge de status online */
|
|
@keyframes pulse-dot {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.8;
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
</style>
|