950 lines
34 KiB
Svelte
950 lines
34 KiB
Svelte
<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
|
|
type="button"
|
|
class="btn btn-sm btn-info btn-square tooltip"
|
|
data-tip="Ver Detalhes"
|
|
aria-label="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
|
|
type="button"
|
|
class="btn btn-sm btn-warning btn-square tooltip"
|
|
data-tip="Editar"
|
|
aria-label="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
|
|
type="button"
|
|
class="btn btn-sm btn-success btn-square tooltip"
|
|
data-tip="Clonar"
|
|
aria-label="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
|
|
type="button"
|
|
class="btn btn-sm btn-error btn-square tooltip"
|
|
data-tip={perfil.numeroUsuarios > 0 ? "Não pode excluir - Perfil em uso" : "Excluir"}
|
|
aria-label="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 type="button" class="btn btn-ghost" onclick={fecharModalExcluir} disabled={processando}>
|
|
Cancelar
|
|
</button>
|
|
<button type="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>
|
|
|