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,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>