refactor: update user role management and enhance UI components

- Updated the user role management logic to improve type safety and error handling, including better handling of role permissions and user associations.
- Refactored the UI components for user management, enhancing the layout and styling for better user experience.
- Removed outdated code related to menu permissions and streamlined the database schema for roles and profiles.
- Improved the overall structure and readability of the codebase, ensuring consistency across components.
This commit is contained in:
2025-11-03 15:12:10 -03:00
parent 5cb63f9437
commit c1d9958c9f
10 changed files with 749 additions and 494 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View File

@@ -126,8 +126,8 @@ npx convex dev
### **Banco vazio:**
```powershell
cd packages\backend
npx convex run seed:clearDatabase
npx convex run seed:seedDatabase
npx convex run seed:limparBanco
npx convex run seed:popularBanco
```
**Mais soluções:** Veja `TESTAR_SISTEMA_COMPLETO.md` seção "Problemas Comuns"

View File

@@ -17,4 +17,59 @@
.btn-error {
@apply font-medium flex items-center justify-center gap-2 text-center px-4 py-2 rounded-xl border border-error bg-base-100 hover:bg-error/60 active:bg-error text-error hover:text-white active:text-white transition-colors;
}
:where(.card, .card-hover) {
position: relative;
overflow: hidden;
transform: translateY(0);
transition: transform 220ms ease, box-shadow 220ms ease;
}
:where(.card, .card-hover)::before {
content: "";
position: absolute;
inset: -2px;
border-radius: 1.15rem;
box-shadow:
0 0 0 1px rgba(15, 23, 42, 0.04),
0 14px 32px -22px rgba(15, 23, 42, 0.45),
0 6px 18px -16px rgba(102, 126, 234, 0.35);
opacity: 0.55;
transition: opacity 220ms ease, transform 220ms ease;
pointer-events: none;
z-index: 0;
}
:where(.card, .card-hover)::after {
content: "";
position: absolute;
inset: 0;
border-radius: 1rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.12));
opacity: 0;
transform: scale(0.96);
transition: opacity 220ms ease, transform 220ms ease;
pointer-events: none;
z-index: 1;
}
:where(.card, .card-hover):hover {
transform: translateY(-6px);
box-shadow: 0 20px 45px -20px rgba(15, 23, 42, 0.35);
}
:where(.card, .card-hover):hover::before {
opacity: 0.9;
transform: scale(1);
}
:where(.card, .card-hover):hover::after {
opacity: 1;
transform: scale(1);
}
:where(.card, .card-hover) > * {
position: relative;
z-index: 2;
}

7
apps/web/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createAuthClient } from "better-auth/client";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "http://localhost:5173",
plugins: [convexClient()],
});

View File

@@ -2,18 +2,17 @@
import { useConvexClient, useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { authStore } from "$lib/stores/auth.svelte";
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
import DashboardFerias from "$lib/components/ferias/DashboardFerias.svelte";
import AprovarFerias from "$lib/components/AprovarFerias.svelte";
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { page } from "$app/stores";
const client = useConvexClient();
let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">(
"meu-perfil"
);
let mostrarFormSolicitar = $state(false);
let solicitacaoSelecionada = $state<any>(null);
let mostrarModalFoto = $state(false);
let uploadandoFoto = $state(false);
@@ -26,6 +25,10 @@
let fotoPerfilLocal = $state<string | null>(null);
let avatarLocal = $state<string | null>(null);
// Estados para Minhas Férias
let mostrarWizard = $state(false);
let filtroStatusFerias = $state<string>("todos");
// Galeria de avatares (30 avatares profissionais 3D realistas)
const avatarGallery = generateAvatarGallery(30);
@@ -54,14 +57,6 @@
: { data: null }
);
const minhasSolicitacoesQuery = $derived(
funcionarioQuery.data
? useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId: funcionarioQuery.data._id,
})
: { data: [] }
);
const solicitacoesSubordinadosQuery = $derived(
authStore.usuario?._id
? useQuery(api.ferias.listarSolicitacoesSubordinados, {
@@ -70,6 +65,14 @@
: { data: [] }
);
const minhasSolicitacoesQuery = $derived(
funcionarioQuery.data
? useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId: funcionarioQuery.data._id,
})
: { data: [] }
);
const meuTimeQuery = $derived(
funcionarioQuery.data
? useQuery(api.times.obterTimeFuncionario, {
@@ -87,18 +90,34 @@
);
const funcionario = $derived(funcionarioQuery.data);
const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []);
const solicitacoesSubordinados = $derived(
solicitacoesSubordinadosQuery?.data || []
);
const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []);
const meuTime = $derived(meuTimeQuery?.data);
const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []);
// Verificar se é gestor
const ehGestor = $derived((meusTimesGestor || []).length > 0);
// Filtrar minhas solicitações
const solicitacoesFiltradas = $derived(
minhasSolicitacoes.filter((s) => {
if (filtroStatusFerias !== "todos" && s.status !== filtroStatusFerias) return false;
return true;
})
);
// Estatísticas das minhas férias
const statsMinhasFerias = $derived({
total: minhasSolicitacoes.length,
aguardando: minhasSolicitacoes.filter((s) => s.status === "aguardando_aprovacao").length,
aprovadas: minhasSolicitacoes.filter((s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length,
reprovadas: minhasSolicitacoes.filter((s) => s.status === "reprovado").length,
emFerias: funcionario?.statusFerias === "em_ferias" ? 1 : 0,
});
async function recarregar() {
mostrarFormSolicitar = false;
solicitacaoSelecionada = null;
}
@@ -1060,72 +1079,147 @@
{/if}
</div>
{:else if abaAtiva === "minhas-ferias"}
<!-- Minhas Férias MODERNO -->
<div class="space-y-8">
{#if !mostrarFormSolicitar}
<!-- Dashboard de Férias -->
{#if funcionario}
<DashboardFerias funcionarioId={funcionario._id} />
<!-- Botão para solicitar -->
<div class="flex justify-center">
<button
type="button"
class="btn btn-lg gap-3 shadow-2xl hover:shadow-3xl transition-all hover:scale-105"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;"
onclick={() => (mostrarFormSolicitar = true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 4v16m8-8H4"
/>
</svg>
Solicitar Novas Férias
</button>
</div>
{:else}
<div class="alert alert-warning shadow-xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
<!-- Minhas Férias -->
<div class="space-y-6">
<!-- Estatísticas -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="stat bg-base-100 shadow-lg rounded-box border border-base-300">
<div class="stat-figure text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" 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>
<div>
<h3 class="font-bold">Perfil de funcionário não encontrado</h3>
<div class="text-xs">
Seu usuário ainda não está associado a um cadastro de
funcionário. Entre em contato com o RH.
</div>
</div>
<div class="stat-title">Total</div>
<div class="stat-value text-primary">{statsMinhasFerias.total}</div>
<div class="stat-desc">Solicitações</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-warning/30">
<div class="stat-figure text-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Aguardando</div>
<div class="stat-value text-warning">{statsMinhasFerias.aguardando}</div>
<div class="stat-desc">Pendentes</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-success/30">
<div class="stat-figure text-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" 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>
<div class="stat-title">Aprovadas</div>
<div class="stat-value text-success">{statsMinhasFerias.aprovadas}</div>
<div class="stat-desc">Deferidas</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-error/30">
<div class="stat-figure text-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
</div>
<div class="stat-title">Reprovadas</div>
<div class="stat-value text-error">{statsMinhasFerias.reprovadas}</div>
<div class="stat-desc">Indeferidas</div>
</div>
<div class="stat bg-gradient-to-br from-purple-500/10 to-purple-600/20 shadow-lg rounded-box border-2 border-purple-500/30">
<div class="stat-figure text-purple-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Em Férias</div>
<div class="stat-value text-purple-600">{statsMinhasFerias.emFerias}</div>
<div class="stat-desc">Agora</div>
</div>
</div>
<!-- Filtros e Botão Nova Solicitação -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<h2 class="card-title text-lg">Filtros</h2>
{#if funcionario}
<button
type="button"
class="btn btn-primary gap-2"
onclick={() => (mostrarWizard = true)}
>
<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="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>
Agendar Férias
</button>
{/if}
</div>
<div class="grid grid-cols-1 md:grid-cols-1 gap-4 mt-4">
<div class="form-control">
<label class="label" for="status">
<span class="label-text">Status</span>
</label>
<select id="status" class="select select-bordered" bind:value={filtroStatusFerias}>
<option value="todos">Todos</option>
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
<option value="aprovado">Aprovado</option>
<option value="reprovado">Reprovado</option>
<option value="data_ajustada_aprovada">Data Ajustada</option>
</select>
</div>
</div>
{/if}
{:else}
<!-- Wizard de Solicitação de Férias -->
{#if funcionario}
<WizardSolicitacaoFerias
funcionarioId={funcionario._id}
onSucesso={recarregar}
onCancelar={() => (mostrarFormSolicitar = false)}
/>
{/if}
{/if}
</div>
</div>
<!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
Minhas Solicitações ({solicitacoesFiltradas.length})
</h2>
{#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 shrink-0 w-6 h-6">
<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>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Ano</th>
<th>Períodos</th>
<th>Total Dias</th>
<th>Status</th>
<th>Solicitado em</th>
</tr>
</thead>
<tbody>
{#each solicitacoesFiltradas as solicitacao}
<tr>
<td>{solicitacao.anoReferencia}</td>
<td>{solicitacao.periodos.length} período(s)</td>
<td class="font-bold">{solicitacao.periodos.reduce((acc: number, p: any) => acc + p.diasCorridos, 0)} dias</td>
<td>
<div class={`badge ${getStatusBadge(solicitacao.status)}`}>
{getStatusTexto(solicitacao.status)}
</div>
</td>
<td class="text-xs">{new Date(solicitacao._creationTime).toLocaleDateString("pt-BR")}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
</div>
{:else if abaAtiva === "aprovar-ferias"}
<!-- Aprovar Férias (Gestores) PREMIUM -->
@@ -1599,3 +1693,26 @@
animation: float 6s ease-in-out infinite;
}
</style>
<!-- Modal Wizard Solicitação de Férias -->
{#if mostrarWizard && funcionario}
<dialog class="modal modal-open">
<div class="modal-box max-w-4xl max-h-[90vh] overflow-hidden">
<h3 class="font-bold text-2xl mb-6 text-center">
Nova Solicitação de Férias
</h3>
<div class="max-h-[80vh] overflow-y-auto">
<WizardSolicitacaoFerias
funcionarioId={funcionario._id}
onSucesso={() => {
mostrarWizard = false;
}}
onCancelar={() => (mostrarWizard = false)}
/>
</div>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-backdrop" onclick={() => (mostrarWizard = false)}></div>
</dialog>
{/if}

View File

@@ -3,49 +3,30 @@
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
// Buscar todas as solicitações (RH vê tudo)
// Buscar TODAS as solicitações de férias (Dashboard RH)
const todasSolicitacoesQuery = useQuery(api.ferias.listarTodas, {});
const todosFuncionariosQuery = useQuery(api.funcionarios.getAll, {});
let filtroStatus = $state<string>("todos");
let filtroTime = $state<string>("todos");
let filtroBusca = $state("");
const solicitacoes = $derived(todasSolicitacoesQuery?.data || []);
const funcionarios = $derived(todosFuncionariosQuery?.data || []);
// Filtrar solicitações
const solicitacoesFiltradas = $derived(
solicitacoes.filter((s: any) => {
// Filtro de status
if (filtroStatus !== "todos" && s.status !== filtroStatus) return false;
// Filtro de time
if (filtroTime !== "todos" && s.time?._id !== filtroTime) return false;
// Filtro de busca
if (filtroBusca && !s.funcionario?.nome.toLowerCase().includes(filtroBusca.toLowerCase())) {
return false;
}
return true;
})
);
// Estatísticas
// Estatísticas gerais
const stats = $derived({
total: solicitacoes.length,
aguardando: solicitacoes.filter((s: any) => s.status === "aguardando_aprovacao").length,
aprovadas: solicitacoes.filter((s: any) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length,
reprovadas: solicitacoes.filter((s: any) => s.status === "reprovado").length,
emFerias: funcionarios.filter((f: any) => f.statusFerias === "em_ferias").length,
});
// Times únicos para filtro
const timesDisponiveis = $derived(
Array.from(new Set(solicitacoes.map((s: any) => s.time).filter(Boolean)))
);
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
aguardando_aprovacao: "badge-warning",
@@ -104,7 +85,7 @@
</div>
<!-- Estatísticas -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="stat bg-base-100 shadow-lg rounded-box border border-base-300">
<div class="stat-figure text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -148,38 +129,13 @@
<div class="stat-value text-error">{stats.reprovadas}</div>
<div class="stat-desc">Indeferidas</div>
</div>
<div class="stat bg-gradient-to-br from-purple-500/10 to-purple-600/20 shadow-lg rounded-box border-2 border-purple-500/30">
<div class="stat-figure text-purple-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Em Férias</div>
<div class="stat-value text-purple-600">{stats.emFerias}</div>
<div class="stat-desc">Agora</div>
</div>
</div>
<!-- Filtros -->
<div class="card bg-base-100 shadow-lg mb-6">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Filtros</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Busca -->
<div class="form-control">
<label class="label" for="busca">
<span class="label-text">Buscar Funcionário</span>
</label>
<input
id="busca"
type="text"
placeholder="Digite o nome..."
class="input input-bordered"
bind:value={filtroBusca}
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
<!-- Filtro Status -->
<div class="form-control">
<label class="label" for="status">
@@ -193,21 +149,6 @@
<option value="data_ajustada_aprovada">Data Ajustada</option>
</select>
</div>
<!-- Filtro Time -->
<div class="form-control">
<label class="label" for="time">
<span class="label-text">Time</span>
</label>
<select id="time" class="select select-bordered" bind:value={filtroTime}>
<option value="todos">Todos os Times</option>
{#each timesDisponiveis as time}
{#if time}
<option value={time._id}>{time.nome}</option>
{/if}
{/each}
</select>
</div>
</div>
</div>
</div>

View File

@@ -72,17 +72,28 @@
}
</script>
<main class="container mx-auto px-4 py-4 max-w-4xl">
<div class="mb-6">
<h1 class="text-3xl font-bold text-primary mb-2">Solicitar Acesso ao SGSE</h1>
<p class="text-base-content/70">
Preencha o formulário abaixo para solicitar acesso ao Sistema de Gerenciamento da Secretaria de Esportes.
Sua solicitação será analisada pela equipe de Tecnologia da Informação.
</p>
</div>
<main class="mx-auto w-full max-w-4xl space-y-8 px-4 py-10">
<!-- Cabeçalho Estilizado -->
<section class="relative overflow-hidden rounded-3xl border border-primary/25 bg-gradient-to-br from-primary/10 via-base-100 to-secondary/20 p-8 shadow-2xl">
<div class="absolute -left-10 top-10 h-40 w-40 rounded-full bg-primary/20 blur-3xl"></div>
<div class="absolute -bottom-16 right-0 h-56 w-56 rounded-full bg-secondary/20 blur-3xl"></div>
<div class="relative z-10 space-y-4">
<span class="inline-flex w-fit items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.28em] text-primary">
Acesso ao Sistema
</span>
<h1 class="text-4xl font-black leading-tight text-base-content sm:text-5xl">
Solicitar Acesso ao SGSE
</h1>
<p class="text-base leading-relaxed text-base-content/70 sm:text-lg">
Preencha o formulário abaixo para solicitar acesso ao Sistema de Gerenciamento da Secretaria de Esportes.
Sua solicitação será analisada pela equipe de Tecnologia da Informação.
</p>
</div>
</section>
<!-- Alertas -->
{#if notice}
<div class="alert {notice.type === 'success' ? 'alert-success' : 'alert-error'} mb-6">
<div class="alert {notice.type === 'success' ? 'alert-success' : 'alert-error'} shadow-xl">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
@@ -105,12 +116,14 @@
/>
{/if}
</svg>
<span>{notice.message}</span>
<span class="font-semibold">{notice.message}</span>
</div>
{/if}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<!-- Formulário -->
<section class="relative overflow-hidden rounded-3xl border border-base-200 bg-base-100/90 p-8 shadow-2xl">
<div class="absolute inset-x-6 top-0 h-24 rounded-b-full bg-gradient-to-b from-base-200/40 to-transparent opacity-50"></div>
<div class="relative z-10">
<form
onsubmit={(e) => {
e.preventDefault();
@@ -118,25 +131,28 @@
form.handleSubmit();
}}
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Nome -->
<form.Field name="nome" validators={{ onChange: formSchema.shape.nome }}>
{#snippet children(field)}
<div class="form-control md:col-span-2">
<label class="label" for="nome">
<span class="label-text">Nome Completo *</span>
<span class="label-text font-semibold">Nome Completo</span>
<span class="text-error">*</span>
</label>
<input
id="nome"
type="text"
placeholder="Digite seu nome completo"
class="input input-bordered w-full"
class="input input-bordered w-full focus:input-primary transition-colors duration-300"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)}
/>
{#if field.state.meta.errors.length > 0}
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if}
</div>
{/snippet}
@@ -147,19 +163,22 @@
{#snippet children(field)}
<div class="form-control">
<label class="label" for="matricula">
<span class="label-text">Matrícula *</span>
<span class="label-text font-semibold">Matrícula</span>
<span class="text-error">*</span>
</label>
<input
id="matricula"
type="text"
placeholder="Digite sua matrícula"
class="input input-bordered w-full"
class="input input-bordered w-full focus:input-primary transition-colors duration-300"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)}
/>
{#if field.state.meta.errors.length > 0}
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if}
</div>
{/snippet}
@@ -170,19 +189,22 @@
{#snippet children(field)}
<div class="form-control">
<label class="label" for="email">
<span class="label-text">E-mail *</span>
<span class="label-text font-semibold">E-mail</span>
<span class="text-error">*</span>
</label>
<input
id="email"
type="email"
placeholder="seu@email.com"
class="input input-bordered w-full"
class="input input-bordered w-full focus:input-primary transition-colors duration-300"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)}
/>
{#if field.state.meta.errors.length > 0}
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if}
</div>
{/snippet}
@@ -193,13 +215,14 @@
{#snippet children(field)}
<div class="form-control md:col-span-2">
<label class="label" for="telefone">
<span class="label-text">Telefone *</span>
<span class="label-text font-semibold">Telefone</span>
<span class="text-error">*</span>
</label>
<input
id="telefone"
type="text"
placeholder="(00) 00000-0000"
class="input input-bordered w-full"
class="input input-bordered w-full focus:input-primary transition-colors duration-300"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e) => {
@@ -210,26 +233,36 @@
maxlength="15"
/>
{#if field.state.meta.errors.length > 0}
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if}
</div>
{/snippet}
</form.Field>
</div>
<div class="card-actions justify-end mt-6 gap-2">
<button type="button" class="btn btn-ghost" onclick={handleCancel}>
<!-- Botões de Ação -->
<div class="flex justify-end gap-4 mt-8 pt-6 border-t border-base-300">
<button type="button" class="btn btn-ghost btn-md" onclick={handleCancel}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" 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>
Cancelar
</button>
<button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-primary btn-md shadow-md transition-all duration-200 hover:shadow-primary/40">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" 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>
Solicitar Acesso
</button>
</div>
</form>
</div>
</div>
</section>
<div class="alert alert-info mt-6">
<!-- Informações Importantes -->
<div class="alert alert-info shadow-xl border border-info/30">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -244,16 +277,42 @@
></path>
</svg>
<div>
<h3 class="font-bold">Informações Importantes</h3>
<div class="text-sm">
<ul class="list-disc list-inside mt-2">
<li>Todos os campos marcados com * são obrigatórios</li>
<li>Sua solicitação será analisada pela equipe de TI em até 48 horas úteis</li>
<li>Você receberá um e-mail com o resultado da análise</li>
<li>Em caso de dúvidas, entre em contato com o suporte técnico</li>
</ul>
<h3 class="font-bold text-lg mb-2">Informações Importantes</h3>
<div class="text-sm space-y-1">
<div class="flex items-start gap-2">
<span class="text-info font-bold"></span>
<span>Todos os campos marcados com * são obrigatórios</span>
</div>
<div class="flex items-start gap-2">
<span class="text-info font-bold"></span>
<span>Sua solicitação será analisada pela equipe de TI em até 48 horas úteis</span>
</div>
<div class="flex items-start gap-2">
<span class="text-info font-bold"></span>
<span>Você receberá um e-mail com o resultado da análise</span>
</div>
<div class="flex items-start gap-2">
<span class="text-info font-bold"></span>
<span>Em caso de dúvidas, entre em contato com o suporte técnico</span>
</div>
</div>
</div>
</div>
</main>
<style>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
section {
animation: fadeInUp 0.5s ease-out;
}
</style>

View File

@@ -1,337 +1,384 @@
<script lang="ts">
import { goto } from "$app/navigation";
type HighlightVariant = "solid" | "outline";
type FeatureIcon =
| "control"
| "support"
| "shieldCheck"
| "envelope"
| "users"
| "bell"
| "monitor"
| "document"
| "teams";
type PaletteKey = "primary" | "success" | "secondary" | "accent" | "info" | "error";
type FeatureCard = {
title: string;
description: string;
ctaLabel: string;
href?: string;
disabled?: boolean;
palette: PaletteKey;
icon: FeatureIcon;
highlightBadges?: Array<{ label: string; variant: HighlightVariant }>;
};
type IconPath = {
d: string;
strokeLinecap?: "butt" | "round" | "square";
strokeLinejoin?: "miter" | "round" | "bevel";
strokeWidth?: number;
};
const paletteStyles: Record<
PaletteKey,
{
cardBorder: string;
iconBg: string;
iconRing: string;
iconColor: string;
button: string;
badgeSolid: string;
badgeOutline: string;
}
> = {
primary: {
cardBorder: "border-primary/25",
iconBg: "bg-primary/15",
iconRing: "ring-1 ring-primary/30",
iconColor: "text-primary",
button: "btn-primary",
badgeSolid: "badge-primary text-primary-content",
badgeOutline: "badge-outline border-primary/30",
},
success: {
cardBorder: "border-success/25",
iconBg: "bg-success/15",
iconRing: "ring-1 ring-success/25",
iconColor: "text-success",
button: "btn-success",
badgeSolid: "badge-success text-success-content",
badgeOutline: "badge-outline border-success/30",
},
secondary: {
cardBorder: "border-secondary/25",
iconBg: "bg-secondary/15",
iconRing: "ring-1 ring-secondary/25",
iconColor: "text-secondary",
button: "btn-secondary",
badgeSolid: "badge-secondary text-secondary-content",
badgeOutline: "badge-outline border-secondary/30",
},
accent: {
cardBorder: "border-accent/25",
iconBg: "bg-accent/15",
iconRing: "ring-1 ring-accent/20",
iconColor: "text-accent",
button: "btn-accent",
badgeSolid: "badge-accent text-accent-content",
badgeOutline: "badge-outline border-accent/30",
},
info: {
cardBorder: "border-info/25",
iconBg: "bg-info/15",
iconRing: "ring-1 ring-info/25",
iconColor: "text-info",
button: "btn-info",
badgeSolid: "badge-info text-info-content",
badgeOutline: "badge-outline border-info/30",
},
error: {
cardBorder: "border-error/20",
iconBg: "bg-error/15",
iconRing: "ring-1 ring-error/25",
iconColor: "text-error",
button: "btn-error",
badgeSolid: "badge-error text-error-content",
badgeOutline: "badge-outline border-error/30",
},
};
const iconPaths = {
control: [
{
d: "M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
support: [
{
d: "M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
shieldCheck: [
{
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",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
envelope: [
{
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",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
users: [
{
d: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
bell: [
{
d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
monitor: [
{
d: "M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
document: [
{
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",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
teams: [
{
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",
strokeLinecap: "round",
strokeLinejoin: "round",
},
],
} satisfies Record<FeatureIcon, IconPath[]>;
const featureCards: Array<FeatureCard> = [
{
title: "Painel Administrativo",
description:
"Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.",
ctaLabel: "Acessar Painel",
href: "/ti/painel-administrativo",
palette: "primary",
icon: "control",
},
{
title: "Suporte Técnico",
description:
"Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.",
ctaLabel: "Em breve",
palette: "info",
icon: "support",
disabled: true,
},
{
title: "Gerenciar Permissões",
description:
"Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados.",
ctaLabel: "Configurar Permissões",
href: "/ti/painel-permissoes",
palette: "success",
icon: "shieldCheck",
},
{
title: "Configuração de Email",
description:
"Configure o servidor SMTP para envio automático de notificações e emails do sistema.",
ctaLabel: "Configurar SMTP",
href: "/ti/configuracoes-email",
palette: "secondary",
icon: "envelope",
},
{
title: "Gerenciar Usuários",
description:
"Criar, editar, bloquear e gerenciar usuários do sistema. Controle total sobre contas de acesso.",
ctaLabel: "Gerenciar Usuários",
href: "/ti/usuarios",
palette: "accent",
icon: "users",
},
{
title: "Gestão de Times",
description:
"Organize funcionários em equipes e defina gestores. Gerencie membros e estrutura organizacional do sistema.",
ctaLabel: "Gerenciar Times",
href: "/ti/times",
palette: "success",
icon: "teams",
},
{
title: "Notificações e Mensagens",
description:
"Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis.",
ctaLabel: "Acessar Painel",
href: "/ti/notificacoes",
palette: "info",
icon: "bell",
},
{
title: "Monitorar SGSE",
description:
"Monitore em tempo real as métricas técnicas do sistema e configure alertas inteligentes para a equipe de TI.",
ctaLabel: "Monitorar Sistema",
href: "/ti/monitoramento",
palette: "error",
icon: "monitor",
highlightBadges: [
{ label: "Tempo Real", variant: "solid" },
{ label: "Alertas", variant: "outline" },
{ label: "Relatórios", variant: "outline" },
],
},
{
title: "Documentação",
description:
"Manuais, guias e documentação técnica do sistema para usuários e administradores.",
ctaLabel: "Em breve",
palette: "primary",
icon: "document",
disabled: true,
},
];
</script>
<main class="container mx-auto px-4 py-4">
<h1 class="text-3xl font-bold text-primary mb-6">Tecnologia da Informação</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Card Painel Administrativo -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
</div>
<h2 class="card-title text-xl">Painel Administrativo</h2>
</div>
<p class="text-base-content/70 mb-4">
Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.
<main class="mx-auto w-full max-w-7xl space-y-12 px-4 py-10">
<section class="relative overflow-hidden rounded-3xl border border-primary/25 bg-gradient-to-br from-primary/10 via-base-100 to-secondary/20 p-8 shadow-2xl">
<div class="absolute -left-10 top-10 h-40 w-40 rounded-full bg-primary/20 blur-3xl"></div>
<div class="absolute -bottom-16 right-0 h-56 w-56 rounded-full bg-secondary/20 blur-3xl"></div>
<div class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="max-w-3xl space-y-4">
<span class="inline-flex w-fit items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.28em] text-primary">
Tecnologia da Informação
</span>
<h1 class="text-4xl font-black leading-tight text-base-content sm:text-5xl">
Sistemas de Informação
</h1>
<p class="text-base leading-relaxed text-base-content/70 sm:text-lg">
Acesso restrito para gerenciamento de solicitações de acesso ao sistema, configuração de permissões e monitoramento técnico das operações do SGSE.
</p>
<div class="card-actions justify-end">
<a href="/ti/painel-administrativo" class="btn btn-primary">
Acessar Painel
</a>
</div>
<div class="grid grid-cols-2 gap-4 rounded-2xl border border-base-200/60 bg-base-100/70 p-6 shadow-lg backdrop-blur sm:max-w-sm">
<div>
<p class="text-sm font-semibold text-base-content/60">Status</p>
<p class="mt-2 text-2xl font-bold text-base-content">Operacional</p>
</div>
<div class="text-right">
<p class="text-sm font-semibold text-base-content/60">Última atualização</p>
<p class="mt-2 text-xl font-bold text-base-content">Agora mesmo</p>
</div>
<div class="col-span-2 h-px bg-gradient-to-r from-transparent via-base-300 to-transparent"></div>
<div class="col-span-2 flex items-center justify-between text-sm text-base-content/70">
<span>Monitoramento em tempo real.</span>
<span class="badge badge-primary badge-sm">SGSE</span>
</div>
</div>
</div>
</section>
<!-- Card Suporte Técnico -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
{#each featureCards as card (card.title)}
<article
class={`card-hover group relative overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300`}
>
<div class="absolute inset-x-6 top-0 h-24 rounded-b-full bg-gradient-to-b from-base-200/40 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"></div>
<div class="relative flex items-start gap-4">
<div
class={`flex h-14 w-14 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
class={`h-7 w-7 ${paletteStyles[card.palette].iconColor}`}
>
{#each iconPaths[card.icon] as path (path.d)}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
/>
d={path.d}
stroke-linecap={path.strokeLinecap ?? "round"}
stroke-linejoin={path.strokeLinejoin ?? "round"}
stroke-width={path.strokeWidth ?? 2}
/>
{/each}
</svg>
</div>
<h2 class="card-title text-xl">Suporte Técnico</h2>
</div>
<p class="text-base-content/70 mb-4">
Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.
</p>
<div class="card-actions justify-end">
<button class="btn btn-primary" disabled>
Em breve
<div class="relative flex-1">
<h2 class="text-xl font-semibold text-base-content">{card.title}</h2>
<p class="mt-2 text-sm leading-relaxed text-base-content/70">{card.description}</p>
</div>
</div>
{#if card.highlightBadges}
<div class="mt-4 flex flex-wrap gap-2">
{#each card.highlightBadges as badge (badge.label)}
{#if badge.variant === "solid"}
<span class={`badge ${paletteStyles[card.palette].badgeSolid}`}>{badge.label}</span>
{:else}
<span
class={`badge ${paletteStyles[card.palette].badgeOutline} ${paletteStyles[card.palette].iconColor}`}
>
{badge.label}
</span>
{/if}
{/each}
</div>
{/if}
<div class="mt-6 flex justify-end">
{#if card.href && !card.disabled}
<a
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md transition-all duration-200 hover:shadow-lg`}
href={card.href}
>
{card.ctaLabel}
</a>
{:else}
<button
type="button"
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md`}
disabled
>
{card.ctaLabel}
</button>
{/if}
</div>
</div>
</div>
</article>
{/each}
</section>
<!-- Card Gerenciar Permissões -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-success/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-success"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-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>
<h2 class="card-title text-xl">Gerenciar Permissões</h2>
</div>
<p class="text-base-content/70 mb-4">
Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados.
</p>
<div class="card-actions justify-end">
<a href="/ti/painel-permissoes" class="btn btn-success">
Configurar Permissões
</a>
</div>
</div>
</div>
<!-- Card Configuração de Email -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-secondary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-secondary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="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>
<h2 class="card-title text-xl">Configuração de Email</h2>
</div>
<p class="text-base-content/70 mb-4">
Configure o servidor SMTP para envio automático de notificações e emails do sistema.
</p>
<div class="card-actions justify-end">
<a href="/ti/configuracoes-email" class="btn btn-secondary">
Configurar SMTP
</a>
</div>
</div>
</div>
<!-- Card Gerenciar Usuários -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-accent/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-accent"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
</div>
<h2 class="card-title text-xl">Gerenciar Usuários</h2>
</div>
<p class="text-base-content/70 mb-4">
Criar, editar, bloquear e gerenciar usuários do sistema. Controle total sobre contas de acesso.
</p>
<div class="card-actions justify-end">
<a href="/ti/usuarios" class="btn btn-accent">
Gerenciar Usuários
</a>
</div>
</div>
</div>
<!-- Card Gerenciar Perfis -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-warning/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-warning"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<h2 class="card-title text-xl">Gerenciar Perfis</h2>
</div>
<p class="text-base-content/70 mb-4">
Crie e gerencie perfis de acesso personalizados com permissões específicas para grupos de usuários.
</p>
<div class="card-actions justify-end">
<a href="/ti/perfis" class="btn btn-warning">
Gerenciar Perfis
</a>
</div>
</div>
</div>
<!-- Card Notificações e Mensagens -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-info/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-info"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</div>
<h2 class="card-title text-xl">Notificações e Mensagens</h2>
</div>
<p class="text-base-content/70 mb-4">
Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis.
</p>
<div class="card-actions justify-end">
<a href="/ti/notificacoes" class="btn btn-info">
Acessar Painel
</a>
</div>
</div>
</div>
<!-- Card Monitorar SGSE -->
<div class="card bg-gradient-to-br from-error/10 to-error/5 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105 border-2 border-error/20">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-gradient-to-br from-error/30 to-error/20 rounded-2xl shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
/>
</svg>
</div>
<h2 class="card-title text-xl text-error">Monitorar SGSE</h2>
</div>
<p class="text-base-content/70 mb-4">
Monitore em tempo real as métricas técnicas do sistema: CPU, memória, rede, usuários online e muito mais. Configure alertas personalizados.
</p>
<div class="flex items-center gap-2 mb-4">
<div class="badge badge-error badge-sm">Tempo Real</div>
<div class="badge badge-outline badge-sm">Alertas</div>
<div class="badge badge-outline badge-sm">Relatórios</div>
</div>
<div class="card-actions justify-end">
<a href="/ti/monitoramento" class="btn btn-error shadow-lg hover:shadow-error/30">
<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="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Monitorar Sistema
</a>
</div>
</div>
</div>
<!-- Card Documentação -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
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>
</div>
<h2 class="card-title text-xl">Documentação</h2>
</div>
<p class="text-base-content/70 mb-4">
Manuais, guias e documentação técnica do sistema para usuários e administradores.
</p>
<div class="card-actions justify-end">
<button type="button" class="btn btn-primary" disabled>
Em breve
</button>
</div>
</div>
</div>
</div>
<div class="alert alert-info mt-8">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<section class="relative overflow-hidden rounded-2xl border border-warning/30 bg-gradient-to-br from-warning/15 via-warning/10 to-warning/5 p-6 shadow-lg">
<div class="absolute -top-10 right-0 h-32 w-32 rounded-full bg-warning/30 blur-3xl"></div>
<div class="relative z-10 flex items-start gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-warning/25 text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 stroke-current">
<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>
</div>
<div>
<h3 class="font-bold">Área Restrita</h3>
<div class="text-sm">
Esta é uma área de acesso restrito. Apenas usuários autorizados pela equipe de TI podem acessar o Painel Administrativo.
<h3 class="text-lg font-bold text-base-content">Área Restrita</h3>
<p class="mt-2 text-sm leading-relaxed text-base-content/70">
Esta área é exclusiva da equipe de Tecnologia da Informação. Garanta que apenas usuários autorizados acessem o Painel Administrativo e mantenha suas credenciais em segurança.
</p>
</div>
</div>
</div>
</section>
</main>

View File

@@ -4,18 +4,35 @@
import StatsCard from "$lib/components/ti/StatsCard.svelte";
const client = useConvexClient();
const usuarios = useQuery(api.usuarios.listar, {});
const usuariosQuery = useQuery(api.usuarios.listar, {});
// Verificar se está carregando
const carregando = $derived(usuariosQuery === undefined);
// Extrair dados dos usuários
const usuarios = $derived(usuariosQuery?.data ?? []);
// Estatísticas derivadas
const stats = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return null;
// Se ainda está carregando, retorna null para mostrar loading
if (carregando) return null;
const ativos = usuarios.data.filter(u => u.ativo && !u.bloqueado).length;
const bloqueados = usuarios.data.filter(u => u.bloqueado).length;
const inativos = usuarios.data.filter(u => !u.ativo).length;
// Se não há usuários, retorna stats zeradas (mas não null para não mostrar loading)
if (!Array.isArray(usuarios) || usuarios.length === 0) {
return {
total: 0,
ativos: 0,
bloqueados: 0,
inativos: 0
};
}
const ativos = usuarios.filter(u => u.ativo && !u.bloqueado).length;
const bloqueados = usuarios.filter(u => u.bloqueado === true).length;
const inativos = usuarios.filter(u => !u.ativo).length;
return {
total: usuarios.data.length,
total: usuarios.length,
ativos,
bloqueados,
inativos
@@ -52,7 +69,7 @@
<StatsCard
title="Usuários Ativos"
value={stats.ativos}
description="{((stats.ativos / stats.total) * 100).toFixed(1)}% do total"
description="{stats.total > 0 ? ((stats.ativos / stats.total) * 100).toFixed(1) + '% do total' : '0% do total'}"
icon='<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" />'
color="success"
/>

View File

@@ -61,7 +61,7 @@
"version": "1.0.0",
"dependencies": {
"@dicebear/avataaars": "^9.2.4",
"convex": "catalog:",
"convex": "^1.17.4",
"nodemailer": "^7.0.10",
},
"devDependencies": {