diff --git a/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte b/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte index b145af0..eb8ff89 100644 --- a/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte +++ b/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte @@ -10,11 +10,15 @@ BarChart3, CheckCircle2 } from 'lucide-svelte'; + import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte'; const client = useConvexClient(); const statsQuery = useQuery(api.almoxarifado.obterEstatisticas, {}); const alertasQuery = useQuery(api.almoxarifado.listarAlertas, { status: 'ativo' }); const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {}); + const ultimosProdutosQuery = useQuery(api.almoxarifado.obterUltimosProdutosCadastrados, { + limit: 10 + }); // Criar mapa de materiais para lookup eficiente const materiaisMap = $derived.by(() => { @@ -25,6 +29,51 @@ } return map; }); + + // Dados formatados para o gráfico + const chartData = $derived.by(() => { + if (!ultimosProdutosQuery?.data || ultimosProdutosQuery.data.length === 0) { + return { + labels: [], + datasets: [] + }; + } + + const produtos = ultimosProdutosQuery.data; + + // Ordenar do mais antigo para o mais recente (para o gráfico) + const produtosOrdenados = [...produtos].reverse(); + + // Criar cores gradientes para cada barra (efeito 3D) + const cores = [ + '#6366f1', // indigo + '#8b5cf6', // violet + '#a855f7', // purple + '#c084fc', // fuchsia + '#d946ef', // pink + '#ec4899', // rose + '#f43f5e', // red + '#fb7185', // pink-400 + '#f87171', // red-400 + '#fb923c' // orange-400 + ]; + + return { + labels: produtosOrdenados.map((p) => { + // Truncar nome se muito longo + return p.nome.length > 20 ? p.nome.substring(0, 20) + '...' : p.nome; + }), + datasets: [ + { + label: 'Quantidade em Estoque', + data: produtosOrdenados.map((p) => p.estoqueAtual), + backgroundColor: produtosOrdenados.map((_, index) => cores[index % cores.length]), + borderColor: produtosOrdenados.map((_, index) => cores[index % cores.length]), + borderWidth: 2 + } + ] + }; + });
@@ -128,6 +177,37 @@ {/if} + +
+
+
+

+
+ +
+ Últimos 10 Produtos Cadastrados +

+ {#if ultimosProdutosQuery === undefined} +
+ +
+ {:else if ultimosProdutosQuery.data && ultimosProdutosQuery.data.length > 0} +
+ +
+
+

Mostrando os últimos 10 produtos cadastrados ordenados por data de criação

+
+ {:else} +
+ + Nenhum produto cadastrado ainda +
+ {/if} +
+
+
+
diff --git a/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte b/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte index a622f72..2fd2c13 100644 --- a/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte +++ b/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte @@ -5,6 +5,7 @@ import { useConvexClient, useQuery } from 'convex-svelte'; import { resolve } from '$app/paths'; import { AlertTriangle, CheckCircle, XCircle, Package } from 'lucide-svelte'; + import ConfirmModal from '$lib/components/ConfirmModal.svelte'; const client = useConvexClient(); @@ -28,6 +29,8 @@ }); let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null); + let showConfirmIgnorar = $state(false); + let alertaParaIgnorar = $state | null>(null); function mostrarMensagem(kind: 'success' | 'error', text: string) { notice = { kind, text }; @@ -36,6 +39,16 @@ }, 5000); } + function abrirModalIgnorar(id: Id<'alertasEstoque'>) { + alertaParaIgnorar = id; + showConfirmIgnorar = true; + } + + function fecharModalIgnorar() { + showConfirmIgnorar = false; + alertaParaIgnorar = null; + } + async function resolverAlerta(id: Id<'alertasEstoque'>) { try { await client.mutation(api.almoxarifado.resolverAlerta, { id }); @@ -47,10 +60,8 @@ } async function ignorarAlerta(id: Id<'alertasEstoque'>) { - if (!confirm('Tem certeza que deseja ignorar este alerta?')) { - return; - } - + fecharModalIgnorar(); + try { await client.mutation(api.almoxarifado.ignorarAlerta, { id }); mostrarMensagem('success', 'Alerta ignorado'); @@ -221,7 +232,7 @@
+ + + alertaParaIgnorar && ignorarAlerta(alertaParaIgnorar)} + onCancel={fecharModalIgnorar} + />
diff --git a/packages/backend/convex/almoxarifado.ts b/packages/backend/convex/almoxarifado.ts index e34e26b..fa083d5 100644 --- a/packages/backend/convex/almoxarifado.ts +++ b/packages/backend/convex/almoxarifado.ts @@ -351,6 +351,42 @@ export const verificarEstoqueBaixo = query({ } }); +export const obterUltimosProdutosCadastrados = query({ + args: { + limit: v.optional(v.number()) + }, + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) return []; + + try { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'almoxarifado', + acao: 'listar' + }); + } catch { + return []; + } + + const limit = args.limit ?? 10; + const materiais = await ctx.db.query('materiais').collect(); + + // Ordenar por data de criação (mais recente primeiro) e pegar os últimos N + const materiaisOrdenados = materiais + .sort((a, b) => b.criadoEm - a.criadoEm) + .slice(0, limit); + + return materiaisOrdenados.map((m) => ({ + _id: m._id, + nome: m.nome, + codigo: m.codigo, + estoqueAtual: m.estoqueAtual, + unidadeMedida: m.unidadeMedida, + criadoEm: m.criadoEm + })); + } +}); + // ========== MUTATIONS ========== async function registrarHistorico( @@ -678,6 +714,7 @@ export const registrarEntrada = mutation({ materialId: v.id('materiais'), quantidade: v.number(), motivo: v.string(), + funcionarioId: v.optional(v.id('funcionarios')), documento: v.optional(v.string()), observacoes: v.optional(v.string()) }, @@ -698,6 +735,9 @@ export const registrarEntrada = mutation({ const usuario = await getCurrentUserFunction(ctx); if (!usuario) throw new Error('Usuário não autenticado'); + // Se funcionarioId não foi fornecido, usar o do usuário logado (se existir) + const funcionarioId = args.funcionarioId || usuario.funcionarioId; + const quantidadeAnterior = material.estoqueAtual; const quantidadeNova = quantidadeAnterior + args.quantidade; @@ -716,6 +756,7 @@ export const registrarEntrada = mutation({ quantidadeNova, motivo: args.motivo, documento: args.documento, + funcionarioId, usuarioId: usuario._id, data: Date.now(), observacoes: args.observacoes @@ -818,6 +859,7 @@ export const ajustarEstoque = mutation({ materialId: v.id('materiais'), quantidadeNova: v.number(), motivo: v.string(), + funcionarioId: v.optional(v.id('funcionarios')), observacoes: v.optional(v.string()) }, handler: async (ctx, args) => { @@ -837,6 +879,9 @@ export const ajustarEstoque = mutation({ const usuario = await getCurrentUserFunction(ctx); if (!usuario) throw new Error('Usuário não autenticado'); + // Se funcionarioId não foi fornecido, usar o do usuário logado (se existir) + const funcionarioId = args.funcionarioId || usuario.funcionarioId; + const quantidadeAnterior = material.estoqueAtual; const diferenca = args.quantidadeNova - quantidadeAnterior; @@ -854,6 +899,7 @@ export const ajustarEstoque = mutation({ quantidadeAnterior, quantidadeNova: args.quantidadeNova, motivo: args.motivo, + funcionarioId, usuarioId: usuario._id, data: Date.now(), observacoes: args.observacoes