Refinament 1 #31

Merged
killer-cf merged 5 commits from refinament-1 into master 2025-11-19 19:25:21 +00:00
11 changed files with 83 additions and 1319 deletions
Showing only changes of commit dac559d9fd - Show all commits

View File

@@ -2,23 +2,13 @@
import { useQuery } from "convex-svelte"; import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from "@sgse-app/backend/convex/_generated/api";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from "$app/stores";
import { goto, replaceState } from "$app/navigation"; import { goto, replaceState } from "$app/navigation";
import { afterNavigate } from "$app/navigation"; import { afterNavigate } from "$app/navigation";
import { resolve } from "$app/paths"; import { resolve } from "$app/paths";
import { UserPlus, Mail } from "lucide-svelte"; import { UserPlus, Mail } from "lucide-svelte";
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte"; import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { loginModalStore } from "$lib/stores/loginModal.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);
$inspect({ isLoading, isAuthenticated });
// Queries para dados do dashboard // Queries para dados do dashboard
const statsQuery = useQuery(api.dashboard.getStats, {}); const statsQuery = useQuery(api.dashboard.getStats, {});
const activityQuery = useQuery(api.dashboard.getRecentActivity, {}); const activityQuery = useQuery(api.dashboard.getRecentActivity, {});
@@ -35,7 +25,6 @@
); );
// Estado para animações // Estado para animações
let mounted = $state(false);
let currentTime = $state(new Date()); let currentTime = $state(new Date());
let showAlert = $state(false); let showAlert = $state(false);
let alertType = $state< let alertType = $state<
@@ -282,10 +271,10 @@
Solicitações Pendentes Solicitações Pendentes
</p> </p>
<h2 class="text-4xl font-bold text-warning mt-2"> <h2 class="text-4xl font-bold text-warning mt-2">
{formatNumber(statsQuery.data.solicitacoesPendentes)} 4
</h2> </h2>
<p class="text-xs text-base-content/60 mt-1"> <p class="text-xs text-base-content/60 mt-1">
de {statsQuery.data.totalSolicitacoesAcesso} total de 5 total
</p> </p>
</div> </div>
<div class="p-4 bg-warning/20 rounded-full"> <div class="p-4 bg-warning/20 rounded-full">
@@ -357,12 +346,6 @@
<p class="text-sm text-base-content/70 font-semibold"> <p class="text-sm text-base-content/70 font-semibold">
Atividade (24h) Atividade (24h)
</p> </p>
<h2 class="text-4xl font-bold text-secondary mt-2">
{formatNumber(
activityQuery.data.funcionariosCadastrados24h +
activityQuery.data.solicitacoesAcesso24h,
)}
</h2>
<p class="text-xs text-base-content/60 mt-1"> <p class="text-xs text-base-content/60 mt-1">
{activityQuery.data.funcionariosCadastrados24h} cadastros {activityQuery.data.funcionariosCadastrados24h} cadastros
</p> </p>

View File

@@ -299,15 +299,6 @@
palette: 'accent', palette: 'accent',
icon: 'users' icon: 'users'
}, },
{
title: 'Solicitações de Acesso',
description:
'Gerencie e analise solicitações de acesso ao sistema. Aprove ou rejeite novas solicitações de forma eficiente.',
ctaLabel: 'Gerenciar Solicitações',
href: '/(dashboard)/ti/solicitacoes-acesso',
palette: 'warning',
icon: 'userPlus'
},
{ {
title: 'Gestão de Times', title: 'Gestão de Times',
description: description:

View File

@@ -1,836 +0,0 @@
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
import StatsCard from '$lib/components/ti/StatsCard.svelte';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { FileText, Clock, CheckCircle2, XCircle } from 'lucide-svelte';
type StatusSolicitacao = 'pendente' | 'aprovado' | 'rejeitado';
type SolicitacaoAcesso = {
_id: Id<'solicitacoesAcesso'>;
_creationTime: number;
nome: string;
matricula: string;
email: string;
telefone: string;
status: StatusSolicitacao;
dataSolicitacao: number;
dataResposta: number | null;
observacoes: string | null;
};
type FiltroStatus = 'todos' | 'pendente' | 'aprovado' | 'rejeitado';
type Mensagem = {
tipo: 'success' | 'error' | 'info';
texto: string;
};
const client = useConvexClient();
// Queries
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
// Estados
let filtroStatus = $state<FiltroStatus>('todos');
let busca = $state('');
let solicitacaoSelecionada = $state<SolicitacaoAcesso | null>(null);
let modalDetalhesAberto = $state(false);
let modalAprovarAberto = $state(false);
let modalRejeitarAberto = $state(false);
let observacoes = $state('');
let mensagem = $state<Mensagem | null>(null);
let processando = $state(false);
// Extrair dados das solicitações
const solicitacoes = $derived.by(() => {
if (solicitacoesQuery === undefined || solicitacoesQuery === null) {
return [];
}
if ('data' in solicitacoesQuery && solicitacoesQuery.data !== undefined) {
return Array.isArray(solicitacoesQuery.data) ? solicitacoesQuery.data : [];
}
if (Array.isArray(solicitacoesQuery)) {
return solicitacoesQuery;
}
return [];
});
const carregando = $derived.by(() => {
return solicitacoesQuery === undefined || solicitacoesQuery === null;
});
// Estatísticas
const stats = $derived.by(() => {
if (carregando) return null;
const total = solicitacoes.length;
const pendentes = solicitacoes.filter((s) => s.status === 'pendente').length;
const aprovadas = solicitacoes.filter((s) => s.status === 'aprovado').length;
const rejeitadas = solicitacoes.filter((s) => s.status === 'rejeitado').length;
return {
total,
pendentes,
aprovadas,
rejeitadas
};
});
// Filtrar e buscar solicitações
const solicitacoesFiltradas = $derived.by(() => {
let resultado = solicitacoes;
// Filtrar por status
if (filtroStatus !== 'todos') {
resultado = resultado.filter((s) => s.status === filtroStatus);
}
// Buscar por nome, matrícula ou email
if (busca.trim()) {
const termo = busca.toLowerCase().trim();
resultado = resultado.filter(
(s) =>
s.nome.toLowerCase().includes(termo) ||
s.matricula.toLowerCase().includes(termo) ||
s.email.toLowerCase().includes(termo)
);
}
// Ordenar por data (mais recente primeiro)
return resultado.sort((a, b) => b.dataSolicitacao - a.dataSolicitacao);
});
// Funções auxiliares
function formatarData(timestamp: number): string {
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
}
function formatarDataRelativa(timestamp: number): string {
const agora = Date.now();
const diff = agora - timestamp;
const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
const horas = Math.floor(diff / (1000 * 60 * 60));
const minutos = Math.floor(diff / (1000 * 60));
if (dias > 0) return `${dias} dia${dias > 1 ? 's' : ''} atrás`;
if (horas > 0) return `${horas} hora${horas > 1 ? 's' : ''} atrás`;
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? 's' : ''} atrás`;
return 'Agora';
}
function getStatusBadge(status: StatusSolicitacao): string {
switch (status) {
case 'pendente':
return 'badge-warning';
case 'aprovado':
return 'badge-success';
case 'rejeitado':
return 'badge-error';
default:
return 'badge-neutral';
}
}
function getStatusTexto(status: StatusSolicitacao): string {
switch (status) {
case 'pendente':
return 'Pendente';
case 'aprovado':
return 'Aprovado';
case 'rejeitado':
return 'Rejeitado';
default:
return status;
}
}
// Funções de modal
function abrirDetalhes(solicitacao: SolicitacaoAcesso) {
solicitacaoSelecionada = solicitacao;
modalDetalhesAberto = true;
}
function fecharDetalhes() {
modalDetalhesAberto = false;
solicitacaoSelecionada = null;
}
function abrirAprovar(solicitacao: SolicitacaoAcesso) {
solicitacaoSelecionada = solicitacao;
observacoes = '';
modalAprovarAberto = true;
}
function fecharAprovar() {
modalAprovarAberto = false;
solicitacaoSelecionada = null;
observacoes = '';
}
function abrirRejeitar(solicitacao: SolicitacaoAcesso) {
solicitacaoSelecionada = solicitacao;
observacoes = '';
modalRejeitarAberto = true;
}
function fecharRejeitar() {
modalRejeitarAberto = false;
solicitacaoSelecionada = null;
observacoes = '';
}
// Funções de ação
async function aprovarSolicitacao() {
if (!solicitacaoSelecionada) return;
processando = true;
mensagem = null;
try {
await client.mutation(api.solicitacoesAcesso.aprovar, {
solicitacaoId: solicitacaoSelecionada._id,
observacoes: observacoes.trim() || undefined
});
mensagem = {
tipo: 'success',
texto: 'Solicitação aprovada com sucesso!'
};
fecharAprovar();
// Limpar mensagem após 3 segundos
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro ao aprovar solicitação';
mensagem = {
tipo: 'error',
texto: errorMessage
};
} finally {
processando = false;
}
}
async function rejeitarSolicitacao() {
if (!solicitacaoSelecionada) return;
processando = true;
mensagem = null;
try {
await client.mutation(api.solicitacoesAcesso.rejeitar, {
solicitacaoId: solicitacaoSelecionada._id,
observacoes: observacoes.trim() || undefined
});
mensagem = {
tipo: 'success',
texto: 'Solicitação rejeitada com sucesso!'
};
fecharRejeitar();
// Limpar mensagem após 3 segundos
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro ao rejeitar solicitação';
mensagem = {
tipo: 'error',
texto: errorMessage
};
} finally {
processando = false;
}
}
</script>
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Mensagem de Feedback -->
{#if mensagem}
<div class="alert alert-{mensagem.tipo} mb-6 shadow-lg">
{#if mensagem.tipo === 'success'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<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>
{:else if mensagem.tipo === 'error'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
<span>{mensagem.texto}</span>
</div>
{/if}
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="bg-primary/10 rounded-xl p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
</div>
<div>
<h1 class="text-base-content text-3xl font-bold">Solicitações de Acesso</h1>
<p class="text-base-content/60 mt-1">
Gerencie e analise solicitações de acesso ao sistema
</p>
</div>
</div>
</div>
<!-- Estatísticas -->
{#if stats}
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<StatsCard
title="Total de Solicitações"
value={stats.total}
Icon={FileText}
color="primary"
/>
<StatsCard
title="Pendentes"
value={stats.pendentes}
description={stats.total > 0
? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total'
: '0% do total'}
Icon={Clock}
color="warning"
/>
<StatsCard
title="Aprovadas"
value={stats.aprovadas}
description={stats.total > 0
? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total'
: '0% do total'}
Icon={CheckCircle2}
color="success"
/>
<StatsCard
title="Rejeitadas"
value={stats.rejeitadas}
description={stats.total > 0
? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total'
: '0% do total'}
Icon={XCircle}
color="error"
/>
</div>
{:else}
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{/if}
<!-- Filtros e Busca -->
<div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body">
<!-- Tabs de Status -->
<div class="tabs tabs-boxed bg-base-200 mb-4 p-2">
<button
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
onclick={() => (filtroStatus = 'todos')}
>
Todas
</button>
<button
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
onclick={() => (filtroStatus = 'pendente')}
>
Pendentes
</button>
<button
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
onclick={() => (filtroStatus = 'aprovado')}
>
Aprovadas
</button>
<button
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
onclick={() => (filtroStatus = 'rejeitado')}
>
Rejeitadas
</button>
</div>
<!-- Campo de Busca -->
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Buscar por nome, matrícula ou e-mail</span>
</label>
<div class="relative">
<input
type="text"
placeholder="Digite para buscar..."
class="input input-bordered w-full pl-10"
bind:value={busca}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- Lista de Solicitações -->
{#if carregando}
<div class="flex items-center justify-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if solicitacoesFiltradas.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body py-20 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/30 mx-auto mb-4 h-16 w-16"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 class="text-base-content/70 mb-2 text-xl font-semibold">
Nenhuma solicitação encontrada
</h3>
<p class="text-base-content/50">
{#if busca.trim() || filtroStatus !== 'todos'}
Tente ajustar os filtros ou a busca.
{:else}
Ainda não há solicitações de acesso cadastradas.
{/if}
</p>
</div>
</div>
{:else}
<div class="grid grid-cols-1 gap-4">
{#each solicitacoesFiltradas as solicitacao}
<div class="card bg-base-100 shadow-xl transition-shadow hover:shadow-2xl">
<div class="card-body">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex-1">
<div class="mb-2 flex items-center gap-3">
<h3 class="text-base-content text-xl font-bold">{solicitacao.nome}</h3>
<span class="badge {getStatusBadge(solicitacao.status)} badge-lg">
{getStatusTexto(solicitacao.status)}
</span>
</div>
<div class="text-base-content/70 grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
<div class="flex items-center gap-2">
<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="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>
<span class="font-semibold">Matrícula:</span>
<span>{solicitacao.matricula}</span>
</div>
<div class="flex items-center gap-2">
<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="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>
<span class="font-semibold">E-mail:</span>
<span>{solicitacao.email}</span>
</div>
<div class="flex items-center gap-2">
<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="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
<span class="font-semibold">Telefone:</span>
<span>{solicitacao.telefone}</span>
</div>
</div>
<div class="text-base-content/50 mt-3 text-xs">
<span class="font-semibold">Solicitado em:</span>
{formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(
solicitacao.dataSolicitacao
)})
{#if solicitacao.dataResposta}
<span class="ml-4 font-semibold">Processado em:</span>
{formatarData(solicitacao.dataResposta)}
{/if}
</div>
</div>
<div class="flex flex-col gap-2">
<button
class="btn btn-sm btn-outline btn-primary"
onclick={() => abrirDetalhes(solicitacao)}
>
<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>
Ver Detalhes
</button>
{#if solicitacao.status === 'pendente'}
<button
class="btn btn-sm btn-success"
onclick={() => abrirAprovar(solicitacao)}
>
<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="M5 13l4 4L19 7"
/>
</svg>
Aprovar
</button>
<button class="btn btn-sm btn-error" onclick={() => abrirRejeitar(solicitacao)}>
<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="M6 18L18 6M6 6l12 12"
/>
</svg>
Rejeitar
</button>
{/if}
</div>
</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- Modal de Detalhes -->
{#if modalDetalhesAberto && solicitacaoSelecionada}
<dialog class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="mb-4 text-2xl font-bold">Detalhes da Solicitação</h3>
<div class="space-y-4">
<div class="mb-4 flex items-center gap-3">
<span class="badge {getStatusBadge(solicitacaoSelecionada.status)} badge-lg">
{getStatusTexto(solicitacaoSelecionada.status)}
</span>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="label">
<span class="label-text font-semibold">Nome Completo</span>
</label>
<div class="input input-bordered">{solicitacaoSelecionada.nome}</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Matrícula</span>
</label>
<div class="input input-bordered">{solicitacaoSelecionada.matricula}</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">E-mail</span>
</label>
<div class="input input-bordered">{solicitacaoSelecionada.email}</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Telefone</span>
</label>
<div class="input input-bordered">{solicitacaoSelecionada.telefone}</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Data da Solicitação</span>
</label>
<div class="input input-bordered">
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
</div>
</div>
{#if solicitacaoSelecionada.dataResposta}
<div>
<label class="label">
<span class="label-text font-semibold">Data de Processamento</span>
</label>
<div class="input input-bordered">
{formatarData(solicitacaoSelecionada.dataResposta)}
</div>
</div>
{/if}
</div>
{#if solicitacaoSelecionada.observacoes}
<div>
<label class="label">
<span class="label-text font-semibold">Observações</span>
</label>
<div class="textarea textarea-bordered min-h-24">
{solicitacaoSelecionada.observacoes}
</div>
</div>
{/if}
</div>
<div class="modal-action">
<button class="btn" onclick={fecharDetalhes}>Fechar</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={fecharDetalhes}>fechar</button>
</form>
</dialog>
{/if}
<!-- Modal de Aprovar -->
{#if modalAprovarAberto && solicitacaoSelecionada}
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="mb-4 text-2xl font-bold">Aprovar Solicitação</h3>
<div class="mb-4">
<p class="text-base-content/70 mb-2">
Você está prestes a aprovar a solicitação de acesso de <strong
>{solicitacaoSelecionada.nome}</strong
>.
</p>
<p class="text-base-content/60 text-sm">
Após aprovar, o sistema permitirá que esta pessoa solicite acesso ao sistema.
</p>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text font-semibold">Observações (opcional)</span>
</label>
<textarea
class="textarea textarea-bordered"
placeholder="Adicione observações sobre a aprovação..."
bind:value={observacoes}
rows="3"
></textarea>
</div>
<div class="modal-action">
<button class="btn" onclick={fecharAprovar} disabled={processando}> Cancelar </button>
<button class="btn btn-success" onclick={aprovarSolicitacao} disabled={processando}>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
Processando...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
Confirmar Aprovação
{/if}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={fecharAprovar}>fechar</button>
</form>
</dialog>
{/if}
<!-- Modal de Rejeitar -->
{#if modalRejeitarAberto && solicitacaoSelecionada}
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="mb-4 text-2xl font-bold">Rejeitar Solicitação</h3>
<div class="mb-4">
<p class="text-base-content/70 mb-2">
Você está prestes a rejeitar a solicitação de acesso de <strong
>{solicitacaoSelecionada.nome}</strong
>.
</p>
<p class="text-base-content/60 text-sm">
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo para a rejeição.
</p>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text font-semibold">Motivo da Rejeição (recomendado)</span>
</label>
<textarea
class="textarea textarea-bordered"
placeholder="Descreva o motivo da rejeição..."
bind:value={observacoes}
rows="3"
></textarea>
</div>
<div class="modal-action">
<button class="btn" onclick={fecharRejeitar} disabled={processando}> Cancelar </button>
<button class="btn btn-error" onclick={rejeitarSolicitacao} disabled={processando}>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
Processando...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Confirmar Rejeição
{/if}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick={fecharRejeitar}>fechar</button>
</form>
</dialog>
{/if}
</div>
</ProtectedRoute>

View File

@@ -47,7 +47,6 @@ import type * as saldoFerias from "../saldoFerias.js";
import type * as security from "../security.js"; import type * as security from "../security.js";
import type * as seed from "../seed.js"; import type * as seed from "../seed.js";
import type * as simbolos from "../simbolos.js"; import type * as simbolos from "../simbolos.js";
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
import type * as templatesMensagens from "../templatesMensagens.js"; import type * as templatesMensagens from "../templatesMensagens.js";
import type * as times from "../times.js"; import type * as times from "../times.js";
import type * as todos from "../todos.js"; import type * as todos from "../todos.js";
@@ -101,7 +100,6 @@ declare const fullApi: ApiFromModules<{
security: typeof security; security: typeof security;
seed: typeof seed; seed: typeof seed;
simbolos: typeof simbolos; simbolos: typeof simbolos;
solicitacoesAcesso: typeof solicitacoesAcesso;
templatesMensagens: typeof templatesMensagens; templatesMensagens: typeof templatesMensagens;
times: typeof times; times: typeof times;
todos: typeof todos; todos: typeof todos;

View File

@@ -7,8 +7,6 @@ export const getStats = query({
returns: v.object({ returns: v.object({
totalFuncionarios: v.number(), totalFuncionarios: v.number(),
totalSimbolos: v.number(), totalSimbolos: v.number(),
totalSolicitacoesAcesso: v.number(),
solicitacoesPendentes: v.number(),
funcionariosAtivos: v.number(), funcionariosAtivos: v.number(),
funcionariosDesligados: v.number(), funcionariosDesligados: v.number(),
cargoComissionado: v.number(), cargoComissionado: v.number(),
@@ -42,19 +40,9 @@ export const getStats = query({
const simbolos = await ctx.db.query("simbolos").collect(); const simbolos = await ctx.db.query("simbolos").collect();
const totalSimbolos = simbolos.length; const totalSimbolos = simbolos.length;
// Contar solicitações de acesso
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const totalSolicitacoesAcesso = solicitacoes.length;
const solicitacoesPendentes = solicitacoes.filter(
(s) => s.status === "pendente"
).length;
return { return {
totalFuncionarios, totalFuncionarios,
totalSimbolos, totalSimbolos,
totalSolicitacoesAcesso,
solicitacoesPendentes,
funcionariosAtivos, funcionariosAtivos,
funcionariosDesligados, funcionariosDesligados,
cargoComissionado, cargoComissionado,
@@ -68,7 +56,6 @@ export const getRecentActivity = query({
args: {}, args: {},
returns: v.object({ returns: v.object({
funcionariosCadastrados24h: v.number(), funcionariosCadastrados24h: v.number(),
solicitacoesAcesso24h: v.number(),
simbolosCadastrados24h: v.number(), simbolosCadastrados24h: v.number(),
}), }),
handler: async (ctx) => { handler: async (ctx) => {
@@ -81,11 +68,6 @@ export const getRecentActivity = query({
(f) => f._creationTime >= last24h (f) => f._creationTime >= last24h
).length; ).length;
// Solicitações de acesso nas últimas 24h
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const solicitacoesAcesso24h = solicitacoes.filter(
(s) => s.dataSolicitacao >= last24h
).length;
// Símbolos cadastrados nas últimas 24h // Símbolos cadastrados nas últimas 24h
const simbolos = await ctx.db.query("simbolos").collect(); const simbolos = await ctx.db.query("simbolos").collect();
@@ -95,7 +77,6 @@ export const getRecentActivity = query({
return { return {
funcionariosCadastrados24h, funcionariosCadastrados24h,
solicitacoesAcesso24h,
simbolosCadastrados24h, simbolosCadastrados24h,
}; };
}, },
@@ -137,15 +118,13 @@ export const getEvolucaoCadastros = query({
v.object({ v.object({
mes: v.string(), mes: v.string(),
funcionarios: v.number(), funcionarios: v.number(),
solicitacoes: v.number(),
}) })
), ),
handler: async (ctx) => { handler: async (ctx) => {
const funcionarios = await ctx.db.query("funcionarios").collect(); const funcionarios = await ctx.db.query("funcionarios").collect();
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const now = new Date(); const now = new Date();
const meses: Array<{ mes: string; funcionarios: number; solicitacoes: number }> = []; const meses: Array<{ mes: string; funcionarios: number }> = [];
// Últimos 6 meses // Últimos 6 meses
for (let i = 5; i >= 0; i--) { for (let i = 5; i >= 0; i--) {
@@ -161,14 +140,9 @@ export const getEvolucaoCadastros = query({
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime() (f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
).length; ).length;
const solCount = solicitacoes.filter(
(s) => s.dataSolicitacao >= date.getTime() && s.dataSolicitacao < nextDate.getTime()
).length;
meses.push({ meses.push({
mes: mesNome, mes: mesNome,
funcionarios: funcCount, funcionarios: funcCount,
solicitacoes: solCount,
}); });
} }

View File

@@ -594,12 +594,11 @@ export const getStatusSistema = query({
} }
// Total de registros (estimativa baseada em tabelas principais) // Total de registros (estimativa baseada em tabelas principais)
const [usuarios, funcionarios, simbolos, solicitacoesAcesso, alertas, metricas] = const [usuarios, funcionarios, simbolos, alertas, metricas] =
await Promise.all([ await Promise.all([
ctx.db.query('usuarios').collect(), ctx.db.query('usuarios').collect(),
ctx.db.query('funcionarios').collect(), ctx.db.query('funcionarios').collect(),
ctx.db.query('simbolos').collect(), ctx.db.query('simbolos').collect(),
ctx.db.query('solicitacoesAcesso').collect(),
ctx.db.query('alertConfigurations').collect(), ctx.db.query('alertConfigurations').collect(),
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
]); ]);
@@ -607,7 +606,6 @@ export const getStatusSistema = query({
usuarios.length + usuarios.length +
funcionarios.length + funcionarios.length +
simbolos.length + simbolos.length +
solicitacoesAcesso.length +
alertas.length + alertas.length +
metricas.length; metricas.length;

View File

@@ -514,24 +514,6 @@ export default defineSchema({
valor: v.string(), valor: v.string(),
}), }),
solicitacoesAcesso: defineTable({
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.optional(v.number()),
observacoes: v.optional(v.string()),
})
.index("by_status", ["status"])
.index("by_matricula", ["matricula"])
.index("by_email", ["email"]),
// Sistema de Autenticação e Controle de Acesso // Sistema de Autenticação e Controle de Acesso
usuarios: defineTable({ usuarios: defineTable({
authId: v.string(), authId: v.string(),

View File

@@ -164,27 +164,6 @@ const funcionariosData = [
} }
]; ];
const solicitacoesAcessoData = [
{
dataResposta: 1761445098933,
dataSolicitacao: 1761445038329,
email: 'severino@gmail.com',
matricula: '3231',
nome: 'Severino Gates',
observacoes: 'Aprovação realizada por Deyvison',
status: 'aprovado' as const,
telefone: '(81) 9942-3551'
},
{
dataSolicitacao: 1761445187258,
email: 'michaeljackson@gmail.com',
matricula: '123321',
nome: 'Michael Jackson',
status: 'pendente' as const,
telefone: '(81) 99423-5551'
}
];
/** /**
* Seed inicial do banco de dados com os dados exportados do Convex Cloud * Seed inicial do banco de dados com os dados exportados do Convex Cloud
*/ */
@@ -338,8 +317,6 @@ export const seedCreateUsuariosParaFuncionarios = internalMutation({
}); });
delay += 50; delay += 50;
} }
// Agenda próxima etapa após as criações individuais
await ctx.scheduler.runAfter(delay + 300, internal.seed.seedInserirSolicitacoesAcesso, {});
return null; return null;
} }
}); });
@@ -402,55 +379,6 @@ export const seedCreateUsuarioParaFuncionario = internalMutation({
} }
}); });
export const seedInserirSolicitacoesAcesso = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log('📋 Inserindo solicitações de acesso...');
for (const solicitacao of solicitacoesAcessoData) {
// Evitar duplicidade por matrícula
const existente = await ctx.db
.query('solicitacoesAcesso')
.withIndex('by_matricula', (q) => q.eq('matricula', solicitacao.matricula))
.first();
if (existente) {
console.log(` Solicitação já existe p/ matrícula ${solicitacao.matricula}`);
continue;
}
const dadosSolicitacao: {
nome: string;
matricula: string;
email: string;
telefone: string;
status: 'pendente' | 'aprovado' | 'rejeitado';
dataSolicitacao: number;
dataResposta?: number;
observacoes?: string;
} = {
nome: solicitacao.nome,
matricula: solicitacao.matricula,
email: solicitacao.email,
telefone: solicitacao.telefone,
status: solicitacao.status,
dataSolicitacao: solicitacao.dataSolicitacao
};
if (solicitacao.dataResposta) {
dadosSolicitacao.dataResposta = solicitacao.dataResposta;
}
if (solicitacao.observacoes) {
dadosSolicitacao.observacoes = solicitacao.observacoes;
}
await ctx.db.insert('solicitacoesAcesso', dadosSolicitacao);
console.log(` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`);
}
console.log('✨ Seed concluído!');
return null;
}
});
export const seedDatabase = internalAction({ export const seedDatabase = internalAction({
args: {}, args: {},
returns: v.null(), returns: v.null(),
@@ -460,7 +388,6 @@ export const seedDatabase = internalAction({
await ctx.runMutation(internal.seed.seedCreateSimbolos, {}); await ctx.runMutation(internal.seed.seedCreateSimbolos, {});
await ctx.runMutation(internal.seed.seedCreateFuncionarios, {}); await ctx.runMutation(internal.seed.seedCreateFuncionarios, {});
await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {}); await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {});
await ctx.runMutation(internal.seed.seedInserirSolicitacoesAcesso, {});
console.log('✨ Seed do banco de dados concluído com sucesso pela action!'); console.log('✨ Seed do banco de dados concluído com sucesso pela action!');
return null; return null;
} }
@@ -677,13 +604,6 @@ export const clearDatabase = internalMutation({
} }
console.log(`${funcionarios.length} funcionários removidos`); console.log(`${funcionarios.length} funcionários removidos`);
// 20. Solicitações de acesso
const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect();
for (const solicitacao of solicitacoesAcesso) {
await ctx.db.delete(solicitacao._id);
}
console.log(`${solicitacoesAcesso.length} solicitações de acesso removidas`);
// 21. Símbolos // 21. Símbolos
const simbolos = await ctx.db.query('simbolos').collect(); const simbolos = await ctx.db.query('simbolos').collect();
for (const simbolo of simbolos) { for (const simbolo of simbolos) {
@@ -907,13 +827,6 @@ export const limparBanco = mutation({
} }
console.log(`${funcionarios.length} funcionários removidos`); console.log(`${funcionarios.length} funcionários removidos`);
// 20. Solicitações de acesso
const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect();
for (const solicitacao of solicitacoesAcesso) {
await ctx.db.delete(solicitacao._id);
}
console.log(`${solicitacoesAcesso.length} solicitações de acesso removidas`);
// 21. Símbolos // 21. Símbolos
const simbolos = await ctx.db.query('simbolos').collect(); const simbolos = await ctx.db.query('simbolos').collect();
for (const simbolo of simbolos) { for (const simbolo of simbolos) {

View File

@@ -1,234 +0,0 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
// Criar uma nova solicitação de acesso
export const create = mutation({
args: {
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
},
returns: v.object({
solicitacaoId: v.id("solicitacoesAcesso"),
}),
handler: async (ctx, args) => {
// Verificar se já existe uma solicitação pendente com a mesma matrícula
const existingByMatricula = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.filter((q) => q.eq(q.field("status"), "pendente"))
.first();
if (existingByMatricula) {
throw new Error("Já existe uma solicitação pendente para esta matrícula.");
}
// Verificar se já existe uma solicitação pendente com o mesmo email
const existingByEmail = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_email", (q) => q.eq("email", args.email))
.filter((q) => q.eq(q.field("status"), "pendente"))
.first();
if (existingByEmail) {
throw new Error("Já existe uma solicitação pendente para este e-mail.");
}
const solicitacaoId = await ctx.db.insert("solicitacoesAcesso", {
nome: args.nome,
matricula: args.matricula,
email: args.email,
telefone: args.telefone,
status: "pendente",
dataSolicitacao: Date.now(),
});
return { solicitacaoId };
},
});
// Listar todas as solicitações (para o painel administrativo)
export const getAll = query({
args: {},
returns: v.array(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
})
),
handler: async (ctx) => {
const solicitacoes = await ctx.db
.query("solicitacoesAcesso")
.order("desc")
.collect();
return solicitacoes.map((s) => ({
_id: s._id,
_creationTime: s._creationTime,
nome: s.nome,
matricula: s.matricula,
email: s.email,
telefone: s.telefone,
status: s.status,
dataSolicitacao: s.dataSolicitacao,
dataResposta: s.dataResposta ?? null,
observacoes: s.observacoes ?? null,
}));
},
});
// Listar apenas solicitações pendentes
export const getPendentes = query({
args: {},
returns: v.array(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
})
),
handler: async (ctx) => {
const solicitacoes = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_status", (q) => q.eq("status", "pendente"))
.order("desc")
.collect();
return solicitacoes.map((s) => ({
_id: s._id,
_creationTime: s._creationTime,
nome: s.nome,
matricula: s.matricula,
email: s.email,
telefone: s.telefone,
status: s.status,
dataSolicitacao: s.dataSolicitacao,
dataResposta: s.dataResposta ?? null,
observacoes: s.observacoes ?? null,
}));
},
});
// Aprovar uma solicitação
export const aprovar = mutation({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
observacoes: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error("Solicitação não encontrada.");
}
if (solicitacao.status !== "pendente") {
throw new Error("Esta solicitação já foi processada.");
}
await ctx.db.patch(args.solicitacaoId, {
status: "aprovado",
dataResposta: Date.now(),
observacoes: args.observacoes,
});
return null;
},
});
// Rejeitar uma solicitação
export const rejeitar = mutation({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
observacoes: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error("Solicitação não encontrada.");
}
if (solicitacao.status !== "pendente") {
throw new Error("Esta solicitação já foi processada.");
}
await ctx.db.patch(args.solicitacaoId, {
status: "rejeitado",
dataResposta: Date.now(),
observacoes: args.observacoes,
});
return null;
},
});
// Obter uma solicitação por ID
export const getById = query({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
},
returns: v.union(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
}),
v.null()
),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
return null;
}
return {
_id: solicitacao._id,
_creationTime: solicitacao._creationTime,
nome: solicitacao.nome,
matricula: solicitacao.matricula,
email: solicitacao.email,
telefone: solicitacao.telefone,
status: solicitacao.status,
dataSolicitacao: solicitacao.dataSolicitacao,
dataResposta: solicitacao.dataResposta ?? null,
observacoes: solicitacao.observacoes ?? null,
};
},
});