refactor: Remove dedicated role management page and update authentication, roles, and permission handling across backend and frontend.
This commit is contained in:
@@ -19,11 +19,16 @@
|
||||
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);
|
||||
|
||||
// Estado para modal de edição
|
||||
let modalEditarPerfilAberto = $state(false);
|
||||
let roleParaEditar = $state<Id<'roles'> | null>(null);
|
||||
let descricaoEditarPerfil = $state('');
|
||||
let editandoPerfil = $state(false);
|
||||
let excluindoPerfil = $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({});
|
||||
@@ -74,13 +79,81 @@
|
||||
$effect(() => {
|
||||
if (rolesFiltradas && catalogoQuery.data) {
|
||||
for (const roleRow of rolesFiltradas) {
|
||||
if (roleRow.nivel > 1) {
|
||||
if (roleRow.admin !== true) {
|
||||
carregarPermissoesRole(roleRow._id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function abrirModalEditar(role: { _id: Id<'roles'>; nome: string; descricao: string }) {
|
||||
roleParaEditar = role._id;
|
||||
descricaoEditarPerfil = role.descricao;
|
||||
modalEditarPerfilAberto = true;
|
||||
}
|
||||
|
||||
function fecharModalEditar() {
|
||||
modalEditarPerfilAberto = false;
|
||||
roleParaEditar = null;
|
||||
}
|
||||
|
||||
async function editarPerfil() {
|
||||
if (!roleParaEditar) return;
|
||||
|
||||
editandoPerfil = true;
|
||||
try {
|
||||
const resultado = await client.mutation(api.roles.editar, {
|
||||
roleId: roleParaEditar,
|
||||
descricao: descricaoEditarPerfil.trim()
|
||||
});
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem('success', 'Perfil atualizado com sucesso!');
|
||||
fecharModalEditar();
|
||||
} else {
|
||||
const mapaErros: Record<string, string> = {
|
||||
nome_ja_utilizado: 'Já existe um perfil com este identificador.',
|
||||
nome_invalido: 'Informe um nome válido com pelo menos 3 caracteres.',
|
||||
sem_permissao: 'Você não possui permissão para editar perfis.',
|
||||
role_nao_encontrada: 'Perfil não encontrado.'
|
||||
};
|
||||
mostrarMensagem('error', mapaErros[resultado.erro] ?? resultado.erro);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const mensagemErro = error instanceof Error ? error.message : 'Erro ao editar perfil.';
|
||||
mostrarMensagem('error', mensagemErro);
|
||||
} finally {
|
||||
editandoPerfil = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function excluirPerfil(roleId: Id<'roles'>) {
|
||||
if (!confirm('Tem certeza que deseja excluir este perfil? Esta ação não pode ser desfeita.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
excluindoPerfil = true;
|
||||
try {
|
||||
const resultado = await client.mutation(api.roles.excluir, { roleId });
|
||||
|
||||
if (resultado.sucesso) {
|
||||
mostrarMensagem('success', 'Perfil excluído com sucesso!');
|
||||
} else {
|
||||
const mapaErros: Record<string, string> = {
|
||||
sem_permissao: 'Você não possui permissão para excluir perfis.',
|
||||
role_nao_encontrada: 'Perfil não encontrado.',
|
||||
role_possui_usuarios: 'Não é possível excluir um perfil que possui usuários vinculados.'
|
||||
};
|
||||
mostrarMensagem('error', mapaErros[resultado.erro] ?? resultado.erro);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const mensagemErro = error instanceof Error ? error.message : 'Erro ao excluir perfil.';
|
||||
mostrarMensagem('error', mensagemErro);
|
||||
} finally {
|
||||
excluindoPerfil = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAcao(roleId: Id<'roles'>, recurso: string, acao: string, conceder: boolean) {
|
||||
try {
|
||||
salvando = true;
|
||||
@@ -130,9 +203,7 @@
|
||||
|
||||
let 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;
|
||||
return nome.length >= 3 && !criandoNovoPerfil;
|
||||
});
|
||||
|
||||
let roleDuplicacaoSelecionada = $derived.by(() => {
|
||||
@@ -158,8 +229,6 @@
|
||||
function abrirModalNovoPerfil() {
|
||||
nomeNovoPerfil = '';
|
||||
descricaoNovoPerfil = '';
|
||||
setorNovoPerfil = '';
|
||||
nivelNovoPerfil = 3;
|
||||
roleParaDuplicar = '';
|
||||
modalNovoPerfilAberto = true;
|
||||
}
|
||||
@@ -173,16 +242,12 @@
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
@@ -211,7 +276,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin']} maxLevel={1}>
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin']}>
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
<ul>
|
||||
@@ -492,10 +557,8 @@
|
||||
<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}
|
||||
{#if roleRow.admin}
|
||||
<div class="badge badge-lg badge-error">Admin</div>
|
||||
<div class="badge badge-lg badge-success gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -513,15 +576,63 @@
|
||||
</svg>
|
||||
Acesso Total
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-lg badge-info">Usuário</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 class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => abrirModalEditar(roleRow)}
|
||||
disabled={salvando}
|
||||
aria-label="Editar perfil"
|
||||
>
|
||||
<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-ghost text-error"
|
||||
onclick={() => excluirPerfil(roleRow._id)}
|
||||
disabled={salvando || excluindoPerfil}
|
||||
aria-label="Excluir perfil"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{#if roleRow.nivel <= 1}
|
||||
{#if roleRow.admin}
|
||||
<div class="alert alert-success shadow-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -680,38 +791,6 @@
|
||||
></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>
|
||||
@@ -725,7 +804,8 @@
|
||||
{#if rolesQuery.data}
|
||||
{#each rolesQuery.data as roleDuplicavel (roleDuplicavel._id)}
|
||||
<option value={roleDuplicavel._id}>
|
||||
{roleDuplicavel.descricao} — nível {roleDuplicavel.nivel}
|
||||
{roleDuplicavel.descricao}
|
||||
{roleDuplicavel.admin ? '(Admin)' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -800,4 +880,66 @@
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
{#if modalEditarPerfilAberto}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box bg-base-100 w-full max-w-lg 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={fecharModalEditar}
|
||||
aria-label="Fechar"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div class="space-y-2 pr-10">
|
||||
<h3 class="text-2xl font-black tracking-tight">Editar perfil de acesso</h3>
|
||||
<p class="text-base-100/80 text-sm">Altere as informações do perfil selecionado.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6 px-8 py-6">
|
||||
<div class="form-control">
|
||||
<label class="label" for="descricao-editar-perfil">
|
||||
<span class="label-text font-semibold">Nome do perfil *</span>
|
||||
</label>
|
||||
<input
|
||||
id="descricao-editar-perfil"
|
||||
type="text"
|
||||
class="input input-bordered input-primary"
|
||||
placeholder="Ex.: Financeiro, RH, Supervisão de Campo"
|
||||
bind:value={descricaoEditarPerfil}
|
||||
maxlength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-3">
|
||||
<button type="button" class="btn" onclick={fecharModalEditar} disabled={editandoPerfil}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
disabled={editandoPerfil || descricaoEditarPerfil.trim().length < 3}
|
||||
onclick={editarPerfil}
|
||||
>
|
||||
{#if editandoPerfil}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Salvando...
|
||||
{:else}
|
||||
Salvar alterações
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={fecharModalEditar} aria-label="Fechar modal">
|
||||
fechar
|
||||
</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
@@ -1,786 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { AlertTriangle, Building2, Info, Shield, Users } from 'lucide-svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||
import StatsCard from '$lib/components/ti/StatsCard.svelte';
|
||||
|
||||
type Role = {
|
||||
_id: Id<'roles'>;
|
||||
_creationTime: number;
|
||||
nome: string;
|
||||
descricao: string;
|
||||
nivel: number;
|
||||
setor?: string;
|
||||
};
|
||||
|
||||
const client = useConvexClient();
|
||||
const rolesQuery = useQuery(api.roles.listar, {});
|
||||
let roles = $derived(rolesQuery?.data ?? []);
|
||||
let carregando = $derived(rolesQuery === undefined);
|
||||
|
||||
let busca = $state('');
|
||||
let filtroSetor = $state('');
|
||||
let filtroNivel = $state<number | ''>('');
|
||||
let roleSelecionada = $state<Role | null>(null);
|
||||
let modalDetalhesAberto = $state(false);
|
||||
|
||||
let setoresDisponiveis = $derived.by(() => {
|
||||
const setores = new Set<string>();
|
||||
roles.forEach((r) => {
|
||||
if (r.setor) setores.add(r.setor);
|
||||
});
|
||||
return Array.from(setores).sort();
|
||||
});
|
||||
|
||||
// Estatísticas
|
||||
let stats = $derived.by(() => {
|
||||
if (carregando) return null;
|
||||
|
||||
const nivelMaximo = roles.filter((r) => r.nivel === 0).length;
|
||||
const nivelAdministrativo = roles.filter((r) => r.nivel === 1).length;
|
||||
const niveisLegado = roles.filter((r) => r.nivel > 1).length;
|
||||
|
||||
return {
|
||||
total: roles.length,
|
||||
nivelMaximo,
|
||||
nivelAdministrativo,
|
||||
niveisLegado,
|
||||
comSetor: roles.filter((r) => r.setor).length
|
||||
};
|
||||
});
|
||||
|
||||
let rolesFiltradas = $derived.by(() => {
|
||||
let resultado = roles;
|
||||
|
||||
// Filtro por busca (nome ou descrição)
|
||||
if (busca.trim()) {
|
||||
const buscaLower = busca.toLowerCase();
|
||||
resultado = resultado.filter(
|
||||
(r) =>
|
||||
r.nome.toLowerCase().includes(buscaLower) ||
|
||||
r.descricao.toLowerCase().includes(buscaLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro por setor
|
||||
if (filtroSetor) {
|
||||
resultado = resultado.filter((r) => r.setor === filtroSetor);
|
||||
}
|
||||
|
||||
// Filtro por nível
|
||||
if (filtroNivel !== '') {
|
||||
if (filtroNivel === 0 || filtroNivel === 1) {
|
||||
resultado = resultado.filter((r) => r.nivel === filtroNivel);
|
||||
} else {
|
||||
// Qualquer outro valor é considerado legado
|
||||
resultado = resultado.filter((r) => r.nivel > 1);
|
||||
}
|
||||
}
|
||||
|
||||
return resultado.sort((a, b) => {
|
||||
// Ordenar por nível primeiro (menor nível = maior privilégio)
|
||||
if (a.nivel !== b.nivel) return a.nivel - b.nivel;
|
||||
// Depois por nome
|
||||
return a.nome.localeCompare(b.nome);
|
||||
});
|
||||
});
|
||||
|
||||
function obterCorNivel(nivel: number): string {
|
||||
if (nivel === 0) return 'badge-error';
|
||||
if (nivel === 1) return 'badge-warning';
|
||||
// Níveis > 1 são considerados legado
|
||||
return 'badge-ghost';
|
||||
}
|
||||
|
||||
function obterTextoNivel(nivel: number): string {
|
||||
if (nivel === 0) return 'Máximo';
|
||||
if (nivel === 1) return 'Administrativo';
|
||||
return `Legado (${nivel})`;
|
||||
}
|
||||
|
||||
function obterCorCardNivel(nivel: number): string {
|
||||
if (nivel === 0) return 'border-l-4 border-error';
|
||||
if (nivel === 1) return 'border-l-4 border-warning';
|
||||
return 'border-l-4 border-base-300';
|
||||
}
|
||||
|
||||
function abrirDetalhes(role: Role) {
|
||||
roleSelecionada = role;
|
||||
modalDetalhesAberto = true;
|
||||
}
|
||||
|
||||
function fecharDetalhes() {
|
||||
modalDetalhesAberto = false;
|
||||
roleSelecionada = null;
|
||||
}
|
||||
|
||||
function formatarData(timestamp: number): string {
|
||||
try {
|
||||
return format(new Date(timestamp), 'dd/MM/yyyy HH:mm', { locale: ptBR });
|
||||
} catch {
|
||||
return 'Data inválida';
|
||||
}
|
||||
}
|
||||
|
||||
function limparFiltros() {
|
||||
busca = '';
|
||||
filtroSetor = '';
|
||||
filtroNivel = '';
|
||||
}
|
||||
|
||||
let temFiltrosAtivos = $derived(
|
||||
busca.trim() !== '' || filtroSetor !== '' || filtroNivel !== ''
|
||||
);
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="bg-primary/10 rounded-xl p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary 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 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>
|
||||
<div>
|
||||
<h1 class="text-base-content text-3xl font-bold">Gestão de Perfis</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Visualize e gerencie os perfis de acesso do sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estatísticas -->
|
||||
{#if stats}
|
||||
<div class="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatsCard title="Total de Perfis" value={stats.total} Icon={Users} color="primary" />
|
||||
<StatsCard
|
||||
title="Nível Máximo (0)"
|
||||
value={stats.nivelMaximo}
|
||||
description="Acesso total ao sistema"
|
||||
Icon={Shield}
|
||||
color="error"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Nível Administrativo (1)"
|
||||
value={stats.nivelAdministrativo}
|
||||
description="Perfis administrativos com acesso total"
|
||||
Icon={AlertTriangle}
|
||||
color="warning"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Perfis com Setor"
|
||||
value={stats.comSetor}
|
||||
description={stats.total > 0
|
||||
? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total'
|
||||
: '0%'}
|
||||
Icon={Building2}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
{#if stats.niveisLegado > 0}
|
||||
<div class="alert alert-warning mb-6">
|
||||
<Info class="h-5 w-5" />
|
||||
<div>
|
||||
<h3 class="font-bold">Perfis com níveis legados</h3>
|
||||
<p class="text-sm">
|
||||
Existem {stats.niveisLegado} perfis com nível acima de 1. Esses perfis continuarão sendo
|
||||
tratados como nível 1 (administrativo) após a migração.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
{#if !carregando && roles.length > 0}
|
||||
<div class="card bg-base-100 border-base-300 mb-6 border shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary 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 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 class="card-title text-lg">Filtros de Busca</h2>
|
||||
</div>
|
||||
{#if temFiltrosAtivos}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-error"
|
||||
onclick={limparFiltros}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
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>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<!-- Busca -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="busca">
|
||||
<span class="label-text font-medium">Buscar</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="busca"
|
||||
type="text"
|
||||
bind:value={busca}
|
||||
placeholder="Buscar por nome ou descrição..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setor -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-setor">
|
||||
<span class="label-text font-medium">Setor</span>
|
||||
</label>
|
||||
<select id="filtro-setor" bind:value={filtroSetor} class="select select-bordered">
|
||||
<option value="">Todos os setores</option>
|
||||
{#each setoresDisponiveis as setor}
|
||||
<option value={setor}>{setor}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Nível -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="filtro-nivel">
|
||||
<span class="label-text font-medium">Nível de Acesso</span>
|
||||
</label>
|
||||
<select id="filtro-nivel" bind:value={filtroNivel} class="select select-bordered">
|
||||
<option value="">Todos os níveis</option>
|
||||
<option value={0}>Máximo (0)</option>
|
||||
<option value={1}>Alto (1)</option>
|
||||
<option value={2}>Médio (2)</option>
|
||||
<option value={3}>Baixo (3+)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="text-base-content/60 text-sm">
|
||||
<span class="text-base-content font-medium">{rolesFiltradas.length}</span>
|
||||
de
|
||||
<span class="text-base-content font-medium">{roles.length}</span>
|
||||
perfil(is)
|
||||
{#if temFiltrosAtivos}
|
||||
<span class="badge badge-primary badge-sm ml-2">Filtrado</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Lista de Perfis -->
|
||||
{#if carregando}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if roles.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/30 h-16 w-16"
|
||||
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>
|
||||
<h3 class="mt-4 text-xl font-semibold">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Não há perfis cadastrados no sistema.</p>
|
||||
</div>
|
||||
{:else if rolesFiltradas.length === 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/30 mb-4 h-16 w-16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-xl font-semibold">Nenhum perfil encontrado</h3>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Nenhum perfil corresponde aos filtros aplicados.
|
||||
</p>
|
||||
{#if temFiltrosAtivos}
|
||||
<button class="btn btn-primary btn-sm mt-4" onclick={limparFiltros}>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each rolesFiltradas as role}
|
||||
<div
|
||||
class="card bg-base-100 border-base-300 cursor-pointer border shadow-xl transition-all duration-300 hover:shadow-2xl {obterCorCardNivel(
|
||||
role.nivel
|
||||
)} hover:scale-[1.02]"
|
||||
onclick={() => abrirDetalhes(role)}
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h2 class="card-title mb-1 text-lg">{role.descricao}</h2>
|
||||
<div class="badge {obterCorNivel(role.nivel)} badge-sm">
|
||||
{obterTextoNivel(role.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="bg-base-200 flex items-center gap-2 rounded-lg p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/40 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-base-content/60 font-medium">Nome técnico:</span>
|
||||
<code class="bg-base-100 rounded px-2 py-1 font-mono text-xs">{role.nome}</code>
|
||||
</div>
|
||||
|
||||
{#if role.setor}
|
||||
<div class="bg-base-200 flex items-center gap-2 rounded-lg p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/40 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-base-content/60 font-medium">Setor:</span>
|
||||
<span class="font-medium">{role.setor}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bg-base-200 flex items-center gap-2 rounded-lg p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/40 h-4 w-4"
|
||||
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>
|
||||
<span class="text-base-content/60 font-medium">Nível:</span>
|
||||
<span class="text-lg font-bold">{role.nivel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions border-base-300 mt-4 justify-end border-t pt-4">
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-outline"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
abrirDetalhes(role);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-1 h-4 w-4"
|
||||
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>
|
||||
Ver Detalhes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal Detalhes -->
|
||||
{#if modalDetalhesAberto && roleSelecionada}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-3xl">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h3 class="text-2xl font-bold">Detalhes do Perfil</h3>
|
||||
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={fecharDetalhes}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header do Perfil -->
|
||||
<div class="card from-primary/10 to-secondary/10 border-primary/20 border bg-linear-to-r">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h2 class="mb-2 text-2xl font-bold">
|
||||
{roleSelecionada.descricao}
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
<span class="text-base-content/60 text-sm">Nível {roleSelecionada.nivel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-100 rounded-lg p-3 shadow-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informações Principais -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="card bg-base-100 border-base-300 border">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
Nome Técnico
|
||||
</span>
|
||||
</label>
|
||||
<code class="bg-base-200 mt-2 block rounded-lg px-4 py-3 font-mono text-sm"
|
||||
>{roleSelecionada.nome}</code
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 border-base-300 border">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary 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 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
Setor
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-2 text-lg font-medium">
|
||||
{#if roleSelecionada.setor}
|
||||
{roleSelecionada.setor}
|
||||
{:else}
|
||||
<span class="text-base-content/40 italic">Não especificado</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nível de Acesso -->
|
||||
<div class="card bg-base-100 border-base-300 border">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary h-5 w-5"
|
||||
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>
|
||||
Nível de Acesso
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center gap-4">
|
||||
<span class="text-4xl font-bold">{roleSelecionada.nivel}</span>
|
||||
<div class="badge {obterCorNivel(roleSelecionada.nivel)} badge-lg">
|
||||
{obterTextoNivel(roleSelecionada.nivel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<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 class="text-sm">
|
||||
{roleSelecionada.nivel === 0 &&
|
||||
'Acesso total irrestrito ao sistema. Pode realizar todas as operações sem restrições.'}
|
||||
{roleSelecionada.nivel === 1 &&
|
||||
'Acesso alto com algumas restrições. Pode realizar a maioria das operações administrativas.'}
|
||||
{roleSelecionada.nivel === 2 &&
|
||||
'Acesso médio com permissões configuráveis. Pode realizar operações padrão do sistema.'}
|
||||
{roleSelecionada.nivel >= 3 &&
|
||||
'Acesso limitado com permissões específicas. Operações restritas conforme configuração.'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data de Criação -->
|
||||
<div class="card bg-base-100 border-base-300 border">
|
||||
<div class="card-body">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary 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 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Data de Criação
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-2 text-lg font-medium">
|
||||
{formatarData(roleSelecionada._creationTime)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre Permissões -->
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<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>
|
||||
<div>
|
||||
<h4 class="mb-1 font-semibold">Configuração de Permissões</h4>
|
||||
<p class="text-sm">
|
||||
Para configurar permissões específicas deste perfil, acesse o <a
|
||||
href={resolve('/ti/painel-permissoes')}
|
||||
class="link link-primary font-semibold">Painel de Permissões</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button type="button" class="btn" onclick={fecharDetalhes}> Fechar </button>
|
||||
<a href={resolve('/ti/painel-permissoes')} class="btn btn-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-2 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.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Configurar Permissões
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={fecharDetalhes}></div>
|
||||
</div>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
@@ -21,13 +21,9 @@
|
||||
role: {
|
||||
_id: Id<'roles'>;
|
||||
_creationTime?: number;
|
||||
criadoPor?: Id<'usuarios'>;
|
||||
customizado?: boolean;
|
||||
descricao: string;
|
||||
editavel?: boolean;
|
||||
nome: string;
|
||||
nivel: number;
|
||||
setor?: string;
|
||||
admin?: boolean;
|
||||
erro?: boolean;
|
||||
};
|
||||
funcionario?: {
|
||||
@@ -278,7 +274,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']}>
|
||||
<main class="container mx-auto max-w-7xl px-4 py-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
type RoleUsuario = {
|
||||
_id: Id<'roles'>;
|
||||
nome: string;
|
||||
nivel: number;
|
||||
admin?: boolean;
|
||||
descricao: string;
|
||||
setor?: string;
|
||||
erro?: boolean;
|
||||
};
|
||||
|
||||
@@ -265,11 +264,6 @@
|
||||
resultado = resultado.filter((u) => u.matricula.toLowerCase().includes(buscaMatricula));
|
||||
}
|
||||
|
||||
// Filtro por setor
|
||||
if (filtroSetor) {
|
||||
resultado = resultado.filter((u) => u.role.setor === filtroSetor);
|
||||
}
|
||||
|
||||
// Filtro por status
|
||||
if (filtroStatus !== 'todos') {
|
||||
if (filtroStatus === 'ativo') {
|
||||
@@ -326,7 +320,6 @@
|
||||
function limparFiltros() {
|
||||
filtroNome = '';
|
||||
filtroMatricula = '';
|
||||
filtroSetor = '';
|
||||
filtroStatus = 'todos';
|
||||
filtroDataCriacaoInicio = '';
|
||||
filtroDataCriacaoFim = '';
|
||||
@@ -535,7 +528,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']}>
|
||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user