refactor: enhance role management UI and integrate profile management features

- Introduced a modal for managing user profiles, allowing for the creation and editing of profiles with improved state management.
- Updated the role filtering logic to enhance type safety and readability.
- Refactored UI components for better user experience, including improved button states and loading indicators.
- Removed outdated code related to permissions and streamlined the overall structure for maintainability.
This commit is contained in:
2025-11-03 15:14:33 -03:00
parent c1d9958c9f
commit 0d011b8f42
38 changed files with 2664 additions and 4919 deletions

View File

@@ -4,6 +4,7 @@
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { goto } from "$app/navigation";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { authStore } from "$lib/stores/auth.svelte";
type RoleRow = {
_id: Id<"roles">;
_creationTime: number;
@@ -32,6 +33,14 @@
// Formato: { "roleId-recurso": true/false }
let recursosExpandidos: Record<string, boolean> = $state({});
// Gerenciamento de Perfis
let modalGerenciarPerfisAberto = $state(false);
let perfilSendoEditado = $state<RoleRow | null>(null);
let nomeNovoPerfil = $state("");
let descricaoNovoPerfil = $state("");
let nivelNovoPerfil = $state(3);
let processando = $state(false);
// Cache de permissões por role
let permissoesPorRole: Record<
string,
@@ -66,13 +75,13 @@
const rolesFiltradas = $derived.by(() => {
if (!rolesQuery.data) return [];
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>;
let rs = rolesQuery.data; // Removed explicit type annotation
if (filtroRole)
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any));
rs = rs.filter((r) => r._id === (filtroRole)); // Removed as any
if (busca.trim()) {
const b = busca.toLowerCase();
rs = rs.filter(
(r: RoleRow) =>
(r) =>
r.descricao.toLowerCase().includes(b) ||
r.nome.toLowerCase().includes(b)
);
@@ -120,8 +129,9 @@
];
}
mostrarMensagem("success", "Permissão atualizada com sucesso!");
} catch (e: any) {
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
} catch (error: unknown) { // Changed to unknown
const message = error instanceof Error ? error.message : "Erro ao atualizar permissão";
mostrarMensagem("error", message);
} finally {
salvando = false;
}
@@ -132,6 +142,90 @@
const entry = dados?.find((e) => e.recurso === recurso);
return entry ? entry.acoes.includes(acao) : false;
}
function abrirModalCriarPerfil() {
nomeNovoPerfil = "";
descricaoNovoPerfil = "";
nivelNovoPerfil = 3; // Default to a common level
perfilSendoEditado = null;
modalGerenciarPerfisAberto = true;
}
function prepararEdicaoPerfil(role: RoleRow) {
perfilSendoEditado = role;
nomeNovoPerfil = role.nome;
descricaoNovoPerfil = role.descricao;
nivelNovoPerfil = role.nivel;
modalGerenciarPerfisAberto = true;
}
function fecharModalGerenciarPerfis() {
modalGerenciarPerfisAberto = false;
perfilSendoEditado = null;
}
async function criarNovoPerfil() {
if (!nomeNovoPerfil.trim()) return;
processando = true;
try {
const result = await client.mutation(api.roles.criar, {
nome: nomeNovoPerfil.trim(),
descricao: descricaoNovoPerfil.trim(),
nivel: nivelNovoPerfil,
customizado: true,
});
if (result.sucesso) {
mostrarMensagem("success", "Perfil criado com sucesso!");
nomeNovoPerfil = "";
descricaoNovoPerfil = "";
nivelNovoPerfil = 3;
fecharModalGerenciarPerfis();
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
rolesQuery.refetch(); // Atualiza a lista de perfis
}
} else {
mostrarMensagem("error", `Erro ao criar perfil: ${result.erro}`);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
mostrarMensagem("error", `Erro ao criar perfil: ${message}`);
} finally {
processando = false;
}
}
async function editarPerfil() {
if (!perfilSendoEditado || !nomeNovoPerfil.trim()) return;
processando = true;
try {
const result = await client.mutation(api.roles.atualizar, {
roleId: perfilSendoEditado._id,
nome: nomeNovoPerfil.trim(),
descricao: descricaoNovoPerfil.trim(),
nivel: nivelNovoPerfil,
setor: perfilSendoEditado.setor, // Manter setor existente
});
if (result.sucesso) {
mostrarMensagem("success", "Perfil atualizado com sucesso!");
fecharModalGerenciarPerfis();
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
rolesQuery.refetch(); // Atualiza a lista de perfis
}
} else {
mostrarMensagem("error", `Erro ao atualizar perfil: ${result.erro}`);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
mostrarMensagem("error", `Erro ao atualizar perfil: ${message}`);
} finally {
processando = false;
}
}
</script>
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
@@ -160,7 +254,7 @@
<li>
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
</li>
<li class="font-semibold">Gerenciar Permissões</li>
<li class="font-semibold">Gerenciar Perfis & Permissões</li>
</ul>
</div>
@@ -185,12 +279,32 @@
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-base-content">
Gerenciar Permissões de Acesso
Gerenciar Perfis & Permissões de Acesso
</h1>
<p class="text-base-content/60 mt-1">
Configure as permissões de acesso aos menus do sistema por função
</p>
</div>
<button
class="btn btn-primary gap-2"
onclick={abrirModalCriarPerfil}
>
<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>
Criar Novo Perfil
</button>
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -479,6 +593,15 @@
>
</p>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-outline btn-sm"
onclick={() => prepararEdicaoPerfil(roleRow)}
>
Editar
</button>
</div>
</div>
{#if roleRow.nivel <= 1}
@@ -574,4 +697,148 @@
</div>
{/each}
{/if}
<!-- Modal Gerenciar Perfis -->
{#if modalGerenciarPerfisAberto}
<dialog class="modal modal-open">
<div
class="modal-box max-w-4xl w-full overflow-hidden border border-base-200/60 bg-base-200/40 p-0 shadow-2xl"
>
<div
class="relative bg-gradient-to-r from-primary via-primary/90 to-secondary/80 px-8 py-6 text-base-100"
>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4 text-base-100/80 hover:text-base-100"
onclick={fecharModalGerenciarPerfis}
aria-label="Fechar modal"
>
</button>
<h3 class="text-3xl font-black tracking-tight">Gerenciar Perfis de Acesso</h3>
<p class="mt-2 max-w-2xl text-sm text-base-100/80 md:text-base">
{perfilSendoEditado
? "Atualize as informações do perfil selecionado para manter a governança de acesso alinhada com as diretrizes do sistema."
: "Crie um novo perfil de acesso definindo nome, descrição e nível hierárquico conforme os padrões adotados pela Secretaria."}
</p>
</div>
<div class="bg-base-100 px-8 py-6">
<section
class="space-y-6 rounded-2xl border border-base-200/60 bg-base-100 p-6 shadow-sm"
>
<div
class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-1">
<h4 class="text-xl font-semibold text-base-content">
{perfilSendoEditado ? "Editar Perfil" : "Criar Novo Perfil"}
</h4>
<p class="text-sm text-base-content/70">
{perfilSendoEditado
? "Os campos bloqueados indicam atributos padronizados do sistema. Ajuste apenas o que estiver disponível."
: "Preencha as informações com atenção para garantir que o novo perfil siga o padrão institucional."}
</p>
</div>
{#if perfilSendoEditado}
<span
class="badge badge-outline badge-primary badge-lg self-start flex flex-col items-center gap-1 px-5 py-3 text-center"
>
<span class="text-[11px] font-semibold uppercase tracking-[0.32em] text-primary">
Nível atual
</span>
<span class="text-2xl font-bold leading-none text-primary">
{perfilSendoEditado.nivel}
</span>
</span>
{/if}
</div>
<div class="grid gap-6 md:grid-cols-2">
<div class="form-control md:col-span-2">
<label class="label" for="nome-perfil-input">
<span class="label-text">Nome do Perfil *</span>
</label>
<input
id="nome-perfil-input"
type="text"
bind:value={nomeNovoPerfil}
class="input input-bordered input-primary"
placeholder="Ex: RH, Financeiro, Gestor"
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
/>
</div>
<div class="form-control md:col-span-2">
<label class="label" for="descricao-perfil-input">
<span class="label-text">Descrição</span>
</label>
<textarea
id="descricao-perfil-input"
bind:value={descricaoNovoPerfil}
class="textarea textarea-bordered textarea-primary min-h-[120px]"
placeholder="Breve descrição das responsabilidades e limites de atuação deste perfil."
></textarea>
</div>
<div class="form-control max-w-xs">
<label class="label" for="nivel-perfil-input">
<span class="label-text">Nível de Acesso (0-5) *</span>
</label>
<input
id="nivel-perfil-input"
type="number"
bind:value={nivelNovoPerfil}
min="0"
max="5"
class="input input-bordered input-secondary"
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
/>
</div>
</div>
</section>
<div
class="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p class="text-sm text-base-content/60">
Campos marcados com * são obrigatórios.
</p>
<div class="flex flex-wrap gap-3">
<button
type="button"
class="btn btn-ghost"
onclick={fecharModalGerenciarPerfis}
disabled={processando}
>
Cancelar
</button>
<button
type="button"
class="btn btn-primary"
disabled={!nomeNovoPerfil.trim() || processando}
onclick={perfilSendoEditado ? editarPerfil : criarNovoPerfil}
>
{#if processando}
<span class="loading loading-spinner"></span>
Processando...
{:else}
{perfilSendoEditado ? "Salvar Alterações" : "Criar Perfil"}
{/if}
</button>
</div>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button
type="button"
onclick={fecharModalGerenciarPerfis}
aria-label="Fechar modal"
>Fechar</button>
</form>
</dialog>
{/if}
</ProtectedRoute>