feat: enhance 'Almoxarifado' functionality by integrating barcode scanning for material entry and exit, improving user experience with loading indicators and error handling for better inventory management

This commit is contained in:
2025-12-22 10:52:46 -03:00
parent e19c24b9ab
commit b1db926ab4
8 changed files with 1125 additions and 287 deletions

View File

@@ -4,14 +4,26 @@
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, Filter, Barcode } 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();
// Usar useQuery para atualização automática
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {});
let materiais = $derived.by(() => {
try {
if (materiaisQuery === undefined || materiaisQuery === null) return [];
@@ -36,7 +48,11 @@
let materialParaExcluir = $state<Doc<'materiais'> | null>(null);
let excluindo = $state(false);
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
let erroExclusao = $state<{ titulo: string; mensagem: string; tipo: 'movimentacoes' | 'requisicoes' | 'outro' } | null>(null);
let erroExclusao = $state<{
titulo: string;
mensagem: string;
tipo: 'movimentacoes' | 'requisicoes' | 'outro';
} | null>(null);
let desativando = $state(false);
const categorias = $derived(
@@ -102,7 +118,11 @@
await buscarPorCodigoBarras(barcode);
if (!materialEncontrado) {
// Produto não encontrado, oferecer cadastro
if (confirm('Produto não encontrado. Deseja cadastrar um novo produto com este código de barras?')) {
if (
confirm(
'Produto não encontrado. Deseja cadastrar um novo produto com este código de barras?'
)
) {
goto(resolve('/almoxarifado/materiais/cadastro'));
}
}
@@ -158,20 +178,20 @@
try {
excluindo = true;
// Remover item da lista localmente imediatamente (otimistic update)
filtered = filtered.filter((m) => m._id !== materialId);
await client.mutation(api.almoxarifado.deletarMaterial, {
id: materialId
});
fecharModalExclusao();
notice = {
kind: 'success',
text: `Material "${materialNome}" excluído com sucesso!`
};
// Limpar notificação após 5 segundos
setTimeout(() => {
notice = null;
@@ -179,24 +199,26 @@
} catch (error: unknown) {
// Se houver erro, recarregar a lista para garantir consistência
applyFilters();
const message = error instanceof Error ? error.message : 'Erro ao excluir material';
// Determinar tipo de erro e criar mensagem mais clara
let tipo: 'movimentacoes' | 'requisicoes' | 'outro' = 'outro';
let titulo = 'Não foi possível excluir o material';
let mensagem = message;
const messageLower = message.toLowerCase();
if (messageLower.includes('movimenta') || messageLower.includes('movimentação')) {
tipo = 'movimentacoes';
titulo = 'Material possui movimentações de estoque';
mensagem = 'Este material possui movimentações de estoque registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
mensagem =
'Este material possui movimentações de estoque registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
} else if (messageLower.includes('requisi') || messageLower.includes('requisição')) {
tipo = 'requisicoes';
titulo = 'Material possui requisições registradas';
mensagem = 'Este material possui requisições registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
mensagem =
'Este material possui requisições registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
}
// Exibir modal de erro
@@ -221,18 +243,18 @@
try {
desativando = true;
await client.mutation(api.almoxarifado.editarMaterial, {
id: materialId,
ativo: false
});
fecharModalErro();
notice = {
kind: 'success',
text: `Material "${materialNome}" desativado com sucesso!`
};
// Limpar notificação após 5 segundos
setTimeout(() => {
notice = null;
@@ -264,17 +286,26 @@
<div class="mb-8">
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div class="flex items-center gap-4">
<div class="rounded-2xl bg-gradient-to-br from-primary/20 via-primary/10 to-primary/5 p-4 shadow-lg border border-primary/20">
<Package class="h-10 w-10 text-primary" strokeWidth={2.5} />
<div
class="from-primary/20 via-primary/10 to-primary/5 border-primary/20 rounded-2xl border bg-gradient-to-br p-4 shadow-lg"
>
<Package class="text-primary h-10 w-10" strokeWidth={2.5} />
</div>
<div class="flex-1">
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
<h1
class="from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-4xl font-bold tracking-tight text-transparent"
>
Materiais
</h1>
<p class="text-base-content/70 text-lg mt-1">Gerencie o cadastro e controle de materiais do almoxarifado</p>
<p class="text-base-content/70 mt-1 text-lg">
Gerencie o cadastro e controle de materiais do almoxarifado
</p>
</div>
</div>
<button class="btn btn-primary btn-lg shadow-lg hover:shadow-xl transition-all min-w-[200px]" onclick={navCadastro}>
<button
class="btn btn-primary btn-lg min-w-[200px] shadow-lg transition-all hover:shadow-xl"
onclick={navCadastro}
>
<Plus class="h-5 w-5" />
Cadastrar Material
</button>
@@ -289,14 +320,14 @@
{/if}
<!-- Filtros -->
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
<div class="card bg-base-100 border-base-300 mb-8 border shadow-2xl">
<div class="card-body p-8">
<div class="mb-6 flex items-center justify-between border-b-2 border-primary/20 pb-4">
<div class="border-primary/20 mb-6 flex items-center justify-between border-b-2 pb-4">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-primary/10 p-2.5">
<Filter class="h-5 w-5 text-primary" strokeWidth={2.5} />
<div class="bg-primary/10 rounded-lg p-2.5">
<Filter class="text-primary h-5 w-5" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Filtros de Busca</h3>
<h3 class="text-base-content text-xl font-bold">Filtros de Busca</h3>
</div>
<BarcodeScanner
enabled={scannerEnabled}
@@ -307,25 +338,31 @@
<div class="grid grid-cols-1 gap-6 md:grid-cols-4">
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<span class="label-text flex items-center gap-2 font-semibold">
<Search class="h-4 w-4" />
Buscar
</span>
</label>
<div class="relative">
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
<input
type="text"
placeholder="Código, nome ou código de barras..."
class="input input-bordered w-full pl-10 h-12 focus:input-primary transition-colors {buscandoPorCodigoBarras ? 'input-info' : ''}"
class="input input-bordered focus:input-primary h-12 w-full pl-10 transition-colors {buscandoPorCodigoBarras
? 'input-info'
: ''}"
bind:value={filtroBusca}
/>
{#if buscandoPorCodigoBarras}
<span class="loading loading-spinner loading-xs absolute right-3 top-1/2 -translate-y-1/2"></span>
<span
class="loading loading-spinner loading-xs absolute top-1/2 right-3 -translate-y-1/2"
></span>
{/if}
</div>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Busque por código, nome ou código de barras</span>
<span class="label-text-alt text-base-content/60"
>Busque por código, nome ou código de barras</span
>
</label>
</div>
@@ -333,7 +370,10 @@
<label class="label pb-2">
<span class="label-text font-semibold">Categoria</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroCategoria}>
<select
class="select select-bordered focus:select-primary h-12 w-full transition-colors"
bind:value={filtroCategoria}
>
<option value="">Todas as categorias</option>
{#each categorias as cat}
<option value={cat}>{cat}</option>
@@ -345,7 +385,10 @@
<label class="label pb-2">
<span class="label-text font-semibold">Status</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroAtivo}>
<select
class="select select-bordered focus:select-primary h-12 w-full transition-colors"
bind:value={filtroAtivo}
>
<option value="">Todos</option>
<option value={true}>Ativos</option>
<option value={false}>Inativos</option>
@@ -356,8 +399,14 @@
<label class="label pb-2">
<span class="label-text font-semibold">Filtros Adicionais</span>
</label>
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-3 hover:bg-base-200 transition-colors">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={filtroEstoqueBaixo} />
<label
class="label border-base-300 hover:bg-base-200 cursor-pointer justify-start gap-3 rounded-lg border p-3 transition-colors"
>
<input
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={filtroEstoqueBaixo}
/>
<span class="label-text font-medium">Apenas estoque baixo</span>
</label>
</div>
@@ -366,26 +415,26 @@
</div>
<!-- Tabela -->
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card bg-base-100 border-base-300 border shadow-2xl">
<div class="card-body p-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-base-300 pb-4">
<div class="rounded-lg bg-info/10 p-2.5">
<Package class="h-5 w-5 text-info" strokeWidth={2.5} />
<div class="border-base-300 mb-6 flex items-center gap-3 border-b-2 pb-4">
<div class="bg-info/10 rounded-lg p-2.5">
<Package class="text-info h-5 w-5" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Lista de Materiais</h3>
<h3 class="text-base-content text-xl font-bold">Lista de Materiais</h3>
</div>
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra">
<div class="border-base-300 overflow-x-auto rounded-lg border">
<table class="table-zebra table">
<thead>
<tr class="bg-base-200">
<th class="font-bold text-base-content">Código</th>
<th class="font-bold text-base-content">Nome</th>
<th class="font-bold text-base-content">Categoria</th>
<th class="font-bold text-base-content">Estoque Atual</th>
<th class="font-bold text-base-content">Estoque Mínimo</th>
<th class="font-bold text-base-content">Unidade</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content">Ações</th>
<th class="text-base-content font-bold">Código</th>
<th class="text-base-content font-bold">Nome</th>
<th class="text-base-content font-bold">Categoria</th>
<th class="text-base-content font-bold">Estoque Atual</th>
<th class="text-base-content font-bold">Estoque Mínimo</th>
<th class="text-base-content font-bold">Unidade</th>
<th class="text-base-content font-bold">Status</th>
<th class="text-base-content font-bold">Ações</th>
</tr>
</thead>
<tbody>
@@ -393,22 +442,28 @@
<tr>
<td colspan="8" class="text-center">
<div class="py-16">
<Package class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
<p class="text-base-content/80 text-xl font-semibold mb-2">Nenhum material encontrado</p>
<p class="text-base-content/60 text-base">Tente ajustar os filtros de busca ou cadastre um novo material</p>
<Package class="text-base-content/30 mx-auto mb-4 h-20 w-20" />
<p class="text-base-content/80 mb-2 text-xl font-semibold">
Nenhum material encontrado
</p>
<p class="text-base-content/60 text-base">
Tente ajustar os filtros de busca ou cadastre um novo material
</p>
</div>
</td>
</tr>
{:else}
{#each filtered as material}
<tr class="hover:bg-base-200/50 transition-colors">
<tr class="hover:bg-base-200/50 transition-colors" key={material._id}>
<td>
<div class="font-mono font-bold text-primary">{material.codigo}</div>
<div class="text-primary font-mono font-bold">{material.codigo}</div>
</td>
<td>
<div class="font-medium">{material.nome}</div>
{#if material.descricao}
<div class="text-sm text-base-content/60 line-clamp-1">{material.descricao}</div>
<div class="text-base-content/60 line-clamp-1 text-sm">
{material.descricao}
</div>
{/if}
</td>
<td>
@@ -416,9 +471,13 @@
</td>
<td>
<div class="flex items-center gap-2">
<span class="font-bold {material.estoqueAtual <= material.estoqueMinimo ? 'text-error' : 'text-success'}">{material.estoqueAtual}</span>
<span
class="font-bold {material.estoqueAtual <= material.estoqueMinimo
? 'text-error'
: 'text-success'}">{material.estoqueAtual}</span
>
{#if material.estoqueAtual <= material.estoqueMinimo}
<AlertTriangle class="h-4 w-4 text-warning" />
<AlertTriangle class="text-warning h-4 w-4" />
{/if}
</div>
</td>
@@ -440,13 +499,7 @@
<button
class="btn btn-sm btn-ghost hover:btn-primary transition-all"
title="Visualizar detalhes"
onclick={() =>
goto(
resolve(
'/almoxarifado/materiais/' +
material._id
)
)}
onclick={() => goto(resolve('/almoxarifado/materiais/' + material._id))}
>
<Eye class="h-4 w-4" />
</button>
@@ -454,13 +507,7 @@
class="btn btn-sm btn-ghost hover:btn-info transition-all"
title="Editar material"
onclick={() =>
goto(
resolve(
'/almoxarifado/materiais/' +
material._id +
'/editar'
)
)}
goto(resolve('/almoxarifado/materiais/' + material._id + '/editar'))}
>
<Edit class="h-4 w-4" />
</button>
@@ -481,9 +528,10 @@
</div>
{#if filtered.length > 0}
<div class="mt-8 flex items-center justify-between border-t-2 border-base-300 pt-6">
<div class="text-base font-semibold text-base-content/80">
Mostrando <span class="text-primary font-bold">{filtered.length}</span> de <span class="text-primary font-bold">{materiais.length}</span> materiais
<div class="border-base-300 mt-8 flex items-center justify-between border-t-2 pt-6">
<div class="text-base-content/80 text-base font-semibold">
Mostrando <span class="text-primary font-bold">{filtered.length}</span> de
<span class="text-primary font-bold">{materiais.length}</span> materiais
</div>
</div>
{/if}
@@ -492,49 +540,53 @@
<!-- Modal de Confirmação de Exclusão -->
<dialog id="modal-excluir-material" class="modal backdrop-blur-sm">
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
<div class="mb-6 flex items-center gap-4 border-b-2 border-error/20 pb-4">
<div class="rounded-2xl bg-error/20 p-3">
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
<div class="modal-box border-base-300 max-w-2xl border shadow-2xl">
<div class="border-error/20 mb-6 flex items-center gap-4 border-b-2 pb-4">
<div class="bg-error/20 rounded-2xl p-3">
<AlertTriangle class="text-error h-8 w-8" strokeWidth={2.5} />
</div>
<div>
<h3 class="text-2xl font-bold text-base-content">Confirmar Exclusão</h3>
<h3 class="text-base-content text-2xl font-bold">Confirmar Exclusão</h3>
<p class="text-base-content/70 mt-1">Esta ação não pode ser desfeita</p>
</div>
</div>
{#if materialParaExcluir}
<div class="space-y-4 mb-6">
<div class="mb-6 space-y-4">
<div class="alert alert-warning border-warning/30 bg-warning/10">
<AlertTriangle class="h-5 w-5 shrink-0 text-warning" />
<AlertTriangle class="text-warning h-5 w-5 shrink-0" />
<div class="flex-1">
<p class="font-semibold text-base-content">Atenção!</p>
<p class="text-sm text-base-content/90 mt-1">
Esta ação não pode ser desfeita. O material será permanentemente excluído do sistema.
<p class="text-base-content font-semibold">Atenção!</p>
<p class="text-base-content/90 mt-1 text-sm">
Esta ação não pode ser desfeita. O material será permanentemente excluído do
sistema.
</p>
</div>
</div>
<div class="bg-base-200 rounded-lg p-5 border border-base-300">
<p class="text-sm text-base-content/70 mb-3 font-semibold">Material a ser excluído:</p>
<p class="font-bold text-lg text-base-content">{materialParaExcluir.nome}</p>
<p class="text-sm text-base-content/60 mt-2">
<div class="bg-base-200 border-base-300 rounded-lg border p-5">
<p class="text-base-content/70 mb-3 text-sm font-semibold">Material a ser excluído:</p>
<p class="text-base-content text-lg font-bold">{materialParaExcluir.nome}</p>
<p class="text-base-content/60 mt-2 text-sm">
Código: <span class="font-mono font-semibold">{materialParaExcluir.codigo}</span>
</p>
{#if materialParaExcluir.codigoBarras}
<p class="text-sm text-base-content/60 mt-1">
Código de Barras: <span class="font-mono font-semibold">{materialParaExcluir.codigoBarras}</span>
<p class="text-base-content/60 mt-1 text-sm">
Código de Barras: <span class="font-mono font-semibold"
>{materialParaExcluir.codigoBarras}</span
>
</p>
{/if}
{#if materialParaExcluir.estoqueAtual > 0}
<div class="mt-3 alert alert-info py-2 border-info/30 bg-info/10">
<div class="alert alert-info border-info/30 bg-info/10 mt-3 py-2">
<p class="text-sm font-medium">
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades em estoque.
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades
em estoque.
</p>
</div>
{/if}
</div>
</div>
{/if}
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
<div class="modal-action border-base-300 gap-3 border-t-2 pt-6">
<button
class="btn btn-ghost btn-lg min-w-[140px]"
onclick={fecharModalExclusao}
@@ -565,53 +617,62 @@
<!-- Modal de Erro na Exclusão -->
{#if erroExclusao}
<dialog id="modal-erro-exclusao" class="modal modal-open backdrop-blur-sm">
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
<div class="flex items-center gap-4 mb-6 border-b-2 border-error/20 pb-4">
<div class="rounded-2xl bg-error/20 p-3">
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
<div class="modal-box border-base-300 max-w-2xl border shadow-2xl">
<div class="border-error/20 mb-6 flex items-center gap-4 border-b-2 pb-4">
<div class="bg-error/20 rounded-2xl p-3">
<AlertTriangle class="text-error h-8 w-8" strokeWidth={2.5} />
</div>
<div>
<h3 class="text-2xl font-bold text-base-content">{erroExclusao.titulo}</h3>
<h3 class="text-base-content text-2xl font-bold">{erroExclusao.titulo}</h3>
<p class="text-base-content/70 mt-1">Não foi possível excluir o material</p>
</div>
</div>
<div class="space-y-4 mb-6">
<div class="mb-6 space-y-4">
<div class="alert alert-error border-error/30 bg-error/10">
<AlertTriangle class="h-5 w-5 shrink-0 text-error" />
<AlertTriangle class="text-error h-5 w-5 shrink-0" />
<div class="flex-1">
<p class="font-semibold text-base-content">Motivo do bloqueio</p>
<p class="text-sm text-base-content/90 mt-1">{erroExclusao.mensagem}</p>
<p class="text-base-content font-semibold">Motivo do bloqueio</p>
<p class="text-base-content/90 mt-1 text-sm">{erroExclusao.mensagem}</p>
</div>
</div>
{#if materialParaExcluir}
<div class="bg-base-200 rounded-lg p-5 border border-base-300">
<p class="text-sm text-base-content/70 mb-3 font-semibold">Material:</p>
<p class="font-bold text-lg text-base-content">{materialParaExcluir.nome}</p>
<p class="text-sm text-base-content/60 mt-2">
<div class="bg-base-200 border-base-300 rounded-lg border p-5">
<p class="text-base-content/70 mb-3 text-sm font-semibold">Material:</p>
<p class="text-base-content text-lg font-bold">{materialParaExcluir.nome}</p>
<p class="text-base-content/60 mt-2 text-sm">
Código: <span class="font-mono font-semibold">{materialParaExcluir.codigo}</span>
</p>
{#if materialParaExcluir.codigoBarras}
<p class="text-sm text-base-content/60 mt-1">
Código de Barras: <span class="font-mono font-semibold">{materialParaExcluir.codigoBarras}</span>
<p class="text-base-content/60 mt-1 text-sm">
Código de Barras: <span class="font-mono font-semibold"
>{materialParaExcluir.codigoBarras}</span
>
</p>
{/if}
</div>
{/if}
<div class="bg-info/10 border border-info/30 rounded-lg p-5">
<div class="bg-info/10 border-info/30 rounded-lg border p-5">
<div class="flex gap-3">
<Info class="h-5 w-5 text-info shrink-0 mt-0.5" />
<Info class="text-info mt-0.5 h-5 w-5 shrink-0" />
<div>
<p class="font-semibold text-base-content mb-2">Solução recomendada</p>
<p class="text-sm text-base-content/80 leading-relaxed">
<p class="text-base-content mb-2 font-semibold">Solução recomendada</p>
<p class="text-base-content/80 text-sm leading-relaxed">
{#if erroExclusao.tipo === 'movimentacoes'}
O material possui histórico de movimentações de estoque. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
O material possui histórico de movimentações de estoque. Para manter a
integridade dos dados históricos, recomendamos <strong>desativar</strong> o material
ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas,
mas seu histórico será preservado.
{:else if erroExclusao.tipo === 'requisicoes'}
O material possui requisições registradas. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
O material possui requisições registradas. Para manter a integridade dos dados
históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo.
Um material desativado não aparecerá nas listagens ativas, mas seu histórico será
preservado.
{:else}
Recomendamos verificar as dependências do material antes de tentar excluí-lo novamente.
Recomendamos verificar as dependências do material antes de tentar excluí-lo
novamente.
{/if}
</p>
</div>
@@ -619,7 +680,7 @@
</div>
</div>
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
<div class="modal-action border-base-300 gap-3 border-t-2 pt-6">
<button
class="btn btn-ghost btn-lg min-w-[140px]"
onclick={fecharModalErro}
@@ -650,6 +711,3 @@
</dialog>
{/if}
</main>

View File

@@ -4,6 +4,7 @@
import { useConvexClient, useQuery } from 'convex-svelte';
import { resolve } from '$app/paths';
import { ArrowLeftRight, ArrowDown, ArrowUp, Settings, History, Package, FileText, User, Building2, AlertCircle } from 'lucide-svelte';
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
const client = useConvexClient();
@@ -16,6 +17,8 @@
let entradaDocumento = $state('');
let entradaObservacoes = $state('');
let entradaLoading = $state(false);
let entradaScannerEnabled = $state(false);
let entradaBuscandoMaterial = $state(false);
// Estados do formulário de saída
let saidaMaterialId = $state<Id<'materiais'> | ''>('');
@@ -25,6 +28,8 @@
let saidaSetorId = $state<Id<'setores'> | ''>('');
let saidaObservacoes = $state('');
let saidaLoading = $state(false);
let saidaScannerEnabled = $state(false);
let saidaBuscandoMaterial = $state(false);
// Estados do formulário de ajuste
let ajusteMaterialId = $state<Id<'materiais'> | ''>('');
@@ -68,6 +73,60 @@
}, 5000);
}
// Função para buscar material por código de barras (entrada)
async function buscarMaterialPorCodigoBarrasEntrada(codigoBarras: string) {
if (!codigoBarras.trim() || codigoBarras.trim().length < 8) {
mostrarMensagem('error', 'Código de barras inválido');
return;
}
entradaBuscandoMaterial = true;
try {
const material = await client.query(api.almoxarifado.buscarMaterialPorCodigoBarras, {
codigoBarras: codigoBarras.trim()
});
if (material) {
entradaMaterialId = material._id;
mostrarMensagem('success', `Material encontrado: ${material.nome}`);
} else {
mostrarMensagem('error', 'Material não encontrado com este código de barras');
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Erro ao buscar material';
mostrarMensagem('error', message);
} finally {
entradaBuscandoMaterial = false;
}
}
// Função para buscar material por código de barras (saída)
async function buscarMaterialPorCodigoBarrasSaida(codigoBarras: string) {
if (!codigoBarras.trim() || codigoBarras.trim().length < 8) {
mostrarMensagem('error', 'Código de barras inválido');
return;
}
saidaBuscandoMaterial = true;
try {
const material = await client.query(api.almoxarifado.buscarMaterialPorCodigoBarras, {
codigoBarras: codigoBarras.trim()
});
if (material) {
saidaMaterialId = material._id;
mostrarMensagem('success', `Material encontrado: ${material.nome}`);
} else {
mostrarMensagem('error', 'Material não encontrado com este código de barras');
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Erro ao buscar material';
mostrarMensagem('error', message);
} finally {
saidaBuscandoMaterial = false;
}
}
async function registrarEntrada() {
if (!entradaMaterialId || entradaQuantidade <= 0 || !entradaMotivo.trim()) {
mostrarMensagem('error', 'Preencha todos os campos obrigatórios');
@@ -249,6 +308,15 @@
<h2 class="text-2xl font-bold text-base-content">Registrar Entrada de Material</h2>
</div>
<form onsubmit={(e) => { e.preventDefault(); registrarEntrada(); }}>
<!-- Leitor de Código de Barras -->
<div class="mb-6 rounded-xl border border-base-300 bg-base-200/50 p-4">
<BarcodeScanner
enabled={entradaScannerEnabled}
onScan={buscarMaterialPorCodigoBarrasEntrada}
onError={(error) => mostrarMensagem('error', error)}
/>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Material -->
<div class="form-control md:col-span-2">
@@ -256,9 +324,12 @@
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Material <span class="text-error">*</span>
{#if entradaBuscandoMaterial}
<span class="loading loading-spinner loading-xs"></span>
{/if}
</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={entradaMaterialId} required>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12 {entradaBuscandoMaterial ? 'input-info' : ''}" bind:value={entradaMaterialId} required>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
@@ -268,6 +339,15 @@
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">
{#if entradaBuscandoMaterial}
<span class="text-info">Buscando material...</span>
{:else}
Selecione manualmente ou use o leitor de código de barras acima
{/if}
</span>
</label>
</div>
<!-- Quantidade -->
@@ -371,6 +451,15 @@
<h2 class="text-2xl font-bold text-base-content">Registrar Saída de Material</h2>
</div>
<form onsubmit={(e) => { e.preventDefault(); registrarSaida(); }}>
<!-- Leitor de Código de Barras -->
<div class="mb-6 rounded-xl border border-base-300 bg-base-200/50 p-4">
<BarcodeScanner
enabled={saidaScannerEnabled}
onScan={buscarMaterialPorCodigoBarrasSaida}
onError={(error) => mostrarMensagem('error', error)}
/>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Material -->
<div class="form-control md:col-span-2">
@@ -378,9 +467,12 @@
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Material <span class="text-error">*</span>
{#if saidaBuscandoMaterial}
<span class="loading loading-spinner loading-xs"></span>
{/if}
</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={saidaMaterialId} required>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12 {saidaBuscandoMaterial ? 'input-info' : ''}" bind:value={saidaMaterialId} required>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
@@ -390,6 +482,15 @@
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">
{#if saidaBuscandoMaterial}
<span class="text-info">Buscando material...</span>
{:else}
Selecione manualmente ou use o leitor de código de barras acima
{/if}
</span>
</label>
</div>
<!-- Quantidade -->

View File

@@ -784,7 +784,7 @@
<h2 class="text-xl font-bold text-base-content">Estatísticas Gerais</h2>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="card bg-gradient-to-br from-primary/10 via-primary/5 to-base-100 border border-primary/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105">
<div class="card bg-gradient-to-br from-primary/10 via-primary/5 to-base-100 border border-primary/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex-1">
@@ -844,6 +844,7 @@
</div>
</div>
</div>
</div>
{/if}
<!-- Relatórios Disponíveis -->