refactor: enhance ProtectedRoute and dashboard components for improved access control and user experience

- Updated the ProtectedRoute component to optimize access checking logic, preventing unnecessary re-checks and improving authentication flow.
- Enhanced the dashboard page to automatically open the login modal for authentication errors and refined loading states for better user feedback.
- Improved UI elements across various components for consistency and visual appeal, including updated tab styles and enhanced alert messages.
- Removed redundant footer from the vacation management page to streamline the interface.
This commit is contained in:
2025-11-18 06:34:55 -03:00
parent 3420872a37
commit 422dc6f022
5 changed files with 398 additions and 541 deletions

View File

@@ -21,11 +21,26 @@
let isChecking = $state(true);
let hasAccess = $state(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let hasCheckedOnce = $state(false);
let lastUserState = $state<typeof currentUser | undefined>(undefined);
const currentUser = useQuery(api.auth.getCurrentUser, {});
// Usar $effect para reagir às mudanças na query
// Usar $effect para reagir apenas às mudanças na query currentUser
$effect(() => {
// Não verificar novamente se já tem acesso concedido e usuário está autenticado
if (hasAccess && currentUser?.data) {
lastUserState = currentUser;
return;
}
// Evitar loop: só verificar se currentUser realmente mudou
// Comparar dados, não o objeto proxy
const currentData = currentUser?.data;
const lastData = lastUserState?.data;
if (currentData !== lastData || (currentUser === undefined) !== (lastUserState === undefined)) {
lastUserState = currentUser;
checkAccess();
}
});
function checkAccess() {
@@ -42,6 +57,9 @@
return;
}
// Marcar que já verificou pelo menos uma vez
hasCheckedOnce = true;
// Se a query retornou dados, verificar autenticação
if (currentUser?.data) {
// Verificar roles
@@ -67,20 +85,29 @@
return;
}
// Se não tem dados e requer autenticação, aguardar um pouco antes de redirecionar
// (pode estar carregando ainda)
// Se não tem dados e requer autenticação
if (requireAuth && !currentUser?.data) {
// Se a query já retornou (não está mais undefined), finalizar estado
if (currentUser !== undefined) {
const currentPath = window.location.pathname;
// Evitar redirecionamento em loop - verificar se já está na URL de erro
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('error')) {
// Só redirecionar se não estiver em loop
if (!hasCheckedOnce || currentUser === null) {
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
return;
}
}
// Se já tem erro na URL, permitir renderização para mostrar o alerta
isChecking = false;
hasAccess = true;
return;
}
// Se ainda está carregando (undefined), aguardar
isChecking = true;
hasAccess = false;
// Aguardar 3 segundos antes de redirecionar (dar tempo para a query carregar)
timeoutId = setTimeout(() => {
// Verificar novamente antes de redirecionar
if (!currentUser?.data) {
const currentPath = window.location.pathname;
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
}
}, 3000);
return;
}

View File

@@ -9,12 +9,13 @@
import { UserPlus, Mail } from "lucide-svelte";
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { loginModalStore } from "$lib/stores/loginModal.svelte";
let { data } = $props();
const auth = useAuth();
const isLoading = $derived(auth.isLoading && !data.currentUser);
const isAuthenticated = $derived(auth.isAuthenticated || !!data.currentUser);
const isLoading = $derived(auth.isLoading && !data?.currentUser);
const isAuthenticated = $derived(auth.isAuthenticated || !!data?.currentUser);
$inspect({ isLoading, isAuthenticated });
@@ -56,6 +57,11 @@
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, {});
@@ -75,6 +81,17 @@
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();

View File

@@ -11,7 +11,7 @@
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import type { FunctionReturnType } from 'convex/server';
import { X, Calendar } from 'lucide-svelte';
import { X, Calendar, Users, Clock, CheckCircle2, Eye, FileCheck, CalendarDays, User, Mail, Shield, Briefcase, Hash, CreditCard, Building2, CheckCircle, ListChecks, Info } from 'lucide-svelte';
import TicketCard from '$lib/components/chamados/TicketCard.svelte';
import TicketTimeline from '$lib/components/chamados/TicketTimeline.svelte';
import { chamadosStore } from '$lib/stores/chamados';
@@ -680,48 +680,26 @@
<!-- Tabs PREMIUM -->
<div
role="tablist"
class="tabs tabs-boxed from-base-200 to-base-300 mb-8 bg-linear-to-r p-2 shadow-xl"
class="tabs tabs-boxed from-base-200 to-base-300 mb-8 bg-gradient-to-r p-2 shadow-xl rounded-xl border border-base-300"
>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'meu-perfil' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'meu-perfil' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'meu-perfil')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<User class="h-5 w-5" strokeWidth={2} />
Meu Perfil
</button>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'meus-chamados' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'meus-chamados' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'meus-chamados')}
aria-label="Meus Chamados"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7h18M3 12h12M3 17h18" />
</svg>
<FileCheck class="h-5 w-5" strokeWidth={2} />
Meus Chamados
</button>
@@ -729,46 +707,20 @@
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ferias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<Calendar class="h-5 w-5" strokeWidth={2} />
Minhas Férias
</button>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ausencias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Clock class="h-5 w-5" strokeWidth={2} />
Minhas Ausências
</button>
@@ -776,26 +728,13 @@
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-green-600 to-emerald-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ferias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<CheckCircle2 class="h-5 w-5" strokeWidth={2} />
Aprovar Férias
{#if (solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
<span class="badge badge-error badge-sm ml-1 animate-pulse">
{(solicitacoesSubordinados || []).filter(
(s) => s.status === 'aguardando_aprovacao'
).length}
@@ -806,26 +745,13 @@
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ausencias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Clock class="h-5 w-5" strokeWidth={2} />
Aprovar Ausências
{#if (ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
<span class="badge badge-error badge-sm ml-1 animate-pulse">
{(ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao')
.length}
</span>
@@ -966,107 +892,68 @@
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Informações Pessoais PREMIUM -->
<div
class="card bg-base-100 hover:shadow-3xl border-t-4 border-purple-500 shadow-2xl transition-shadow"
class="card bg-gradient-to-br from-base-100 to-base-200 hover:shadow-3xl border-t-4 border-purple-500 shadow-2xl transition-shadow overflow-hidden"
>
<div class="card-body">
<h2 class="card-title mb-6 flex items-center gap-2 text-2xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-7 w-7 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg ring-2 ring-purple-500/20">
<User class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
Informações Pessoais
</h2>
<div class="space-y-4">
<p class="text-sm text-base-content/60 mt-0.5">
Seus dados pessoais e de acesso
</p>
</div>
</div>
</div>
<div class="space-y-3">
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500/20 to-purple-600/20">
<User class="text-purple-600 h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold"
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
>Nome Completo</span
>
<p class="text-base-content text-lg font-semibold">
<p class="text-base-content text-base font-semibold mt-1">
{currentUser.data?.nome}
</p>
</div>
</div>
<div class="divider my-1"></div>
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20">
<Mail class="text-blue-600 h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold"
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
>E-mail Institucional</span
>
<p class="text-base-content text-lg font-semibold break-all">
<p class="text-base-content text-base font-semibold break-all mt-1">
{currentUser.data?.email}
</p>
</div>
</div>
<div class="divider my-1"></div>
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary/20 to-primary/30">
<Shield class="text-primary h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold"
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
>Perfil de Acesso</span
>
<div class="badge badge-primary badge-lg mt-1 font-bold">
<div class="badge badge-primary badge-lg mt-2 font-bold shadow-sm">
{currentUser.data?.role?.nome || 'Usuário'}
</div>
</div>
@@ -1078,149 +965,97 @@
<!-- Dados Funcionais PREMIUM -->
{#if funcionario}
<div
class="card bg-base-100 hover:shadow-3xl border-t-4 border-blue-500 shadow-2xl transition-shadow"
class="card bg-gradient-to-br from-base-100 to-base-200 hover:shadow-3xl border-t-4 border-blue-500 shadow-2xl transition-shadow overflow-hidden"
>
<div class="card-body">
<h2 class="card-title mb-6 flex items-center gap-2 text-2xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-7 w-7 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg ring-2 ring-blue-500/20">
<Briefcase class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
Dados Funcionais
</h2>
<div class="space-y-4">
<p class="text-sm text-base-content/60 mt-0.5">
Informações profissionais e organizacionais
</p>
</div>
</div>
</div>
<div class="space-y-3">
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20">
<Hash class="text-blue-600 h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold"
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
>Matrícula</span
>
<p class="text-base-content text-lg font-semibold">
<p class="text-base-content text-base font-semibold mt-1">
{funcionario.matricula || 'Não informada'}
</p>
</div>
</div>
<div class="divider my-1"></div>
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-green-500/20 to-green-600/20">
<CreditCard class="text-green-600 h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold">CPF</span>
<p class="text-base-content text-lg font-semibold">
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide">CPF</span>
<p class="text-base-content text-base font-semibold mt-1">
{funcionario.cpf}
</p>
</div>
</div>
<div class="divider my-1"></div>
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
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 class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500/20 to-orange-600/20">
<Building2 class="text-orange-600 h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold">Time</span>
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide">Time</span>
{#if meuTime}
<div class="mt-1 flex items-center gap-2">
<div class="mt-2">
<div
class="badge badge-lg font-semibold"
class="badge badge-lg font-semibold shadow-sm"
style="background-color: {meuTime.cor}20; border-color: {meuTime.cor}; color: {meuTime.cor}"
>
{meuTime.nome}
</div>
</div>
<p class="text-base-content/60 mt-1 text-xs">
<p class="text-base-content/60 mt-2 text-xs">
Gestor: <span class="font-semibold">{meuTime.gestor?.nome}</span>
</p>
</div>
{:else}
<p class="text-base-content/50 mt-1 text-sm">Não atribuído a um time</p>
{/if}
</div>
</div>
<div class="divider my-1"></div>
<div
class="hover:bg-base-200 flex items-start gap-3 rounded-lg p-3 transition-colors"
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary mt-1 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-success/20 to-success/30">
<CheckCircle class="text-success h-5 w-5" strokeWidth={2} />
</div>
<div class="flex-1">
<span class="label-text text-base-content/70 text-sm font-bold"
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
>Status Atual</span
>
{#if funcionario.statusFerias === 'em_ferias'}
<div class="badge badge-warning badge-lg mt-1 font-bold">
<div class="badge badge-warning badge-lg mt-2 font-bold shadow-sm">
🏖️ Em Férias
</div>
{:else}
<div class="badge badge-success badge-lg mt-1 font-bold">✅ Ativo</div>
<div class="badge badge-success badge-lg mt-2 font-bold shadow-sm">✅ Ativo</div>
{/if}
</div>
</div>
@@ -1751,57 +1586,68 @@
</div>
<!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title mb-4 text-lg">
Minhas Solicitações ({solicitacoesFiltradas.length})
<div class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-primary shadow-2xl overflow-hidden">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-primary/80 shadow-lg ring-2 ring-primary/20">
<ListChecks class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
Minhas Solicitações
</h2>
<p class="text-sm text-base-content/60 mt-0.5">
Histórico de solicitações de férias
</p>
</div>
</div>
<div class="badge badge-lg badge-primary gap-2 px-4 py-3 shadow-md">
<ListChecks class="h-4 w-4" strokeWidth={2} />
{solicitacoesFiltradas.length}
</div>
</div>
{#if solicitacoesFiltradas.length === 0}
<div class="alert">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhuma solicitação encontrada com os filtros aplicados.</span>
<div class="alert alert-info shadow-lg border border-info/20">
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span class="font-semibold">Nenhuma solicitação encontrada com os filtros aplicados.</span>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table-zebra table">
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
<table class="table table-zebra">
<thead>
<tr>
<th>Ano</th>
<th>Período</th>
<th>Dias</th>
<th>Status</th>
<th>Solicitado em</th>
<tr class="bg-gradient-to-r from-base-200 to-base-300">
<th class="font-bold text-base-content">Ano</th>
<th class="font-bold text-base-content">Período</th>
<th class="font-bold text-base-content">Dias</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content">Solicitado em</th>
</tr>
</thead>
<tbody>
{#each solicitacoesFiltradas as periodo (periodo._id)}
<tr>
<td>{periodo.anoReferencia}</td>
<td>
{formatarDataString(periodo.dataInicio)} - {formatarDataString(
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
<td class="font-semibold text-base-content/80">{periodo.anoReferencia}</td>
<td class="font-medium text-base-content/70">
<div class="flex items-center gap-1.5">
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
<span>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
periodo.dataFim
)}
)}</span>
</div>
</td>
<td class="font-bold">{periodo.diasFerias} dias</td>
<td>
<div class={`badge ${getStatusBadge(periodo.status)}`}>
<div class="badge badge-primary badge-lg font-bold shadow-sm">
{periodo.diasFerias} dias
</div>
</td>
<td>
<div class={`badge badge-sm font-semibold shadow-sm ${getStatusBadge(periodo.status)}`}>
{getStatusTexto(periodo.status)}
</div>
</td>
<td class="text-xs"
<td class="text-xs text-base-content/60 font-medium"
>{new Date(periodo._creationTime).toLocaleDateString('pt-BR')}</td
>
</tr>
@@ -2104,72 +1950,59 @@
</div>
{:else if abaAtiva === 'aprovar-ferias'}
<!-- Aprovar Férias (Gestores) PREMIUM -->
<div class="card bg-base-100 border-t-4 border-green-500 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-6 flex items-center gap-2 text-2xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-7 w-7 text-green-600"
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 class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-green-500 shadow-2xl overflow-hidden">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600 shadow-lg ring-2 ring-green-500/20">
<Users class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
Solicitações da Equipe
<div class="badge badge-lg badge-primary ml-2">
</h2>
<p class="text-sm text-base-content/60 mt-0.5">
Gerencie as solicitações de férias da sua equipe
</p>
</div>
</div>
<div class="badge badge-lg badge-primary gap-2 px-4 py-3 shadow-md">
<FileCheck class="h-4 w-4" strokeWidth={2} />
{solicitacoesSubordinados.length}
</div>
</h2>
</div>
{#if solicitacoesSubordinados.length === 0}
<div class="alert alert-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div class="alert alert-success shadow-lg border border-success/20">
<CheckCircle2 class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span class="font-semibold">Nenhuma solicitação pendente no momento.</span>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table-zebra table-lg table">
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-bold">Funcionário</th>
<th class="font-bold">Time</th>
<th class="font-bold">Ano</th>
<th class="font-bold">Período</th>
<th class="font-bold">Dias</th>
<th class="font-bold">Status</th>
<th class="font-bold">Ações</th>
<tr class="bg-gradient-to-r from-base-200 to-base-300">
<th class="font-bold text-base-content">Funcionário</th>
<th class="font-bold text-base-content">Time</th>
<th class="font-bold text-base-content">Ano</th>
<th class="font-bold text-base-content">Período</th>
<th class="font-bold text-base-content">Dias</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content text-center">Ações</th>
</tr>
</thead>
<tbody>
{#each solicitacoesSubordinados as periodo (periodo._id)}
<tr class="hover:bg-base-200 transition-colors">
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
<td>
<div class="font-bold">
<div class="font-semibold text-base-content">
{periodo.funcionario?.nome}
</div>
</td>
<td>
{#if periodo.time}
<div
class="badge badge-lg font-semibold"
class="badge badge-sm font-semibold shadow-sm"
style="background-color: {periodo.time.cor}20; border-color: {periodo
.time.cor}; color: {periodo.time.cor}"
>
@@ -2177,72 +2010,49 @@
</div>
{/if}
</td>
<td class="font-semibold">{periodo.anoReferencia}</td>
<td class="font-semibold">
{formatarDataString(periodo.dataInicio)} - {formatarDataString(
<td class="font-semibold text-base-content/80">{periodo.anoReferencia}</td>
<td class="font-medium text-base-content/70">
<div class="flex items-center gap-1.5">
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
<span>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
periodo.dataFim
)}
)}</span>
</div>
</td>
<td>
<div class="badge badge-primary badge-lg font-bold shadow-sm">
{periodo.diasFerias} dias
</div>
</td>
<td class="text-lg font-bold">{periodo.diasFerias}</td>
<td>
<div
class={`badge badge-lg font-semibold ${getStatusBadge(periodo.status)}`}
class={`badge badge-sm font-semibold shadow-sm ${getStatusBadge(periodo.status)}`}
>
{getStatusTexto(periodo.status)}
</div>
</td>
<td>
<div class="flex justify-center">
{#if periodo.status === 'aguardando_aprovacao'}
<button
type="button"
class="btn btn-primary btn-sm gap-2 shadow-lg transition-transform hover:scale-105"
class="btn btn-primary btn-sm gap-2 shadow-md transition-all duration-200 hover:scale-105 hover:shadow-lg"
onclick={() => selecionarPeriodo(periodo._id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
<FileCheck class="h-4 w-4" strokeWidth={2} />
Analisar
</button>
{:else}
<button
type="button"
class="btn btn-sm gap-2"
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300 transition-all duration-200"
onclick={() => selecionarPeriodo(periodo._id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
<Eye class="h-4 w-4" strokeWidth={2} />
Detalhes
</button>
{/if}
</div>
</td>
</tr>
{/each}
@@ -2254,71 +2064,58 @@
</div>
{:else if abaAtiva === 'aprovar-ausencias'}
<!-- Aprovar Ausências (Gestores) -->
<div class="card bg-base-100 border-t-4 border-orange-500 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-6 flex items-center gap-2 text-2xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-7 w-7 text-orange-600"
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 class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-orange-500 shadow-2xl overflow-hidden">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 shadow-lg ring-2 ring-orange-500/20">
<Clock class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
Solicitações de Ausências da Equipe
<div class="badge badge-lg badge-warning ml-2">
</h2>
<p class="text-sm text-base-content/60 mt-0.5">
Gerencie as solicitações de ausências da sua equipe
</p>
</div>
</div>
<div class="badge badge-lg badge-warning gap-2 px-4 py-3 shadow-md">
<FileCheck class="h-4 w-4" strokeWidth={2} />
{ausenciasSubordinados.length}
</div>
</h2>
</div>
{#if ausenciasSubordinados.length === 0}
<div class="alert alert-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div class="alert alert-success shadow-lg border border-success/20">
<CheckCircle2 class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span class="font-semibold">Nenhuma solicitação pendente no momento.</span>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table-zebra table-lg table">
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-bold">Funcionário</th>
<th class="font-bold">Time</th>
<th class="font-bold">Período</th>
<th class="font-bold">Dias</th>
<th class="font-bold">Status</th>
<th class="font-bold">Ações</th>
<tr class="bg-gradient-to-r from-base-200 to-base-300">
<th class="font-bold text-base-content">Funcionário</th>
<th class="font-bold text-base-content">Time</th>
<th class="font-bold text-base-content">Período</th>
<th class="font-bold text-base-content">Dias</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content text-center">Ações</th>
</tr>
</thead>
<tbody>
{#each ausenciasSubordinados as ausencia (ausencia._id)}
<tr class="hover:bg-base-200 transition-colors">
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
<td>
<div class="font-bold">
<div class="font-semibold text-base-content">
{ausencia.funcionario?.nome || 'N/A'}
</div>
</td>
<td>
{#if ausencia.time}
<div
class="badge badge-lg font-semibold"
class="badge badge-sm font-semibold shadow-sm"
style="background-color: {ausencia.time
.cor}20; border-color: {ausencia.time.cor}; color: {ausencia.time
.cor}"
@@ -2327,76 +2124,53 @@
</div>
{/if}
</td>
<td class="font-semibold">
<td class="font-medium text-base-content/70">
<div class="flex items-center gap-1.5">
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
<span>
{new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até
{new Date(ausencia.dataFim).toLocaleDateString('pt-BR')}
</span>
</div>
</td>
<td class="text-lg font-bold">
<td>
<div class="badge badge-warning badge-lg font-bold shadow-sm">
{Math.ceil(
(new Date(ausencia.dataFim).getTime() -
new Date(ausencia.dataInicio).getTime()) /
(1000 * 60 * 60 * 24)
) + 1} dias
</div>
</td>
<td>
<div
class={`badge badge-lg font-semibold ${getStatusBadge(ausencia.status)}`}
class={`badge badge-sm font-semibold shadow-sm ${getStatusBadge(ausencia.status)}`}
>
{getStatusTexto(ausencia.status)}
</div>
</td>
<td>
<div class="flex justify-center">
{#if ausencia.status === 'aguardando_aprovacao'}
<button
type="button"
class="btn btn-warning btn-sm gap-2 shadow-lg transition-transform hover:scale-105"
class="btn btn-warning btn-sm gap-2 shadow-md transition-all duration-200 hover:scale-105 hover:shadow-lg"
onclick={() => (solicitacaoAusenciaAprovar = ausencia._id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
<FileCheck class="h-4 w-4" strokeWidth={2} />
Analisar
</button>
{:else}
<button
type="button"
class="btn btn-sm gap-2"
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300 transition-all duration-200"
onclick={() => (solicitacaoAusenciaAprovar = ausencia._id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
<Eye class="h-4 w-4" strokeWidth={2} />
Detalhes
</button>
{/if}
</div>
</td>
</tr>
{/each}

View File

@@ -1993,12 +1993,6 @@
{/if}
{/await}
{/if}
<footer
class="border-base-300/60 bg-base-100 text-base-content/70 mt-8 border-t py-6 text-center text-sm"
>
SGSE - Sistema de Gerenciamento de Secretaria.
</footer>
<style>
/* Calendário de Férias */

View File

@@ -636,7 +636,7 @@ export const getStatusSistema = query({
/**
* Atividade do banco no último minuto (agregada em buckets)
* Usa mensagensPorMinuto como proxy de atividade quando disponível.
* Usa logsAtividades e systemMetrics para calcular atividade real.
*/
export const getAtividadeBancoDados = query({
args: {},
@@ -652,6 +652,14 @@ export const getAtividadeBancoDados = query({
const agora = Date.now();
const haUmMinuto = agora - 60 * 1000;
// Buscar atividades reais do sistema
const atividadesRecentes = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
.order('asc')
.collect();
// Buscar métricas também (para mensagens se houver)
const metricasRecentes = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
@@ -666,15 +674,30 @@ export const getAtividadeBancoDados = query({
for (let i = 0; i < numBuckets; i++) {
const inicio = haUmMinuto + i * bucketSizeMs;
const fim = inicio + bucketSizeMs;
// Contar atividades de criação/inserção (entradas)
const atividadesBucket = atividadesRecentes.filter(
(a) => a.timestamp >= inicio && a.timestamp < fim
);
const entradasAtividades = atividadesBucket.filter(
a => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
).length;
// Contar atividades de exclusão/remoção (saídas)
const saidasAtividades = atividadesBucket.filter(
a => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
).length;
// Usar mensagensPorMinuto como adicional se disponível
const bucketMetricas = metricasRecentes.filter(
(m) => m.timestamp >= inicio && m.timestamp < fim
);
// Usar mensagensPorMinuto como proxy de "entradas"; "saídas" como fração
const somaMensagens =
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
const entradas = Math.max(0, Math.round(somaMensagens));
const saidas = Math.max(0, Math.round(entradas * 0.6));
// Combinar atividades reais com métricas de mensagens
const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
historico.push({ entradas, saidas });
}
@@ -684,7 +707,7 @@ export const getAtividadeBancoDados = query({
});
/**
* Distribuição de operações (estimada a partir das métricas)
* Distribuição de operações (calculada a partir de logsAtividades e métricas)
*/
export const getDistribuicaoRequisicoes = query({
args: {},
@@ -696,21 +719,43 @@ export const getDistribuicaoRequisicoes = query({
}),
handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
// Buscar atividades reais do sistema
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.collect();
// Buscar métricas também
const metricas = await ctx.db
.query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.order('desc')
.take(100);
const totalOps = Math.max(
// Contar operações de leitura (consultas, visualizações)
const leituras = atividades.filter(
a => a.acao === 'consultar' || a.acao === 'visualizar' || a.acao === 'listar' || a.acao === 'buscar'
).length;
// Contar operações de escrita (criar, editar, excluir)
const escritas = atividades.filter(
a => a.acao === 'criar' || a.acao === 'editar' || a.acao === 'excluir' ||
a.acao === 'inserir' || a.acao === 'atualizar' || a.acao === 'deletar' ||
a.acao === 'cadastrar' || a.acao === 'remover'
).length;
// Adicionar estimativa baseada em mensagens se disponível
const totalMensagens = Math.max(
0,
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
);
const queries = Math.round(totalOps * 0.7);
const mutations = Math.max(0, totalOps - queries);
const leituras = queries;
const escritas = mutations;
// Queries são leituras + parte das mensagens (como consultas de chat)
const queries = leituras + Math.round(totalMensagens * 0.5);
// Mutations são escritas + parte das mensagens (como envio de mensagens)
const mutations = escritas + Math.round(totalMensagens * 0.3);
return { queries, mutations, leituras, escritas };
}