feat: Implement dynamic sidebar menu in the frontend, filtered by new backend user permissions.

This commit is contained in:
2025-12-04 16:05:26 -03:00
parent 2cdf66375c
commit 68475f549a
4 changed files with 527 additions and 269 deletions

View File

@@ -1,41 +1,38 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient } from 'convex-svelte';
import { onMount } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte';
import { resolve } from '$app/paths';
const client = useConvexClient();
let isLoading = true;
let list: Array<any> = [];
const filtroNome = '';
const filtroTipo: '' | 'cargo_comissionado' | 'funcao_gratificada' = '';
const filtroDescricao = '';
let filtered: Array<any> = [];
let notice: { kind: 'success' | 'error'; text: string } | null = null;
$: needsScroll = filtered.length > 8;
let openMenuId: string | null = null;
const simbolosQuery = useQuery(api.simbolos.getAll, {});
let list = $derived(simbolosQuery.data ?? []);
let isLoading = $derived(simbolosQuery.isLoading);
let filtroNome = $state('');
let filtroTipo = $state<'' | 'cargo_comissionado' | 'funcao_gratificada'>('');
let filtroDescricao = $state('');
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
let openMenuId = $state<string | null>(null);
let filtered = $derived(
list.filter((s) => {
const nome = filtroNome.toLowerCase();
const desc = filtroDescricao.toLowerCase();
const okNome = !nome || (s.nome || '').toLowerCase().includes(nome);
const okDesc = !desc || (s.descricao || '').toLowerCase().includes(desc);
const okTipo = !filtroTipo || s.tipo === filtroTipo;
return okNome && okDesc && okTipo;
})
);
function toggleMenu(id: string) {
openMenuId = openMenuId === id ? null : id;
}
$: filtered = (list ?? []).filter((s) => {
const nome = (filtroNome || '').toLowerCase();
const desc = (filtroDescricao || '').toLowerCase();
const okNome = !nome || (s.nome || '').toLowerCase().includes(nome);
const okDesc = !desc || (s.descricao || '').toLowerCase().includes(desc);
const okTipo = !filtroTipo || s.tipo === filtroTipo;
return okNome && okDesc && okTipo;
});
onMount(async () => {
try {
list = await client.query(api.simbolos.getAll, {} as any);
} finally {
isLoading = false;
}
});
let deletingId: Id<'simbolos'> | null = null;
let simboloToDelete: { id: Id<'simbolos'>; nome: string } | null = null;
let deletingId = $state<Id<'simbolos'> | null>(null);
let simboloToDelete = $state<{ id: Id<'simbolos'>; nome: string } | null>(null);
function openDeleteModal(id: Id<'simbolos'>, nome: string) {
simboloToDelete = { id, nome };
@@ -52,19 +49,17 @@
try {
deletingId = simboloToDelete.id;
await client.mutation(api.simbolos.remove, { id: simboloToDelete.id });
// reload list
list = await client.query(api.simbolos.getAll, {} as any);
notice = { kind: 'success', text: 'Símbolo excluído com sucesso.' };
closeDeleteModal();
} catch (error) {
} catch {
notice = { kind: 'error', text: 'Erro ao excluir símbolo.' };
} finally {
deletingId = null;
}
}
function formatMoney(value: string) {
const num = parseFloat(value);
function formatMoney(value: string | number) {
const num = typeof value === 'number' ? value : parseFloat(value);
if (isNaN(num)) return 'R$ 0,00';
return `R$ ${num.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
@@ -72,6 +67,12 @@
function getTipoLabel(tipo: string) {
return tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada';
}
function clearFilters() {
filtroNome = '';
filtroTipo = '';
filtroDescricao = '';
}
</script>
<main class="container mx-auto px-4 py-4">
@@ -223,14 +224,7 @@
</div>
{#if filtroNome || filtroTipo || filtroDescricao}
<div class="mt-4">
<button
class="btn btn-sm gap-2"
onclick={() => {
filtroNome = '';
filtroTipo = '';
filtroDescricao = '';
}}
>
<button class="btn btn-sm gap-2" onclick={clearFilters}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
@@ -279,7 +273,7 @@
</thead>
<tbody>
{#if filtered.length > 0}
{#each filtered as simbolo}
{#each filtered as simbolo (simbolo._id)}
<tr class="hover">
<td class="font-medium">{simbolo.nome}</td>
<td>
@@ -304,6 +298,7 @@
type="button"
class="btn btn-sm"
onclick={() => toggleMenu(simbolo._id)}
aria-label="Menu de ações"
>
<svg
xmlns="http://www.w3.org/2000/svg"