Fix usuarios page #6

Merged
killer-cf merged 28 commits from fix-usuarios-page into master 2025-11-04 17:42:21 +00:00
36 changed files with 2677 additions and 1221 deletions
Showing only changes of commit c1d9958c9f - Show all commits

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:** ### **Banco vazio:**
```powershell ```powershell
cd packages\backend cd packages\backend
npx convex run seed:clearDatabase npx convex run seed:limparBanco
npx convex run seed:seedDatabase npx convex run seed:popularBanco
``` ```
**Mais soluções:** Veja `TESTAR_SISTEMA_COMPLETO.md` seção "Problemas Comuns" **Mais soluções:** Veja `TESTAR_SISTEMA_COMPLETO.md` seção "Problemas Comuns"

View File

@@ -18,3 +18,58 @@
.btn-error { .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; @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 { useConvexClient, useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from "@sgse-app/backend/convex/_generated/api";
import { authStore } from "$lib/stores/auth.svelte"; 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 AprovarFerias from "$lib/components/AprovarFerias.svelte";
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars"; import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { page } from "$app/stores";
const client = useConvexClient(); const client = useConvexClient();
let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">( let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">(
"meu-perfil" "meu-perfil"
); );
let mostrarFormSolicitar = $state(false);
let solicitacaoSelecionada = $state<any>(null); let solicitacaoSelecionada = $state<any>(null);
let mostrarModalFoto = $state(false); let mostrarModalFoto = $state(false);
let uploadandoFoto = $state(false); let uploadandoFoto = $state(false);
@@ -26,6 +25,10 @@
let fotoPerfilLocal = $state<string | null>(null); let fotoPerfilLocal = $state<string | null>(null);
let avatarLocal = $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) // Galeria de avatares (30 avatares profissionais 3D realistas)
const avatarGallery = generateAvatarGallery(30); const avatarGallery = generateAvatarGallery(30);
@@ -54,14 +57,6 @@
: { data: null } : { data: null }
); );
const minhasSolicitacoesQuery = $derived(
funcionarioQuery.data
? useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId: funcionarioQuery.data._id,
})
: { data: [] }
);
const solicitacoesSubordinadosQuery = $derived( const solicitacoesSubordinadosQuery = $derived(
authStore.usuario?._id authStore.usuario?._id
? useQuery(api.ferias.listarSolicitacoesSubordinados, { ? useQuery(api.ferias.listarSolicitacoesSubordinados, {
@@ -70,6 +65,14 @@
: { data: [] } : { data: [] }
); );
const minhasSolicitacoesQuery = $derived(
funcionarioQuery.data
? useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId: funcionarioQuery.data._id,
})
: { data: [] }
);
const meuTimeQuery = $derived( const meuTimeQuery = $derived(
funcionarioQuery.data funcionarioQuery.data
? useQuery(api.times.obterTimeFuncionario, { ? useQuery(api.times.obterTimeFuncionario, {
@@ -87,18 +90,34 @@
); );
const funcionario = $derived(funcionarioQuery.data); const funcionario = $derived(funcionarioQuery.data);
const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []);
const solicitacoesSubordinados = $derived( const solicitacoesSubordinados = $derived(
solicitacoesSubordinadosQuery?.data || [] solicitacoesSubordinadosQuery?.data || []
); );
const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []);
const meuTime = $derived(meuTimeQuery?.data); const meuTime = $derived(meuTimeQuery?.data);
const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []); const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []);
// Verificar se é gestor // Verificar se é gestor
const ehGestor = $derived((meusTimesGestor || []).length > 0); 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() { async function recarregar() {
mostrarFormSolicitar = false;
solicitacaoSelecionada = null; solicitacaoSelecionada = null;
} }
@@ -1060,73 +1079,148 @@
{/if} {/if}
</div> </div>
{:else if abaAtiva === "minhas-ferias"} {:else if abaAtiva === "minhas-ferias"}
<!-- Minhas Férias MODERNO --> <!-- Minhas Férias -->
<div class="space-y-8"> <div class="space-y-6">
{#if !mostrarFormSolicitar} <!-- Estatísticas -->
<!-- Dashboard de Férias --> <div class="grid grid-cols-1 md:grid-cols-5 gap-4">
{#if funcionario} <div class="stat bg-base-100 shadow-lg rounded-box border border-base-300">
<DashboardFerias funcionarioId={funcionario._id} /> <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>
<div class="stat-title">Total</div>
<div class="stat-value text-primary">{statsMinhasFerias.total}</div>
<div class="stat-desc">Solicitações</div>
</div>
<!-- Botão para solicitar --> <div class="stat bg-base-100 shadow-lg rounded-box border border-warning/30">
<div class="flex justify-center"> <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 <button
type="button" type="button"
class="btn btn-lg gap-3 shadow-2xl hover:shadow-3xl transition-all hover:scale-105" class="btn btn-primary gap-2"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;" onclick={() => (mostrarWizard = true)}
onclick={() => (mostrarFormSolicitar = true)}
> >
<svg <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
xmlns="http://www.w3.org/2000/svg" <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" />
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> </svg>
Solicitar Novas Férias Agendar Férias
</button> </button>
{/if}
</div> </div>
{:else} <div class="grid grid-cols-1 md:grid-cols-1 gap-4 mt-4">
<div class="alert alert-warning shadow-xl"> <div class="form-control">
<svg <label class="label" for="status">
xmlns="http://www.w3.org/2000/svg" <span class="label-text">Status</span>
class="stroke-current shrink-0 h-6 w-6" </label>
fill="none" <select id="status" class="select select-bordered" bind:value={filtroStatusFerias}>
viewBox="0 0 24 24" <option value="todos">Todos</option>
> <option value="aguardando_aprovacao">Aguardando Aprovação</option>
<path <option value="aprovado">Aprovado</option>
stroke-linecap="round" <option value="reprovado">Reprovado</option>
stroke-linejoin="round" <option value="data_ajustada_aprovada">Data Ajustada</option>
stroke-width="2" </select>
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" </div>
/> </div>
</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> </svg>
<div> <span>Nenhuma solicitação encontrada com os filtros aplicados.</span>
<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>
</div>
{/if}
{:else} {:else}
<!-- Wizard de Solicitação de Férias --> <div class="overflow-x-auto">
{#if funcionario} <table class="table table-zebra">
<WizardSolicitacaoFerias <thead>
funcionarioId={funcionario._id} <tr>
onSucesso={recarregar} <th>Ano</th>
onCancelar={() => (mostrarFormSolicitar = false)} <th>Períodos</th>
/> <th>Total Dias</th>
{/if} <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} {/if}
</div> </div>
</div>
</div>
{:else if abaAtiva === "aprovar-ferias"} {:else if abaAtiva === "aprovar-ferias"}
<!-- Aprovar Férias (Gestores) PREMIUM --> <!-- Aprovar Férias (Gestores) PREMIUM -->
<div class="card bg-base-100 shadow-2xl border-t-4 border-green-500"> <div class="card bg-base-100 shadow-2xl border-t-4 border-green-500">
@@ -1599,3 +1693,26 @@
animation: float 6s ease-in-out infinite; animation: float 6s ease-in-out infinite;
} }
</style> </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 { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api"; 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 todasSolicitacoesQuery = useQuery(api.ferias.listarTodas, {});
const todosFuncionariosQuery = useQuery(api.funcionarios.getAll, {});
let filtroStatus = $state<string>("todos"); let filtroStatus = $state<string>("todos");
let filtroTime = $state<string>("todos");
let filtroBusca = $state("");
const solicitacoes = $derived(todasSolicitacoesQuery?.data || []); const solicitacoes = $derived(todasSolicitacoesQuery?.data || []);
const funcionarios = $derived(todosFuncionariosQuery?.data || []);
// Filtrar solicitações // Filtrar solicitações
const solicitacoesFiltradas = $derived( const solicitacoesFiltradas = $derived(
solicitacoes.filter((s: any) => { solicitacoes.filter((s: any) => {
// Filtro de status // Filtro de status
if (filtroStatus !== "todos" && s.status !== filtroStatus) return false; 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; return true;
}) })
); );
// Estatísticas // Estatísticas gerais
const stats = $derived({ const stats = $derived({
total: solicitacoes.length, total: solicitacoes.length,
aguardando: solicitacoes.filter((s: any) => s.status === "aguardando_aprovacao").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, aprovadas: solicitacoes.filter((s: any) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length,
reprovadas: solicitacoes.filter((s: any) => s.status === "reprovado").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) { function getStatusBadge(status: string) {
const badges: Record<string, string> = { const badges: Record<string, string> = {
aguardando_aprovacao: "badge-warning", aguardando_aprovacao: "badge-warning",
@@ -104,7 +85,7 @@
</div> </div>
<!-- Estatísticas --> <!-- 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 bg-base-100 shadow-lg rounded-box border border-base-300">
<div class="stat-figure text-primary"> <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"> <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-value text-error">{stats.reprovadas}</div>
<div class="stat-desc">Indeferidas</div> <div class="stat-desc">Indeferidas</div>
</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> </div>
<!-- Filtros --> <!-- Filtros -->
<div class="card bg-base-100 shadow-lg mb-6"> <div class="card bg-base-100 shadow-lg mb-6">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-lg mb-4">Filtros</h2> <h2 class="card-title text-lg mb-4">Filtros</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-1 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>
<!-- Filtro Status --> <!-- Filtro Status -->
<div class="form-control"> <div class="form-control">
<label class="label" for="status"> <label class="label" for="status">
@@ -193,21 +149,6 @@
<option value="data_ajustada_aprovada">Data Ajustada</option> <option value="data_ajustada_aprovada">Data Ajustada</option>
</select> </select>
</div> </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> </div>
</div> </div>

View File

@@ -72,17 +72,28 @@
} }
</script> </script>
<main class="container mx-auto px-4 py-4 max-w-4xl"> <main class="mx-auto w-full max-w-4xl space-y-8 px-4 py-10">
<div class="mb-6"> <!-- Cabeçalho Estilizado -->
<h1 class="text-3xl font-bold text-primary mb-2">Solicitar Acesso ao SGSE</h1> <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">
<p class="text-base-content/70"> <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. 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. Sua solicitação será analisada pela equipe de Tecnologia da Informação.
</p> </p>
</div> </div>
</section>
<!-- Alertas -->
{#if notice} {#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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="stroke-current shrink-0 h-6 w-6"
@@ -105,12 +116,14 @@
/> />
{/if} {/if}
</svg> </svg>
<span>{notice.message}</span> <span class="font-semibold">{notice.message}</span>
</div> </div>
{/if} {/if}
<div class="card bg-base-100 shadow-xl"> <!-- Formulário -->
<div class="card-body"> <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 <form
onsubmit={(e) => { onsubmit={(e) => {
e.preventDefault(); e.preventDefault();
@@ -118,25 +131,28 @@
form.handleSubmit(); 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 --> <!-- Nome -->
<form.Field name="nome" validators={{ onChange: formSchema.shape.nome }}> <form.Field name="nome" validators={{ onChange: formSchema.shape.nome }}>
{#snippet children(field)} {#snippet children(field)}
<div class="form-control md:col-span-2"> <div class="form-control md:col-span-2">
<label class="label" for="nome"> <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> </label>
<input <input
id="nome" id="nome"
type="text" type="text"
placeholder="Digite seu nome completo" 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} value={field.state.value}
onblur={field.handleBlur} onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)} oninput={(e) => field.handleChange(e.currentTarget.value)}
/> />
{#if field.state.meta.errors.length > 0} {#if field.state.meta.errors.length > 0}
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span> <span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
@@ -147,19 +163,22 @@
{#snippet children(field)} {#snippet children(field)}
<div class="form-control"> <div class="form-control">
<label class="label" for="matricula"> <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> </label>
<input <input
id="matricula" id="matricula"
type="text" type="text"
placeholder="Digite sua matrícula" 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} value={field.state.value}
onblur={field.handleBlur} onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)} oninput={(e) => field.handleChange(e.currentTarget.value)}
/> />
{#if field.state.meta.errors.length > 0} {#if field.state.meta.errors.length > 0}
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span> <span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
@@ -170,19 +189,22 @@
{#snippet children(field)} {#snippet children(field)}
<div class="form-control"> <div class="form-control">
<label class="label" for="email"> <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> </label>
<input <input
id="email" id="email"
type="email" type="email"
placeholder="seu@email.com" 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} value={field.state.value}
onblur={field.handleBlur} onblur={field.handleBlur}
oninput={(e) => field.handleChange(e.currentTarget.value)} oninput={(e) => field.handleChange(e.currentTarget.value)}
/> />
{#if field.state.meta.errors.length > 0} {#if field.state.meta.errors.length > 0}
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span> <span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
@@ -193,13 +215,14 @@
{#snippet children(field)} {#snippet children(field)}
<div class="form-control md:col-span-2"> <div class="form-control md:col-span-2">
<label class="label" for="telefone"> <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> </label>
<input <input
id="telefone" id="telefone"
type="text" type="text"
placeholder="(00) 00000-0000" 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} value={field.state.value}
onblur={field.handleBlur} onblur={field.handleBlur}
oninput={(e) => { oninput={(e) => {
@@ -210,26 +233,36 @@
maxlength="15" maxlength="15"
/> />
{#if field.state.meta.errors.length > 0} {#if field.state.meta.errors.length > 0}
<label class="label">
<span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span> <span class="text-error text-sm mt-1">{field.state.meta.errors[0]}</span>
</label>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
</form.Field> </form.Field>
</div> </div>
<div class="card-actions justify-end mt-6 gap-2"> <!-- Botões de Ação -->
<button type="button" class="btn btn-ghost" onclick={handleCancel}> <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 Cancelar
</button> </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 Solicitar Acesso
</button> </button>
</div> </div>
</form> </form>
</div> </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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@@ -244,16 +277,42 @@
></path> ></path>
</svg> </svg>
<div> <div>
<h3 class="font-bold">Informações Importantes</h3> <h3 class="font-bold text-lg mb-2">Informações Importantes</h3>
<div class="text-sm"> <div class="text-sm space-y-1">
<ul class="list-disc list-inside mt-2"> <div class="flex items-start gap-2">
<li>Todos os campos marcados com * são obrigatórios</li> <span class="text-info font-bold"></span>
<li>Sua solicitação será analisada pela equipe de TI em até 48 horas úteis</li> <span>Todos os campos marcados com * são obrigatórios</span>
<li>Você receberá um e-mail com o resultado da análise</li> </div>
<li>Em caso de dúvidas, entre em contato com o suporte técnico</li> <div class="flex items-start gap-2">
</ul> <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> </div>
</div> </div>
</main> </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"> <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> </script>
<main class="container mx-auto px-4 py-4"> <main class="mx-auto w-full max-w-7xl space-y-12 px-4 py-10">
<h1 class="text-3xl font-bold text-primary mb-6">Tecnologia da Informação</h1> <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>
<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>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
<!-- Card Painel Administrativo --> {#each featureCards as card (card.title)}
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"> <article
<div class="card-body"> 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="flex items-center gap-4 mb-4"> >
<div class="p-3 bg-primary/20 rounded-lg"> <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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke="currentColor"
class={`h-7 w-7 ${paletteStyles[card.palette].iconColor}`}
> >
{#each iconPaths[card.icon] as path (path.d)}
<path <path
stroke-linecap="round" d={path.d}
stroke-linejoin="round" stroke-linecap={path.strokeLinecap ?? "round"}
stroke-width="2" stroke-linejoin={path.strokeLinejoin ?? "round"}
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" stroke-width={path.strokeWidth ?? 2}
/> />
{/each}
</svg> </svg>
</div> </div>
<h2 class="card-title text-xl">Painel Administrativo</h2> <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>
<p class="text-base-content/70 mb-4"> </div>
Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.
</p> {#if card.highlightBadges}
<div class="card-actions justify-end"> <div class="mt-4 flex flex-wrap gap-2">
<a href="/ti/painel-administrativo" class="btn btn-primary"> {#each card.highlightBadges as badge (badge.label)}
Acessar Painel {#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> </a>
</div> {:else}
</div> <button
</div> type="button"
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md`}
<!-- Card Suporte Técnico --> disabled
<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 {card.ctaLabel}
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"
/>
</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
</button> </button>
{/if}
</div> </div>
</div> </article>
</div> {/each}
</section>
<!-- Card Gerenciar Permissões --> <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="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"> <div class="absolute -top-10 right-0 h-32 w-32 rounded-full bg-warning/30 blur-3xl"></div>
<div class="card-body"> <div class="relative z-10 flex items-start gap-4">
<div class="flex items-center gap-4 mb-4"> <div class="flex h-12 w-12 items-center justify-center rounded-full bg-warning/25 text-warning">
<div class="p-3 bg-success/20 rounded-lg"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 stroke-current">
<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"
>
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path> />
</svg> </svg>
</div>
<div> <div>
<h3 class="font-bold">Área Restrita</h3> <h3 class="text-lg font-bold text-base-content">Área Restrita</h3>
<div class="text-sm"> <p class="mt-2 text-sm leading-relaxed text-base-content/70">
Esta é uma área de acesso restrito. Apenas usuários autorizados pela equipe de TI podem acessar o Painel Administrativo. 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.
</div> </p>
</div> </div>
</div> </div>
</section>
</main> </main>

View File

@@ -4,18 +4,35 @@
import StatsCard from "$lib/components/ti/StatsCard.svelte"; import StatsCard from "$lib/components/ti/StatsCard.svelte";
const client = useConvexClient(); 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 // Estatísticas derivadas
const stats = $derived.by(() => { 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; // Se não há usuários, retorna stats zeradas (mas não null para não mostrar loading)
const bloqueados = usuarios.data.filter(u => u.bloqueado).length; if (!Array.isArray(usuarios) || usuarios.length === 0) {
const inativos = usuarios.data.filter(u => !u.ativo).length; 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 { return {
total: usuarios.data.length, total: usuarios.length,
ativos, ativos,
bloqueados, bloqueados,
inativos inativos
@@ -52,7 +69,7 @@
<StatsCard <StatsCard
title="Usuários Ativos" title="Usuários Ativos"
value={stats.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" />' 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" color="success"
/> />

View File

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