664 lines
21 KiB
Svelte
664 lines
21 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 { goto } from '$app/navigation';
|
|
import { resolve } from '$app/paths';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import {
|
|
Home,
|
|
ShieldCheck,
|
|
Plus,
|
|
ArrowLeft,
|
|
CheckCircle,
|
|
XCircle,
|
|
Search,
|
|
X,
|
|
ChevronDown
|
|
} from 'lucide-svelte';
|
|
|
|
const client = useConvexClient();
|
|
|
|
// Carregar lista de roles e catálogo de recursos/ações
|
|
const rolesQuery = useQuery(api.roles.listar, {});
|
|
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
|
|
|
|
let salvando = $state(false);
|
|
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
|
let busca = $state('');
|
|
let filtroRole = $state<Id<'roles'> | ''>('');
|
|
let modalNovoPerfilAberto = $state(false);
|
|
let nomeNovoPerfil = $state('');
|
|
let descricaoNovoPerfil = $state('');
|
|
let setorNovoPerfil = $state('');
|
|
let nivelNovoPerfil = $state(3);
|
|
let roleParaDuplicar = $state<Id<'roles'> | ''>('');
|
|
let criandoNovoPerfil = $state(false);
|
|
|
|
// Controla quais recursos estão expandidos (mostrando as ações) por perfil
|
|
// Formato: { "roleId-recurso": true/false }
|
|
let recursosExpandidos: Record<string, boolean> = $state({});
|
|
|
|
// Cache de permissões por role
|
|
let permissoesPorRole: Record<string, Array<{ recurso: string; acoes: Array<string> }>> = $state(
|
|
{}
|
|
);
|
|
|
|
async function carregarPermissoesRole(roleId: Id<'roles'>) {
|
|
if (permissoesPorRole[roleId]) return;
|
|
const dados = await client.query(api.permissoesAcoes.listarPermissoesAcoesPorRole, { roleId });
|
|
permissoesPorRole[roleId] = dados;
|
|
}
|
|
|
|
function toggleRecurso(roleId: Id<'roles'>, recurso: string) {
|
|
const key = `${roleId}-${recurso}`;
|
|
recursosExpandidos[key] = !recursosExpandidos[key];
|
|
}
|
|
|
|
function isRecursoExpandido(roleId: Id<'roles'>, recurso: string) {
|
|
const key = `${roleId}-${recurso}`;
|
|
return recursosExpandidos[key] ?? false;
|
|
}
|
|
|
|
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
|
|
mensagem = { tipo, texto };
|
|
setTimeout(() => {
|
|
mensagem = null;
|
|
}, 3000);
|
|
}
|
|
|
|
const rolesFiltradas = $derived.by(() => {
|
|
if (!rolesQuery.data) return [];
|
|
let rs = rolesQuery.data; // Removed explicit type annotation
|
|
if (filtroRole) rs = rs.filter((r) => r._id === filtroRole); // Removed as any
|
|
if (busca.trim()) {
|
|
const b = busca.toLowerCase();
|
|
rs = rs.filter(
|
|
(r) => r.descricao.toLowerCase().includes(b) || r.nome.toLowerCase().includes(b)
|
|
);
|
|
}
|
|
return rs;
|
|
});
|
|
|
|
// Carregar permissões para todos os perfis filtrados quando necessário
|
|
$effect(() => {
|
|
if (rolesFiltradas && catalogoQuery.data) {
|
|
for (const roleRow of rolesFiltradas) {
|
|
if (roleRow.nivel > 1) {
|
|
carregarPermissoesRole(roleRow._id);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
async function toggleAcao(roleId: Id<'roles'>, recurso: string, acao: string, conceder: boolean) {
|
|
try {
|
|
salvando = true;
|
|
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
|
|
roleId,
|
|
recurso,
|
|
acao,
|
|
conceder
|
|
});
|
|
// Atualizar cache local
|
|
const atual = permissoesPorRole[roleId] || [];
|
|
const entry = atual.find((e) => e.recurso === recurso);
|
|
if (entry) {
|
|
entry.acoes = conceder
|
|
? [...entry.acoes.filter((valor) => valor !== acao), acao]
|
|
: entry.acoes.filter((valor) => valor !== acao);
|
|
} else if (conceder) {
|
|
permissoesPorRole[roleId] = [...atual, { recurso, acoes: [acao] }];
|
|
}
|
|
mostrarMensagem('success', 'Permissão atualizada com sucesso!');
|
|
} catch (error: unknown) {
|
|
// Changed to unknown
|
|
const message = error instanceof Error ? error.message : 'Erro ao atualizar permissão';
|
|
mostrarMensagem('error', message);
|
|
} finally {
|
|
salvando = false;
|
|
}
|
|
}
|
|
|
|
function isConcedida(roleId: Id<'roles'>, recurso: string, acao: string) {
|
|
const dados = permissoesPorRole[roleId];
|
|
const entry = dados?.find((e) => e.recurso === recurso);
|
|
return entry ? entry.acoes.includes(acao) : false;
|
|
}
|
|
|
|
const gerarIdentificador = (valor: string) =>
|
|
valor
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '_')
|
|
.replace(/^_+|_+$/g, '')
|
|
.replace(/_{2,}/g, '_');
|
|
|
|
const identificadorSugerido = $derived.by(() => gerarIdentificador(nomeNovoPerfil));
|
|
|
|
const podeSalvarNovoPerfil = $derived.by(() => {
|
|
const nome = nomeNovoPerfil.trim();
|
|
const nivel = Number(nivelNovoPerfil);
|
|
const nivelValido = Number.isFinite(nivel) && nivel >= 0 && nivel <= 10;
|
|
return nome.length >= 3 && nivelValido && !criandoNovoPerfil;
|
|
});
|
|
|
|
const roleDuplicacaoSelecionada = $derived.by(() => {
|
|
if (!roleParaDuplicar || !rolesQuery.data) return null;
|
|
return rolesQuery.data.find((role) => role._id === roleParaDuplicar) ?? null;
|
|
});
|
|
|
|
$effect(() => {
|
|
if (roleParaDuplicar) {
|
|
carregarPermissoesRole(roleParaDuplicar);
|
|
}
|
|
});
|
|
|
|
const resumoPermissoesDuplicacao = $derived.by(() => {
|
|
if (!roleParaDuplicar) return null;
|
|
const permissoes = permissoesPorRole[roleParaDuplicar];
|
|
if (!permissoes) return null;
|
|
const totalRecursos = permissoes.length;
|
|
const totalAcoes = permissoes.reduce((acc, item) => acc + item.acoes.length, 0);
|
|
return { totalRecursos, totalAcoes };
|
|
});
|
|
|
|
function abrirModalNovoPerfil() {
|
|
nomeNovoPerfil = '';
|
|
descricaoNovoPerfil = '';
|
|
setorNovoPerfil = '';
|
|
nivelNovoPerfil = 3;
|
|
roleParaDuplicar = '';
|
|
modalNovoPerfilAberto = true;
|
|
}
|
|
|
|
function fecharModalNovoPerfil() {
|
|
modalNovoPerfilAberto = false;
|
|
}
|
|
|
|
async function criarNovoPerfil() {
|
|
if (!podeSalvarNovoPerfil) return;
|
|
|
|
const nome = nomeNovoPerfil.trim();
|
|
const descricao = descricaoNovoPerfil.trim();
|
|
const setor = setorNovoPerfil.trim();
|
|
const nivel = Math.min(Math.max(Math.round(Number(nivelNovoPerfil)), 0), 10);
|
|
|
|
criandoNovoPerfil = true;
|
|
try {
|
|
const resultado = await client.mutation(api.roles.criar, {
|
|
nome,
|
|
descricao,
|
|
nivel,
|
|
setor: setor.length > 0 ? setor : undefined,
|
|
copiarDeRoleId: roleParaDuplicar || undefined
|
|
});
|
|
|
|
if (resultado.sucesso) {
|
|
mostrarMensagem('success', 'Perfil criado com sucesso!');
|
|
modalNovoPerfilAberto = false;
|
|
} else {
|
|
const mapaErros: Record<string, string> = {
|
|
nome_ja_utilizado:
|
|
'Já existe um perfil com este identificador. Ajuste o nome e tente novamente.',
|
|
nome_invalido: 'Informe um nome válido com pelo menos 3 caracteres.',
|
|
sem_permissao: 'Você não possui permissão para criar novos perfis.',
|
|
role_origem_nao_encontrada: 'Não foi possível carregar o perfil escolhido como base.'
|
|
};
|
|
const mensagemErro =
|
|
mapaErros[resultado.erro] ?? 'Não foi possível criar o perfil. Tente novamente.';
|
|
mostrarMensagem('error', mensagemErro);
|
|
}
|
|
} catch (error: unknown) {
|
|
const mensagemErro =
|
|
error instanceof Error ? error.message : 'Erro inesperado ao criar o perfil.';
|
|
mostrarMensagem('error', mensagemErro);
|
|
} finally {
|
|
criandoNovoPerfil = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<ProtectedRoute allowedRoles={['ti_master', 'admin']} maxLevel={1}>
|
|
<!-- Breadcrumb -->
|
|
<div class="breadcrumbs mb-4 text-sm">
|
|
<ul>
|
|
<li>
|
|
<a href={resolve('/')} class="text-primary hover:text-primary-focus">
|
|
<Home class="h-4 w-4" strokeWidth={2} />
|
|
Dashboard
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href={resolve('/ti')} class="text-primary hover:text-primary-focus">TI</a>
|
|
</li>
|
|
<li class="font-semibold">Gerenciar Perfis & Permissões</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="mb-2 flex flex-wrap items-center gap-3">
|
|
<div class="bg-primary/10 rounded-xl p-3">
|
|
<ShieldCheck class="text-primary h-8 w-8" strokeWidth={2} />
|
|
</div>
|
|
<div class="flex-1">
|
|
<h1 class="text-base-content text-3xl font-bold">
|
|
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={abrirModalNovoPerfil}>
|
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
|
Criar novo perfil
|
|
</button>
|
|
<button class="btn gap-2" onclick={() => goto(resolve('/ti'))}>
|
|
<ArrowLeft class="h-5 w-5" strokeWidth={2} />
|
|
Voltar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alertas -->
|
|
{#if mensagem}
|
|
<div
|
|
class="alert mb-6 shadow-lg"
|
|
class:alert-success={mensagem.tipo === 'success'}
|
|
class:alert-error={mensagem.tipo === 'error'}
|
|
>
|
|
{#if mensagem.tipo === 'success'}
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{:else}
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{/if}
|
|
<span class="font-semibold">{mensagem.texto}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Filtros e Busca -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<!-- Busca por menu -->
|
|
<div class="form-control">
|
|
<label class="label" for="busca">
|
|
<span class="label-text font-semibold">Buscar Perfil</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
id="busca"
|
|
type="text"
|
|
placeholder="Digite o nome/descrição do perfil..."
|
|
class="input input-bordered w-full pr-10"
|
|
bind:value={busca}
|
|
/>
|
|
<Search class="text-base-content/40 absolute top-3.5 right-3 h-5 w-5" strokeWidth={2} />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtro por perfil -->
|
|
<div class="form-control">
|
|
<label class="label" for="filtroRole">
|
|
<span class="label-text font-semibold">Filtrar por Perfil</span>
|
|
</label>
|
|
<select id="filtroRole" class="select select-bordered w-full" bind:value={filtroRole}>
|
|
<option value="">Todos os perfis</option>
|
|
{#if rolesQuery.data}
|
|
{#each rolesQuery.data as roleRow (roleRow._id)}
|
|
<option value={roleRow._id}>
|
|
{roleRow.descricao} ({roleRow.nome})
|
|
</option>
|
|
{/each}
|
|
{/if}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{#if busca || filtroRole}
|
|
<div class="mt-2 flex items-center gap-2">
|
|
<span class="text-base-content/60 text-sm">Filtros ativos:</span>
|
|
{#if busca}
|
|
<div class="badge badge-primary gap-2">
|
|
Busca: {busca}
|
|
<button class="btn btn-xs" onclick={() => (busca = '')} aria-label="Limpar busca">
|
|
✕
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{#if filtroRole}
|
|
<div class="badge badge-secondary gap-2">
|
|
Perfil filtrado
|
|
<button
|
|
class="btn btn-xs"
|
|
onclick={() => (filtroRole = '')}
|
|
aria-label="Limpar filtro"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Matriz de Permissões por Ação -->
|
|
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
|
|
<div class="flex items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
{:else if rolesQuery.error}
|
|
<div class="alert alert-error">
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<span>Erro ao carregar perfis: {rolesQuery.error.message}</span>
|
|
</div>
|
|
{:else if rolesQuery.data && catalogoQuery.data}
|
|
{#if rolesFiltradas.length === 0}
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body items-center text-center">
|
|
<Search class="text-base-content/30 h-16 w-16" strokeWidth={2} />
|
|
<h3 class="mt-4 text-xl font-bold">Nenhum resultado encontrado</h3>
|
|
<p class="text-base-content/60">
|
|
{busca
|
|
? `Não foram encontrados perfis com "${busca}"`
|
|
: 'Nenhum perfil corresponde aos filtros aplicados'}
|
|
</p>
|
|
<button
|
|
class="btn btn-primary btn-sm mt-4"
|
|
onclick={() => {
|
|
busca = '';
|
|
filtroRole = '';
|
|
}}
|
|
>
|
|
Limpar Filtros
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#each rolesFiltradas as roleRow (roleRow._id)}
|
|
{@const roleId = roleRow._id}
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="mb-4 flex flex-wrap items-center gap-4">
|
|
<div class="min-w-[200px] flex-1">
|
|
<div class="mb-2 flex items-center gap-3">
|
|
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
|
|
<div class="badge badge-lg badge-primary">
|
|
Nível {roleRow.nivel}
|
|
</div>
|
|
{#if roleRow.nivel <= 1}
|
|
<div class="badge badge-lg badge-success gap-1">
|
|
<CheckCircle class="h-4 w-4" strokeWidth={2} />
|
|
Acesso Total
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<p class="text-base-content/60 text-sm">
|
|
<span class="bg-base-200 rounded px-2 py-1 font-mono">{roleRow.nome}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{#if roleRow.nivel <= 1}
|
|
<div class="alert alert-success shadow-md">
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div>
|
|
<h3 class="font-bold">Perfil Administrativo</h3>
|
|
<div class="text-sm">
|
|
Este perfil possui acesso total ao sistema automaticamente, sem necessidade de
|
|
configuração manual.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if catalogoQuery.data && catalogoQuery.data.length > 0}
|
|
<div class="space-y-2">
|
|
{#each catalogoQuery.data as item (item.recurso)}
|
|
{@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)}
|
|
<div class="border-base-300 overflow-hidden rounded-lg border">
|
|
<!-- Cabeçalho do recurso (clicável) -->
|
|
<button
|
|
type="button"
|
|
class="bg-base-200 hover:bg-base-300 flex w-full items-center justify-between px-4 py-3 transition-colors"
|
|
onclick={() => toggleRecurso(roleId, item.recurso)}
|
|
disabled={salvando}
|
|
>
|
|
<span class="text-lg font-semibold">{item.recurso}</span>
|
|
<ChevronDown
|
|
class="h-5 w-5 transition-transform {recursoExpandido ? 'rotate-180' : ''}"
|
|
strokeWidth={2}
|
|
/>
|
|
</button>
|
|
|
|
<!-- Lista de ações (visível quando expandido) -->
|
|
{#if recursoExpandido}
|
|
<div class="bg-base-100 border-base-300 border-t px-4 py-3">
|
|
{#if item.acoes.length === 0}
|
|
<p class="text-base-content/60 text-sm">
|
|
Nenhuma permissão cadastrada para este recurso.
|
|
</p>
|
|
{:else}
|
|
<div class="space-y-2">
|
|
{#each item.acoes as acao (acao)}
|
|
<label
|
|
class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-primary"
|
|
checked={isConcedida(roleId, item.recurso, acao)}
|
|
disabled={salvando}
|
|
onchange={(e) =>
|
|
toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)}
|
|
/>
|
|
<span class="flex-1 font-medium">{acao}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="alert alert-info mt-4">
|
|
<span class="font-semibold">
|
|
Nenhuma permissão cadastrada ainda. Use o botão “Criar permissão” para começar.
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if modalNovoPerfilAberto}
|
|
<dialog class="modal modal-open">
|
|
<div
|
|
class="modal-box bg-base-100 w-full max-w-4xl overflow-hidden rounded-2xl p-0 shadow-2xl"
|
|
>
|
|
<div
|
|
class="from-primary via-primary/85 to-secondary/80 text-base-100 relative bg-linear-to-r px-8 py-6"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-circle btn-ghost btn-sm text-base-100/80 hover:text-base-100 absolute top-4 right-4"
|
|
onclick={fecharModalNovoPerfil}
|
|
aria-label="Fechar"
|
|
>
|
|
✕
|
|
</button>
|
|
<div class="space-y-2 pr-10">
|
|
<h3 class="text-3xl font-black tracking-tight">Criar novo perfil de acesso</h3>
|
|
<p class="text-base-100/80 max-w-2xl text-sm md:text-base">
|
|
Defina as informações do perfil e, se desejar, reutilize as permissões de um perfil
|
|
existente para acelerar a configuração.
|
|
</p>
|
|
{#if identificadorSugerido}
|
|
<div class="text-base-100/80 flex flex-wrap items-center gap-2 text-xs">
|
|
<span
|
|
class="badge badge-outline badge-sm border-base-100/40 tracking-widest uppercase"
|
|
>
|
|
Identificador
|
|
</span>
|
|
<span class="font-mono text-sm">{identificadorSugerido}</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div class="space-y-6 px-8 py-6">
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label" for="nome-novo-perfil">
|
|
<span class="label-text font-semibold">Nome do perfil *</span>
|
|
</label>
|
|
<input
|
|
id="nome-novo-perfil"
|
|
type="text"
|
|
class="input input-bordered input-primary"
|
|
placeholder="Ex.: Financeiro, RH, Supervisão de Campo"
|
|
bind:value={nomeNovoPerfil}
|
|
maxlength={60}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label" for="descricao-novo-perfil">
|
|
<span class="label-text font-semibold">Descrição</span>
|
|
</label>
|
|
<textarea
|
|
id="descricao-novo-perfil"
|
|
class="textarea textarea-bordered textarea-primary min-h-[96px]"
|
|
placeholder="Explique como este perfil será utilizado e quais áreas ele atende."
|
|
bind:value={descricaoNovoPerfil}
|
|
maxlength={240}
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="setor-novo-perfil">
|
|
<span class="label-text font-semibold">Setor (opcional)</span>
|
|
</label>
|
|
<input
|
|
id="setor-novo-perfil"
|
|
type="text"
|
|
class="input input-bordered"
|
|
placeholder="Informe o setor ou departamento responsável"
|
|
bind:value={setorNovoPerfil}
|
|
maxlength={40}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="nivel-novo-perfil">
|
|
<span class="label-text font-semibold">Nível de acesso (0 a 10)</span>
|
|
</label>
|
|
<input
|
|
id="nivel-novo-perfil"
|
|
type="number"
|
|
class="input input-bordered"
|
|
min={0}
|
|
max={10}
|
|
step={1}
|
|
bind:value={nivelNovoPerfil}
|
|
/>
|
|
<span class="text-base-content/60 mt-1 text-xs">
|
|
Níveis menores representam maior privilégio (ex.: 0 = administrativo).
|
|
</span>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label" for="duplicar-novo-perfil">
|
|
<span class="label-text font-semibold">Duplicar permissões de</span>
|
|
</label>
|
|
<select
|
|
id="duplicar-novo-perfil"
|
|
class="select select-bordered"
|
|
bind:value={roleParaDuplicar}
|
|
>
|
|
<option value="">Iniciar perfil vazio</option>
|
|
{#if rolesQuery.data}
|
|
{#each rolesQuery.data as roleDuplicavel (roleDuplicavel._id)}
|
|
<option value={roleDuplicavel._id}>
|
|
{roleDuplicavel.descricao} — nível {roleDuplicavel.nivel}
|
|
</option>
|
|
{/each}
|
|
{/if}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{#if roleDuplicacaoSelecionada}
|
|
<div class="border-base-200 bg-base-100/80 rounded-2xl border p-4">
|
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
<div>
|
|
<p class="text-base-content font-semibold">
|
|
Permissões a serem copiadas de <span class="font-bold"
|
|
>{roleDuplicacaoSelecionada.descricao}</span
|
|
>
|
|
</p>
|
|
<p class="text-base-content/70 text-sm">
|
|
As permissões atuais serão reaproveitadas automaticamente para o novo perfil.
|
|
</p>
|
|
</div>
|
|
{#if resumoPermissoesDuplicacao}
|
|
<div class="flex items-center gap-4 text-sm">
|
|
<span class="badge badge-outline badge-sm">
|
|
{resumoPermissoesDuplicacao.totalRecursos} recursos
|
|
</span>
|
|
<span class="badge badge-outline badge-sm">
|
|
{resumoPermissoesDuplicacao.totalAcoes} ações
|
|
</span>
|
|
</div>
|
|
{:else}
|
|
<span class="loading loading-dots loading-sm text-primary"></span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p class="text-base-content/60 text-xs">
|
|
Campos marcados com * são obrigatórios. As alterações entram em vigor imediatamente
|
|
após a criação.
|
|
</p>
|
|
<div class="flex flex-wrap gap-3">
|
|
<button
|
|
type="button"
|
|
class="btn"
|
|
onclick={fecharModalNovoPerfil}
|
|
disabled={criandoNovoPerfil}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
disabled={!podeSalvarNovoPerfil}
|
|
onclick={criarNovoPerfil}
|
|
>
|
|
{#if criandoNovoPerfil}
|
|
<span class="loading loading-spinner"></span>
|
|
Criando perfil...
|
|
{:else}
|
|
Salvar perfil
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button type="button" onclick={fecharModalNovoPerfil} aria-label="Fechar modal">
|
|
fechar
|
|
</button>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|
|
</ProtectedRoute>
|