Files
sgse-app/apps/web/src/routes/(dashboard)/+page.svelte
killer-cf dac559d9fd refactor: remove access request functionality and related components
- Deleted the solicitacoesAcesso route and its associated components to streamline the dashboard.
- Updated dashboard stats to remove references to access requests, ensuring accurate data representation.
- Refactored backend queries to eliminate access request data handling, enhancing performance and maintainability.
- Adjusted type definitions to reflect the removal of access request functionalities.
2025-11-19 12:30:42 -03:00

851 lines
31 KiB
Svelte

<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { onMount } from "svelte";
import { goto, replaceState } from "$app/navigation";
import { afterNavigate } from "$app/navigation";
import { resolve } from "$app/paths";
import { UserPlus, Mail } from "lucide-svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { loginModalStore } from "$lib/stores/loginModal.svelte";
// Queries para dados do dashboard
const statsQuery = useQuery(api.dashboard.getStats, {});
const activityQuery = useQuery(api.dashboard.getRecentActivity, {});
// Queries para monitoramento em tempo real
const statusSistemaQuery = useQuery(api.monitoramento.getStatusSistema, {});
const atividadeBDQuery = useQuery(
api.monitoramento.getAtividadeBancoDados,
{},
);
const distribuicaoQuery = useQuery(
api.monitoramento.getDistribuicaoRequisicoes,
{},
);
// Estado para animações
let currentTime = $state(new Date());
let showAlert = $state(false);
let alertType = $state<
"auth_required" | "access_denied" | "invalid_token" | null
>(null);
let redirectRoute = $state("");
// Forçar atualização das queries de monitoramento a cada 1 segundo
let refreshKey = $state(0);
// Limpar URL após navegação estar completa
afterNavigate(({ to }) => {
if (to?.url.searchParams.has("error")) {
const error = to.url.searchParams.get("error");
const route = to.url.searchParams.get("route") || to.url.searchParams.get("redirect") || "";
if (error) {
alertType = error as any;
redirectRoute = route;
showAlert = true;
// Se for erro de autenticação, abrir modal de login automaticamente
if (error === "auth_required") {
loginModalStore.open(route || to.url.pathname);
}
// Limpar URL usando SvelteKit (após router estar inicializado)
try {
replaceState(to.url.pathname, {});
} catch (e) {
// Se ainda não estiver pronto, usar goto com replaceState
goto(to.url.pathname, { replaceState: true, noScroll: true });
}
// Auto-fechar após 10 segundos
setTimeout(() => {
showAlert = false;
}, 10000);
}
}
});
onMount(() => {
mounted = true;
// Verificar se há erro na URL ao carregar a página
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("error")) {
const error = urlParams.get("error");
const route = urlParams.get("route") || urlParams.get("redirect") || "";
if (error === "auth_required") {
loginModalStore.open(route || window.location.pathname);
}
}
// Atualizar relógio e forçar refresh das queries a cada segundo
const interval = setInterval(() => {
currentTime = new Date();
refreshKey = (refreshKey + 1) % 1000; // Incrementar para forçar re-render
}, 1000);
return () => clearInterval(interval);
});
function closeAlert() {
showAlert = false;
}
function getAlertMessage(): { title: string; message: string; icon: string } {
switch (alertType) {
case "auth_required":
return {
title: "Autenticação Necessária",
message: `Para acessar "${redirectRoute}", você precisa fazer login no sistema.`,
icon: "🔐",
};
case "access_denied":
return {
title: "Acesso Negado",
message: `Você não tem permissão para acessar "${redirectRoute}". Entre em contato com a equipe de TI para solicitar acesso.`,
icon: "⛔",
};
case "invalid_token":
return {
title: "Sessão Expirada",
message: "Sua sessão expirou. Por favor, faça login novamente.",
icon: "⏰",
};
default:
return {
title: "Aviso",
message: "Ocorreu um erro. Tente novamente.",
icon: "⚠️",
};
}
}
// Função para formatar números
function formatNumber(num: number): string {
return new Intl.NumberFormat("pt-BR").format(num);
}
// Função para calcular porcentagem
function calcPercentage(value: number, total: number): number {
if (total === 0) return 0;
return Math.round((value / total) * 100);
}
// Obter saudação baseada na hora
function getSaudacao(): string {
const hora = currentTime.getHours();
if (hora < 12) return "Bom dia";
if (hora < 18) return "Boa tarde";
return "Boa noite";
}
</script>
<ProtectedRoute>
<main class="container mx-auto px-4 py-4">
<!-- Alerta de Acesso Negado / Autenticação -->
{#if showAlert}
{@const alertData = getAlertMessage()}
<div
class="alert {alertType === 'access_denied'
? 'alert-error'
: alertType === 'auth_required'
? 'alert-warning'
: 'alert-info'} mb-6 shadow-xl animate-pulse"
>
<div class="flex items-start gap-4">
<span class="text-4xl">{alertData.icon}</span>
<div class="flex-1">
<h3 class="font-bold text-lg mb-1">{alertData.title}</h3>
<p class="text-sm">{alertData.message}</p>
{#if alertType === "access_denied"}
<div class="mt-3 flex gap-2">
<a href={resolve("/abrir-chamado")} class="btn btn-sm btn-primary">
<svelte:component
this={UserPlus}
class="h-4 w-4"
strokeWidth={2}
/>
Abrir Chamado
</a>
<a href={resolve("/ti")} class="btn btn-sm btn-ghost">
<svelte:component this={Mail} class="h-4 w-4" strokeWidth={2} />
Contatar TI
</a>
</div>
{/if}
</div>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={closeAlert}>✕</button
>
</div>
</div>
{/if}
<!-- Cabeçalho com Boas-vindas -->
<div
class="bg-linear-to-r from-primary/20 to-secondary/20 rounded-2xl p-8 mb-6 shadow-lg"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"
>
<div>
<h1 class="text-4xl font-bold text-primary mb-2">
{getSaudacao()}! 👋
</h1>
<p class="text-xl text-base-content/80">
Bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria
</p>
<p class="text-sm text-base-content/60 mt-2">
{currentTime.toLocaleDateString("pt-BR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
{" - "}
{currentTime.toLocaleTimeString("pt-BR")}
</p>
</div>
<div class="flex gap-2">
<div class="badge badge-primary badge-lg">Sistema Online</div>
<div class="badge badge-success badge-lg">Atualizado</div>
</div>
</div>
</div>
<!-- Cards de Estatísticas Principais -->
{#if statsQuery.isLoading}
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if statsQuery.data}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Total de Funcionários -->
<div
class="card bg-linear-to-br from-blue-500/10 to-blue-600/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/70 font-semibold">
Total de Funcionários
</p>
<h2 class="text-4xl font-bold text-primary mt-2">
{formatNumber(statsQuery.data.totalFuncionarios)}
</h2>
<p class="text-xs text-base-content/60 mt-1">
{statsQuery.data.funcionariosAtivos} ativos
</p>
</div>
<div
class="radial-progress text-primary"
style="--value:{calcPercentage(
statsQuery.data.funcionariosAtivos,
statsQuery.data.totalFuncionarios,
)}; --size:4rem;"
>
<span class="text-xs font-bold"
>{calcPercentage(
statsQuery.data.funcionariosAtivos,
statsQuery.data.totalFuncionarios,
)}%</span
>
</div>
</div>
</div>
</div>
<!-- Solicitações Pendentes -->
<div
class="card bg-linear-to-br from-yellow-500/10 to-yellow-600/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/70 font-semibold">
Solicitações Pendentes
</p>
<h2 class="text-4xl font-bold text-warning mt-2">
4
</h2>
<p class="text-xs text-base-content/60 mt-1">
de 5 total
</p>
</div>
<div class="p-4 bg-warning/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-warning"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Símbolos Cadastrados -->
<div
class="card bg-linear-to-br from-green-500/10 to-green-600/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/70 font-semibold">
Símbolos Cadastrados
</p>
<h2 class="text-4xl font-bold text-success mt-2">
{formatNumber(statsQuery.data.totalSimbolos)}
</h2>
<p class="text-xs text-base-content/60 mt-1">
{statsQuery.data.cargoComissionado} CC / {statsQuery.data
.funcaoGratificada} FG
</p>
</div>
<div class="p-4 bg-success/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-success"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Atividade 24h -->
{#if activityQuery.data}
<div
class="card bg-linear-to-br from-purple-500/10 to-purple-600/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/70 font-semibold">
Atividade (24h)
</p>
<p class="text-xs text-base-content/60 mt-1">
{activityQuery.data.funcionariosCadastrados24h} cadastros
</p>
</div>
<div class="p-4 bg-secondary/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-secondary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
</div>
</div>
</div>
</div>
{/if}
</div>
<!-- Monitoramento em Tempo Real -->
{#if statusSistemaQuery.data}
{@const status = statusSistemaQuery.data}
{@const atividade = atividadeBDQuery.data || { historico: Array.from({ length: 30 }, () => ({ entradas: 0, saidas: 0 })) }}
{@const distribuicao = distribuicaoQuery.data || { queries: 0, mutations: 0, leituras: 0, escritas: 0 }}
{@const maxAtividade = Math.max(
1,
...atividade.historico.map((p) =>
Math.max(p.entradas, p.saidas),
),
)}
<div class="mb-6">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-error/10 rounded-lg animate-pulse">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<div>
<h2 class="text-2xl font-bold text-base-content">
Monitoramento em Tempo Real
</h2>
<p class="text-sm text-base-content/60">
Atualizado a cada segundo • {new Date(
status.ultimaAtualizacao,
).toLocaleTimeString("pt-BR")}
</p>
</div>
<div class="ml-auto badge badge-error badge-lg gap-2">
<span
class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-error opacity-75"
></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-error"
></span>
LIVE
</div>
</div>
<!-- Cards de Status do Sistema -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Usuários Online -->
<div
class="card bg-linear-to-br from-primary/10 to-primary/5 border-2 border-primary/20 shadow-lg"
>
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p
class="text-xs text-base-content/70 font-semibold uppercase"
>
Usuários Online
</p>
<h3 class="text-3xl font-bold text-primary mt-1">
{status.usuariosOnline}
</h3>
<p class="text-xs text-base-content/60 mt-1">
sessões ativas
</p>
</div>
<div class="p-3 bg-primary/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Total de Registros -->
<div
class="card bg-linear-to-br from-success/10 to-success/5 border-2 border-success/20 shadow-lg"
>
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p
class="text-xs text-base-content/70 font-semibold uppercase"
>
Total Registros
</p>
<h3 class="text-3xl font-bold text-success mt-1">
{status.totalRegistros.toLocaleString("pt-BR")}
</h3>
<p class="text-xs text-base-content/60 mt-1">
no banco de dados
</p>
</div>
<div class="p-3 bg-success/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-success"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Tempo Médio de Resposta -->
<div
class="card bg-linear-to-br from-info/10 to-info/5 border-2 border-info/20 shadow-lg"
>
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p
class="text-xs text-base-content/70 font-semibold uppercase"
>
Tempo Resposta
</p>
<h3 class="text-3xl font-bold text-info mt-1">
{status.tempoMedioResposta}ms
</h3>
<p class="text-xs text-base-content/60 mt-1">média atual</p>
</div>
<div class="p-3 bg-info/20 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-info"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Uso de Sistema -->
<div
class="card bg-linear-to-br from-warning/10 to-warning/5 border-2 border-warning/20 shadow-lg"
>
<div class="card-body p-4">
<div>
<p
class="text-xs text-base-content/70 font-semibold uppercase mb-2"
>
Uso do Sistema
</p>
<div class="space-y-2">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-base-content/70">CPU</span>
<span class="font-bold text-warning"
>{status.cpuUsada}%</span
>
</div>
<progress
class="progress progress-warning w-full"
value={status.cpuUsada}
max="100"
></progress>
</div>
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-base-content/70">Memória</span>
<span class="font-bold text-warning"
>{status.memoriaUsada}%</span
>
</div>
<progress
class="progress progress-warning w-full"
value={status.memoriaUsada}
max="100"
></progress>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Gráfico de Atividade do Banco de Dados em Tempo Real -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-xl font-bold text-base-content">
Atividade do Banco de Dados
</h3>
<p class="text-sm text-base-content/60">
Entradas e saídas em tempo real (último minuto)
</p>
</div>
<div class="badge badge-success gap-2">
<span class="loading loading-spinner loading-xs"></span>
Atualizando
</div>
</div>
<div class="relative h-64">
<!-- Eixo Y -->
<div
class="absolute left-0 top-0 bottom-8 w-10 flex flex-col justify-between text-right pr-2"
>
{#each [10, 8, 6, 4, 2, 0] as val}
<span class="text-xs text-base-content/60">{val}</span>
{/each}
</div>
<!-- Grid e Barras -->
<div class="absolute left-12 right-4 top-0 bottom-8">
<!-- Grid horizontal -->
{#each Array.from({ length: 6 }) as _, i}
<div
class="absolute left-0 right-0 border-t border-base-content/10"
style="top: {(i / 5) * 100}%;"
></div>
{/each}
<!-- Barras de atividade -->
<div class="flex items-end justify-around h-full gap-1">
{#each atividade.historico as ponto, idx}
<div class="flex-1 flex items-end gap-0.5 h-full group relative">
<!-- Entradas (verde) -->
<div
class="flex-1 bg-linear-to-t from-success to-success/70 rounded-t transition-all duration-300 hover:scale-110"
style="height: {(ponto.entradas / maxAtividade) * 100}%; min-height: 2px;"
title="Entradas: {ponto.entradas}"
></div>
<!-- Saídas (vermelho) -->
<div
class="flex-1 bg-linear-to-t from-error to-error/70 rounded-t transition-all duration-300 hover:scale-110"
style="height: {(ponto.saidas / maxAtividade) * 100}%; min-height: 2px;"
title="Saídas: {ponto.saidas}"
></div>
<!-- Tooltip no hover -->
<div
class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-base-300 text-base-content px-2 py-1 rounded text-xs whitespace-nowrap shadow-lg z-10"
>
<div>{ponto.entradas} entradas</div>
<div>{ponto.saidas} saídas</div>
</div>
</div>
{/each}
</div>
</div>
<!-- Linha do eixo X -->
<div
class="absolute left-12 right-4 bottom-8 border-t-2 border-base-content/30"
></div>
<!-- Labels do eixo X -->
<div
class="absolute left-12 right-4 bottom-0 flex justify-between text-xs text-base-content/60"
>
<span>-60s</span>
<span>-30s</span>
<span>agora</span>
</div>
</div>
<!-- Legenda -->
<div
class="flex justify-center gap-6 mt-4 pt-4 border-t border-base-300"
>
<div class="flex items-center gap-2">
<div
class="w-4 h-4 bg-linear-to-t from-success to-success/70 rounded"
></div>
<span class="text-sm text-base-content/70">Entradas no BD</span>
</div>
<div class="flex items-center gap-2">
<div
class="w-4 h-4 bg-linear-to-t from-error to-error/70 rounded"
></div>
<span class="text-sm text-base-content/70">Saídas do BD</span>
</div>
</div>
</div>
</div>
<!-- Distribuição de Requisições -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="text-lg font-bold text-base-content mb-4">
Tipos de Operações
</h3>
<div class="space-y-3">
<div>
<div class="flex justify-between text-sm mb-1">
<span>Queries (Leituras)</span>
<span class="font-bold text-primary"
>{distribuicao.queries}</span
>
</div>
<progress
class="progress progress-primary w-full"
value={distribuicao.queries}
max={Math.max(distribuicao.queries + distribuicao.mutations, 1)}
></progress>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span>Mutations (Escritas)</span>
<span class="font-bold text-secondary"
>{distribuicao.mutations}</span
>
</div>
<progress
class="progress progress-secondary w-full"
value={distribuicao.mutations}
max={Math.max(distribuicao.queries + distribuicao.mutations, 1)}
></progress>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="text-lg font-bold text-base-content mb-4">
Operações no Banco
</h3>
<div class="space-y-3">
<div>
<div class="flex justify-between text-sm mb-1">
<span>Leituras</span>
<span class="font-bold text-info"
>{distribuicao.leituras}</span
>
</div>
<progress
class="progress progress-info w-full"
value={distribuicao.leituras}
max={Math.max(distribuicao.leituras + distribuicao.escritas, 1)}
></progress>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span>Escritas</span>
<span class="font-bold text-warning"
>{distribuicao.escritas}</span
>
</div>
<progress
class="progress progress-warning w-full"
value={distribuicao.escritas}
max={Math.max(distribuicao.leituras + distribuicao.escritas, 1)}
></progress>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Cards de Status -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg">Status do Sistema</h3>
<div class="space-y-2 mt-4">
<div class="flex justify-between items-center">
<span class="text-sm">Banco de Dados</span>
<span class="badge badge-success">Online</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm">API</span>
<span class="badge badge-success">Operacional</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm">Backup</span>
<span class="badge badge-success">Atualizado</span>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg">Acesso Rápido</h3>
<div class="space-y-2 mt-4">
<a
href={resolve("/recursos-humanos/funcionarios/cadastro")}
class="btn btn-sm btn-primary w-full"
>
Novo Funcionário
</a>
<a
href={resolve("/recursos-humanos/simbolos/cadastro")}
class="btn btn-sm btn-primary w-full"
>
Novo Símbolo
</a>
<a
href={resolve("/ti/painel-administrativo")}
class="btn btn-sm btn-primary w-full"
>
Painel Admin
</a>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg">Informações</h3>
<div class="space-y-2 mt-4 text-sm">
<p class="text-base-content/70">
<strong>Versão:</strong> 1.0.0
</p>
<p class="text-base-content/70">
<strong>Última Atualização:</strong>
{new Date().toLocaleDateString("pt-BR")}
</p>
<p class="text-base-content/70">
<strong>Suporte:</strong> TI SGSE
</p>
</div>
</div>
</div>
</div>
{/if}
</main>
</ProtectedRoute>
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeIn 0.5s ease-out;
}
</style>