feat: enhance 'Almoxarifado' UI with improved styling, updated component layouts, and added barcode functionality for better inventory management and user experience

This commit is contained in:
2025-12-22 00:08:13 -03:00
parent ef9dbedb34
commit ae4f8fc6b3
10 changed files with 1283 additions and 753 deletions

View File

@@ -71,11 +71,7 @@
reader.readAsDataURL(file);
}
function resizeImage(
dataUrl: string,
maxWidth: number,
maxHeight: number
): Promise<string> {
function resizeImage(dataUrl: string, maxWidth: number, maxHeight: number): Promise<string> {
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<void>((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}
<div class="relative inline-block">
<img src={preview} alt="Preview da imagem do produto" class="max-w-full max-h-64 rounded-lg" />
<img
src={preview}
alt="Preview da imagem do produto"
class="max-h-64 max-w-full rounded-lg"
/>
<button
type="button"
class="btn btn-sm btn-circle btn-error absolute top-2 right-2"
@@ -318,7 +322,7 @@
{:else}
<div class="flex flex-col gap-4">
<div
class="border-2 border-dashed border-base-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary transition-colors"
class="border-base-300 hover:border-primary cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors"
onclick={triggerFileInput}
role="button"
tabindex="0"
@@ -329,18 +333,14 @@
}
}}
>
<Upload class="h-12 w-12 mx-auto mb-4 text-base-content/40" />
<p class="text-base-content/70 font-medium mb-2">Clique para fazer upload da imagem</p>
<p class="text-sm text-base-content/50">
<Upload class="text-base-content/40 mx-auto mb-4 h-12 w-12" />
<p class="text-base-content/70 mb-2 font-medium">Clique para fazer upload da imagem</p>
<p class="text-base-content/50 text-sm">
PNG, JPG ou GIF até {maxSizeMB}MB
</p>
</div>
<div class="divider text-sm">ou</div>
<button
type="button"
class="btn btn-outline btn-primary w-full"
onclick={openCamera}
>
<button type="button" class="btn btn-outline btn-primary w-full" onclick={openCamera}>
<Camera class="h-5 w-5" />
Capturar da Câmera
</button>
@@ -354,7 +354,7 @@
{/if}
{#if preview}
<div class="flex gap-2 mt-4">
<div class="mt-4 flex gap-2">
<button
type="button"
class="btn btn-sm btn-outline btn-primary flex-1"
@@ -363,11 +363,7 @@
<ImageIcon class="h-4 w-4" />
Alterar Imagem
</button>
<button
type="button"
class="btn btn-sm btn-outline btn-primary flex-1"
onclick={openCamera}
>
<button type="button" class="btn btn-sm btn-outline btn-primary flex-1" onclick={openCamera}>
<Camera class="h-4 w-4" />
Capturar Foto
</button>
@@ -377,12 +373,15 @@
<!-- Modal da Câmera -->
{#if showCamera}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80" onclick={closeCamera}>
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
onclick={closeCamera}
>
<div
class="bg-base-100 rounded-lg shadow-2xl p-6 max-w-2xl w-full mx-4"
class="bg-base-100 mx-4 w-full max-w-2xl rounded-lg p-6 shadow-2xl"
onclick={(e) => e.stopPropagation()}
>
<div class="flex items-center justify-between mb-4">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-xl font-bold">Capturar Foto</h3>
<button
type="button"
@@ -394,19 +393,24 @@
</button>
</div>
<div class="relative bg-black rounded-lg overflow-hidden mb-4" style="aspect-ratio: 4/3; min-height: 300px;">
<div
class="relative mb-4 overflow-hidden rounded-lg bg-black"
style="aspect-ratio: 4/3; min-height: 300px;"
>
{#if showCamera}
<video
bind:this={videoElement}
autoplay
playsinline
muted
class="w-full h-full object-cover"
style="transform: scaleX(-1); opacity: {capturing ? '1' : '0'}; transition: opacity 0.3s;"
class="h-full w-full object-cover"
style="transform: scaleX(-1); opacity: {capturing
? '1'
: '0'}; transition: opacity 0.3s;"
></video>
{/if}
{#if !capturing}
<div class="flex items-center justify-center h-full absolute inset-0 z-10">
<div class="absolute inset-0 z-10 flex h-full items-center justify-center">
<div class="text-center">
<span class="loading loading-spinner loading-lg text-primary mb-2"></span>
<p class="text-base-content/70 text-sm">Iniciando câmera...</p>
@@ -415,20 +419,9 @@
{/if}
</div>
<div class="flex gap-2 justify-end">
<button
type="button"
class="btn btn-ghost"
onclick={closeCamera}
>
Cancelar
</button>
<button
type="button"
class="btn btn-primary"
onclick={capturePhoto}
disabled={!capturing}
>
<div class="flex justify-end gap-2">
<button type="button" class="btn btn-ghost" onclick={closeCamera}> Cancelar </button>
<button type="button" class="btn btn-primary" onclick={capturePhoto} disabled={!capturing}>
<Camera class="h-5 w-5" />
Capturar Foto
</button>
@@ -442,4 +435,3 @@
width: 100%;
}
</style>

View File

@@ -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 @@
</div>
</button>
<button
class="card bg-base-100 border border-base-300 shadow-xl hover:shadow-2xl hover:border-secondary/50 transition-all duration-300 hover:scale-[1.02] group"
onclick={() => goto('/almoxarifado/materiais')}
>
<div class="card-body">
<div class="flex items-center gap-4 mb-2">
<div class="rounded-xl bg-secondary/20 p-3 group-hover:bg-secondary/30 transition-colors">
<List class="h-7 w-7 text-secondary" strokeWidth={2.5} />
</div>
<h3 class="card-title text-lg mb-0">Listar Materiais</h3>
</div>
<p class="text-base-content/70 text-sm">Visualizar e gerenciar materiais cadastrados</p>
</div>
</button>
<button
class="card bg-base-100 border border-base-300 shadow-xl hover:shadow-2xl hover:border-success/50 transition-all duration-300 hover:scale-[1.02] group"
onclick={() => goto('/almoxarifado/relatorios')}
@@ -337,21 +352,6 @@
<p class="text-base-content/70 text-sm">Visualizar relatórios e estatísticas</p>
</div>
</button>
<button
class="card bg-base-100 border border-base-300 shadow-xl hover:shadow-2xl hover:border-warning/50 transition-all duration-300 hover:scale-[1.02] group"
onclick={() => goto('/ti/configuracoes-almoxarifado')}
>
<div class="card-body">
<div class="flex items-center gap-4 mb-2">
<div class="rounded-xl bg-warning/20 p-3 group-hover:bg-warning/30 transition-colors">
<Settings class="h-7 w-7 text-warning" strokeWidth={2.5} />
</div>
<h3 class="card-title text-lg mb-0">Configurações</h3>
</div>
<p class="text-base-content/70 text-sm">Configurar sistema de almoxarifado</p>
</div>
</button>
</div>
</main>

View File

@@ -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 @@
<!-- Cabeçalho -->
<div class="mb-8">
<div class="flex items-center gap-4">
<div class="rounded-2xl bg-gradient-to-br from-warning/20 to-warning/30 p-4 shadow-lg">
<div class="rounded-2xl bg-gradient-to-br from-warning/20 via-warning/10 to-warning/5 p-4 shadow-lg border border-warning/20">
<AlertTriangle class="h-10 w-10 text-warning" strokeWidth={2.5} />
</div>
<div>
<h1 class="text-3xl font-bold tracking-tight">Alertas de Estoque</h1>
<p class="text-base-content/70 text-lg">Visualize e gerencie alertas de estoque baixo</p>
<div class="flex-1">
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-warning to-warning/70 bg-clip-text text-transparent">
Alertas de Estoque
</h1>
<p class="text-base-content/70 text-lg mt-1">Visualize e gerencie alertas de estoque baixo</p>
</div>
</div>
</div>
@@ -132,57 +134,74 @@
{/if}
<!-- Filtros -->
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
<div class="card-body">
<h3 class="text-lg font-semibold mb-4">Filtros</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
<div class="card-body p-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
<div class="rounded-lg bg-primary/10 p-2.5">
<Filter class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Filtros de Busca</h3>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="form-control">
<label class="label">
<span class="label-text">Tipo de Alerta</span>
<label class="label pb-2">
<span class="label-text font-semibold">Tipo de Alerta</span>
</label>
<select class="select select-bordered" bind:value={filtroTipo}>
<option value="">Todos</option>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroTipo}>
<option value="">Todos os tipos</option>
<option value="estoque_zerado">Estoque Zerado</option>
<option value="estoque_minimo">Estoque Mínimo</option>
<option value="reposicao_necessaria">Reposição Necessária</option>
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Filtre por tipo de alerta</span>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Status</span>
<label class="label pb-2">
<span class="label-text font-semibold">Status</span>
</label>
<select class="select select-bordered" bind:value={filtroStatus}>
<option value="">Todos</option>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroStatus}>
<option value="">Todos os status</option>
<option value="ativo">Ativo</option>
<option value="resolvido">Resolvido</option>
<option value="ignorado">Ignorado</option>
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Filtre por status do alerta</span>
</label>
</div>
</div>
</div>
</div>
<!-- Lista de Alertas -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="card bg-base-100 border border-base-300 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-warning/10 p-2.5">
<AlertTriangle class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Lista de Alertas</h3>
</div>
{#if alertasQuery === undefined}
<div class="flex items-center justify-center py-12">
<div class="flex items-center justify-center py-16">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if alertasQuery.data && alertasQuery.data.length > 0}
<div class="overflow-x-auto">
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-semibold">Material</th>
<th class="font-semibold">Tipo</th>
<th class="font-semibold">Quantidade Atual</th>
<th class="font-semibold">Quantidade Mínima</th>
<th class="font-semibold">Diferença</th>
<th class="font-semibold">Status</th>
<th class="font-semibold">Data</th>
<th class="font-semibold">Ações</th>
<th class="font-bold text-base-content">Material</th>
<th class="font-bold text-base-content">Tipo</th>
<th class="font-bold text-base-content">Quantidade Atual</th>
<th class="font-bold text-base-content">Quantidade Mínima</th>
<th class="font-bold text-base-content">Diferença</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content">Data</th>
<th class="font-bold text-base-content">Ações</th>
</tr>
</thead>
<tbody>
@@ -224,15 +243,17 @@
{#if alerta.status === 'ativo'}
<div class="flex gap-2">
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success transition-all"
onclick={() => resolverAlerta(alerta._id)}
title="Resolver alerta"
>
<CheckCircle class="h-4 w-4" />
Resolver
</button>
<button
class="btn btn-sm btn-ghost hover:btn-error"
class="btn btn-sm btn-ghost hover:btn-error transition-all"
onclick={() => abrirModalIgnorar(alerta._id)}
title="Ignorar alerta"
>
<XCircle class="h-4 w-4" />
Ignorar
@@ -245,11 +266,19 @@
</tbody>
</table>
</div>
{#if alertasQuery.data.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">{alertasQuery.data.length}</span> alerta{alertasQuery.data.length !== 1 ? 's' : ''}
</div>
</div>
{/if}
{:else}
<div class="text-center py-12">
<AlertTriangle class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
<h3 class="text-2xl font-bold mb-2">Nenhum alerta encontrado</h3>
<p class="text-base-content/70 text-lg mb-4">
<div class="text-center py-16">
<AlertTriangle class="mx-auto mb-6 h-24 w-24 text-base-content/30" />
<h3 class="text-2xl font-bold mb-3 text-base-content">Nenhum alerta encontrado</h3>
<p class="text-base-content/70 text-lg mb-6">
{#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}
</p>
<div class="alert alert-info max-w-2xl mx-auto">
<AlertTriangle class="h-6 w-6" />
<div class="alert alert-info max-w-2xl mx-auto border-info/30 bg-info/10">
<AlertTriangle class="h-6 w-6 text-info shrink-0" />
<div class="text-left">
<h4 class="font-bold mb-2">Como os alertas funcionam?</h4>
<ul class="text-sm space-y-1 list-disc list-inside">
<h4 class="font-bold mb-3 text-base-content">Como os alertas funcionam?</h4>
<ul class="text-sm space-y-2 list-disc list-inside text-base-content/80">
<li>Os alertas são criados automaticamente quando o estoque de um material fica abaixo do mínimo configurado</li>
<li>O sistema permite apenas <strong>um alerta ativo por material</strong> para evitar duplicações</li>
<li>Quando o estoque volta ao normal, você pode resolver o alerta manualmente</li>

View File

@@ -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<Array<Doc<'materiais'>>>([]);
let filtroBusca = $state('');
let filtroCategoria = $state('');
@@ -253,15 +264,17 @@
<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-amber-500/20 to-amber-600/30 p-4 shadow-lg">
<Package class="h-10 w-10 text-amber-600" strokeWidth={2.5} />
<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>
<div>
<h1 class="text-3xl font-bold tracking-tight">Materiais</h1>
<p class="text-base-content/70 text-lg">Gerencie o cadastro de materiais do almoxarifado</p>
<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">
Materiais
</h1>
<p class="text-base-content/70 text-lg mt-1">Gerencie o cadastro e controle de materiais do almoxarifado</p>
</div>
</div>
<button class="btn btn-primary shadow-lg hover:shadow-xl transition-all" onclick={navCadastro}>
<button class="btn btn-primary btn-lg shadow-lg hover:shadow-xl transition-all min-w-[200px]" onclick={navCadastro}>
<Plus class="h-5 w-5" />
Cadastrar Material
</button>
@@ -276,41 +289,52 @@
{/if}
<!-- Filtros -->
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Filtros de Busca</h3>
<div class="card bg-base-100 border border-base-300 mb-8 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="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>
<h3 class="text-xl font-bold text-base-content">Filtros de Busca</h3>
</div>
<BarcodeScanner
enabled={scannerEnabled}
onScan={handleBarcodeScanned}
onError={(error) => console.error('Erro no scanner:', error)}
/>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="grid grid-cols-1 gap-6 md:grid-cols-4">
<div class="form-control">
<label class="label">
<span class="label-text">Buscar</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<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" />
<input
type="text"
placeholder="Código, nome ou código de barras..."
class="input input-bordered w-full pl-10 {buscandoPorCodigoBarras ? 'input-info' : ''}"
class="input input-bordered w-full pl-10 h-12 focus:input-primary 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>
{/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>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Categoria</span>
<label class="label pb-2">
<span class="label-text font-semibold">Categoria</span>
</label>
<select class="select select-bordered" bind:value={filtroCategoria}>
<option value="">Todas</option>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroCategoria}>
<option value="">Todas as categorias</option>
{#each categorias as cat}
<option value={cat}>{cat}</option>
{/each}
@@ -318,10 +342,10 @@
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Status</span>
<label class="label pb-2">
<span class="label-text font-semibold">Status</span>
</label>
<select class="select select-bordered" bind:value={filtroAtivo}>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroAtivo}>
<option value="">Todos</option>
<option value={true}>Ativos</option>
<option value={false}>Inativos</option>
@@ -329,9 +353,12 @@
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" class="checkbox" bind:checked={filtroEstoqueBaixo} />
<span class="label-text">Apenas estoque baixo</span>
<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} />
<span class="label-text font-medium">Apenas estoque baixo</span>
</label>
</div>
</div>
@@ -339,30 +366,36 @@
</div>
<!-- Tabela -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="overflow-x-auto">
<div class="card bg-base-100 border border-base-300 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>
<h3 class="text-xl font-bold text-base-content">Lista de Materiais</h3>
</div>
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-semibold">Código</th>
<th class="font-semibold">Nome</th>
<th class="font-semibold">Categoria</th>
<th class="font-semibold">Estoque Atual</th>
<th class="font-semibold">Estoque Mínimo</th>
<th class="font-semibold">Unidade</th>
<th class="font-semibold">Status</th>
<th class="font-semibold">Ações</th>
<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>
</tr>
</thead>
<tbody>
{#if filtered.length === 0}
<tr>
<td colspan="8" class="text-center">
<div class="py-12">
<Package class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
<p class="text-base-content/70 text-lg font-medium">Nenhum material encontrado</p>
<p class="text-base-content/50 text-sm mt-2">Tente ajustar os filtros de busca</p>
<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>
</div>
</td>
</tr>
@@ -405,8 +438,8 @@
<td>
<div class="flex gap-2">
<button
class="btn btn-sm btn-ghost hover:btn-primary"
title="Visualizar"
class="btn btn-sm btn-ghost hover:btn-primary transition-all"
title="Visualizar detalhes"
onclick={() =>
goto(
resolve(
@@ -418,8 +451,8 @@
<Eye class="h-4 w-4" />
</button>
<button
class="btn btn-sm btn-ghost hover:btn-info"
title="Editar"
class="btn btn-sm btn-ghost hover:btn-info transition-all"
title="Editar material"
onclick={() =>
goto(
resolve(
@@ -432,8 +465,8 @@
<Edit class="h-4 w-4" />
</button>
<button
class="btn btn-sm btn-ghost hover:btn-error"
title="Excluir"
class="btn btn-sm btn-ghost hover:btn-error transition-all"
title="Excluir material"
onclick={() => abrirModalExclusao(material)}
>
<Trash2 class="h-4 w-4" />
@@ -448,9 +481,9 @@
</div>
{#if filtered.length > 0}
<div class="mt-6 flex items-center justify-between border-t border-base-300 pt-4">
<div class="text-sm text-base-content/70">
Mostrando <span class="font-semibold text-base-content">{filtered.length}</span> de <span class="font-semibold text-base-content">{materiais.length}</span> materiais
<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>
</div>
{/if}
@@ -458,29 +491,42 @@
</div>
<!-- Modal de Confirmação de Exclusão -->
<dialog id="modal-excluir-material" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold mb-4">Confirmar Exclusão</h3>
<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>
<div>
<h3 class="text-2xl font-bold text-base-content">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">
<div class="alert alert-warning">
<AlertTriangle class="h-5 w-5" />
<div>
<p class="font-semibold">Atenção!</p>
<p class="text-sm">
Esta ação não pode ser desfeita. O material será permanentemente excluído.
<div class="space-y-4 mb-6">
<div class="alert alert-warning border-warning/30 bg-warning/10">
<AlertTriangle class="h-5 w-5 shrink-0 text-warning" />
<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>
</div>
</div>
<div class="bg-base-200 rounded-lg p-4">
<p class="text-sm text-base-content/70 mb-2">Material a ser excluído:</p>
<p class="font-semibold text-base-content">{materialParaExcluir.nome}</p>
<p class="text-sm text-base-content/60 mt-1">
Código: <span class="font-mono">{materialParaExcluir.codigo}</span>
<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">
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>
{/if}
{#if materialParaExcluir.estoqueAtual > 0}
<div class="mt-2 alert alert-info py-2">
<p class="text-xs">
<div class="mt-3 alert alert-info py-2 border-info/30 bg-info/10">
<p class="text-sm font-medium">
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades em estoque.
</p>
</div>
@@ -488,16 +534,16 @@
</div>
</div>
{/if}
<div class="modal-action">
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
<button
class="btn btn-ghost"
class="btn btn-ghost btn-lg min-w-[140px]"
onclick={fecharModalExclusao}
disabled={excluindo}
>
Cancelar
</button>
<button
class="btn btn-error"
class="btn btn-error btn-lg min-w-[180px] shadow-lg hover:shadow-xl"
onclick={confirmarExclusao}
disabled={excluindo}
>
@@ -505,8 +551,8 @@
<span class="loading loading-spinner loading-sm"></span>
Excluindo...
{:else}
<Trash2 class="h-4 w-4" />
Excluir
<Trash2 class="h-5 w-5" />
Excluir Material
{/if}
</button>
</div>
@@ -518,9 +564,9 @@
<!-- Modal de Erro na Exclusão -->
{#if erroExclusao}
<dialog id="modal-erro-exclusao" class="modal modal-open">
<div class="modal-box max-w-2xl">
<div class="flex items-center gap-4 mb-6">
<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>
@@ -540,21 +586,26 @@
</div>
{#if materialParaExcluir}
<div class="bg-base-200 rounded-lg p-4 border border-base-300">
<p class="text-sm text-base-content/70 mb-2 font-semibold">Material:</p>
<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-1">
<p class="text-sm text-base-content/60 mt-2">
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>
{/if}
</div>
{/if}
<div class="bg-info/10 border border-info/30 rounded-lg p-4">
<div class="bg-info/10 border border-info/30 rounded-lg p-5">
<div class="flex gap-3">
<Info class="h-5 w-5 text-info shrink-0 mt-0.5" />
<div>
<p class="font-semibold text-base-content mb-1">Solução recomendada</p>
<p class="text-sm text-base-content/80">
<p class="font-semibold text-base-content mb-2">Solução recomendada</p>
<p class="text-sm text-base-content/80 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.
{:else if erroExclusao.tipo === 'requisicoes'}
@@ -568,9 +619,9 @@
</div>
</div>
<div class="modal-action gap-3">
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
<button
class="btn btn-ghost"
class="btn btn-ghost btn-lg min-w-[140px]"
onclick={fecharModalErro}
disabled={desativando}
>
@@ -578,7 +629,7 @@
</button>
{#if erroExclusao.tipo === 'movimentacoes' || erroExclusao.tipo === 'requisicoes'}
<button
class="btn btn-warning gap-2"
class="btn btn-warning btn-lg min-w-[200px] gap-2 shadow-lg hover:shadow-xl"
onclick={desativarMaterial}
disabled={desativando}
>
@@ -586,7 +637,7 @@
<span class="loading loading-spinner loading-sm"></span>
Desativando...
{:else}
<X class="h-4 w-4" />
<X class="h-5 w-5" />
Desativar Material
{/if}
</button>
@@ -601,3 +652,4 @@
</main>

View File

@@ -21,6 +21,8 @@
let estoqueMaximo = $state<number | undefined>(undefined);
let localizacao = $state('');
let fornecedor = $state('');
let codigoBarras = $state('');
let imagemBase64 = $state<string | null>(null);
let ativo = $state(true);
let loading = $state(false);
let loadingData = $state(true);
@@ -69,6 +71,8 @@
estoqueMaximo = material.estoqueMaximo;
localizacao = material.localizacao || '';
fornecedor = material.fornecedor || '';
codigoBarras = material.codigoBarras || '';
imagemBase64 = material.imagemBase64 || null;
ativo = material.ativo;
} catch (error) {
console.error('Erro ao carregar material:', error);
@@ -112,6 +116,8 @@
estoqueMaximo,
localizacao: localizacao.trim() || undefined,
fornecedor: fornecedor.trim() || undefined,
codigoBarras: codigoBarras.trim() || undefined,
imagemBase64: imagemBase64 || undefined,
ativo
});
@@ -323,6 +329,33 @@
/>
</div>
<!-- Código de Barras -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Código de Barras</span>
</label>
<input
type="text"
class="input input-bordered"
placeholder="Código de barras (opcional)"
bind:value={codigoBarras}
/>
<label class="label">
<span class="label-text-alt">Código EAN, UPC ou similar</span>
</label>
</div>
<!-- Imagem do Produto -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Imagem do Produto</span>
</label>
<ImageUpload bind:value={imagemBase64} />
<label class="label">
<span class="label-text-alt">Upload opcional da imagem do produto</span>
</label>
</div>
<!-- Status Ativo -->
<div class="form-control md:col-span-2">
<label class="label cursor-pointer justify-start gap-2">

View File

@@ -4,7 +4,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { tick } from 'svelte';
import { Package, Save, ArrowLeft, Check, X, ExternalLink, Loader2, AlertCircle, Info } from 'lucide-svelte';
import { Package, Save, ArrowLeft, Check, X, ExternalLink, Loader2, AlertCircle, Info, Barcode, Box, Warehouse, ShoppingCart, Image } from 'lucide-svelte';
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
@@ -130,7 +130,7 @@
fornecedor = dadosExternos.marca;
}
// Carregar imagem se disponível (aguardar conversão para garantir que seja salva e exibida)
// Carregar imagem se disponível - garantir que seja carregada antes de fechar o modal
if (dadosExternos.imagemUrl) {
let imagemParaSalvar: string | null = null;
const imagemUrlAtual = dadosExternos.imagemUrl;
@@ -141,16 +141,15 @@
imagemParaSalvar = imagemUrlAtual;
console.log('Imagem já carregada em background, usando diretamente');
} else {
// Ainda é uma URL, aguardar um pouco para ver se o carregamento em background terminou
// Se após 500ms ainda for URL, carregar agora
await new Promise((resolve) => setTimeout(resolve, 500));
// Ainda é uma URL, tentar aguardar um pouco para ver se o carregamento em background terminou
await new Promise((resolve) => setTimeout(resolve, 1000));
// Verificar novamente se foi carregada em background
if (dadosExternos.imagemUrl && dadosExternos.imagemUrl.startsWith('data:')) {
imagemParaSalvar = dadosExternos.imagemUrl;
console.log('Imagem foi carregada em background durante a espera');
} else {
// Ainda é URL, carregar agora
// Ainda é URL, carregar agora de forma síncrona
console.log('Carregando imagem da URL:', imagemUrlAtual);
try {
const imagemBase64Carregada = await carregarImagemDeUrl(imagemUrlAtual);
@@ -168,16 +167,22 @@
// Atribuir a imagem após o carregamento completo
if (imagemParaSalvar) {
// Atribuir diretamente ao estado - isso deve atualizar o componente ImageUpload automaticamente
imagemBase64 = imagemParaSalvar;
// Aguardar o tick para garantir que o componente ImageUpload detecte a mudança
await tick();
// Aguardar um frame adicional para garantir renderização
await new Promise((resolve) => requestAnimationFrame(resolve));
console.log('Imagem atribuída ao campo imagemBase64, tamanho:', imagemParaSalvar.length, 'caracteres');
} else {
console.warn('Nenhuma imagem disponível para salvar');
}
}
// Fechar modal
// Fechar modal após garantir que tudo foi carregado
modalDadosExternos = false;
const dadosTemp = dadosExternos;
dadosExternos = null;
@@ -454,12 +459,14 @@
>
<ArrowLeft class="h-5 w-5" />
</button>
<div class="rounded-2xl bg-gradient-to-br from-amber-500/20 to-amber-600/30 p-4 shadow-lg">
<Package class="h-10 w-10 text-amber-600" strokeWidth={2.5} />
<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>
<div>
<h1 class="text-3xl font-bold tracking-tight">Cadastrar Material</h1>
<p class="text-base-content/70 text-lg">Adicione um novo material ao almoxarifado</p>
<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">
Cadastrar Material
</h1>
<p class="text-base-content/70 text-lg mt-1">Preencha as informações abaixo para adicionar um novo material ao estoque</p>
</div>
</div>
</div>
@@ -472,225 +479,285 @@
{/if}
<!-- Formulário -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-8">
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<!-- Leitor de Código de Barras -->
<div class="mb-6">
<BarcodeScanner
enabled={scannerEnabled}
onScan={handleBarcodeScanned}
onError={(error) => mostrarMensagem('error', error)}
/>
<!-- Seção: Identificação -->
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
<div class="rounded-lg bg-primary/10 p-2.5">
<Box class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<h2 class="text-xl font-bold text-base-content">Identificação do Material</h2>
</div>
<!-- Leitor de Código de Barras -->
<div class="mb-8 rounded-xl border border-base-300 bg-base-200/50 p-4">
<BarcodeScanner
enabled={scannerEnabled}
onScan={handleBarcodeScanned}
onError={(error) => mostrarMensagem('error', error)}
/>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Código -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Código <span class="text-error">*</span>
</span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Ex: MAT-001"
bind:value={codigo}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Código único identificador do material</span>
</label>
</div>
<!-- Código de Barras -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Barcode class="h-4 w-4" />
Código de Barras
{#if buscandoProduto || buscandoExterno}
<span class="loading loading-spinner loading-xs"></span>
{/if}
</span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12 {(buscandoProduto || buscandoExterno) ? 'input-info' : ''}"
placeholder="EAN-13, UPC, etc."
bind:value={codigoBarras}
onkeydown={handleBarcodeKeydown}
oninput={() => verificarEExecutarBusca(codigoBarras)}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">
{#if buscandoProduto}
<span class="text-info">Buscando produto no banco de dados...</span>
{:else if buscandoExterno}
<span class="text-info">Buscando produto em base externa...</span>
{:else}
Digite ou use o leitor acima para escanear
{/if}
</span>
</label>
</div>
<!-- Nome -->
<div class="form-control md:col-span-2">
<label class="label pb-2">
<span class="label-text font-semibold">Nome do Material <span class="text-error">*</span></span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Digite o nome completo do material"
bind:value={nome}
required
/>
</div>
<!-- Descrição -->
<div class="form-control md:col-span-2">
<label class="label pb-2">
<span class="label-text font-semibold">Descrição</span>
</label>
<textarea
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
placeholder="Descrição detalhada do material (opcional)"
bind:value={descricao}
rows="3"
></textarea>
</div>
<!-- Categoria -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Categoria <span class="text-error">*</span></span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
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">
<label class="label pb-2">
<span class="label-text font-semibold">Unidade de Medida <span class="text-error">*</span></span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={unidadeMedida} required>
{#each unidadesMedida as un}
<option value={un}>{un}</option>
{/each}
</select>
</div>
</div>
</div>
<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>
<!-- Seção: Estoque -->
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b-2 border-success/20 pb-4">
<div class="rounded-lg bg-success/10 p-2.5">
<Warehouse class="h-5 w-5 text-success" strokeWidth={2.5} />
</div>
<h2 class="text-xl font-bold text-base-content">Controle de Estoque</h2>
</div>
<!-- Código de Barras -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Código de Barras</span>
{#if buscandoProduto || buscandoExterno}
<span class="loading loading-spinner loading-xs"></span>
{/if}
</label>
<input
type="text"
class="input input-bordered {(buscandoProduto || buscandoExterno) ? 'input-info' : ''}"
placeholder="EAN-13, UPC, etc."
bind:value={codigoBarras}
onkeydown={handleBarcodeKeydown}
oninput={() => verificarEExecutarBusca(codigoBarras)}
/>
<label class="label">
<span class="label-text-alt">
{#if buscandoProduto}
Buscando produto no banco de dados...
{:else if buscandoExterno}
Buscando produto em base externa...
{:else}
Digite ou use o leitor acima para escanear
{/if}
</span>
</label>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Estoque Mínimo -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Estoque Mínimo <span class="text-error">*</span></span>
</label>
<input
type="number"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0"
bind:value={estoqueMinimo}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Quantidade mínima para alerta</span>
</label>
</div>
<!-- Estoque Máximo -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Estoque Máximo</span>
</label>
<input
type="number"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0"
bind:value={estoqueMaximo}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Opcional - Capacidade máxima</span>
</label>
</div>
<!-- Estoque Atual -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Estoque Inicial</span>
</label>
<input
type="number"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0"
bind:value={estoqueAtual}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Quantidade inicial em estoque</span>
</label>
</div>
<!-- Localização -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Localização Física</span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Ex: Prateleira A-01, Setor B"
bind:value={localizacao}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Onde o material está armazenado</span>
</label>
</div>
</div>
</div>
<!-- Seção: Informações Adicionais -->
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
<div class="rounded-lg bg-info/10 p-2.5">
<ShoppingCart class="h-5 w-5 text-info" strokeWidth={2.5} />
</div>
<h2 class="text-xl font-bold text-base-content">Informações Adicionais</h2>
</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 class="grid grid-cols-1 gap-6">
<!-- Fornecedor -->
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Fornecedor</span>
</label>
<input
type="text"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Nome do fornecedor (opcional)"
bind:value={fornecedor}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Fornecedor principal do material</span>
</label>
</div>
</div>
</div>
<!-- Seção: Imagem -->
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
<div class="rounded-lg bg-warning/10 p-2.5">
<Image class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<h2 class="text-xl font-bold text-base-content">Imagem do Produto</h2>
</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>
<!-- Estoque Atual -->
<div class="form-control md:col-span-1">
<label class="label">
<span class="label-text">Estoque Inicial</span>
</label>
<input
type="number"
class="input input-bordered"
min="0"
bind:value={estoqueAtual}
/>
<label class="label">
<span class="label-text-alt">Quantidade inicial em estoque</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-2">
<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>
<!-- Imagem do Produto -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Imagem do Produto</span>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Upload de Imagem</span>
</label>
<ImageUpload bind:value={imagemBase64} />
<label class="label">
<span class="label-text-alt">Upload opcional da imagem do produto</span>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Upload opcional da imagem do produto para melhor identificação</span>
</label>
</div>
</div>
<!-- Botões -->
<div class="card-actions mt-6 justify-end">
<div class="card-actions mt-10 justify-end gap-4 border-t-2 border-base-300 pt-8">
<button
type="button"
class="btn btn-ghost"
class="btn btn-ghost btn-lg min-w-[140px]"
onclick={() => goto(resolve('/almoxarifado/materiais'))}
disabled={loading}
>
<ArrowLeft class="h-5 w-5" />
Cancelar
</button>
<button type="submit" class="btn btn-primary" disabled={loading}>
<button type="submit" class="btn btn-primary btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={loading}>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
Cadastrando...
{:else}
<Save class="h-5 w-5" />
Cadastrar Material
{/if}
Cadastrar
</button>
</div>
</form>

View File

@@ -3,7 +3,7 @@
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import { resolve } from '$app/paths';
import { ArrowLeftRight, ArrowDown, ArrowUp, Settings, History } from 'lucide-svelte';
import { ArrowLeftRight, ArrowDown, ArrowUp, Settings, History, Package, FileText, User, Building2, AlertCircle } from 'lucide-svelte';
const client = useConvexClient();
@@ -37,7 +37,7 @@
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, { ativo: true });
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const setoresQuery = useQuery(api.setores.list, {});
const movimentacoesQuery = useQuery(api.almoxarifado.listarMovimentacoes, {});
const movimentacoesQuery = useQuery(api.almoxarifado.listarMovimentacoesComHistorico, {});
// Criar mapa de funcionários para lookup eficiente
const funcionariosMap = $derived.by(() => {
@@ -187,12 +187,14 @@
<!-- Cabeçalho -->
<div class="mb-8">
<div class="flex items-center gap-4">
<div class="rounded-2xl bg-gradient-to-br from-info/20 to-info/30 p-4 shadow-lg">
<div class="rounded-2xl bg-gradient-to-br from-info/20 via-info/10 to-info/5 p-4 shadow-lg border border-info/20">
<ArrowLeftRight class="h-10 w-10 text-info" strokeWidth={2.5} />
</div>
<div>
<h1 class="text-3xl font-bold tracking-tight">Movimentações de Estoque</h1>
<p class="text-base-content/70 text-lg">Registre entradas, saídas e ajustes de estoque</p>
<div class="flex-1">
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-info to-info/70 bg-clip-text text-transparent">
Movimentações de Estoque
</h1>
<p class="text-base-content/70 text-lg mt-1">Registre entradas, saídas e ajustes de estoque do almoxarifado</p>
</div>
</div>
</div>
@@ -205,30 +207,30 @@
{/if}
<!-- Abas -->
<div class="tabs tabs-boxed mb-6 bg-base-200 shadow-md">
<div class="tabs tabs-boxed mb-8 bg-base-200 shadow-lg rounded-xl p-1">
<button
class="tab {abaAtiva === 'entrada' ? 'tab-active' : ''} transition-all"
class="tab {abaAtiva === 'entrada' ? 'tab-active' : ''} transition-all font-semibold"
onclick={() => (abaAtiva = 'entrada')}
>
<ArrowDown class="h-5 w-5 mr-2" />
Entrada
</button>
<button
class="tab {abaAtiva === 'saida' ? 'tab-active' : ''} transition-all"
class="tab {abaAtiva === 'saida' ? 'tab-active' : ''} transition-all font-semibold"
onclick={() => (abaAtiva = 'saida')}
>
<ArrowUp class="h-5 w-5 mr-2" />
Saída
</button>
<button
class="tab {abaAtiva === 'ajuste' ? 'tab-active' : ''} transition-all"
class="tab {abaAtiva === 'ajuste' ? 'tab-active' : ''} transition-all font-semibold"
onclick={() => (abaAtiva = 'ajuste')}
>
<Settings class="h-5 w-5 mr-2" />
Ajuste
</button>
<button
class="tab {abaAtiva === 'historico' ? 'tab-active' : ''} transition-all"
class="tab {abaAtiva === 'historico' ? 'tab-active' : ''} transition-all font-semibold"
onclick={() => (abaAtiva = 'historico')}
>
<History class="h-5 w-5 mr-2" />
@@ -238,21 +240,25 @@
<!-- Conteúdo das Abas -->
{#if abaAtiva === 'entrada'}
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-6 text-xl">
<div class="rounded-lg bg-success/20 p-2">
<ArrowDown class="h-6 w-6 text-success" />
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-8">
<div class="mb-8 flex items-center gap-3 border-b-2 border-success/20 pb-4">
<div class="rounded-lg bg-success/10 p-2.5">
<ArrowDown class="h-6 w-6 text-success" strokeWidth={2.5} />
</div>
Registrar Entrada de Material
</h2>
<h2 class="text-2xl font-bold text-base-content">Registrar Entrada de Material</h2>
</div>
<form onsubmit={(e) => { e.preventDefault(); registrarEntrada(); }}>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Material -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Material *</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Material <span class="text-error">*</span>
</span>
</label>
<select class="select select-bordered" bind:value={entradaMaterialId} required>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={entradaMaterialId} required>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
@@ -264,51 +270,67 @@
</select>
</div>
<!-- Quantidade -->
<div class="form-control">
<label class="label">
<span class="label-text font-bold">Quantidade *</span>
<label class="label pb-2">
<span class="label-text font-semibold">Quantidade <span class="text-error">*</span></span>
</label>
<input
type="number"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0.01"
step="0.01"
bind:value={entradaQuantidade}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Quantidade a ser adicionada ao estoque</span>
</label>
</div>
<!-- Documento -->
<div class="form-control">
<label class="label">
<span class="label-text">Documento (NF, etc.)</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<FileText class="h-4 w-4" />
Documento
</span>
</label>
<input
type="text"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Número da nota fiscal"
bind:value={entradaDocumento}
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">NF, nota fiscal ou documento relacionado</span>
</label>
</div>
<!-- Motivo -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Motivo *</span>
<label class="label pb-2">
<span class="label-text font-semibold">Motivo <span class="text-error">*</span></span>
</label>
<input
type="text"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Ex: Compra, Doação, Devolução"
bind:value={entradaMotivo}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Razão da entrada do material no estoque</span>
</label>
</div>
<!-- Observações -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Observações</span>
<label class="label pb-2">
<span class="label-text font-semibold">Observações</span>
</label>
<textarea
class="textarea textarea-bordered"
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
placeholder="Observações adicionais (opcional)"
bind:value={entradaObservacoes}
rows="3"
@@ -316,35 +338,40 @@
</div>
</div>
<div class="card-actions mt-6 justify-end">
<button type="submit" class="btn btn-primary" disabled={entradaLoading}>
<div class="card-actions mt-8 justify-end gap-4 border-t-2 border-base-300 pt-6">
<button type="submit" class="btn btn-success btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={entradaLoading}>
{#if entradaLoading}
<span class="loading loading-spinner loading-sm"></span>
Registrando...
{:else}
<ArrowDown class="h-5 w-5" />
Registrar Entrada
{/if}
Registrar Entrada
</button>
</div>
</form>
</div>
</div>
{:else if abaAtiva === 'saida'}
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-6 text-xl">
<div class="rounded-lg bg-error/20 p-2">
<ArrowUp class="h-6 w-6 text-error" />
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-8">
<div class="mb-8 flex items-center gap-3 border-b-2 border-error/20 pb-4">
<div class="rounded-lg bg-error/10 p-2.5">
<ArrowUp class="h-6 w-6 text-error" strokeWidth={2.5} />
</div>
Registrar Saída de Material
</h2>
<h2 class="text-2xl font-bold text-base-content">Registrar Saída de Material</h2>
</div>
<form onsubmit={(e) => { e.preventDefault(); registrarSaida(); }}>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Material -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Material *</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Material <span class="text-error">*</span>
</span>
</label>
<select class="select select-bordered" bind:value={saidaMaterialId} required>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={saidaMaterialId} required>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
@@ -356,25 +383,33 @@
</select>
</div>
<!-- Quantidade -->
<div class="form-control">
<label class="label">
<span class="label-text font-bold">Quantidade *</span>
<label class="label pb-2">
<span class="label-text font-semibold">Quantidade <span class="text-error">*</span></span>
</label>
<input
type="number"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0.01"
step="0.01"
bind:value={saidaQuantidade}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Quantidade a ser retirada do estoque</span>
</label>
</div>
<!-- Funcionário -->
<div class="form-control">
<label class="label">
<span class="label-text">Funcionário</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<User class="h-4 w-4" />
Funcionário
</span>
</label>
<select class="select select-bordered" bind:value={saidaFuncionarioId}>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={saidaFuncionarioId}>
<option value="">Selecione (opcional)</option>
{#if funcionariosQuery.data}
{#each funcionariosQuery.data as funcionario}
@@ -382,13 +417,20 @@
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Funcionário que receberá o material</span>
</label>
</div>
<!-- Setor -->
<div class="form-control">
<label class="label">
<span class="label-text">Setor</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Building2 class="h-4 w-4" />
Setor
</span>
</label>
<select class="select select-bordered" bind:value={saidaSetorId}>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={saidaSetorId}>
<option value="">Selecione (opcional)</option>
{#if setoresQuery.data}
{#each setoresQuery.data as setor}
@@ -396,27 +438,35 @@
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Setor que receberá o material</span>
</label>
</div>
<!-- Motivo -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Motivo *</span>
<label class="label pb-2">
<span class="label-text font-semibold">Motivo <span class="text-error">*</span></span>
</label>
<input
type="text"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Ex: Uso interno, Empréstimo, Descarte"
bind:value={saidaMotivo}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Razão da saída do material do estoque</span>
</label>
</div>
<!-- Observações -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Observações</span>
<label class="label pb-2">
<span class="label-text font-semibold">Observações</span>
</label>
<textarea
class="textarea textarea-bordered"
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
placeholder="Observações adicionais (opcional)"
bind:value={saidaObservacoes}
rows="3"
@@ -424,39 +474,44 @@
</div>
</div>
<div class="card-actions mt-6 justify-end">
<button type="submit" class="btn btn-primary" disabled={saidaLoading}>
<div class="card-actions mt-8 justify-end gap-4 border-t-2 border-base-300 pt-6">
<button type="submit" class="btn btn-error btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={saidaLoading}>
{#if saidaLoading}
<span class="loading loading-spinner loading-sm"></span>
Registrando...
{:else}
<ArrowUp class="h-5 w-5" />
Registrar Saída
{/if}
Registrar Saída
</button>
</div>
</form>
</div>
</div>
{:else if abaAtiva === 'ajuste'}
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-6 text-xl">
<div class="rounded-lg bg-warning/20 p-2">
<Settings class="h-6 w-6 text-warning" />
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-8">
<div class="mb-8 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
<div class="rounded-lg bg-warning/10 p-2.5">
<Settings class="h-6 w-6 text-warning" strokeWidth={2.5} />
</div>
Ajustar Estoque
</h2>
<div class="alert alert-warning mb-6 shadow-lg">
<Settings class="h-6 w-6" />
<span class="font-medium">Ajustes de estoque devem ser justificados e são registrados no histórico.</span>
<h2 class="text-2xl font-bold text-base-content">Ajustar Estoque</h2>
</div>
<div class="alert alert-warning mb-8 shadow-lg border-2 border-warning/30">
<AlertCircle class="h-6 w-6" />
<span class="font-semibold">Atenção: Ajustes de estoque devem ser justificados e são registrados permanentemente no histórico.</span>
</div>
<form onsubmit={(e) => { e.preventDefault(); ajustarEstoque(); }}>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Material -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Material *</span>
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Package class="h-4 w-4" />
Material <span class="text-error">*</span>
</span>
</label>
<select class="select select-bordered" bind:value={ajusteMaterialId} required>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={ajusteMaterialId} required>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
@@ -468,38 +523,47 @@
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-bold">Nova Quantidade *</span>
<!-- Nova Quantidade -->
<div class="form-control md:col-span-2">
<label class="label pb-2">
<span class="label-text font-semibold">Nova Quantidade <span class="text-error">*</span></span>
</label>
<input
type="number"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0"
bind:value={ajusteQuantidadeNova}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Quantidade correta após o ajuste</span>
</label>
</div>
<!-- Justificativa -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-bold">Justificativa *</span>
<label class="label pb-2">
<span class="label-text font-semibold">Justificativa <span class="text-error">*</span></span>
</label>
<input
type="text"
class="input input-bordered"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
placeholder="Ex: Inventário físico, Correção de erro, Perda"
bind:value={ajusteMotivo}
required
/>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Razão para o ajuste de estoque</span>
</label>
</div>
<!-- Observações -->
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Observações</span>
<label class="label pb-2">
<span class="label-text font-semibold">Observações</span>
</label>
<textarea
class="textarea textarea-bordered"
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
placeholder="Observações adicionais (opcional)"
bind:value={ajusteObservacoes}
rows="3"
@@ -507,74 +571,111 @@
</div>
</div>
<div class="card-actions mt-6 justify-end">
<button type="submit" class="btn btn-warning" disabled={ajusteLoading}>
<div class="card-actions mt-8 justify-end gap-4 border-t-2 border-base-300 pt-6">
<button type="submit" class="btn btn-warning btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={ajusteLoading}>
{#if ajusteLoading}
<span class="loading loading-spinner loading-sm"></span>
Ajustando...
{:else}
<Settings class="h-5 w-5" />
Ajustar Estoque
{/if}
Ajustar Estoque
</button>
</div>
</form>
</div>
</div>
{:else if abaAtiva === 'historico'}
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-6 text-xl">
<div class="rounded-lg bg-info/20 p-2">
<History class="h-6 w-6 text-info" />
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-8">
<div class="mb-8 flex items-center gap-3 border-b-2 border-info/20 pb-4">
<div class="rounded-lg bg-info/10 p-2.5">
<History class="h-6 w-6 text-info" strokeWidth={2.5} />
</div>
Histórico de Movimentações
</h2>
<div class="overflow-x-auto">
<h2 class="text-2xl font-bold text-base-content">Histórico de Movimentações</h2>
</div>
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-semibold">Data</th>
<th class="font-semibold">Material</th>
<th class="font-semibold">Tipo</th>
<th class="font-semibold">Quantidade</th>
<th class="font-semibold">Anterior</th>
<th class="font-semibold">Nova</th>
<th class="font-semibold">Funcionário</th>
<th class="font-semibold">Motivo</th>
<th class="font-bold text-base-content">Data</th>
<th class="font-bold text-base-content">Material</th>
<th class="font-bold text-base-content">Tipo</th>
<th class="font-bold text-base-content">Quantidade</th>
<th class="font-bold text-base-content">Anterior</th>
<th class="font-bold text-base-content">Nova</th>
<th class="font-bold text-base-content">Funcionário</th>
<th class="font-bold text-base-content">Usuário</th>
<th class="font-bold text-base-content">Motivo</th>
</tr>
</thead>
<tbody>
{#if movimentacoesQuery.data && movimentacoesQuery.data.length > 0}
{#each movimentacoesQuery.data.slice(0, 50) as mov}
{@const material = materiaisMap.get(mov.materialId)}
{@const funcionario = mov.funcionarioId ? funcionariosMap.get(mov.funcionarioId) : null}
{#each movimentacoesQuery.data.slice(0, 100) as item}
{@const material = materiaisMap.get(item.materialId)}
{@const funcionario = item.funcionarioId ? funcionariosMap.get(item.funcionarioId) : null}
{@const isMovimentacao = item.tipo === 'movimentacao'}
{@const isAlteracao = item.tipo === 'alteracao'}
<tr class="hover:bg-base-200/50 transition-colors">
<td>
<span class="text-sm">{new Date(mov.data).toLocaleString('pt-BR')}</span>
<span class="text-sm">{new Date(item.data).toLocaleString('pt-BR')}</span>
</td>
<td>
<div class="font-medium">{material?.nome || 'Carregando...'}</div>
{#if material?.codigo}
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
{/if}
</td>
<td>
{#if mov.tipo === 'entrada'}
<span class="badge badge-success badge-lg">Entrada</span>
{:else if mov.tipo === 'saida'}
<span class="badge badge-error badge-lg">Saída</span>
{#if isAlteracao && item.tipoAlteracao === 'exclusao'}
<div class="font-medium text-base-content/60 italic">
Material excluído (ID: {item.materialId})
</div>
{:else if material}
<div class="font-medium">{material.nome}</div>
{#if material.codigo}
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
{/if}
{:else}
<span class="badge badge-warning badge-lg">Ajuste</span>
<div class="font-medium text-base-content/60">Material não encontrado</div>
<div class="text-xs text-base-content/50 font-mono">ID: {item.materialId}</div>
{/if}
</td>
<td>
<span class="font-bold">{mov.quantidade}</span>
{#if isMovimentacao}
{#if item.tipoMovimentacao === 'entrada'}
<span class="badge badge-success badge-lg">Entrada</span>
{:else if item.tipoMovimentacao === 'saida'}
<span class="badge badge-error badge-lg">Saída</span>
{:else}
<span class="badge badge-warning badge-lg">Ajuste</span>
{/if}
{:else if isAlteracao}
{#if item.tipoAlteracao === 'criacao'}
<span class="badge badge-info badge-lg">Criação</span>
{:else if item.tipoAlteracao === 'edicao'}
<span class="badge badge-primary badge-lg">Edição</span>
{:else if item.tipoAlteracao === 'exclusao'}
<span class="badge badge-error badge-lg">Exclusão</span>
{:else}
<span class="badge badge-neutral badge-lg">{item.tipoAlteracao}</span>
{/if}
{/if}
</td>
<td>
<span class="text-base-content/70">{mov.quantidadeAnterior}</span>
{#if isMovimentacao && item.quantidade !== undefined}
<span class="font-bold">{item.quantidade}</span>
{:else}
<span class="text-base-content/50 text-sm italic"></span>
{/if}
</td>
<td>
<span class="font-semibold">{mov.quantidadeNova}</span>
{#if isMovimentacao && item.quantidadeAnterior !== undefined}
<span class="text-base-content/70">{item.quantidadeAnterior}</span>
{:else}
<span class="text-base-content/50 text-sm italic"></span>
{/if}
</td>
<td>
{#if isMovimentacao && item.quantidadeNova !== undefined}
<span class="font-semibold">{item.quantidadeNova}</span>
{:else}
<span class="text-base-content/50 text-sm italic"></span>
{/if}
</td>
<td>
{#if funcionario}
@@ -587,13 +688,31 @@
{/if}
</td>
<td>
<span class="text-sm">{mov.motivo}</span>
{#if item.usuarioNome}
<div class="font-medium">{item.usuarioNome}</div>
{#if isAlteracao}
<div class="text-xs text-base-content/50">
{#if item.tipoAlteracao === 'criacao'}
Criou
{:else if item.tipoAlteracao === 'edicao'}
Editou
{:else if item.tipoAlteracao === 'exclusao'}
Excluiu
{/if}
</div>
{/if}
{:else}
<span class="text-base-content/50 text-sm italic"></span>
{/if}
</td>
<td>
<span class="text-sm">{item.motivo || '—'}</span>
</td>
</tr>
{/each}
{:else}
<tr>
<td colspan="8" class="text-center">
<td colspan="9" class="text-center">
<div class="py-12">
<History class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
<p class="text-base-content/70 text-lg font-medium">Nenhuma movimentação registrada</p>

View File

@@ -762,19 +762,28 @@
<!-- Cabeçalho -->
<div class="mb-8">
<div class="flex items-center gap-4">
<div class="rounded-2xl bg-gradient-to-br from-success/20 to-success/30 p-4 shadow-lg">
<BarChart3 class="h-10 w-10 text-success" strokeWidth={2.5} />
<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">
<BarChart3 class="h-10 w-10 text-primary" strokeWidth={2.5} />
</div>
<div>
<h1 class="text-3xl font-bold tracking-tight">Relatórios</h1>
<p class="text-base-content/70 text-lg">Estatísticas e relatórios do almoxarifado</p>
<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">
Relatórios
</h1>
<p class="text-base-content/70 text-lg mt-1">Estatísticas e relatórios do almoxarifado</p>
</div>
</div>
</div>
<!-- Estatísticas Gerais -->
{#if statsQuery.data}
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4 mb-8">
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
<div class="rounded-lg bg-primary/10 p-2.5">
<BarChart3 class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<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-body">
<div class="flex items-center justify-between">
@@ -838,15 +847,27 @@
{/if}
<!-- Relatórios Disponíveis -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="mb-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
<div class="rounded-lg bg-info/10 p-2.5">
<FileText class="h-5 w-5 text-info" strokeWidth={2.5} />
</div>
<h2 class="text-xl font-bold text-base-content">Relatórios Disponíveis</h2>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Relatório de Materiais por Categoria -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title">Materiais por Categoria</h2>
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-primary/10 p-2">
<Package class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<h2 class="text-lg font-bold text-base-content">Materiais por Categoria</h2>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
onclick={gerarPDFMateriaisCategoria}
disabled={gerandoRelatorio}
title="Gerar PDF"
@@ -859,7 +880,7 @@
PDF
</button>
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
onclick={gerarExcelMateriaisCategoria}
disabled={gerandoRelatorio}
title="Gerar Excel"
@@ -874,38 +895,46 @@
</div>
</div>
{#if materiaisQuery.data && Object.keys(materiaisPorCategoria).length > 0}
<div class="space-y-2">
<div class="space-y-3">
{#each Object.entries(materiaisPorCategoria) as [categoria, quantidade]}
<div class="flex items-center justify-between">
<span class="font-medium">{categoria}</span>
<div class="flex items-center gap-2">
<div class="w-32">
<div class="h-2 bg-base-300 rounded-full overflow-hidden">
<div class="flex items-center justify-between p-2 rounded-lg hover:bg-base-200/50 transition-colors">
<span class="font-semibold text-base-content">{categoria}</span>
<div class="flex items-center gap-3">
<div class="w-40">
<div class="h-3 bg-base-300 rounded-full overflow-hidden shadow-inner">
<div
class="h-full bg-primary transition-all"
class="h-full bg-gradient-to-r from-primary to-primary/70 transition-all rounded-full"
style="width: {(quantidade / (materiaisQuery.data?.length || 1)) * 100}%"
></div>
</div>
</div>
<span class="text-sm font-bold w-12 text-right">{quantidade}</span>
<span class="text-base font-bold text-primary w-12 text-right">{quantidade}</span>
</div>
</div>
{/each}
</div>
{:else}
<p class="text-base-content/70">Nenhum dado disponível</p>
<div class="alert alert-info border-info/30 bg-info/10">
<Package class="h-5 w-5 text-info" />
<span class="font-medium">Nenhum dado disponível</span>
</div>
{/if}
</div>
</div>
<!-- Movimentações do Mês -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title">Movimentações do Mês</h2>
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-info/10 p-2">
<ArrowLeftRight class="h-5 w-5 text-info" strokeWidth={2.5} />
</div>
<h2 class="text-lg font-bold text-base-content">Movimentações do Mês</h2>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
onclick={gerarPDFMovimentacoesMes}
disabled={gerandoRelatorio}
title="Gerar PDF"
@@ -918,7 +947,7 @@
PDF
</button>
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
onclick={gerarExcelMovimentacoesMes}
disabled={gerandoRelatorio}
title="Gerar Excel"
@@ -933,39 +962,50 @@
</div>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<ArrowDown class="h-5 w-5 text-success" />
<span>Entradas</span>
<div class="flex items-center justify-between p-3 rounded-lg bg-success/10 border border-success/20 hover:bg-success/15 transition-colors">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-success/20 p-2">
<ArrowDown class="h-5 w-5 text-success" strokeWidth={2.5} />
</div>
<span class="font-semibold text-base-content">Entradas</span>
</div>
<span class="font-bold text-success">{movimentacoesMes.entrada}</span>
<span class="text-2xl font-bold text-success">{movimentacoesMes.entrada}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<ArrowUp class="h-5 w-5 text-error" />
<span>Saídas</span>
<div class="flex items-center justify-between p-3 rounded-lg bg-error/10 border border-error/20 hover:bg-error/15 transition-colors">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-error/20 p-2">
<ArrowUp class="h-5 w-5 text-error" strokeWidth={2.5} />
</div>
<span class="font-semibold text-base-content">Saídas</span>
</div>
<span class="font-bold text-error">{movimentacoesMes.saida}</span>
<span class="text-2xl font-bold text-error">{movimentacoesMes.saida}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Settings class="h-5 w-5 text-warning" />
<span>Ajustes</span>
<div class="flex items-center justify-between p-3 rounded-lg bg-warning/10 border border-warning/20 hover:bg-warning/15 transition-colors">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-warning/20 p-2">
<Settings class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<span class="font-semibold text-base-content">Ajustes</span>
</div>
<span class="font-bold text-warning">{movimentacoesMes.ajuste}</span>
<span class="text-2xl font-bold text-warning">{movimentacoesMes.ajuste}</span>
</div>
</div>
</div>
</div>
<!-- Materiais com Estoque Baixo -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title">Materiais com Estoque Baixo</h2>
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-warning/10 p-2">
<AlertTriangle class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<h2 class="text-lg font-bold text-base-content">Materiais com Estoque Baixo</h2>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
onclick={gerarPDFEstoqueBaixo}
disabled={gerandoRelatorio}
title="Gerar PDF"
@@ -978,7 +1018,7 @@
PDF
</button>
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
onclick={gerarExcelEstoqueBaixo}
disabled={gerandoRelatorio}
title="Gerar Excel"
@@ -995,40 +1035,42 @@
{#if materiaisQuery.data}
{@const estoqueBaixo = materiaisQuery.data.filter(m => m.estoqueAtual <= m.estoqueMinimo)}
{#if estoqueBaixo.length > 0}
<div class="overflow-x-auto">
<table class="table table-sm">
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra table-sm">
<thead>
<tr>
<th>Material</th>
<th>Atual</th>
<th>Mínimo</th>
<tr class="bg-base-200">
<th class="font-bold text-base-content">Material</th>
<th class="font-bold text-base-content">Atual</th>
<th class="font-bold text-base-content">Mínimo</th>
</tr>
</thead>
<tbody>
{#each estoqueBaixo.slice(0, 10) as material}
<tr>
<tr class="hover:bg-base-200/50 transition-colors">
<td>
<div class="font-medium">{material.nome}</div>
<div class="text-xs text-base-content/60">{material.codigo}</div>
<div class="text-xs text-base-content/60 font-mono">{material.codigo}</div>
</td>
<td>
<span class="font-bold text-error">{material.estoqueAtual}</span>
</td>
<td>{material.estoqueMinimo}</td>
<td>
<span class="font-medium">{material.estoqueMinimo}</span>
</td>
</tr>
{/each}
</tbody>
</table>
{#if estoqueBaixo.length > 10}
<p class="text-sm text-base-content/70 mt-2">
E mais {estoqueBaixo.length - 10} materiais...
</p>
{/if}
</div>
{#if estoqueBaixo.length > 10}
<p class="text-sm text-base-content/70 mt-4 text-center font-medium">
E mais <span class="text-primary font-bold">{estoqueBaixo.length - 10}</span> materiais...
</p>
{/if}
{:else}
<div class="alert alert-success">
<CheckCircle class="h-6 w-6" />
<span>Todos os materiais estão com estoque adequado!</span>
<div class="alert alert-success border-success/30 bg-success/10">
<CheckCircle class="h-6 w-6 text-success" />
<span class="font-medium">Todos os materiais estão com estoque adequado!</span>
</div>
{/if}
{/if}
@@ -1036,13 +1078,18 @@
</div>
<!-- Alertas Recentes -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title">Alertas Recentes</h2>
<div class="card bg-base-100 border border-base-300 shadow-2xl">
<div class="card-body p-6">
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-warning/10 p-2">
<AlertTriangle class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<h2 class="text-lg font-bold text-base-content">Alertas Recentes</h2>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
onclick={gerarPDFAlertas}
disabled={gerandoRelatorio}
title="Gerar PDF"
@@ -1055,7 +1102,7 @@
PDF
</button>
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
onclick={gerarExcelAlertas}
disabled={gerandoRelatorio}
title="Gerar Excel"
@@ -1070,21 +1117,21 @@
</div>
</div>
{#if alertasQuery.data && alertasQuery.data.length > 0}
<div class="space-y-2">
<div class="space-y-3">
{#each alertasQuery.data.slice(0, 5) as alerta}
<div class="flex items-center justify-between p-2 bg-warning/10 rounded">
<div>
<div class="font-medium text-sm">
<div class="flex items-center justify-between p-3 bg-warning/10 rounded-lg border border-warning/20 hover:bg-warning/15 transition-colors">
<div class="flex-1">
<div class="font-semibold text-sm text-base-content">
{#if materiaisQuery.data}
{@const material = materiaisQuery.data.find(m => m._id === alerta.materialId)}
{material?.nome || 'Carregando...'}
{/if}
</div>
<div class="text-xs text-base-content/60">
{alerta.quantidadeAtual} / {alerta.quantidadeMinima}
<div class="text-xs text-base-content/60 mt-1 font-mono">
Estoque: <span class="font-bold text-error">{alerta.quantidadeAtual}</span> / Mínimo: <span class="font-bold">{alerta.quantidadeMinima}</span>
</div>
</div>
<span class="badge badge-warning badge-sm">
<span class="badge badge-warning badge-lg ml-3">
{#if alerta.tipo === 'estoque_zerado'}
Zerado
{:else}
@@ -1095,9 +1142,9 @@
{/each}
</div>
{:else}
<div class="alert alert-success">
<CheckCircle class="h-6 w-6" />
<span>Nenhum alerta ativo</span>
<div class="alert alert-success border-success/30 bg-success/10">
<CheckCircle class="h-6 w-6 text-success" />
<span class="font-medium">Nenhum alerta ativo</span>
</div>
{/if}
</div>

View File

@@ -3,7 +3,7 @@
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import { resolve } from '$app/paths';
import { ClipboardList, Plus, CheckCircle, XCircle, Package } from 'lucide-svelte';
import { ClipboardList, Plus, CheckCircle, XCircle, Package, Filter, User, Building2, Calendar, FileText } from 'lucide-svelte';
import ErrorModal from '$lib/components/ErrorModal.svelte';
const client = useConvexClient();
@@ -250,15 +250,17 @@
<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-purple-500/20 to-purple-600/30 p-4 shadow-lg">
<ClipboardList class="h-10 w-10 text-purple-600" strokeWidth={2.5} />
<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">
<ClipboardList class="h-10 w-10 text-primary" strokeWidth={2.5} />
</div>
<div>
<h1 class="text-3xl font-bold tracking-tight">Requisições de Material</h1>
<p class="text-base-content/70 text-lg">Gerencie requisições de material dos funcionários</p>
<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">
Requisições de Material
</h1>
<p class="text-base-content/70 text-lg mt-1">Gerencie requisições de material dos funcionários</p>
</div>
</div>
<button class="btn btn-primary shadow-lg hover:shadow-xl transition-all" onclick={abrirModalNova}>
<button class="btn btn-primary btn-lg shadow-lg hover:shadow-xl transition-all min-w-[200px]" onclick={abrirModalNova}>
<Plus class="h-5 w-5" />
Nova Requisição
</button>
@@ -282,48 +284,62 @@
/>
<!-- Filtros -->
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
<div class="card-body">
<h3 class="text-lg font-semibold mb-4">Filtros</h3>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Filtrar por Status</span>
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
<div class="card-body p-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
<div class="rounded-lg bg-primary/10 p-2.5">
<Filter class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Filtros de Busca</h3>
</div>
<div class="form-control max-w-md">
<label class="label pb-2">
<span class="label-text font-semibold">Filtrar por Status</span>
</label>
<select class="select select-bordered" bind:value={filtroStatus}>
<option value="">Todos</option>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroStatus}>
<option value="">Todos os status</option>
<option value="pendente">Pendente</option>
<option value="aprovada">Aprovada</option>
<option value="atendida">Atendida</option>
<option value="rejeitada">Rejeitada</option>
<option value="cancelada">Cancelada</option>
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Filtre as requisições por status</span>
</label>
</div>
</div>
</div>
<!-- Lista de Requisições -->
<div class="card bg-base-100 border border-base-300 shadow-xl">
<div class="card-body">
<div class="overflow-x-auto">
<div class="card bg-base-100 border border-base-300 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">
<ClipboardList class="h-5 w-5 text-info" strokeWidth={2.5} />
</div>
<h3 class="text-xl font-bold text-base-content">Lista de Requisições</h3>
</div>
<div class="overflow-x-auto rounded-lg border border-base-300">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-semibold">Número</th>
<th class="font-semibold">Solicitante</th>
<th class="font-semibold">Setor</th>
<th class="font-semibold">Status</th>
<th class="font-semibold">Data</th>
<th class="font-semibold">Ações</th>
<th class="font-bold text-base-content">Número</th>
<th class="font-bold text-base-content">Solicitante</th>
<th class="font-bold text-base-content">Setor</th>
<th class="font-bold text-base-content">Status</th>
<th class="font-bold text-base-content">Data</th>
<th class="font-bold text-base-content">Ações</th>
</tr>
</thead>
<tbody>
{#if requisicoes.length === 0}
<tr>
<td colspan="6" class="text-center">
<div class="py-12">
<ClipboardList class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
<p class="text-base-content/70 text-lg font-medium">Nenhuma requisição encontrada</p>
<p class="text-base-content/50 text-sm mt-2">Tente ajustar os filtros ou criar uma nova requisição</p>
<div class="py-16">
<ClipboardList 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">Nenhuma requisição encontrada</p>
<p class="text-base-content/60 text-base">Tente ajustar os filtros ou criar uma nova requisição</p>
</div>
</td>
</tr>
@@ -353,16 +369,18 @@
<div class="flex gap-2">
{#if requisicao.status === 'pendente'}
<button
class="btn btn-sm btn-success"
class="btn btn-sm btn-success transition-all"
onclick={() => aprovarRequisicao(requisicao._id)}
title="Aprovar requisição"
>
<CheckCircle class="h-4 w-4" />
Aprovar
</button>
{:else if requisicao.status === 'aprovada'}
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary transition-all"
onclick={() => atenderRequisicao(requisicao._id)}
title="Atender requisição"
>
<Package class="h-4 w-4" />
Atender
@@ -376,147 +394,225 @@
</tbody>
</table>
</div>
{#if requisicoes.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">{requisicoes.length}</span> requisição{requisicoes.length !== 1 ? 'ões' : ''}
{#if requisicoesQuery.data && requisicoesQuery.data.length !== requisicoes.length}
<span class="text-base-content/60"> de <span class="text-primary font-bold">{requisicoesQuery.data.length}</span> total</span>
{/if}
</div>
</div>
{/if}
</div>
</div>
<!-- Modal Nova Requisição -->
{#if showModalNova}
<div class="modal modal-open">
<div class="modal modal-open backdrop-blur-sm">
<div class="modal-box max-w-4xl border border-base-300 shadow-2xl">
<h3 class="text-2xl font-bold mb-4 flex items-center gap-2">
<ClipboardList class="h-6 w-6 text-primary" />
Nova Requisição de Material
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 mt-4">
<div class="form-control">
<label class="label">
<span class="label-text font-bold">Solicitante *</span>
</label>
<select class="select select-bordered" bind:value={novaRequisicaoSolicitanteId} required>
<option value="">Selecione um funcionário</option>
{#if funcionariosQuery.data}
{#each funcionariosQuery.data as funcionario}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
{/if}
</select>
<div class="mb-6 flex items-center gap-4 border-b-2 border-primary/20 pb-4">
<div class="rounded-2xl bg-primary/20 p-3">
<ClipboardList class="h-8 w-8 text-primary" strokeWidth={2.5} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-bold">Setor *</span>
</label>
<select class="select select-bordered" bind:value={novaRequisicaoSetorId} required>
<option value="">Selecione um setor</option>
{#if setoresQuery.data}
{#each setoresQuery.data as setor}
<option value={setor._id}>{setor.nome}</option>
{/each}
{/if}
</select>
<div>
<h3 class="text-2xl font-bold text-base-content">Nova Requisição de Material</h3>
<p class="text-base-content/70 mt-1">Preencha os dados da requisição</p>
</div>
</div>
<div class="divider">Itens da Requisição</div>
<!-- Adicionar Item -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 mb-4">
<div class="form-control">
<label class="label">
<span class="label-text">Material</span>
</label>
<select class="select select-bordered" bind:value={novoItemMaterialId}>
<option value="">Selecione</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
<option value={material._id}>
{material.codigo} - {material.nome}
</option>
{/each}
{/if}
</select>
<!-- Seção: Informações Básicas -->
<div class="mb-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
<div class="rounded-lg bg-info/10 p-2.5">
<FileText class="h-5 w-5 text-info" strokeWidth={2.5} />
</div>
<h4 class="text-lg font-bold text-base-content">Informações Básicas</h4>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Quantidade</span>
</label>
<input
type="number"
class="input input-bordered"
min="0.01"
step="0.01"
bind:value={novoItemQuantidade}
/>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<User class="h-4 w-4" />
Solicitante <span class="text-error">*</span>
</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novaRequisicaoSolicitanteId} required>
<option value="">Selecione um funcionário</option>
{#if funcionariosQuery.data}
{#each funcionariosQuery.data as funcionario}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Funcionário que está solicitando</span>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Ações</span>
</label>
<button type="button" class="btn btn-primary" onclick={adicionarItem}>
<Plus class="h-4 w-4" />
Adicionar
</button>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold flex items-center gap-2">
<Building2 class="h-4 w-4" />
Setor <span class="text-error">*</span>
</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novaRequisicaoSetorId} required>
<option value="">Selecione um setor</option>
{#if setoresQuery.data}
{#each setoresQuery.data as setor}
<option value={setor._id}>{setor.nome}</option>
{/each}
{/if}
</select>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Setor do solicitante</span>
</label>
</div>
</div>
</div>
<!-- Lista de Itens -->
{#if novaRequisicaoItens.length > 0}
<div class="overflow-x-auto mb-4">
<table class="table table-sm">
<thead>
<tr>
<th>Material</th>
<th>Quantidade</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each novaRequisicaoItens as item}
{@const material = materiaisQuery.data?.find(m => m._id === item.materialId)}
<tr>
<td>{material?.nome || 'Carregando...'}</td>
<td>{item.quantidade} {material?.unidadeMedida || ''}</td>
<td>
<button
class="btn btn-sm btn-ghost"
onclick={() => removerItem(item.id)}
>
<XCircle class="h-4 w-4" />
</button>
</td>
<!-- Seção: Itens da Requisição -->
<div class="mb-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
<div class="rounded-lg bg-warning/10 p-2.5">
<Package class="h-5 w-5 text-warning" strokeWidth={2.5} />
</div>
<h4 class="text-lg font-bold text-base-content">Itens da Requisição</h4>
</div>
<!-- Adicionar Item -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-3 mb-6">
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Material</span>
</label>
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novoItemMaterialId}>
<option value="">Selecione um material</option>
{#if materiaisQuery.data}
{#each materiaisQuery.data as material}
<option value={material._id}>
{material.codigo} - {material.nome}
</option>
{/each}
{/if}
</select>
</div>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Quantidade</span>
</label>
<input
type="number"
class="input input-bordered w-full focus:input-primary transition-colors h-12"
min="0.01"
step="0.01"
bind:value={novoItemQuantidade}
placeholder="0.00"
/>
</div>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Ações</span>
</label>
<button type="button" class="btn btn-primary w-full h-12 shadow-lg hover:shadow-xl transition-all" onclick={adicionarItem}>
<Plus class="h-5 w-5" />
Adicionar Item
</button>
</div>
</div>
<!-- Lista de Itens -->
{#if novaRequisicaoItens.length > 0}
<div class="overflow-x-auto mb-6 rounded-lg border border-base-300">
<table class="table table-zebra">
<thead>
<tr class="bg-base-200">
<th class="font-bold text-base-content">Material</th>
<th class="font-bold text-base-content">Quantidade</th>
<th class="font-bold text-base-content">Ações</th>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Observações</span>
</label>
<textarea
class="textarea textarea-bordered"
placeholder="Observações gerais da requisição (opcional)"
bind:value={novaRequisicaoObservacoes}
rows="3"
></textarea>
</thead>
<tbody>
{#each novaRequisicaoItens as item}
{@const material = materiaisQuery.data?.find(m => m._id === item.materialId)}
<tr class="hover:bg-base-200/50 transition-colors">
<td>
<div class="font-medium">{material?.nome || 'Carregando...'}</div>
{#if material?.codigo}
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
{/if}
</td>
<td>
<span class="font-semibold">{item.quantidade}</span>
<span class="text-sm text-base-content/60 ml-1">{material?.unidadeMedida || ''}</span>
</td>
<td>
<button
class="btn btn-sm btn-ghost hover:btn-error transition-all"
onclick={() => removerItem(item.id)}
title="Remover item"
>
<XCircle class="h-4 w-4" />
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="alert alert-info mb-6 border-info/30 bg-info/10">
<Package class="h-5 w-5 text-info" />
<div>
<p class="font-semibold text-base-content">Nenhum item adicionado</p>
<p class="text-sm text-base-content/80 mt-1">Adicione pelo menos um item à requisição</p>
</div>
</div>
{/if}
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick={fecharModalNova} disabled={criandoRequisicao}>
<!-- Seção: Observações -->
<div class="mb-8">
<div class="mb-6 flex items-center gap-3 border-b-2 border-success/20 pb-4">
<div class="rounded-lg bg-success/10 p-2.5">
<FileText class="h-5 w-5 text-success" strokeWidth={2.5} />
</div>
<h4 class="text-lg font-bold text-base-content">Observações</h4>
</div>
<div class="form-control">
<label class="label pb-2">
<span class="label-text font-semibold">Observações Gerais</span>
</label>
<textarea
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors"
placeholder="Observações gerais da requisição (opcional)"
bind:value={novaRequisicaoObservacoes}
rows="4"
></textarea>
<label class="label pt-1">
<span class="label-text-alt text-base-content/60">Informações adicionais sobre a requisição</span>
</label>
</div>
</div>
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
<button class="btn btn-ghost btn-lg min-w-[140px]" onclick={fecharModalNova} disabled={criandoRequisicao}>
Cancelar
</button>
<button class="btn btn-primary" onclick={criarRequisicao} disabled={criandoRequisicao}>
<button class="btn btn-primary btn-lg min-w-[200px] shadow-lg hover:shadow-xl" onclick={criarRequisicao} disabled={criandoRequisicao}>
{#if criandoRequisicao}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Plus class="h-5 w-5" />
{/if}
Criar Requisição
<span class="loading loading-spinner loading-sm"></span>
Criando...
{:else}
<Plus class="h-5 w-5" />
Criar Requisição
{/if}
</button>
</div>
</div>