From ae4f8fc6b30073e28f29b81a15950d707b7f1fdd Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 22 Dec 2025 00:08:13 -0300 Subject: [PATCH] feat: enhance 'Almoxarifado' UI with improved styling, updated component layouts, and added barcode functionality for better inventory management and user experience --- .../almoxarifado/ImageUpload.svelte | 92 ++-- .../(dashboard)/almoxarifado/+page.svelte | 32 +- .../almoxarifado/alertas/+page.svelte | 107 ++-- .../almoxarifado/materiais/+page.svelte | 222 ++++---- .../[materialId]/editar/+page.svelte | 33 ++ .../materiais/cadastro/+page.svelte | 473 ++++++++++-------- .../almoxarifado/movimentacoes/+page.svelte | 377 +++++++++----- .../almoxarifado/relatorios/+page.svelte | 211 +++++--- .../almoxarifado/requisicoes/+page.svelte | 394 +++++++++------ packages/backend/convex/almoxarifado.ts | 95 ++++ 10 files changed, 1283 insertions(+), 753 deletions(-) diff --git a/apps/web/src/lib/components/almoxarifado/ImageUpload.svelte b/apps/web/src/lib/components/almoxarifado/ImageUpload.svelte index f62d939..1423304 100644 --- a/apps/web/src/lib/components/almoxarifado/ImageUpload.svelte +++ b/apps/web/src/lib/components/almoxarifado/ImageUpload.svelte @@ -71,11 +71,7 @@ reader.readAsDataURL(file); } - function resizeImage( - dataUrl: string, - maxWidth: number, - maxHeight: number - ): Promise { + function resizeImage(dataUrl: string, maxWidth: number, maxHeight: number): Promise { return new Promise((resolve, reject) => { const img = new window.Image(); img.onload = () => { @@ -171,10 +167,10 @@ // Atribuir stream ao vídeo videoElement.srcObject = stream; - + // Aguardar o vídeo estar pronto e começar a reproduzir await videoElement.play(); - + // Aguardar metadata estar carregado if (videoElement.readyState < 2) { await new Promise((resolve, reject) => { @@ -204,10 +200,11 @@ } catch (err) { console.error('Erro ao acessar câmera:', err); let errorMessage = 'Erro ao acessar câmera'; - + if (err instanceof Error) { if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { - errorMessage = 'Permissão de acesso à câmera negada. Por favor, permita o acesso à câmera nas configurações do navegador.'; + errorMessage = + 'Permissão de acesso à câmera negada. Por favor, permita o acesso à câmera nas configurações do navegador.'; } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') { errorMessage = 'Nenhuma câmera encontrada no dispositivo.'; } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') { @@ -216,7 +213,7 @@ errorMessage = err.message || errorMessage; } } - + error = errorMessage; showCamera = false; capturing = false; @@ -280,11 +277,14 @@ // Sincronizar preview com value sempre que value mudar $effect(() => { + // Acessar value para criar dependência reativa + const currentValue = value; // Sempre sincronizar quando value mudar - preview = value; + if (currentValue !== preview) { + preview = currentValue; + } }); - // Limpar stream quando o componente for desmontado $effect(() => { return () => { @@ -305,7 +305,11 @@ {#if preview}
- Preview da imagem do produto + Preview da imagem do produto @@ -354,7 +354,7 @@ {/if} {#if preview} -
+
- @@ -377,12 +373,15 @@ {#if showCamera} -
+
e.stopPropagation()} > -
+

Capturar Foto

-
+
{#if showCamera} {/if} {#if !capturing} -
+

Iniciando câmera...

@@ -415,20 +419,9 @@ {/if}
-
- - + @@ -442,4 +435,3 @@ width: 100%; } - diff --git a/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte b/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte index d4d0895..d55b30b 100644 --- a/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte +++ b/apps/web/src/routes/(dashboard)/almoxarifado/+page.svelte @@ -9,7 +9,7 @@ ArrowLeftRight, BarChart3, CheckCircle2, - Settings + List } from 'lucide-svelte'; import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte'; @@ -323,6 +323,21 @@
+ +
- -
diff --git a/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte b/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte index 2fd2c13..c80f8bf 100644 --- a/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte +++ b/apps/web/src/routes/(dashboard)/almoxarifado/alertas/+page.svelte @@ -4,7 +4,7 @@ import type { AlertaStatus, AlertaTipo } from '@sgse-app/backend/convex/tables/almoxarifado'; import { useConvexClient, useQuery } from 'convex-svelte'; import { resolve } from '$app/paths'; - import { AlertTriangle, CheckCircle, XCircle, Package } from 'lucide-svelte'; + import { AlertTriangle, CheckCircle, XCircle, Package, Filter, TrendingDown, Calendar } from 'lucide-svelte'; import ConfirmModal from '$lib/components/ConfirmModal.svelte'; const client = useConvexClient(); @@ -114,12 +114,14 @@
-
+
-
-

Alertas de Estoque

-

Visualize e gerencie alertas de estoque baixo

+
+

+ Alertas de Estoque +

+

Visualize e gerencie alertas de estoque baixo

@@ -132,57 +134,74 @@ {/if} -
-
-

Filtros

-
+
+
+
+
+ +
+

Filtros de Busca

+
+
-
-
-
-
+
+
+
+
+ +
+

Lista de Alertas

+
{#if alertasQuery === undefined} -
+
{:else if alertasQuery.data && alertasQuery.data.length > 0} -
+
- - - - - - - - + + + + + + + + @@ -224,15 +243,17 @@ {#if alerta.status === 'ativo'}
MaterialTipoQuantidade AtualQuantidade MínimaDiferençaStatusDataAçõesMaterialTipoQuantidade AtualQuantidade MínimaDiferençaStatusDataAções
+ + {#if alertasQuery.data.length > 0} +
+
+ Mostrando {alertasQuery.data.length} alerta{alertasQuery.data.length !== 1 ? 's' : ''} +
+
+ {/if} {:else} -
- -

Nenhum alerta encontrado

-

+

+ +

Nenhum alerta encontrado

+

{#if filtroStatus === 'ativo'} Não há alertas ativos no momento. Todos os materiais estão com estoque adequado! {:else if filtroStatus || filtroTipo} @@ -258,11 +287,11 @@ Ainda não há alertas registrados no sistema. {/if}

-
- +
+
-

Como os alertas funcionam?

-
    +

    Como os alertas funcionam?

    +
    • Os alertas são criados automaticamente quando o estoque de um material fica abaixo do mínimo configurado
    • O sistema permite apenas um alerta ativo por material para evitar duplicações
    • Quando o estoque volta ao normal, você pode resolver o alerta manualmente
    • diff --git a/apps/web/src/routes/(dashboard)/almoxarifado/materiais/+page.svelte b/apps/web/src/routes/(dashboard)/almoxarifado/materiais/+page.svelte index 4e2c35f..bc1a529 100644 --- a/apps/web/src/routes/(dashboard)/almoxarifado/materiais/+page.svelte +++ b/apps/web/src/routes/(dashboard)/almoxarifado/materiais/+page.svelte @@ -4,7 +4,7 @@ import { useConvexClient, useQuery } from 'convex-svelte'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; - import { Package, Plus, Search, Edit, Eye, AlertTriangle, Trash2, Info, X } from 'lucide-svelte'; + import { Package, Plus, Search, Edit, Eye, AlertTriangle, Trash2, Info, X, Filter, Barcode } from 'lucide-svelte'; import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte'; const client = useConvexClient(); @@ -12,7 +12,18 @@ // Usar useQuery para atualização automática const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {}); - let materiais = $derived(materiaisQuery?.data ?? []); + let materiais = $derived.by(() => { + try { + if (materiaisQuery === undefined || materiaisQuery === null) return []; + if (typeof materiaisQuery === 'object' && Object.keys(materiaisQuery).length === 0) return []; + const data = 'data' in materiaisQuery ? materiaisQuery.data : materiaisQuery; + if (data === undefined || data === null) return []; + return Array.isArray(data) ? data : []; + } catch (error) { + console.error('Erro ao processar materiaisQuery:', error); + return []; + } + }); let filtered = $state>>([]); let filtroBusca = $state(''); let filtroCategoria = $state(''); @@ -253,15 +264,17 @@
      -
      - +
      +
      -
      -

      Materiais

      -

      Gerencie o cadastro de materiais do almoxarifado

      +
      +

      + Materiais +

      +

      Gerencie o cadastro e controle de materiais do almoxarifado

      - @@ -276,41 +289,52 @@ {/if} -
      -
      -
      -

      Filtros de Busca

      +
      +
      +
      +
      +
      + +
      +

      Filtros de Busca

      +
      console.error('Erro no scanner:', error)} />
      -
      +
      -
      -
      -
      -
      @@ -339,30 +366,36 @@
      -
      -
      -
      +
      +
      +
      +
      + +
      +

      Lista de Materiais

      +
      +
      - - - - - - - - + + + + + + + + {#if filtered.length === 0} @@ -405,8 +438,8 @@ + {#each novaRequisicaoItens as item} + {@const material = materiaisQuery.data?.find(m => m._id === item.materialId)} + + + + + + {/each} + +
      CódigoNomeCategoriaEstoque AtualEstoque MínimoUnidadeStatusAçõesCódigoNomeCategoriaEstoque AtualEstoque MínimoUnidadeStatusAções
      -
      - -

      Nenhum material encontrado

      -

      Tente ajustar os filtros de busca

      +
      + +

      Nenhum material encontrado

      +

      Tente ajustar os filtros de busca ou cadastre um novo material

      {#if filtered.length > 0} -
      -
      - Mostrando {filtered.length} de {materiais.length} materiais +
      +
      + Mostrando {filtered.length} de {materiais.length} materiais
      {/if} @@ -458,29 +491,42 @@
      - -
      +
      {material?.nome || 'Carregando...'}
      + {#if material?.codigo} +
      {material.codigo}
      + {/if} +
      + {item.quantidade} + {material?.unidadeMedida || ''} + + +
      +
      + {:else} +
      + +
      +

      Nenhum item adicionado

      +

      Adicione pelo menos um item à requisição

      +
      +
      + {/if}
      -
      diff --git a/packages/backend/convex/almoxarifado.ts b/packages/backend/convex/almoxarifado.ts index 4cf54eb..16bbab3 100644 --- a/packages/backend/convex/almoxarifado.ts +++ b/packages/backend/convex/almoxarifado.ts @@ -165,6 +165,101 @@ export const listarMovimentacoes = query({ } }); +export const listarMovimentacoesComHistorico = query({ + args: {}, + handler: async (ctx) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) return []; + + try { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: 'almoxarifado', + acao: 'listar' + }); + } catch { + return []; + } + + // Buscar movimentações de estoque + const movimentacoes = await ctx.db + .query('movimentacoesEstoque') + .withIndex('by_data') + .collect(); + + // Buscar histórico de alterações de materiais (criação, edição, exclusão) + const historicoMateriais = await ctx.db + .query('historicoAlteracoes') + .withIndex('by_tipoEntidade', (q) => q.eq('tipoEntidade', 'material')) + .collect(); + + // Buscar todos os usuários únicos para enriquecer os dados + const usuarioIds = new Set>(); + for (const mov of movimentacoes) { + usuarioIds.add(mov.usuarioId); + } + for (const hist of historicoMateriais) { + usuarioIds.add(hist.usuarioId); + } + + // Buscar informações dos usuários + const usuariosMap = new Map, Doc<'usuarios'>>(); + for (const userId of usuarioIds) { + const usuario = await ctx.db.get(userId); + if (usuario) { + usuariosMap.set(userId, usuario); + } + } + + // Transformar movimentações em formato unificado + const movimentacoesFormatadas = movimentacoes.map((mov) => { + const usuario = usuariosMap.get(mov.usuarioId); + return { + id: mov._id, + tipo: 'movimentacao' as const, + tipoMovimentacao: mov.tipo, + materialId: mov.materialId, + data: mov.data, + quantidade: mov.quantidade, + quantidadeAnterior: mov.quantidadeAnterior, + quantidadeNova: mov.quantidadeNova, + motivo: mov.motivo, + funcionarioId: mov.funcionarioId, + usuarioId: mov.usuarioId, + usuarioNome: usuario?.nome || 'Usuário desconhecido', + observacoes: mov.observacoes + }; + }); + + // Transformar histórico de alterações em formato unificado + const historicoFormatado = historicoMateriais.map((hist) => { + const usuario = usuariosMap.get(hist.usuarioId); + return { + id: hist._id, + tipo: 'alteracao' as const, + tipoAlteracao: hist.acao, // 'criacao', 'edicao', 'exclusao' + materialId: hist.entidadeId as Id<'materiais'>, // Converter string para Id + data: hist.timestamp, + quantidade: undefined, + quantidadeAnterior: undefined, + quantidadeNova: undefined, + motivo: hist.observacoes || hist.acao, + funcionarioId: undefined, + usuarioId: hist.usuarioId, + usuarioNome: usuario?.nome || 'Usuário desconhecido', + observacoes: hist.observacoes, + dadosAnteriores: hist.dadosAnteriores, + dadosNovos: hist.dadosNovos + }; + }); + + // Combinar e ordenar por data (mais recente primeiro) + const todos = [...movimentacoesFormatadas, ...historicoFormatado]; + todos.sort((a, b) => b.data - a.data); + + return todos; + } +}); + export const listarRequisicoes = query({ args: { status: v.optional(requisicaoStatus),