feat: expand 'Almoxarifado' sidebar section with detailed submenus for improved navigation and user permissions

This commit is contained in:
2025-12-20 14:08:35 -03:00
parent 8f0452bd87
commit fc633c5708
5 changed files with 650 additions and 17 deletions

View File

@@ -146,7 +146,52 @@
label: 'Almoxarifado', label: 'Almoxarifado',
icon: 'Package', icon: 'Package',
link: '/almoxarifado', link: '/almoxarifado',
permission: { recurso: 'almoxarifado', acao: 'listar' } permission: { recurso: 'almoxarifado', acao: 'listar' },
submenus: [
{
label: 'Dashboard',
link: '/almoxarifado',
permission: { recurso: 'almoxarifado', acao: 'listar' },
excludePaths: [
'/almoxarifado/materiais',
'/almoxarifado/materiais/cadastro',
'/almoxarifado/movimentacoes',
'/almoxarifado/requisicoes',
'/almoxarifado/alertas',
'/almoxarifado/relatorios'
]
},
{
label: 'Cadastrar Material',
link: '/almoxarifado/materiais/cadastro',
permission: { recurso: 'almoxarifado', acao: 'criar_material' }
},
{
label: 'Listar Materiais',
link: '/almoxarifado/materiais',
permission: { recurso: 'almoxarifado', acao: 'listar' }
},
{
label: 'Movimentações',
link: '/almoxarifado/movimentacoes',
permission: { recurso: 'almoxarifado', acao: 'registrar_movimentacao' }
},
{
label: 'Requisições',
link: '/almoxarifado/requisicoes',
permission: { recurso: 'almoxarifado', acao: 'listar' }
},
{
label: 'Alertas',
link: '/almoxarifado/alertas',
permission: { recurso: 'almoxarifado', acao: 'listar' }
},
{
label: 'Relatórios',
link: '/almoxarifado/relatorios',
permission: { recurso: 'almoxarifado', acao: 'listar' }
}
]
}, },
{ {
label: 'Objetos', label: 'Objetos',

View File

@@ -57,14 +57,7 @@
<div class="breadcrumbs mb-4 text-sm"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li> <li>
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline" <a href={resolve('/almoxarifado')} class="text-primary hover:underline">Almoxarifado</a>
>Recursos Humanos</a
>
</li>
<li>
<a href={resolve('/almoxarifado')} class="text-primary hover:underline"
>Almoxarifado</a
>
</li> </li>
<li>Materiais</li> <li>Materiais</li>
</ul> </ul>

View File

@@ -0,0 +1,239 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient } from 'convex-svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import {
Package,
ArrowLeft,
Edit,
AlertTriangle,
CheckCircle2,
XCircle,
MapPin,
Truck,
Boxes,
Tag
} from 'lucide-svelte';
const client = useConvexClient();
let materialId = $derived($page.params.materialId as Id<'materiais'>);
let material = $state<Doc<'materiais'> | null>(null);
let loading = $state(true);
async function load() {
try {
loading = true;
const data = await client.query(api.almoxarifado.obterMaterial, {
id: materialId
});
if (!data) {
goto(resolve('/almoxarifado/materiais'));
return;
}
material = data;
} catch (error) {
console.error('Erro ao carregar material:', error);
goto(resolve('/almoxarifado/materiais'));
} finally {
loading = false;
}
}
$effect(() => {
if (materialId) {
load();
}
});
function navEditar() {
goto(resolve(`/almoxarifado/materiais/${materialId}/editar`));
}
</script>
{#if loading}
<div class="flex min-h-screen items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if material}
<main class="container mx-auto px-4 py-4">
<!-- Breadcrumb -->
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li>
<a href={resolve('/almoxarifado')} class="text-primary hover:underline">Almoxarifado</a>
</li>
<li>
<a href={resolve('/almoxarifado/materiais')} class="text-primary hover:underline"
>Materiais</a
>
</li>
<li>{material.nome}</li>
</ul>
</div>
<!-- Cabeçalho -->
<div class="mb-6">
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div class="flex items-center gap-4">
<button
class="btn btn-ghost btn-sm"
onclick={() => goto(resolve('/almoxarifado/materiais'))}
>
<ArrowLeft class="h-5 w-5" />
</button>
<div class="rounded-xl bg-amber-500/20 p-3">
<Package class="h-8 w-8 text-amber-600" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold">{material.nome}</h1>
<p class="text-base-content/70">Detalhes do material</p>
</div>
</div>
<button class="btn btn-primary" onclick={navEditar}>
<Edit class="h-5 w-5" />
Editar Material
</button>
</div>
</div>
<!-- Informações Principais -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Card: Informações Básicas -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">
<Package class="h-5 w-5" />
Informações Básicas
</h2>
<div class="space-y-4">
<div>
<label class="text-sm font-semibold text-base-content/60">Código</label>
<p class="font-mono text-lg font-bold">{material.codigo}</p>
</div>
<div>
<label class="text-sm font-semibold text-base-content/60">Nome</label>
<p class="text-lg">{material.nome}</p>
</div>
{#if material.descricao}
<div>
<label class="text-sm font-semibold text-base-content/60">Descrição</label>
<p class="text-base">{material.descricao}</p>
</div>
{/if}
<div>
<label class="text-sm font-semibold text-base-content/60">Categoria</label>
<div class="mt-1">
<span class="badge badge-outline badge-lg">{material.categoria}</span>
</div>
</div>
<div>
<label class="text-sm font-semibold text-base-content/60">Unidade de Medida</label>
<p class="text-lg">{material.unidadeMedida}</p>
</div>
<div>
<label class="text-sm font-semibold text-base-content/60">Status</label>
<div class="mt-1">
{#if material.ativo}
<span class="badge badge-success badge-lg">
<CheckCircle2 class="h-4 w-4" />
Ativo
</span>
{:else}
<span class="badge badge-error badge-lg">
<XCircle class="h-4 w-4" />
Inativo
</span>
{/if}
</div>
</div>
</div>
</div>
</div>
<!-- Card: Estoque -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">
<Boxes class="h-5 w-5" />
Estoque
</h2>
<div class="space-y-4">
<div>
<label class="text-sm font-semibold text-base-content/60">Estoque Atual</label>
<div class="mt-1 flex items-center gap-2">
<p class="text-2xl font-bold">{material.estoqueAtual}</p>
<span class="text-base-content/60">{material.unidadeMedida}</span>
{#if material.estoqueAtual <= material.estoqueMinimo}
<AlertTriangle class="h-5 w-5 text-warning" />
{/if}
</div>
</div>
<div>
<label class="text-sm font-semibold text-base-content/60">Estoque Mínimo</label>
<p class="text-lg">{material.estoqueMinimo} {material.unidadeMedida}</p>
</div>
{#if material.estoqueMaximo !== undefined}
<div>
<label class="text-sm font-semibold text-base-content/60">Estoque Máximo</label>
<p class="text-lg">{material.estoqueMaximo} {material.unidadeMedida}</p>
</div>
{/if}
{#if material.estoqueAtual <= material.estoqueMinimo}
<div class="alert alert-warning mt-4">
<AlertTriangle class="h-5 w-5" />
<span>Estoque abaixo do mínimo! Reposição necessária.</span>
</div>
{/if}
</div>
</div>
</div>
</div>
<!-- Informações Adicionais -->
<div class="card bg-base-100 mt-6 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">
<Tag class="h-5 w-5" />
Informações Adicionais
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#if material.localizacao}
<div>
<label class="text-sm font-semibold text-base-content/60">
<MapPin class="inline h-4 w-4" />
Localização
</label>
<p class="text-base">{material.localizacao}</p>
</div>
{/if}
{#if material.fornecedor}
<div>
<label class="text-sm font-semibold text-base-content/60">
<Truck class="inline h-4 w-4" />
Fornecedor
</label>
<p class="text-base">{material.fornecedor}</p>
</div>
{/if}
</div>
</div>
</div>
</main>
{:else}
<div class="flex min-h-screen items-center justify-center">
<div class="text-center">
<Package class="mx-auto mb-4 h-12 w-12 text-base-content/30" />
<p class="text-base-content/70">Material não encontrado</p>
<button class="btn btn-primary mt-4" onclick={() => goto(resolve('/almoxarifado/materiais'))}>
Voltar para Materiais
</button>
</div>
</div>
{/if}

View File

@@ -0,0 +1,363 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient } from 'convex-svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import { Package, Save, ArrowLeft } from 'lucide-svelte';
const client = useConvexClient();
let materialId = $derived($page.params.materialId as Id<'materiais'>);
let codigo = $state('');
let nome = $state('');
let descricao = $state('');
let categoria = $state('');
let unidadeMedida = $state('UN');
let estoqueMinimo = $state(10);
let estoqueMaximo = $state<number | undefined>(undefined);
let localizacao = $state('');
let fornecedor = $state('');
let ativo = $state(true);
let loading = $state(false);
let loadingData = $state(true);
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
const unidadesMedida = ['UN', 'CX', 'KG', 'L', 'M', 'M²', 'M³', 'PC', 'DZ'];
const categoriasComuns = [
'Escritório',
'Limpeza',
'TI',
'Manutenção',
'Elétrico',
'Hidráulico',
'Outros'
];
function mostrarMensagem(kind: 'success' | 'error', text: string) {
notice = { kind, text };
setTimeout(() => {
notice = null;
}, 5000);
}
async function loadMaterial() {
try {
loadingData = true;
const material = await client.query(api.almoxarifado.obterMaterial, {
id: materialId
});
if (!material) {
mostrarMensagem('error', 'Material não encontrado');
setTimeout(() => {
goto(resolve('/almoxarifado/materiais'));
}, 1500);
return;
}
// Preencher campos
codigo = material.codigo;
nome = material.nome;
descricao = material.descricao || '';
categoria = material.categoria;
unidadeMedida = material.unidadeMedida;
estoqueMinimo = material.estoqueMinimo;
estoqueMaximo = material.estoqueMaximo;
localizacao = material.localizacao || '';
fornecedor = material.fornecedor || '';
ativo = material.ativo;
} catch (error) {
console.error('Erro ao carregar material:', error);
mostrarMensagem('error', 'Erro ao carregar dados do material');
setTimeout(() => {
goto(resolve('/almoxarifado/materiais'));
}, 1500);
} finally {
loadingData = false;
}
}
async function handleSubmit() {
// Validação
if (!codigo.trim() || !nome.trim() || !categoria.trim()) {
mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
return;
}
if (estoqueMinimo < 0) {
mostrarMensagem('error', 'Estoque mínimo não pode ser negativo');
return;
}
if (estoqueMaximo !== undefined && estoqueMaximo < estoqueMinimo) {
mostrarMensagem('error', 'Estoque máximo deve ser maior ou igual ao mínimo');
return;
}
try {
loading = true;
await client.mutation(api.almoxarifado.editarMaterial, {
id: materialId,
codigo: codigo.trim(),
nome: nome.trim(),
descricao: descricao.trim() || undefined,
categoria: categoria.trim(),
unidadeMedida,
estoqueMinimo,
estoqueMaximo,
localizacao: localizacao.trim() || undefined,
fornecedor: fornecedor.trim() || undefined,
ativo
});
mostrarMensagem('success', 'Material atualizado com sucesso!');
setTimeout(() => {
goto(resolve(`/almoxarifado/materiais/${materialId}`));
}, 1500);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Erro ao atualizar material';
mostrarMensagem('error', message);
} finally {
loading = false;
}
}
$effect(() => {
if (materialId) {
loadMaterial();
}
});
</script>
{#if loadingData}
<div class="flex min-h-screen items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<main class="container mx-auto px-4 py-4">
<!-- Breadcrumb -->
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li>
<a href={resolve('/almoxarifado')} class="text-primary hover:underline">Almoxarifado</a>
</li>
<li>
<a href={resolve('/almoxarifado/materiais')} class="text-primary hover:underline"
>Materiais</a
>
</li>
<li>
<a href={resolve(`/almoxarifado/materiais/${materialId}`)} class="text-primary hover:underline"
>Detalhes</a
>
</li>
<li>Editar</li>
</ul>
</div>
<!-- Cabeçalho -->
<div class="mb-6">
<div class="flex items-center gap-4">
<button
class="btn btn-ghost btn-sm"
onclick={() => goto(resolve(`/almoxarifado/materiais/${materialId}`))}
>
<ArrowLeft class="h-5 w-5" />
</button>
<div class="rounded-xl bg-amber-500/20 p-3">
<Package class="h-8 w-8 text-amber-600" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold">Editar Material</h1>
<p class="text-base-content/70">Atualize as informações do material</p>
</div>
</div>
</div>
<!-- Notificações -->
{#if notice}
<div class="alert alert-{notice.kind} mb-6 shadow-lg">
<span>{notice.text}</span>
</div>
{/if}
<!-- Formulário -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Código -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text font-bold">Código *</span>
</label>
<input
type="text"
class="input input-bordered"
placeholder="Ex: MAT-001"
bind:value={codigo}
required
/>
<label class="label">
<span class="label-text-alt">Código único do material</span>
</label>
</div>
<!-- Nome -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text font-bold">Nome *</span>
</label>
<input
type="text"
class="input input-bordered"
placeholder="Nome do material"
bind:value={nome}
required
/>
</div>
<!-- Descrição -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Descrição</span>
</label>
<textarea
class="textarea textarea-bordered"
placeholder="Descrição detalhada do material (opcional)"
bind:value={descricao}
rows="3"
></textarea>
</div>
<!-- Categoria -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text font-bold">Categoria *</span>
</label>
<input
type="text"
class="input input-bordered"
list="categorias"
placeholder="Ex: Escritório"
bind:value={categoria}
required
/>
<datalist id="categorias">
{#each categoriasComuns as cat}
<option value={cat} />
{/each}
</datalist>
</div>
<!-- Unidade de Medida -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text font-bold">Unidade de Medida *</span>
</label>
<select class="select select-bordered" bind:value={unidadeMedida} required>
{#each unidadesMedida as un}
<option value={un}>{un}</option>
{/each}
</select>
</div>
<!-- Estoque Mínimo -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text font-bold">Estoque Mínimo *</span>
</label>
<input
type="number"
class="input input-bordered"
min="0"
bind:value={estoqueMinimo}
required
/>
</div>
<!-- Estoque Máximo -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Estoque Máximo</span>
</label>
<input
type="number"
class="input input-bordered"
min="0"
bind:value={estoqueMaximo}
/>
<label class="label">
<span class="label-text-alt">Opcional</span>
</label>
</div>
<!-- Localização -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Localização</span>
</label>
<input
type="text"
class="input input-bordered"
placeholder="Ex: Prateleira A-01"
bind:value={localizacao}
/>
</div>
<!-- Fornecedor -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Fornecedor</span>
</label>
<input
type="text"
class="input input-bordered"
placeholder="Nome do fornecedor (opcional)"
bind:value={fornecedor}
/>
</div>
<!-- Status Ativo -->
<div class="form-control md:col-span-2">
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" class="checkbox" bind:checked={ativo} />
<span class="label-text font-bold">Material Ativo</span>
</label>
<label class="label">
<span class="label-text-alt"
>Materiais inativos não aparecerão nas listagens principais</span
>
</label>
</div>
</div>
<!-- Botões -->
<div class="card-actions mt-6 justify-end">
<button
type="button"
class="btn btn-ghost"
onclick={() => goto(resolve(`/almoxarifado/materiais/${materialId}`))}
disabled={loading}
>
Cancelar
</button>
<button type="submit" class="btn btn-primary" disabled={loading}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Save class="h-5 w-5" />
{/if}
Salvar Alterações
</button>
</div>
</form>
</div>
</div>
</main>
{/if}

View File

@@ -94,14 +94,7 @@
<div class="breadcrumbs mb-4 text-sm"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li> <li>
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline" <a href={resolve('/almoxarifado')} class="text-primary hover:underline">Almoxarifado</a>
>Recursos Humanos</a
>
</li>
<li>
<a href={resolve('/almoxarifado')} class="text-primary hover:underline"
>Almoxarifado</a
>
</li> </li>
<li> <li>
<a href={resolve('/almoxarifado/materiais')} class="text-primary hover:underline" <a href={resolve('/almoxarifado/materiais')} class="text-primary hover:underline"