feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user