feat: Implement dynamic sidebar menu in the frontend, filtered by new backend user permissions.
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user