feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates

This commit is contained in:
2025-10-26 22:21:53 -03:00
parent 5dd00b63e1
commit 2c2b792b4a
48 changed files with 9513 additions and 672 deletions

View File

@@ -0,0 +1,197 @@
<script lang="ts">
import { goto } from "$app/navigation";
</script>
<main class="container mx-auto px-4 py-4">
<h1 class="text-3xl font-bold text-primary mb-6">Tecnologia da Informação</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Card Painel Administrativo -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
</div>
<h2 class="card-title text-xl">Painel Administrativo</h2>
</div>
<p class="text-base-content/70 mb-4">
Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.
</p>
<div class="card-actions justify-end">
<a href="/ti/painel-administrativo" class="btn btn-primary">
Acessar Painel
</a>
</div>
</div>
</div>
<!-- Card Suporte Técnico -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
</div>
<h2 class="card-title text-xl">Suporte Técnico</h2>
</div>
<p class="text-base-content/70 mb-4">
Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.
</p>
<div class="card-actions justify-end">
<button class="btn btn-primary" disabled>
Em breve
</button>
</div>
</div>
</div>
<!-- Card Gerenciar Permissões -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-success/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-success"
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>
</div>
<h2 class="card-title text-xl">Gerenciar Permissões</h2>
</div>
<p class="text-base-content/70 mb-4">
Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados.
</p>
<div class="card-actions justify-end">
<a href="/ti/painel-permissoes" class="btn btn-success">
Configurar Permissões
</a>
</div>
</div>
</div>
<!-- Card Personalizar por Matrícula -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-info/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-info"
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>
<h2 class="card-title text-xl">Personalizar por Matrícula</h2>
</div>
<p class="text-base-content/70 mb-4">
Configure permissões específicas para usuários individuais por matrícula, sobrepondo as permissões da função.
</p>
<div class="card-actions justify-end">
<a href="/ti/personalizar-permissoes" class="btn btn-info">
Personalizar Acessos
</a>
</div>
</div>
</div>
<!-- Card Documentação -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-primary/20 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h2 class="card-title text-xl">Documentação</h2>
</div>
<p class="text-base-content/70 mb-4">
Manuais, guias e documentação técnica do sistema para usuários e administradores.
</p>
<div class="card-actions justify-end">
<button class="btn btn-primary" disabled>
Em breve
</button>
</div>
</div>
</div>
</div>
<div class="alert alert-info mt-8">
<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>
<div>
<h3 class="font-bold">Área Restrita</h3>
<div class="text-sm">
Esta é uma área de acesso restrito. Apenas usuários autorizados pela equipe de TI podem acessar o Painel Administrativo.
</div>
</div>
</div>
</main>

View File

@@ -0,0 +1,977 @@
<script lang="ts">
import { useConvexClient, useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { authStore } from "$lib/stores/auth.svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
const convex = useConvexClient();
// Aba ativa
let abaAtiva = $state<"solicitacoes" | "usuarios" | "logs">("solicitacoes");
// ========================================
// ABA 1: SOLICITAÇÕES DE ACESSO
// ========================================
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
let filtroStatusSolicitacao = $state<"todas" | "pendente" | "aprovado" | "rejeitado">("todas");
let filtroNomeSolicitacao = $state("");
let filtroMatriculaSolicitacao = $state("");
let modalSolicitacaoAberto = $state(false);
let solicitacaoSelecionada = $state<Id<"solicitacoesAcesso"> | null>(null);
let acaoModalSolicitacao = $state<"aprovar" | "rejeitar" | null>(null);
let observacoesSolicitacao = $state("");
let filteredSolicitacoes = $derived(() => {
if (!solicitacoesQuery.data) return [];
return solicitacoesQuery.data.filter((s: any) => {
const matchStatus = filtroStatusSolicitacao === "todas" || s.status === filtroStatusSolicitacao;
const matchNome = s.nome.toLowerCase().includes(filtroNomeSolicitacao.toLowerCase());
const matchMatricula = s.matricula.toLowerCase().includes(filtroMatriculaSolicitacao.toLowerCase());
return matchStatus && matchNome && matchMatricula;
});
});
function abrirModalSolicitacao(solicitacaoId: Id<"solicitacoesAcesso">, acao: "aprovar" | "rejeitar") {
solicitacaoSelecionada = solicitacaoId;
acaoModalSolicitacao = acao;
observacoesSolicitacao = "";
modalSolicitacaoAberto = true;
}
function fecharModalSolicitacao() {
modalSolicitacaoAberto = false;
solicitacaoSelecionada = null;
acaoModalSolicitacao = null;
observacoesSolicitacao = "";
}
async function confirmarAcaoSolicitacao() {
if (!solicitacaoSelecionada || !acaoModalSolicitacao) return;
try {
if (acaoModalSolicitacao === "aprovar") {
await convex.mutation(api.solicitacoesAcesso.aprovar, {
solicitacaoId: solicitacaoSelecionada,
observacoes: observacoesSolicitacao || undefined,
});
mostrarNotice("success", "Solicitação aprovada com sucesso!");
} else {
await convex.mutation(api.solicitacoesAcesso.rejeitar, {
solicitacaoId: solicitacaoSelecionada,
observacoes: observacoesSolicitacao || undefined,
});
mostrarNotice("success", "Solicitação rejeitada com sucesso!");
}
fecharModalSolicitacao();
} catch (error: any) {
mostrarNotice("error", error.message || "Erro ao processar solicitação.");
}
}
// ========================================
// ABA 2: GERENCIAMENTO DE USUÁRIOS
// ========================================
const usuariosQuery = useQuery(api.usuarios.listar, {});
const rolesQuery = useQuery(api.roles.listar, {});
let filtroNomeUsuario = $state("");
let filtroMatriculaUsuario = $state("");
let filtroRoleUsuario = $state<string>("todos");
let filtroStatusUsuario = $state<"todos" | "ativo" | "inativo">("todos");
let modalUsuarioAberto = $state(false);
let usuarioSelecionado = $state<Id<"usuarios"> | null>(null);
let acaoModalUsuario = $state<"ativar" | "desativar" | "resetar" | "alterar_role" | null>(null);
let novaRoleId = $state<Id<"roles"> | null>(null);
let filteredUsuarios = $derived(() => {
if (!usuariosQuery.data) return [];
return usuariosQuery.data.filter((u: any) => {
const matchNome = u.nome.toLowerCase().includes(filtroNomeUsuario.toLowerCase());
const matchMatricula = u.matricula.toLowerCase().includes(filtroMatriculaUsuario.toLowerCase());
const matchRole = filtroRoleUsuario === "todos" || u.role.nome === filtroRoleUsuario;
const matchStatus = filtroStatusUsuario === "todos" ||
(filtroStatusUsuario === "ativo" && u.ativo) ||
(filtroStatusUsuario === "inativo" && !u.ativo);
return matchNome && matchMatricula && matchRole && matchStatus;
});
});
function abrirModalUsuario(usuarioId: Id<"usuarios">, acao: "ativar" | "desativar" | "resetar" | "alterar_role") {
usuarioSelecionado = usuarioId;
acaoModalUsuario = acao;
// Se for alterar role, pegar a role atual do usuário
if (acao === "alterar_role" && usuariosQuery.data) {
const usuario = usuariosQuery.data.find((u: any) => u._id === usuarioId);
if (usuario) {
novaRoleId = usuario.role._id;
}
}
modalUsuarioAberto = true;
}
function fecharModalUsuario() {
modalUsuarioAberto = false;
usuarioSelecionado = null;
acaoModalUsuario = null;
novaRoleId = null;
}
async function confirmarAcaoUsuario() {
if (!usuarioSelecionado || !acaoModalUsuario) return;
try {
if (acaoModalUsuario === "ativar") {
await convex.mutation(api.usuarios.ativar, { id: usuarioSelecionado });
mostrarNotice("success", "Usuário ativado com sucesso!");
} else if (acaoModalUsuario === "desativar") {
await convex.mutation(api.usuarios.desativar, { id: usuarioSelecionado });
mostrarNotice("success", "Usuário desativado com sucesso!");
} else if (acaoModalUsuario === "resetar") {
await convex.mutation(api.usuarios.resetarSenha, {
usuarioId: usuarioSelecionado,
novaSenha: "Mudar@123"
});
mostrarNotice("success", "Senha resetada para 'Mudar@123' com sucesso!");
} else if (acaoModalUsuario === "alterar_role" && novaRoleId) {
await convex.mutation(api.usuarios.alterarRole, {
usuarioId: usuarioSelecionado,
novaRoleId: novaRoleId
});
mostrarNotice("success", "Função/Nível alterado com sucesso!");
}
fecharModalUsuario();
} catch (error: any) {
mostrarNotice("error", error.message || "Erro ao processar ação.");
}
}
// ========================================
// ABA 3: HISTÓRICO DE ACESSOS
// ========================================
const logsQuery = useQuery(api.logsAcesso.listar, { limite: 100 });
let filtroTipoLog = $state<string>("todos");
let filtroUsuarioLog = $state("");
let modalLimparLogsAberto = $state(false);
let filteredLogs = $derived(() => {
if (!logsQuery.data) return [];
return logsQuery.data.filter((log: any) => {
const matchTipo = filtroTipoLog === "todos" || log.tipo === filtroTipoLog;
const matchUsuario = !filtroUsuarioLog ||
(log.usuario && log.usuario.nome.toLowerCase().includes(filtroUsuarioLog.toLowerCase()));
return matchTipo && matchUsuario;
});
});
async function limparLogs() {
try {
await convex.mutation(api.logsAcesso.limparTodos, {});
mostrarNotice("success", "Histórico de logs limpo com sucesso!");
modalLimparLogsAberto = false;
} catch (error: any) {
mostrarNotice("error", error.message || "Erro ao limpar logs.");
}
}
// ========================================
// UTILITÁRIOS
// ========================================
let notice = $state<{ type: "success" | "error"; message: string } | null>(null);
function mostrarNotice(type: "success" | "error", message: string) {
notice = { type, message };
setTimeout(() => {
notice = null;
}, 3000);
}
function formatarData(timestamp: number): string {
return new Date(timestamp).toLocaleString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function getStatusBadgeClass(status: string): string {
switch (status) {
case "pendente":
return "badge-warning";
case "aprovado":
return "badge-success";
case "rejeitado":
return "badge-error";
default:
return "badge-ghost";
}
}
function getStatusLabel(status: string): string {
switch (status) {
case "pendente":
return "Pendente";
case "aprovado":
return "Aprovado";
case "rejeitado":
return "Rejeitado";
default:
return status;
}
}
function getTipoLogIcon(tipo: string): string {
switch (tipo) {
case "login":
return "🔓";
case "logout":
return "🔒";
case "acesso_negado":
return "⛔";
case "senha_alterada":
return "🔑";
case "sessao_expirada":
return "⏰";
default:
return "📝";
}
}
function getTipoLogLabel(tipo: string): string {
switch (tipo) {
case "login":
return "Login";
case "logout":
return "Logout";
case "acesso_negado":
return "Acesso Negado";
case "senha_alterada":
return "Senha Alterada";
case "sessao_expirada":
return "Sessão Expirada";
default:
return tipo;
}
}
</script>
<ProtectedRoute requireAuth={true} allowedRoles={["admin", "ti"]} maxLevel={1}>
<main class="container mx-auto px-4 py-4">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-3 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<h1 class="text-4xl font-bold text-primary">Painel Administrativo</h1>
</div>
<p class="text-base-content/70 text-lg">
Controle total de acesso, usuários e auditoria do sistema SGSE
</p>
</div>
{#if notice}
<div class="alert {notice.type === 'success' ? 'alert-success' : 'alert-error'} mb-6">
<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 notice.type === "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}
<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"
/>
{/if}
</svg>
<span>{notice.message}</span>
</div>
{/if}
<!-- Tabs -->
<div class="tabs tabs-boxed bg-base-200 p-2 mb-6">
<button
type="button"
class="tab {abaAtiva === 'solicitacoes' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = "solicitacoes")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Solicitações de Acesso
</button>
<button
type="button"
class="tab {abaAtiva === 'usuarios' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = "usuarios")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" 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>
Gerenciar Usuários
</button>
<button
type="button"
class="tab {abaAtiva === 'logs' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = "logs")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Histórico de Acessos
</button>
</div>
<!-- ABA 1: SOLICITAÇÕES -->
{#if abaAtiva === "solicitacoes"}
<!-- Estatísticas -->
{#if solicitacoesQuery.data}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
<div class="stat-title">Total</div>
<div class="stat-value text-primary">{solicitacoesQuery.data.length}</div>
</div>
<div class="stat bg-gradient-to-br from-warning/20 to-warning/10 shadow-lg rounded-xl border border-warning/30">
<div class="stat-title">Pendentes</div>
<div class="stat-value text-warning">
{solicitacoesQuery.data.filter((s: any) => s.status === "pendente").length}
</div>
</div>
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
<div class="stat-title">Aprovadas</div>
<div class="stat-value text-success">
{solicitacoesQuery.data.filter((s: any) => s.status === "aprovado").length}
</div>
</div>
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
<div class="stat-title">Rejeitadas</div>
<div class="stat-value text-error">
{solicitacoesQuery.data.filter((s: any) => s.status === "rejeitado").length}
</div>
</div>
</div>
{/if}
<!-- Filtros -->
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
<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="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>
Filtros
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label" for="filtro-status-solicitacao">
<span class="label-text font-semibold">Status</span>
</label>
<select
id="filtro-status-solicitacao"
class="select select-bordered select-primary w-full"
bind:value={filtroStatusSolicitacao}
>
<option value="todas">Todas</option>
<option value="pendente">Pendente</option>
<option value="aprovado">Aprovado</option>
<option value="rejeitado">Rejeitado</option>
</select>
</div>
<div class="form-control">
<label class="label" for="filtro-nome-solicitacao">
<span class="label-text font-semibold">Nome</span>
</label>
<input
id="filtro-nome-solicitacao"
type="text"
placeholder="Buscar por nome..."
class="input input-bordered input-primary w-full"
bind:value={filtroNomeSolicitacao}
/>
</div>
<div class="form-control">
<label class="label" for="filtro-matricula-solicitacao">
<span class="label-text font-semibold">Matrícula</span>
</label>
<input
id="filtro-matricula-solicitacao"
type="text"
placeholder="Buscar por matrícula..."
class="input input-bordered input-primary w-full"
bind:value={filtroMatriculaSolicitacao}
/>
</div>
</div>
</div>
</div>
<!-- Tabela de Solicitações -->
<div class="card bg-base-100 shadow-xl border border-base-300">
<div class="card-body p-0">
{#if solicitacoesQuery.isLoading}
<div class="flex justify-center items-center p-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if solicitacoesQuery.error}
<div class="alert alert-error m-4">
<span>Erro ao carregar solicitações: {solicitacoesQuery.error.message}</span>
</div>
{:else if filteredSolicitacoes().length === 0}
<div class="text-center py-12 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<p class="text-lg font-semibold">Nenhuma solicitação encontrada</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>Data</th>
<th>Nome</th>
<th>Matrícula</th>
<th>E-mail</th>
<th>Telefone</th>
<th>Status</th>
<th>Resposta</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each filteredSolicitacoes() as solicitacao (solicitacao._id)}
<tr class="hover">
<td class="font-mono text-sm">{formatarData(solicitacao.dataSolicitacao)}</td>
<td class="font-semibold">{solicitacao.nome}</td>
<td><code class="bg-base-200 px-2 py-1 rounded">{solicitacao.matricula}</code></td>
<td class="text-sm">{solicitacao.email}</td>
<td class="text-sm">{solicitacao.telefone}</td>
<td>
<span class="badge {getStatusBadgeClass(solicitacao.status)} badge-lg">
{getStatusLabel(solicitacao.status)}
</span>
</td>
<td class="font-mono text-sm">
{solicitacao.dataResposta ? formatarData(solicitacao.dataResposta) : "-"}
</td>
<td>
{#if solicitacao.status === "pendente"}
<div class="flex gap-2">
<button
class="btn btn-success btn-sm"
onclick={() => abrirModalSolicitacao(solicitacao._id, "aprovar")}
>
<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="M5 13l4 4L19 7" />
</svg>
Aprovar
</button>
<button
class="btn btn-error btn-sm"
onclick={() => abrirModalSolicitacao(solicitacao._id, "rejeitar")}
>
<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>
Rejeitar
</button>
</div>
{:else}
<span class="text-base-content/50 text-sm italic">
{solicitacao.observacoes || "Sem observações"}
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
<!-- ABA 2: USUÁRIOS -->
{#if abaAtiva === "usuarios"}
<!-- Estatísticas -->
{#if usuariosQuery.data}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
<div class="stat-title">Total de Usuários</div>
<div class="stat-value text-primary">{usuariosQuery.data.length}</div>
</div>
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
<div class="stat-title">Ativos</div>
<div class="stat-value text-success">
{usuariosQuery.data.filter((u: any) => u.ativo).length}
</div>
</div>
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
<div class="stat-title">Inativos</div>
<div class="stat-value text-error">
{usuariosQuery.data.filter((u: any) => !u.ativo).length}
</div>
</div>
</div>
{/if}
<!-- Filtros -->
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
<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="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>
Filtros
</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="form-control">
<label class="label" for="filtro-nome-usuario">
<span class="label-text font-semibold">Nome</span>
</label>
<input
id="filtro-nome-usuario"
type="text"
placeholder="Buscar por nome..."
class="input input-bordered input-primary w-full"
bind:value={filtroNomeUsuario}
/>
</div>
<div class="form-control">
<label class="label" for="filtro-matricula-usuario">
<span class="label-text font-semibold">Matrícula</span>
</label>
<input
id="filtro-matricula-usuario"
type="text"
placeholder="Buscar por matrícula..."
class="input input-bordered input-primary w-full"
bind:value={filtroMatriculaUsuario}
/>
</div>
<div class="form-control">
<label class="label" for="filtro-role-usuario">
<span class="label-text font-semibold">Função</span>
</label>
<select
id="filtro-role-usuario"
class="select select-bordered select-primary w-full"
bind:value={filtroRoleUsuario}
>
<option value="todos">Todos</option>
{#if rolesQuery.data}
{#each rolesQuery.data as role}
<option value={role.nome}>{role.nome}</option>
{/each}
{/if}
</select>
</div>
<div class="form-control">
<label class="label" for="filtro-status-usuario">
<span class="label-text font-semibold">Status</span>
</label>
<select
id="filtro-status-usuario"
class="select select-bordered select-primary w-full"
bind:value={filtroStatusUsuario}
>
<option value="todos">Todos</option>
<option value="ativo">Ativo</option>
<option value="inativo">Inativo</option>
</select>
</div>
</div>
</div>
</div>
<!-- Tabela de Usuários -->
<div class="card bg-base-100 shadow-xl border border-base-300">
<div class="card-body p-0">
{#if usuariosQuery.isLoading}
<div class="flex justify-center items-center p-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if usuariosQuery.error}
<div class="alert alert-error m-4">
<span>Erro ao carregar usuários: {usuariosQuery.error.message}</span>
</div>
{:else if filteredUsuarios().length === 0}
<div class="text-center py-12 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" 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>
<p class="text-lg font-semibold">Nenhum usuário encontrado</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>Status</th>
<th>Nome</th>
<th>Matrícula</th>
<th>E-mail</th>
<th>Função</th>
<th>Nível</th>
<th>Último Acesso</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each filteredUsuarios() as usuario (usuario._id)}
<tr class="hover">
<td>
{#if usuario.ativo}
<span class="badge badge-success badge-lg">🟢 Ativo</span>
{:else}
<span class="badge badge-error badge-lg">🔴 Inativo</span>
{/if}
</td>
<td class="font-semibold">{usuario.nome}</td>
<td><code class="bg-base-200 px-2 py-1 rounded">{usuario.matricula}</code></td>
<td class="text-sm">{usuario.email}</td>
<td>
<span class="badge badge-primary">{usuario.role.nome}</span>
</td>
<td class="text-center">
<span class="badge badge-outline">{usuario.role.nivel}</span>
</td>
<td class="font-mono text-sm">
{usuario.ultimoAcesso ? formatarData(usuario.ultimoAcesso) : "Nunca"}
</td>
<td>
<div class="dropdown dropdown-end">
<button type="button" tabindex="0" class="btn btn-ghost btn-sm">
<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 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</button>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52">
{#if usuario.ativo}
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "desativar")} class="text-error">Desativar</button></li>
{:else}
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "ativar")} class="text-success">Ativar</button></li>
{/if}
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "alterar_role")} class="text-primary">Alterar Função/Nível</button></li>
<li><button type="button" onclick={() => abrirModalUsuario(usuario._id, "resetar")}>Resetar Senha</button></li>
</ul>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
<!-- ABA 3: LOGS -->
{#if abaAtiva === "logs"}
<!-- Estatísticas -->
{#if logsQuery.data}
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<div class="stat bg-gradient-to-br from-primary/20 to-primary/10 shadow-lg rounded-xl border border-primary/30">
<div class="stat-title">Total</div>
<div class="stat-value text-primary text-3xl">{logsQuery.data.length}</div>
</div>
<div class="stat bg-gradient-to-br from-success/20 to-success/10 shadow-lg rounded-xl border border-success/30">
<div class="stat-title">Logins</div>
<div class="stat-value text-success text-3xl">
{logsQuery.data.filter((log: any) => log.tipo === "login").length}
</div>
</div>
<div class="stat bg-gradient-to-br from-info/20 to-info/10 shadow-lg rounded-xl border border-info/30">
<div class="stat-title">Logouts</div>
<div class="stat-value text-info text-3xl">
{logsQuery.data.filter((log: any) => log.tipo === "logout").length}
</div>
</div>
<div class="stat bg-gradient-to-br from-error/20 to-error/10 shadow-lg rounded-xl border border-error/30">
<div class="stat-title">Negados</div>
<div class="stat-value text-error text-3xl">
{logsQuery.data.filter((log: any) => log.tipo === "acesso_negado").length}
</div>
</div>
<div class="stat bg-gradient-to-br from-warning/20 to-warning/10 shadow-lg rounded-xl border border-warning/30">
<div class="stat-title">Outros</div>
<div class="stat-value text-warning text-3xl">
{logsQuery.data.filter((log: any) => !["login", "logout", "acesso_negado"].includes(log.tipo)).length}
</div>
</div>
</div>
{/if}
<!-- Filtros e Ações -->
<div class="card bg-base-100 shadow-xl mb-6 border border-base-300">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-lg">
<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="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>
Filtros
</h2>
<button
type="button"
class="btn btn-error btn-sm"
onclick={() => (modalLimparLogsAberto = true)}
>
<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>
Limpar Histórico
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="filtro-tipo-log">
<span class="label-text font-semibold">Tipo de Evento</span>
</label>
<select
id="filtro-tipo-log"
class="select select-bordered select-primary w-full"
bind:value={filtroTipoLog}
>
<option value="todos">Todos</option>
<option value="login">Login</option>
<option value="logout">Logout</option>
<option value="acesso_negado">Acesso Negado</option>
<option value="senha_alterada">Senha Alterada</option>
<option value="sessao_expirada">Sessão Expirada</option>
</select>
</div>
<div class="form-control">
<label class="label" for="filtro-usuario-log">
<span class="label-text font-semibold">Usuário</span>
</label>
<input
id="filtro-usuario-log"
type="text"
placeholder="Buscar por usuário..."
class="input input-bordered input-primary w-full"
bind:value={filtroUsuarioLog}
/>
</div>
</div>
</div>
</div>
<!-- Tabela de Logs -->
<div class="card bg-base-100 shadow-xl border border-base-300">
<div class="card-body p-0">
{#if logsQuery.isLoading}
<div class="flex justify-center items-center p-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if logsQuery.error}
<div class="alert alert-error m-4">
<span>Erro ao carregar logs: {logsQuery.error.message}</span>
</div>
{:else if filteredLogs().length === 0}
<div class="text-center py-12 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="text-lg font-semibold">Nenhum log encontrado</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>Data/Hora</th>
<th>Tipo</th>
<th>Usuário</th>
<th>IP</th>
<th>Detalhes</th>
</tr>
</thead>
<tbody>
{#each filteredLogs() as log (log._id)}
<tr class="hover">
<td class="font-mono text-sm">{formatarData(log.timestamp)}</td>
<td>
<span class="badge badge-lg">
{getTipoLogIcon(log.tipo)} {getTipoLogLabel(log.tipo)}
</span>
</td>
<td class="font-semibold">
{log.usuario ? log.usuario.nome : "Sistema"}
</td>
<td class="font-mono text-sm">
{log.ipAddress || "-"}
</td>
<td class="text-sm text-base-content/70">
{log.detalhes || "-"}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
<!-- Modal Solicitação -->
{#if modalSolicitacaoAberto}
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">
{acaoModalSolicitacao === "aprovar" ? "Aprovar Solicitação" : "Rejeitar Solicitação"}
</h3>
<p class="mb-4">
{acaoModalSolicitacao === "aprovar"
? "Tem certeza que deseja aprovar esta solicitação de acesso?"
: "Tem certeza que deseja rejeitar esta solicitação de acesso?"}
</p>
<div class="form-control mb-4">
<label class="label" for="observacoes-solicitacao">
<span class="label-text">Observações (opcional)</span>
</label>
<textarea
id="observacoes-solicitacao"
class="textarea textarea-bordered h-24"
placeholder="Adicione observações sobre esta decisão..."
bind:value={observacoesSolicitacao}
></textarea>
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick={fecharModalSolicitacao}>Cancelar</button>
<button
class="btn {acaoModalSolicitacao === 'aprovar' ? 'btn-success' : 'btn-error'}"
onclick={confirmarAcaoSolicitacao}
>
{acaoModalSolicitacao === "aprovar" ? "Aprovar" : "Rejeitar"}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" onclick={fecharModalSolicitacao}>
<button type="button">close</button>
</form>
</dialog>
{/if}
<!-- Modal Usuário -->
{#if modalUsuarioAberto}
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">
{acaoModalUsuario === "ativar" ? "Ativar Usuário" :
acaoModalUsuario === "desativar" ? "Desativar Usuário" :
acaoModalUsuario === "alterar_role" ? "Alterar Função/Nível" : "Resetar Senha"}
</h3>
{#if acaoModalUsuario === "alterar_role"}
<p class="mb-4">Selecione a nova função/nível para este usuário:</p>
<div class="form-control mb-4">
<label class="label" for="select-role">
<span class="label-text font-semibold">Função</span>
</label>
{#if rolesQuery.isLoading}
<div class="flex justify-center p-4">
<span class="loading loading-spinner loading-sm"></span>
</div>
{:else if rolesQuery.data && rolesQuery.data.length > 0}
<select
id="select-role"
class="select select-bordered select-primary w-full"
bind:value={novaRoleId}
>
{#each rolesQuery.data as role}
<option value={role._id}>
{role.nome} (Nível {role.nivel}){#if role.setor} - {role.setor}{/if}
</option>
{/each}
</select>
<label class="label">
<span class="label-text-alt text-base-content/60">
Quanto menor o nível, maior o acesso (0 = Admin)
</span>
</label>
{:else}
<div class="alert alert-warning">
<span>Nenhuma função disponível. Verifique se as roles foram criadas no banco de dados.</span>
</div>
{/if}
</div>
{:else}
<p class="mb-4">
{acaoModalUsuario === "ativar" ? "Tem certeza que deseja ativar este usuário?" :
acaoModalUsuario === "desativar" ? "Tem certeza que deseja desativar este usuário?" :
"Tem certeza que deseja resetar a senha deste usuário? A nova senha será 'Mudar@123'."}
</p>
{/if}
<div class="modal-action">
<button class="btn btn-ghost" onclick={fecharModalUsuario}>Cancelar</button>
<button
class="btn {acaoModalUsuario === 'ativar' ? 'btn-success' : acaoModalUsuario === 'alterar_role' ? 'btn-primary' : 'btn-error'}"
onclick={confirmarAcaoUsuario}
disabled={acaoModalUsuario === 'alterar_role' && !novaRoleId}
>
Confirmar
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" onclick={fecharModalUsuario}>
<button type="button">close</button>
</form>
</dialog>
{/if}
<!-- Modal Limpar Logs -->
{#if modalLimparLogsAberto}
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4 text-error">⚠️ Limpar Histórico de Logs</h3>
<p class="mb-4">
<strong>ATENÇÃO:</strong> Esta ação irá remover TODOS os logs de acesso do sistema.
Esta ação é <strong>IRREVERSÍVEL</strong>.
</p>
<p class="mb-4 text-base-content/70">
Tem certeza que deseja continuar?
</p>
<div class="modal-action">
<button class="btn btn-ghost" onclick={() => (modalLimparLogsAberto = false)}>Cancelar</button>
<button class="btn btn-error" onclick={limparLogs}>
<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>
Sim, Limpar Tudo
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" onclick={() => (modalLimparLogsAberto = false)}>
<button type="button">close</button>
</form>
</dialog>
{/if}
</main>
</ProtectedRoute>

View File

@@ -0,0 +1,331 @@
<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 type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
const client = useConvexClient();
// Buscar matriz de permissões
const matrizQuery = useQuery(api.menuPermissoes.obterMatrizPermissoes, {});
let salvando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
async function atualizarPermissao(
roleId: Id<"roles">,
menuPath: string,
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
valor: boolean
) {
try {
salvando = true;
// Se está marcando podeGravar, deve marcar podeConsultar e podeAcessar também
let podeAcessar = valor;
let podeConsultar = valor;
let podeGravar = campo === "podeGravar" ? valor : false;
if (campo === "podeConsultar") {
podeConsultar = valor;
podeGravar = false; // Desmarcar gravar se desmarcou consultar
}
if (campo === "podeAcessar") {
podeAcessar = valor;
if (!valor) {
podeConsultar = false;
podeGravar = false;
}
}
// Buscar a permissão atual para aplicar lógica correta
const roleData = matrizQuery.data?.find((r) => r.role._id === roleId);
const permissaoAtual = roleData?.permissoes.find((p) => p.menuPath === menuPath);
if (permissaoAtual) {
// Aplicar lógica de dependências
if (campo === "podeGravar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = true;
} else if (campo === "podeConsultar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = permissaoAtual.podeGravar;
} else if (campo === "podeAcessar" && !valor) {
podeAcessar = false;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeConsultar" && !valor) {
podeAcessar = permissaoAtual.podeAcessar;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeGravar" && !valor) {
podeAcessar = permissaoAtual.podeAcessar;
podeConsultar = permissaoAtual.podeConsultar;
podeGravar = false;
} else {
podeAcessar = permissaoAtual.podeAcessar;
podeConsultar = permissaoAtual.podeConsultar;
podeGravar = permissaoAtual.podeGravar;
}
}
await client.mutation(api.menuPermissoes.atualizarPermissao, {
roleId,
menuPath,
podeAcessar,
podeConsultar,
podeGravar,
});
mensagem = { tipo: "success", texto: "Permissão atualizada com sucesso!" };
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao atualizar permissão" };
} finally {
salvando = false;
}
}
async function inicializarPermissoes(roleId: Id<"roles">) {
try {
salvando = true;
await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId });
mensagem = { tipo: "success", texto: "Permissões inicializadas!" };
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao inicializar permissões" };
} finally {
salvando = false;
}
}
</script>
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li>
<a href="/" class="text-primary hover:text-primary-focus">
<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="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>
Dashboard
</a>
</li>
<li>
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
</li>
<li class="font-semibold">Gerenciar Permissões</li>
</ul>
</div>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-3 mb-2">
<div class="p-3 bg-primary/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" 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>
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-base-content">Gerenciar 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-ghost gap-2" onclick={() => goto("/ti")}>
<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>
</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"}
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{:else}
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
<span class="font-semibold">{mensagem.texto}</span>
</div>
{/if}
<!-- Informações sobre o sistema de permissões -->
<div class="alert alert-info mb-6">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-bold">Como funciona:</h3>
<ul class="text-sm mt-2 space-y-1">
<li><strong>Acessar:</strong> Permite visualizar o menu e entrar na página</li>
<li><strong>Consultar:</strong> Permite visualizar dados (requer "Acessar")</li>
<li><strong>Gravar:</strong> Permite criar, editar e excluir dados (requer "Consultar")</li>
<li><strong>Admin e TI:</strong> Têm acesso total automático a todos os recursos</li>
<li><strong>Dashboard e Solicitar Acesso:</strong> São públicos para todos os usuários</li>
</ul>
</div>
</div>
<!-- Matriz de Permissões -->
{#if matrizQuery.isLoading}
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if matrizQuery.error}
<div class="alert alert-error">
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Erro ao carregar permissões: {matrizQuery.error.message}</span>
</div>
{:else if matrizQuery.data}
{#each matrizQuery.data as roleData}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="card-title text-xl">
{roleData.role.nome}
<div class="badge badge-primary">Nível {roleData.role.nivel}</div>
{#if roleData.role.nivel <= 1}
<div class="badge badge-success">Acesso Total</div>
{/if}
</h2>
<p class="text-sm text-base-content/60 mt-1">{roleData.role.descricao}</p>
</div>
{#if roleData.role.nivel > 1}
<button
class="btn btn-sm btn-outline btn-primary"
onclick={() => inicializarPermissoes(roleData.role._id)}
disabled={salvando}
>
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Inicializar Permissões
</button>
{/if}
</div>
{#if roleData.role.nivel <= 1}
<div class="alert alert-success">
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Esta função possui acesso total ao sistema automaticamente.</span>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead class="bg-base-200">
<tr>
<th class="w-1/3">Menu</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="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>
Acessar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Consultar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="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>
Gravar
</div>
</th>
</tr>
</thead>
<tbody>
{#each roleData.permissoes as permissao}
<tr class="hover">
<td>
<div class="flex flex-col">
<span class="font-semibold">{permissao.menuNome}</span>
<span class="text-xs text-base-content/60">{permissao.menuPath}</span>
</div>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={permissao.podeAcessar}
disabled={salvando}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeAcessar",
e.currentTarget.checked
)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-info"
checked={permissao.podeConsultar}
disabled={salvando || !permissao.podeAcessar}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeConsultar",
e.currentTarget.checked
)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-success"
checked={permissao.podeGravar}
disabled={salvando || !permissao.podeConsultar}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeGravar",
e.currentTarget.checked
)}
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/each}
{/if}
</ProtectedRoute>

View File

@@ -0,0 +1,383 @@
<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";
const client = useConvexClient();
let matriculaBusca = $state("");
let usuarioEncontrado = $state<any>(null);
let buscando = $state(false);
let salvando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
// Buscar permissões personalizadas do usuário
const permissoesQuery = $derived(
usuarioEncontrado
? useQuery(api.menuPermissoes.listarPermissoesPersonalizadas, {
matricula: usuarioEncontrado.matricula,
})
: null
);
// Buscar menus disponíveis
const menusQuery = useQuery(api.menuPermissoes.listarMenus, {});
async function buscarUsuario() {
if (!matriculaBusca.trim()) {
mensagem = { tipo: "error", texto: "Digite uma matrícula para buscar" };
return;
}
try {
buscando = true;
const usuario = await client.query(api.menuPermissoes.buscarUsuarioPorMatricula, {
matricula: matriculaBusca.trim(),
});
if (usuario) {
usuarioEncontrado = usuario;
mensagem = null;
} else {
usuarioEncontrado = null;
mensagem = { tipo: "error", texto: "Usuário não encontrado com esta matrícula" };
}
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao buscar usuário" };
} finally {
buscando = false;
}
}
async function atualizarPermissao(
menuPath: string,
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
valor: boolean
) {
if (!usuarioEncontrado) return;
try {
salvando = true;
// Obter permissão atual do menu
const permissaoAtual = permissoesQuery?.data?.find((p) => p.menuPath === menuPath);
let podeAcessar = valor;
let podeConsultar = false;
let podeGravar = false;
// Aplicar lógica de dependências
if (campo === "podeGravar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = true;
} else if (campo === "podeConsultar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = permissaoAtual?.podeGravar || false;
} else if (campo === "podeAcessar" && !valor) {
podeAcessar = false;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeConsultar" && !valor) {
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeGravar" && !valor) {
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
podeConsultar = permissaoAtual?.podeConsultar !== undefined ? permissaoAtual.podeConsultar : false;
podeGravar = false;
} else if (permissaoAtual) {
podeAcessar = permissaoAtual.podeAcessar;
podeConsultar = permissaoAtual.podeConsultar;
podeGravar = permissaoAtual.podeGravar;
}
await client.mutation(api.menuPermissoes.atualizarPermissaoPersonalizada, {
matricula: usuarioEncontrado.matricula,
menuPath,
podeAcessar,
podeConsultar,
podeGravar,
});
mensagem = { tipo: "success", texto: "Permissão personalizada atualizada!" };
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao atualizar permissão" };
} finally {
salvando = false;
}
}
function limparBusca() {
matriculaBusca = "";
usuarioEncontrado = null;
mensagem = null;
}
</script>
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li>
<a href="/" class="text-primary hover:text-primary-focus">
<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="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>
Dashboard
</a>
</li>
<li>
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
</li>
<li class="font-semibold">Personalizar Permissões</li>
</ul>
</div>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-3 mb-2">
<div class="p-3 bg-info/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-info" 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 class="flex-1">
<h1 class="text-3xl font-bold text-base-content">Personalizar Permissões por Matrícula</h1>
<p class="text-base-content/60 mt-1">Configure permissões específicas para usuários individuais</p>
</div>
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
<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>
</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"}
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{:else}
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
<span class="font-semibold">{mensagem.texto}</span>
</div>
{/if}
<!-- Card de Busca -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title">Buscar Usuário</h2>
<p class="text-sm text-base-content/60">Digite a matrícula do usuário para personalizar suas permissões</p>
<div class="flex gap-4 mt-4">
<div class="form-control flex-1">
<label class="label" for="matricula-busca">
<span class="label-text font-semibold">Matrícula</span>
</label>
<input
id="matricula-busca"
type="text"
class="input input-bordered input-primary w-full"
placeholder="Digite a matrícula..."
bind:value={matriculaBusca}
disabled={buscando}
onkeydown={(e) => e.key === "Enter" && buscarUsuario()}
/>
</div>
<div class="flex items-end gap-2">
<button
class="btn btn-primary"
onclick={buscarUsuario}
disabled={buscando || !matriculaBusca.trim()}
>
{#if buscando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{/if}
Buscar
</button>
{#if usuarioEncontrado}
<button class="btn btn-ghost" onclick={limparBusca}>
<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="M6 18L18 6M6 6l12 12" />
</svg>
Limpar
</button>
{/if}
</div>
</div>
</div>
</div>
<!-- Informações do Usuário -->
{#if usuarioEncontrado}
<div class="card bg-gradient-to-br from-info/10 to-info/5 shadow-xl mb-6 border-2 border-info/20">
<div class="card-body">
<div class="flex items-center gap-4">
<div class="avatar placeholder">
<div class="bg-info text-info-content rounded-full w-16">
<span class="text-2xl font-bold">{usuarioEncontrado.nome.charAt(0)}</span>
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-bold">{usuarioEncontrado.nome}</h3>
<div class="flex gap-4 mt-1 text-sm">
<span class="flex items-center gap-1">
<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="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>
<strong>Matrícula:</strong> {usuarioEncontrado.matricula}
</span>
<span class="flex items-center gap-1">
<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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<strong>Email:</strong> {usuarioEncontrado.email}
</span>
</div>
</div>
<div class="text-right">
<div class="badge badge-primary badge-lg">
Nível {usuarioEncontrado.role.nivel}
</div>
<p class="text-sm mt-1">{usuarioEncontrado.role.descricao}</p>
<div class="badge mt-2" class:badge-success={usuarioEncontrado.ativo} class:badge-error={!usuarioEncontrado.ativo}>
{usuarioEncontrado.ativo ? "Ativo" : "Inativo"}
</div>
</div>
</div>
</div>
</div>
<!-- Tabela de Permissões -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Permissões Personalizadas</h2>
<div class="alert alert-info mb-4">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="text-sm">
<strong>Permissões personalizadas sobrepõem as permissões da função.</strong><br />
Configure apenas os menus que deseja personalizar para este usuário.
</p>
</div>
</div>
{#if menusQuery.isLoading}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if menusQuery.data}
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead class="bg-base-200">
<tr>
<th class="w-1/3">Menu</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="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>
Acessar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Consultar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<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="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>
Gravar
</div>
</th>
<th class="text-center">Status</th>
</tr>
</thead>
<tbody>
{#each menusQuery.data as menu}
{@const permissao = permissoesQuery?.data?.find((p) => p.menuPath === menu.path)}
<tr class="hover">
<td>
<div class="flex flex-col">
<span class="font-semibold">{menu.nome}</span>
<span class="text-xs text-base-content/60">{menu.path}</span>
</div>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={permissao?.podeAcessar || false}
disabled={salvando}
onchange={(e) =>
atualizarPermissao(menu.path, "podeAcessar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-info"
checked={permissao?.podeConsultar || false}
disabled={salvando || !permissao?.podeAcessar}
onchange={(e) =>
atualizarPermissao(menu.path, "podeConsultar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-success"
checked={permissao?.podeGravar || false}
disabled={salvando || !permissao?.podeConsultar}
onchange={(e) =>
atualizarPermissao(menu.path, "podeGravar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
{#if permissao}
<div class="badge badge-warning badge-sm">Personalizado</div>
{:else}
<div class="badge badge-ghost badge-sm">Padrão da Função</div>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
</ProtectedRoute>