feat: implement advanced access control system with user blocking, rate limiting, and enhanced login security; update UI components for improved user experience and documentation
This commit is contained in:
941
apps/web/src/routes/(dashboard)/ti/perfis/+page.svelte
Normal file
941
apps/web/src/routes/(dashboard)/ti/perfis/+page.svelte
Normal file
@@ -0,0 +1,941 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Queries
|
||||
const perfisQuery = useQuery(api.perfisCustomizados.listarPerfisCustomizados, {});
|
||||
const rolesQuery = useQuery(api.roles.listar, {});
|
||||
|
||||
// Estados
|
||||
let modo = $state<"listar" | "criar" | "editar" | "detalhes">("listar");
|
||||
let perfilSelecionado = $state<any>(null);
|
||||
let processando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error" | "warning"; texto: string } | null>(null);
|
||||
let modalExcluir = $state(false);
|
||||
let perfilParaExcluir = $state<any>(null);
|
||||
|
||||
// Formulário
|
||||
let formNome = $state("");
|
||||
let formDescricao = $state("");
|
||||
let formNivel = $state(3);
|
||||
let formClonarDeRoleId = $state<string>("");
|
||||
|
||||
// Detalhes do perfil
|
||||
let detalhesQuery = $state<any>(null);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error" | "warning", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
setTimeout(() => {
|
||||
mensagem = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function abrirCriar() {
|
||||
modo = "criar";
|
||||
formNome = "";
|
||||
formDescricao = "";
|
||||
formNivel = 3;
|
||||
formClonarDeRoleId = "";
|
||||
}
|
||||
|
||||
function abrirEditar(perfil: any) {
|
||||
modo = "editar";
|
||||
perfilSelecionado = perfil;
|
||||
formNome = perfil.nome;
|
||||
formDescricao = perfil.descricao;
|
||||
formNivel = perfil.nivel;
|
||||
}
|
||||
|
||||
async function abrirDetalhes(perfil: any) {
|
||||
modo = "detalhes";
|
||||
perfilSelecionado = perfil;
|
||||
|
||||
// Buscar detalhes completos
|
||||
try {
|
||||
const detalhes = await client.query(api.perfisCustomizados.obterPerfilComPermissoes, {
|
||||
perfilId: perfil._id,
|
||||
});
|
||||
detalhesQuery = detalhes;
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao carregar detalhes");
|
||||
}
|
||||
}
|
||||
|
||||
function voltar() {
|
||||
modo = "listar";
|
||||
perfilSelecionado = null;
|
||||
detalhesQuery = null;
|
||||
}
|
||||
|
||||
async function criarPerfil() {
|
||||
if (!formNome.trim() || !formDescricao.trim()) {
|
||||
mostrarMensagem("warning", "Preencha todos os campos obrigatórios");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formNivel < 3) {
|
||||
mostrarMensagem("warning", "O nível mínimo para perfis customizados é 3");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authStore.usuario) {
|
||||
mostrarMensagem("error", "Usuário não autenticado");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processando = true;
|
||||
|
||||
const resultado = await client.mutation(api.perfisCustomizados.criarPerfilCustomizado, {
|
||||
nome: formNome.trim(),
|
||||
descricao: formDescricao.trim(),
|
||||
nivel: formNivel,
|
||||
clonarDeRoleId: formClonarDeRoleId ? (formClonarDeRoleId as Id<"roles">) : undefined,
|
||||
criadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Perfil criado com sucesso!");
|
||||
voltar();
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
}
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao criar perfil");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function editarPerfil() {
|
||||
if (!perfilSelecionado) return;
|
||||
|
||||
if (!formNome.trim() || !formDescricao.trim()) {
|
||||
mostrarMensagem("warning", "Preencha todos os campos obrigatórios");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authStore.usuario) {
|
||||
mostrarMensagem("error", "Usuário não autenticado");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processando = true;
|
||||
|
||||
const resultado = await client.mutation(api.perfisCustomizados.editarPerfilCustomizado, {
|
||||
perfilId: perfilSelecionado._id,
|
||||
nome: formNome.trim(),
|
||||
descricao: formDescricao.trim(),
|
||||
editadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Perfil atualizado com sucesso!");
|
||||
voltar();
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
}
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao editar perfil");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function abrirModalExcluir(perfil: any) {
|
||||
perfilParaExcluir = perfil;
|
||||
modalExcluir = true;
|
||||
}
|
||||
|
||||
function fecharModalExcluir() {
|
||||
modalExcluir = false;
|
||||
perfilParaExcluir = null;
|
||||
}
|
||||
|
||||
async function confirmarExclusao() {
|
||||
if (!perfilParaExcluir || !authStore.usuario) {
|
||||
mostrarMensagem("error", "Erro ao excluir perfil");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processando = true;
|
||||
modalExcluir = false;
|
||||
|
||||
const resultado = await client.mutation(api.perfisCustomizados.excluirPerfilCustomizado, {
|
||||
perfilId: perfilParaExcluir._id,
|
||||
excluidoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Perfil excluído com sucesso!");
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
}
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao excluir perfil");
|
||||
} finally {
|
||||
processando = false;
|
||||
perfilParaExcluir = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function clonarPerfil(perfil: any) {
|
||||
const novoNome = prompt(`Digite o nome para o novo perfil (clone de "${perfil.nome}"):`);
|
||||
if (!novoNome?.trim()) return;
|
||||
|
||||
const novaDescricao = prompt("Digite a descrição para o novo perfil:");
|
||||
if (!novaDescricao?.trim()) return;
|
||||
|
||||
if (!authStore.usuario) {
|
||||
mostrarMensagem("error", "Usuário não autenticado");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processando = true;
|
||||
|
||||
const resultado = await client.mutation(api.perfisCustomizados.clonarPerfil, {
|
||||
perfilOrigemId: perfil._id,
|
||||
novoNome: novoNome.trim(),
|
||||
novaDescricao: novaDescricao.trim(),
|
||||
criadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem("success", "Perfil clonado com sucesso!");
|
||||
} else {
|
||||
mostrarMensagem("error", resultado.erro);
|
||||
}
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao clonar perfil");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatarData(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString("pt-BR");
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-secondary/10 rounded-xl">
|
||||
<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="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>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Gerenciar Perfis Customizados</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Crie e gerencie perfis de acesso personalizados para os usuários
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{#if modo !== "listar"}
|
||||
<button class="btn btn-ghost gap-2" onclick={voltar} disabled={processando}>
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</button>
|
||||
{/if}
|
||||
{#if modo === "listar"}
|
||||
<a href="/ti" class="btn btn-outline btn-primary gap-2">
|
||||
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Voltar para TI
|
||||
</a>
|
||||
<button class="btn btn-primary gap-2" onclick={abrirCriar} disabled={processando}>
|
||||
<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="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Novo Perfil
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensagens -->
|
||||
{#if mensagem}
|
||||
<div
|
||||
class="alert mb-6"
|
||||
class:alert-success={mensagem.tipo === "success"}
|
||||
class:alert-error={mensagem.tipo === "error"}
|
||||
class:alert-warning={mensagem.tipo === "warning"}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if mensagem.tipo === "success"}
|
||||
<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"
|
||||
/>
|
||||
{:else if mensagem.tipo === "error"}
|
||||
<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"
|
||||
/>
|
||||
{:else}
|
||||
<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"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{mensagem.texto}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modo: Listar -->
|
||||
{#if modo === "listar"}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
{#if !perfisQuery}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if perfisQuery.data && perfisQuery.data.length === 0}
|
||||
<div class="text-center py-20">
|
||||
<div class="text-6xl mb-4">📋</div>
|
||||
<h3 class="text-2xl font-bold mb-2">Nenhum perfil customizado</h3>
|
||||
<p class="text-base-content/60 mb-6">
|
||||
Crie seu primeiro perfil personalizado clicando no botão acima
|
||||
</p>
|
||||
</div>
|
||||
{:else if perfisQuery.data}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrição</th>
|
||||
<th>Nível</th>
|
||||
<th>Usuários</th>
|
||||
<th>Criado Por</th>
|
||||
<th>Criado Em</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each perfisQuery.data as perfil}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-bold">{perfil.nome}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm opacity-70 max-w-xs truncate">
|
||||
{perfil.descricao}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge badge-primary">{perfil.nivel}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge badge-ghost">
|
||||
{perfil.numeroUsuarios} usuário{perfil.numeroUsuarios !== 1 ? "s" : ""}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm">{perfil.criadorNome}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm">{formatarData(perfil.criadoEm)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
class="btn btn-sm btn-info btn-square tooltip"
|
||||
data-tip="Ver Detalhes"
|
||||
onclick={() => abrirDetalhes(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-warning btn-square tooltip"
|
||||
data-tip="Editar"
|
||||
onclick={() => abrirEditar(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success btn-square tooltip"
|
||||
data-tip="Clonar"
|
||||
onclick={() => clonarPerfil(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
<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 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-error btn-square tooltip"
|
||||
data-tip={perfil.numeroUsuarios > 0 ? "Não pode excluir - Perfil em uso" : "Excluir"}
|
||||
onclick={() => abrirModalExcluir(perfil)}
|
||||
disabled={processando || perfil.numeroUsuarios > 0}
|
||||
>
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modo: Criar -->
|
||||
{#if modo === "criar"}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">Criar Novo Perfil Customizado</h2>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
criarPerfil();
|
||||
}}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
<span class="label-text font-semibold">Nome do Perfil *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nome"
|
||||
type="text"
|
||||
placeholder="Ex: Coordenador de Esportes"
|
||||
class="input input-bordered"
|
||||
bind:value={formNome}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nível -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="nivel">
|
||||
<span class="label-text font-semibold">Nível de Acesso *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nivel"
|
||||
type="number"
|
||||
min="3"
|
||||
class="input input-bordered"
|
||||
bind:value={formNivel}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Mínimo: 3 (perfis customizados)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="descricao">
|
||||
<span class="label-text font-semibold">Descrição *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="descricao"
|
||||
placeholder="Descreva as responsabilidades deste perfil..."
|
||||
class="textarea textarea-bordered h-24"
|
||||
bind:value={formDescricao}
|
||||
required
|
||||
disabled={processando}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Clonar Permissões -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="clonar">
|
||||
<span class="label-text font-semibold">Clonar Permissões de (Opcional)</span>
|
||||
</label>
|
||||
<select
|
||||
id="clonar"
|
||||
class="select select-bordered"
|
||||
bind:value={formClonarDeRoleId}
|
||||
disabled={processando || !rolesQuery?.data}
|
||||
>
|
||||
<option value="">Não clonar (perfil vazio)</option>
|
||||
{#if rolesQuery?.data}
|
||||
{#each rolesQuery.data as role}
|
||||
<option value={role._id}>{role.nome} - {role.descricao}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt"
|
||||
>Selecione um perfil existente para copiar suas permissões</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={voltar}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" disabled={processando}>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{/if}
|
||||
Criar Perfil
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modo: Editar -->
|
||||
{#if modo === "editar" && perfilSelecionado}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">Editar Perfil: {perfilSelecionado.nome}</h2>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
editarPerfil();
|
||||
}}
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="edit-nome">
|
||||
<span class="label-text font-semibold">Nome do Perfil *</span>
|
||||
</label>
|
||||
<input
|
||||
id="edit-nome"
|
||||
type="text"
|
||||
placeholder="Ex: Coordenador de Esportes"
|
||||
class="input input-bordered"
|
||||
bind:value={formNome}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="edit-descricao">
|
||||
<span class="label-text font-semibold">Descrição *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="edit-descricao"
|
||||
placeholder="Descreva as responsabilidades deste perfil..."
|
||||
class="textarea textarea-bordered h-24"
|
||||
bind:value={formDescricao}
|
||||
required
|
||||
disabled={processando}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Info sobre nível -->
|
||||
<div class="alert alert-info">
|
||||
<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
|
||||
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>O nível de acesso não pode ser alterado após a criação (Nível: {formNivel})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={voltar}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" disabled={processando}>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{/if}
|
||||
Salvar Alterações
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modo: Detalhes -->
|
||||
{#if modo === "detalhes" && perfilSelecionado}
|
||||
<div class="space-y-6">
|
||||
<!-- Informações Básicas -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">{perfilSelecionado.nome}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Descrição</p>
|
||||
<p class="text-base-content">{perfilSelecionado.descricao}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Nível de Acesso</p>
|
||||
<p class="text-base-content">
|
||||
<span class="badge badge-primary">{perfilSelecionado.nivel}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Criado Por</p>
|
||||
<p class="text-base-content">{perfilSelecionado.criadorNome}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Criado Em</p>
|
||||
<p class="text-base-content">{formatarData(perfilSelecionado.criadoEm)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Usuários com este Perfil</p>
|
||||
<p class="text-base-content">
|
||||
<span class="badge badge-ghost"
|
||||
>{perfilSelecionado.numeroUsuarios} usuário{perfilSelecionado.numeroUsuarios !==
|
||||
1
|
||||
? "s"
|
||||
: ""}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissões -->
|
||||
{#if !detalhesQuery}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Permissões de Menu -->
|
||||
{#if detalhesQuery.menuPermissoes && detalhesQuery.menuPermissoes.length > 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-xl mb-4">Permissões de Menu</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Menu</th>
|
||||
<th>Acessar</th>
|
||||
<th>Consultar</th>
|
||||
<th>Gravar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each detalhesQuery.menuPermissoes as perm}
|
||||
<tr>
|
||||
<td class="font-medium">{perm.menuPath}</td>
|
||||
<td>
|
||||
{#if perm.podeAcessar}
|
||||
<span class="badge badge-success badge-sm">Sim</span>
|
||||
{:else}
|
||||
<span class="badge badge-ghost badge-sm">Não</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if perm.podeConsultar}
|
||||
<span class="badge badge-success badge-sm">Sim</span>
|
||||
{:else}
|
||||
<span class="badge badge-ghost badge-sm">Não</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if perm.podeGravar}
|
||||
<span class="badge badge-success badge-sm">Sim</span>
|
||||
{:else}
|
||||
<span class="badge badge-ghost badge-sm">Não</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<a href="/ti/painel-permissoes" class="btn btn-sm btn-primary">
|
||||
Editar Permissões
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-warning">
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Sem permissões de menu configuradas</h3>
|
||||
<div class="text-sm">
|
||||
Configure as permissões de menu no <a
|
||||
href="/ti/painel-permissoes"
|
||||
class="link">Painel de Permissões</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Usuários com este Perfil -->
|
||||
{#if detalhesQuery.usuarios && detalhesQuery.usuarios.length > 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-xl mb-4">Usuários com este Perfil</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Matrícula</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each detalhesQuery.usuarios as usuario}
|
||||
<tr>
|
||||
<td>{usuario.nome}</td>
|
||||
<td>{usuario.matricula}</td>
|
||||
<td>{usuario.email}</td>
|
||||
<td>
|
||||
{#if usuario.ativo && !usuario.bloqueado}
|
||||
<span class="badge badge-success badge-sm">Ativo</span>
|
||||
{:else if usuario.bloqueado}
|
||||
<span class="badge badge-error badge-sm">Bloqueado</span>
|
||||
{:else}
|
||||
<span class="badge badge-ghost badge-sm">Inativo</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
{#if modalExcluir && perfilParaExcluir}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-error"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
Confirmar Exclusão
|
||||
</h3>
|
||||
<p class="py-4">
|
||||
Tem certeza que deseja excluir o perfil <strong>"{perfilParaExcluir.nome}"</strong>?
|
||||
</p>
|
||||
<div class="alert alert-warning">
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
<span>Esta ação não pode ser desfeita!</span>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" onclick={fecharModalExcluir} disabled={processando}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button class="btn btn-error" onclick={confirmarExclusao} disabled={processando}>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Excluir Perfil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModalExcluir}>
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
Reference in New Issue
Block a user