feat: enhance dashboard functionality by adding user statistics, improving data filtering for dispensas, and refining timestamp handling to ensure accurate time zone management
This commit is contained in:
@@ -1,20 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate, goto, replaceState } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { UserPlus, Mail, Clock, Award, TrendingUp, Zap, Users, Database } from 'lucide-svelte';
|
||||
import { UserPlus, Mail, Users, Clock, Calendar, BadgeCheck, Package } from 'lucide-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import ProtectedRoute from '$lib/components/ProtectedRoute.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());
|
||||
@@ -36,6 +30,7 @@
|
||||
// Se for erro de autenticação, abrir modal de login automaticamente
|
||||
if (error === 'auth_required') {
|
||||
const redirectTo = route || to.url.pathname;
|
||||
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
||||
replaceState: true,
|
||||
noScroll: true
|
||||
@@ -70,6 +65,7 @@
|
||||
|
||||
if (error === 'auth_required') {
|
||||
const redirectTo = route || window.location.pathname;
|
||||
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
|
||||
replaceState: true,
|
||||
noScroll: true
|
||||
@@ -119,17 +115,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -137,6 +122,43 @@
|
||||
if (hora < 18) return 'Boa tarde';
|
||||
return 'Boa noite';
|
||||
}
|
||||
|
||||
// Função para animar contador numérico
|
||||
function animateCounter(element: HTMLElement, target: number, duration: number = 2000): void {
|
||||
const start = 0;
|
||||
const increment = target / (duration / 16);
|
||||
let current = start;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
element.textContent = target.toLocaleString('pt-BR');
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
element.textContent = Math.floor(current).toLocaleString('pt-BR');
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
|
||||
// Action para animar contador
|
||||
function animateCounterAction(node: HTMLElement, value: number) {
|
||||
if (value > 0 && value !== undefined) {
|
||||
// Pequeno delay para garantir que o elemento está renderizado
|
||||
setTimeout(() => {
|
||||
animateCounter(node, value);
|
||||
}, 100);
|
||||
}
|
||||
return {
|
||||
update(newValue: number) {
|
||||
if (newValue > 0 && newValue !== undefined && newValue !== value) {
|
||||
node.textContent = '0';
|
||||
setTimeout(() => {
|
||||
animateCounter(node, newValue);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute>
|
||||
@@ -176,17 +198,23 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Cabeçalho com Boas-vindas -->
|
||||
<div class="from-primary/20 to-secondary/20 mb-6 rounded-2xl bg-linear-to-r p-8 shadow-lg">
|
||||
<!-- Hero Section com Boas-vindas -->
|
||||
<div
|
||||
class="fade-in from-primary/20 to-secondary/20 mb-8 rounded-2xl bg-linear-to-r p-8 shadow-xl"
|
||||
>
|
||||
<div class="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||
<div>
|
||||
<h1 class="text-primary mb-2 text-4xl font-bold">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-primary mb-3 text-5xl font-bold">
|
||||
{getSaudacao()}! 👋
|
||||
</h1>
|
||||
<p class="text-base-content/80 text-xl">
|
||||
Bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria
|
||||
<p class="text-base-content/80 mb-3 text-2xl font-semibold">Bem-vindo ao SGSE</p>
|
||||
<p
|
||||
class="from-primary to-secondary mb-4 bg-gradient-to-r bg-clip-text text-3xl font-bold text-transparent"
|
||||
>
|
||||
Simplificando a Gestão Pública
|
||||
</p>
|
||||
<p class="text-base-content/60 mt-2 text-sm">
|
||||
<p class="text-base-content/70 text-lg">Sistema de Gerenciamento de Secretaria</p>
|
||||
<p class="text-base-content/60 mt-3 text-sm">
|
||||
{currentTime.toLocaleDateString('pt-BR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
@@ -197,518 +225,259 @@
|
||||
{currentTime.toLocaleTimeString('pt-BR')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="badge badge-primary badge-lg">Sistema Online</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="badge badge-primary badge-lg animate-pulse">Sistema Online</div>
|
||||
<div class="badge badge-success badge-lg">Atualizado</div>
|
||||
<div class="badge badge-info badge-lg">Disponível 24h</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards de Estatísticas Principais -->
|
||||
<!-- Seção de Estatísticas -->
|
||||
{#if statsQuery.isLoading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if statsQuery.data}
|
||||
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Total de Funcionários -->
|
||||
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Card Usuários Cadastrados -->
|
||||
<div
|
||||
class="card transform bg-linear-to-br from-blue-500/10 to-blue-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
||||
class="stats-card card transform bg-linear-to-br from-blue-500/20 to-blue-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-sm font-semibold">Total de Funcionários</p>
|
||||
<h2 class="text-primary mt-2 text-4xl font-bold">
|
||||
{formatNumber(statsQuery.data.totalFuncionarios)}
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-1 text-xs">
|
||||
{statsQuery.data.funcionariosAtivos} ativos
|
||||
<div class="flex-1">
|
||||
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||
Usuários Cadastrados
|
||||
</p>
|
||||
<h2 class="text-primary text-4xl font-bold">
|
||||
{#if statsQuery.data}
|
||||
<span use:animateCounterAction={statsQuery.data.totalUsuarios}>0</span>
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-2 text-xs">no sistema</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 class="bg-primary/20 rounded-full p-4">
|
||||
<Users class="text-primary h-8 w-8" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Solicitações Pendentes -->
|
||||
<!-- Card Funcionários Ativos -->
|
||||
<div
|
||||
class="card transform bg-linear-to-br from-yellow-500/10 to-yellow-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
||||
class="stats-card card transform bg-linear-to-br from-green-500/20 to-green-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-sm font-semibold">Solicitações Pendentes</p>
|
||||
<h2 class="text-warning mt-2 text-4xl font-bold">4</h2>
|
||||
<p class="text-base-content/60 mt-1 text-xs">de 5 total</p>
|
||||
<div class="flex-1">
|
||||
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||
Funcionários Ativos
|
||||
</p>
|
||||
<h2 class="text-success text-4xl font-bold">
|
||||
{#if statsQuery.data}
|
||||
<span use:animateCounterAction={statsQuery.data.funcionariosAtivos}>0</span>
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-2 text-xs">em atividade</p>
|
||||
</div>
|
||||
<div class="bg-warning/20 rounded-full p-4">
|
||||
<div class="bg-success/20 rounded-full p-4">
|
||||
<Users class="text-success h-8 w-8" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Cadastros Realizados -->
|
||||
<div
|
||||
class="stats-card card transform bg-linear-to-br from-purple-500/20 to-purple-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||
Cadastros Realizados
|
||||
</p>
|
||||
<h2 class="text-secondary text-4xl font-bold">
|
||||
{#if statsQuery.data}
|
||||
<span use:animateCounterAction={statsQuery.data.totalCadastros}>0</span>
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-2 text-xs">total de registros</p>
|
||||
</div>
|
||||
<div class="bg-secondary/20 rounded-full p-4">
|
||||
<BadgeCheck class="text-secondary h-8 w-8" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Disponibilidade 24h -->
|
||||
<div
|
||||
class="stats-card card transform bg-linear-to-br from-orange-500/20 to-orange-600/30 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-base-content/70 mb-2 text-sm font-semibold uppercase">
|
||||
Disponibilidade
|
||||
</p>
|
||||
<h2 class="text-warning text-4xl font-bold">24h</h2>
|
||||
<p class="text-base-content/60 mt-2 text-xs">funcionando continuamente</p>
|
||||
</div>
|
||||
<div class="bg-warning/20 animate-pulse rounded-full p-4">
|
||||
<Clock class="text-warning h-8 w-8" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Símbolos Cadastrados -->
|
||||
<div
|
||||
class="card transform bg-linear-to-br from-green-500/10 to-green-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-sm font-semibold">Símbolos Cadastrados</p>
|
||||
<h2 class="text-success mt-2 text-4xl font-bold">
|
||||
{formatNumber(statsQuery.data.totalSimbolos)}
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-1 text-xs">
|
||||
{statsQuery.data.cargoComissionado} CC / {statsQuery.data.funcaoGratificada} FG
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-success/20 rounded-full p-4">
|
||||
<Award class="text-success h-8 w-8" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Atividade 24h -->
|
||||
{#if activityQuery.data}
|
||||
<div
|
||||
class="card transform bg-linear-to-br from-purple-500/10 to-purple-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-sm font-semibold">Atividade (24h)</p>
|
||||
<p class="text-base-content/60 mt-1 text-xs">
|
||||
{activityQuery.data.funcionariosCadastrados24h} cadastros
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-secondary/20 rounded-full p-4">
|
||||
<TrendingUp class="text-secondary h-8 w-8" strokeWidth={2} />
|
||||
</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 =
|
||||
atividade.historico && atividade.historico.length > 0
|
||||
? Math.max(
|
||||
1,
|
||||
...atividade.historico.map((p) => Math.max(p.entradas || 0, p.saidas || 0))
|
||||
)
|
||||
: 1}
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="bg-error/10 animate-pulse rounded-lg p-2">
|
||||
<Zap class="text-error h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base-content text-2xl font-bold">Monitoramento em Tempo Real</h2>
|
||||
<p class="text-base-content/60 text-sm">
|
||||
Atualizado a cada segundo • {new Date(status.ultimaAtualizacao).toLocaleTimeString(
|
||||
'pt-BR'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="badge badge-error badge-lg ml-auto gap-2">
|
||||
<span
|
||||
class="bg-error absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="bg-error relative inline-flex h-3 w-3 rounded-full"></span>
|
||||
LIVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards de Status do Sistema -->
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Usuários Online -->
|
||||
<div
|
||||
class="card from-primary/10 to-primary/5 border-primary/20 border-2 bg-linear-to-br shadow-lg"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
||||
Usuários Online
|
||||
</p>
|
||||
<h3 class="text-primary mt-1 text-3xl font-bold">
|
||||
{status.usuariosOnline}
|
||||
</h3>
|
||||
<p class="text-base-content/60 mt-1 text-xs">sessões ativas</p>
|
||||
</div>
|
||||
<div class="bg-primary/20 rounded-full p-3">
|
||||
<Users class="text-primary h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total de Registros -->
|
||||
<div
|
||||
class="card from-success/10 to-success/5 border-success/20 border-2 bg-linear-to-br shadow-lg"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
||||
Total Registros
|
||||
</p>
|
||||
<h3 class="text-success mt-1 text-3xl font-bold">
|
||||
{status.totalRegistros.toLocaleString('pt-BR')}
|
||||
</h3>
|
||||
<p class="text-base-content/60 mt-1 text-xs">no banco de dados</p>
|
||||
</div>
|
||||
<div class="bg-success/20 rounded-full p-3">
|
||||
<Database class="text-success h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tempo Médio de Resposta -->
|
||||
<div
|
||||
class="card from-info/10 to-info/5 border-info/20 border-2 bg-linear-to-br shadow-lg"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-base-content/70 text-xs font-semibold uppercase">
|
||||
Tempo Resposta
|
||||
</p>
|
||||
<h3 class="text-info mt-1 text-3xl font-bold">
|
||||
{status.tempoMedioResposta}ms
|
||||
</h3>
|
||||
<p class="text-base-content/60 mt-1 text-xs">média atual</p>
|
||||
</div>
|
||||
<div class="bg-info/20 rounded-full p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-info h-6 w-6"
|
||||
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 from-warning/10 to-warning/5 border-warning/20 border-2 bg-linear-to-br shadow-lg"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div>
|
||||
<p class="text-base-content/70 mb-2 text-xs font-semibold uppercase">
|
||||
Uso do Sistema
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-xs">
|
||||
<span class="text-base-content/70">CPU</span>
|
||||
<span class="text-warning font-bold">{status.cpuUsada}%</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress progress-warning w-full"
|
||||
value={status.cpuUsada}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-xs">
|
||||
<span class="text-base-content/70">Memória</span>
|
||||
<span class="text-warning font-bold">{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 mb-6 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-base-content text-xl font-bold">Atividade do Banco de Dados</h3>
|
||||
<p class="text-base-content/60 text-sm">
|
||||
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 top-0 bottom-8 left-0 flex w-10 flex-col justify-between pr-2 text-right"
|
||||
>
|
||||
{#each [10, 8, 6, 4, 2, 0] as val (val)}
|
||||
<span class="text-base-content/60 text-xs">{val}</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Grid e Barras -->
|
||||
<div class="absolute top-0 right-4 bottom-8 left-12">
|
||||
<!-- Grid horizontal -->
|
||||
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
||||
<div
|
||||
class="border-base-content/10 absolute right-0 left-0 border-t"
|
||||
style="top: {(i / 5) * 100}%;"
|
||||
></div>
|
||||
{/each}
|
||||
|
||||
<!-- Barras de atividade -->
|
||||
<div class="flex h-full items-end justify-around gap-1">
|
||||
{#each atividade.historico || [] as ponto, idx (idx)}
|
||||
{@const entradas = ponto?.entradas || 0}
|
||||
{@const saidas = ponto?.saidas || 0}
|
||||
<div class="group relative flex h-full flex-1 items-end gap-0.5">
|
||||
<!-- Entradas (verde) -->
|
||||
<div
|
||||
class="from-success to-success/70 flex-1 rounded-t bg-linear-to-t transition-all duration-300 hover:scale-110"
|
||||
style="height: {(entradas / maxAtividade) * 100}%; min-height: 2px;"
|
||||
title="Entradas: {entradas}"
|
||||
></div>
|
||||
<!-- Saídas (vermelho) -->
|
||||
<div
|
||||
class="from-error to-error/70 flex-1 rounded-t bg-linear-to-t transition-all duration-300 hover:scale-110"
|
||||
style="height: {(saidas / maxAtividade) * 100}%; min-height: 2px;"
|
||||
title="Saídas: {saidas}"
|
||||
></div>
|
||||
|
||||
<!-- Tooltip no hover -->
|
||||
<div
|
||||
class="bg-base-300 text-base-content absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 rounded px-2 py-1 text-xs whitespace-nowrap opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<div>↑ {entradas} entradas</div>
|
||||
<div>↓ {saidas} saídas</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Linha do eixo X -->
|
||||
<div
|
||||
class="border-base-content/30 absolute right-4 bottom-8 left-12 border-t-2"
|
||||
></div>
|
||||
|
||||
<!-- Labels do eixo X -->
|
||||
<div
|
||||
class="text-base-content/60 absolute right-4 bottom-0 left-12 flex justify-between text-xs"
|
||||
>
|
||||
<span>-60s</span>
|
||||
<span>-30s</span>
|
||||
<span>agora</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="border-base-300 mt-4 flex justify-center gap-6 border-t pt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="from-success to-success/70 h-4 w-4 rounded bg-linear-to-t"></div>
|
||||
<span class="text-base-content/70 text-sm">Entradas no BD</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="from-error to-error/70 h-4 w-4 rounded bg-linear-to-t"></div>
|
||||
<span class="text-base-content/70 text-sm">Saídas do BD</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Distribuição de Requisições -->
|
||||
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="text-base-content mb-4 text-lg font-bold">Tipos de Operações</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-sm">
|
||||
<span>Queries (Leituras)</span>
|
||||
<span class="text-primary font-bold">{distribuicao?.queries ?? 0}</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress progress-primary w-full"
|
||||
value={distribuicao?.queries ?? 0}
|
||||
max={Math.max(
|
||||
(distribuicao?.queries ?? 0) + (distribuicao?.mutations ?? 0),
|
||||
1
|
||||
)}
|
||||
></progress>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-sm">
|
||||
<span>Mutations (Escritas)</span>
|
||||
<span class="text-secondary font-bold">{distribuicao?.mutations ?? 0}</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress progress-secondary w-full"
|
||||
value={distribuicao?.mutations ?? 0}
|
||||
max={Math.max(
|
||||
(distribuicao?.queries ?? 0) + (distribuicao?.mutations ?? 0),
|
||||
1
|
||||
)}
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="text-base-content mb-4 text-lg font-bold">Operações no Banco</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-sm">
|
||||
<span>Leituras</span>
|
||||
<span class="text-info font-bold">{distribuicao?.leituras ?? 0}</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress progress-info w-full"
|
||||
value={distribuicao?.leituras ?? 0}
|
||||
max={Math.max(
|
||||
(distribuicao?.leituras ?? 0) + (distribuicao?.escritas ?? 0),
|
||||
1
|
||||
)}
|
||||
></progress>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 flex justify-between text-sm">
|
||||
<span>Escritas</span>
|
||||
<span class="text-warning font-bold">{distribuicao?.escritas ?? 0}</span>
|
||||
</div>
|
||||
<progress
|
||||
class="progress progress-warning w-full"
|
||||
value={distribuicao?.escritas ?? 0}
|
||||
max={Math.max(
|
||||
(distribuicao?.leituras ?? 0) + (distribuicao?.escritas ?? 0),
|
||||
1
|
||||
)}
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Cards de Status -->
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Status do Sistema</h3>
|
||||
<div class="mt-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">Banco de Dados</span>
|
||||
<span class="badge badge-success">Online</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">API</span>
|
||||
<span class="badge badge-success">Operacional</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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="mt-4 space-y-2">
|
||||
<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="mt-4 space-y-2 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>
|
||||
{:else}
|
||||
<!-- Mensagem de erro ou estado vazio -->
|
||||
<div class="alert alert-warning">
|
||||
<span>Não foi possível carregar os dados do dashboard.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Seção Sobre o SGSE -->
|
||||
<div
|
||||
class="fade-in-delay from-base-200 to-base-300 mb-8 rounded-2xl bg-linear-to-br p-8 shadow-xl"
|
||||
>
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<h2 class="text-base-content mb-4 text-3xl font-bold">Sobre o SGSE</h2>
|
||||
<p
|
||||
class="from-primary to-secondary mb-6 bg-gradient-to-r bg-clip-text text-2xl font-bold text-transparent"
|
||||
>
|
||||
Simplificando a Gestão Pública
|
||||
</p>
|
||||
<p class="text-base-content/80 mb-4 text-lg leading-relaxed">
|
||||
O Sistema de Gerenciamento de Secretaria (SGSE) é uma solução completa e moderna
|
||||
desenvolvida para otimizar e simplificar os processos administrativos da gestão pública.
|
||||
Com tecnologia de ponta e interface intuitiva, oferecemos rapidez, comodidade e
|
||||
disponibilidade 24 horas por dia para atender às necessidades dos nossos usuários.
|
||||
</p>
|
||||
<p class="text-base-content/70 text-base leading-relaxed">
|
||||
Nossa plataforma integra todas as funcionalidades essenciais em um único ambiente,
|
||||
permitindo gestão eficiente de funcionários, controle de ponto, férias, licenças, símbolos
|
||||
e muito mais. Trabalhamos continuamente para garantir que você tenha acesso rápido e
|
||||
seguro a todas as informações e ferramentas necessárias para uma gestão pública de
|
||||
excelência.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Funcionalidades Principais -->
|
||||
<div class="fade-in-delay-2 mb-8">
|
||||
<h2 class="text-base-content mb-6 text-center text-3xl font-bold">
|
||||
Principais Funcionalidades
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Gestão de Funcionários -->
|
||||
<a
|
||||
href={resolve('/recursos-humanos/funcionarios')}
|
||||
class="feature-card group card transform bg-linear-to-br from-blue-500/10 to-blue-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="rounded-xl bg-blue-500/20 p-4 transition-colors duration-300 group-hover:bg-blue-500/30"
|
||||
>
|
||||
<Users class="h-10 w-10 text-blue-600" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Funcionários</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Gerencie o cadastro completo de funcionários, informações pessoais, documentos e muito
|
||||
mais.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Controle de Ponto -->
|
||||
<a
|
||||
href={resolve('/recursos-humanos/registro-pontos')}
|
||||
class="feature-card group card transform bg-linear-to-br from-cyan-500/10 to-cyan-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="rounded-xl bg-cyan-500/20 p-4 transition-colors duration-300 group-hover:bg-cyan-500/30"
|
||||
>
|
||||
<Clock class="h-10 w-10 text-cyan-600" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-base-content mb-2 text-xl font-bold">Controle de Ponto</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Registre e gerencie pontos de funcionários, banco de horas e homologações de forma
|
||||
eficiente.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Gestão de Férias e Licenças -->
|
||||
<a
|
||||
href={resolve('/recursos-humanos/ferias')}
|
||||
class="feature-card group card transform bg-linear-to-br from-purple-500/10 to-purple-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="rounded-xl bg-purple-500/20 p-4 transition-colors duration-300 group-hover:bg-purple-500/30"
|
||||
>
|
||||
<Calendar class="h-10 w-10 text-purple-600" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Férias e Licenças</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Controle períodos de férias, atestados médicos e licenças de forma organizada e
|
||||
simplificada.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Gestão de Símbolos -->
|
||||
<a
|
||||
href={resolve('/recursos-humanos/simbolos')}
|
||||
class="feature-card group card transform bg-linear-to-br from-green-500/10 to-green-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="rounded-xl bg-green-500/20 p-4 transition-colors duration-300 group-hover:bg-green-500/30"
|
||||
>
|
||||
<BadgeCheck class="h-10 w-10 text-green-600" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-base-content mb-2 text-xl font-bold">Gestão de Símbolos</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Gerencie cargos comissionados e funções gratificadas com facilidade e organização.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Almoxarifado -->
|
||||
<a
|
||||
href={resolve('/almoxarifado')}
|
||||
class="feature-card group card transform bg-linear-to-br from-amber-500/10 to-amber-600/20 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="rounded-xl bg-amber-500/20 p-4 transition-colors duration-300 group-hover:bg-amber-500/30"
|
||||
>
|
||||
<Package class="h-10 w-10 text-amber-600" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-base-content mb-2 text-xl font-bold">Almoxarifado</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Controle de estoque, materiais, movimentações e requisições de forma integrada e
|
||||
eficiente.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
|
||||
@@ -724,7 +493,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.fade-in-delay {
|
||||
animation: fadeIn 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.fade-in-delay-2 {
|
||||
animation: fadeIn 1s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
let modoCriacao = $state(false);
|
||||
let mostrandoModalExcluir = $state(false);
|
||||
let dispensaParaExcluir = $state<Id<'dispensasRegistro'> | null>(null);
|
||||
let filtroStatus = $state<'todas' | 'ativas' | 'expiradas'>('todas');
|
||||
|
||||
// Formulário
|
||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
||||
@@ -25,22 +26,33 @@
|
||||
// Computed para converter time string para hora/minuto
|
||||
let horaInicio = $derived.by(() => {
|
||||
const [hora, minuto] = horaInicioTime.split(':').map(Number);
|
||||
return { hora: hora || 8, minuto: minuto || 0 };
|
||||
return { hora: isNaN(hora) ? 8 : hora, minuto: isNaN(minuto) ? 0 : minuto };
|
||||
});
|
||||
|
||||
let horaFim = $derived.by(() => {
|
||||
const [hora, minuto] = horaFimTime.split(':').map(Number);
|
||||
return { hora: hora || 18, minuto: minuto || 0 };
|
||||
return { hora: isNaN(hora) ? 18 : hora, minuto: isNaN(minuto) ? 0 : minuto };
|
||||
});
|
||||
|
||||
// Queries
|
||||
const subordinadosQuery = useQuery(api.times.listarSubordinadosDoGestorAtual, {});
|
||||
const dispensasQuery = useQuery(api.pontos.listarDispensas, {
|
||||
apenasAtivas: true // Mostrar apenas dispensas ativas
|
||||
});
|
||||
const dispensasQuery = useQuery(api.pontos.listarDispensas, {});
|
||||
|
||||
let subordinados = $derived(subordinadosQuery?.data || []);
|
||||
let dispensas = $derived(dispensasQuery?.data || []);
|
||||
let todasDispensas = $derived(dispensasQuery?.data || []);
|
||||
|
||||
// Filtrar dispensas baseado no filtro selecionado
|
||||
let dispensas = $derived.by(() => {
|
||||
if (filtroStatus === 'todas') {
|
||||
return todasDispensas;
|
||||
} else if (filtroStatus === 'ativas') {
|
||||
// Ativas: não expiradas (inclui isentos que já começaram)
|
||||
return todasDispensas.filter((d) => !d.expirada);
|
||||
} else {
|
||||
// Expiradas: apenas dispensas não isentas que expiraram
|
||||
return todasDispensas.filter((d) => d.expirada && !d.isento);
|
||||
}
|
||||
});
|
||||
|
||||
// Lista de funcionários do time
|
||||
let funcionarios = $derived.by(() => {
|
||||
@@ -313,11 +325,42 @@
|
||||
<!-- Lista de Dispensas -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Dispensas Ativas</h2>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="card-title">Dispensas</h2>
|
||||
<!-- Filtro de Status -->
|
||||
<div class="join">
|
||||
<button
|
||||
class="btn join-item btn-sm {filtroStatus === 'todas' ? 'btn-active' : ''}"
|
||||
onclick={() => (filtroStatus = 'todas')}
|
||||
>
|
||||
Todas
|
||||
</button>
|
||||
<button
|
||||
class="btn join-item btn-sm {filtroStatus === 'ativas' ? 'btn-active' : ''}"
|
||||
onclick={() => (filtroStatus = 'ativas')}
|
||||
>
|
||||
Ativas
|
||||
</button>
|
||||
<button
|
||||
class="btn join-item btn-sm {filtroStatus === 'expiradas' ? 'btn-active' : ''}"
|
||||
onclick={() => (filtroStatus = 'expiradas')}
|
||||
>
|
||||
Expiradas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if dispensas.length === 0}
|
||||
<div class="alert alert-info">
|
||||
<span>Nenhuma dispensa ativa encontrada</span>
|
||||
<span>
|
||||
{#if filtroStatus === 'todas'}
|
||||
Nenhuma dispensa encontrada
|
||||
{:else if filtroStatus === 'ativas'}
|
||||
Nenhuma dispensa ativa encontrada
|
||||
{:else}
|
||||
Nenhuma dispensa expirada encontrada
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
@@ -373,7 +416,7 @@
|
||||
{#if dispensa.isento}
|
||||
<span class="badge badge-warning">Isento (sem expiração)</span>
|
||||
{:else if dispensa.expirada}
|
||||
<span class="badge badge-error">Expirada</span>
|
||||
<span class="badge badge-error">Não ativo</span>
|
||||
{:else}
|
||||
<span class="badge badge-success">Ativa</span>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user