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:
@@ -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 = () => {
|
||||
@@ -207,7 +203,8 @@
|
||||
|
||||
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') {
|
||||
@@ -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="bg-base-100 rounded-lg shadow-2xl p-6 max-w-2xl w-full mx-4"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
|
||||
onclick={closeCamera}
|
||||
>
|
||||
<div
|
||||
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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{#if materialParaExcluir}
|
||||
<div class="space-y-4">
|
||||
<div class="alert alert-warning">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
<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>
|
||||
<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.
|
||||
<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 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>
|
||||
<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: <span class="font-mono">{materialParaExcluir.codigo}</span>
|
||||
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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,11 +479,20 @@
|
||||
{/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(); }}>
|
||||
<!-- 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-6">
|
||||
<div class="mb-8 rounded-xl border border-base-300 bg-base-200/50 p-4">
|
||||
<BarcodeScanner
|
||||
enabled={scannerEnabled}
|
||||
onScan={handleBarcodeScanned}
|
||||
@@ -486,44 +502,50 @@
|
||||
|
||||
<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>
|
||||
<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"
|
||||
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||
placeholder="Ex: MAT-001"
|
||||
bind:value={codigo}
|
||||
required
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Código único do material</span>
|
||||
<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 md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text">Código de Barras</span>
|
||||
<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 {(buscandoProduto || buscandoExterno) ? 'input-info' : ''}"
|
||||
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">
|
||||
<span class="label-text-alt">
|
||||
<label class="label pt-1">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
{#if buscandoProduto}
|
||||
Buscando produto no banco de dados...
|
||||
<span class="text-info">Buscando produto no banco de dados...</span>
|
||||
{:else if buscandoExterno}
|
||||
Buscando produto em base externa...
|
||||
<span class="text-info">Buscando produto em base externa...</span>
|
||||
{:else}
|
||||
Digite ou use o leitor acima para escanear
|
||||
{/if}
|
||||
@@ -532,14 +554,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text font-bold">Nome *</span>
|
||||
<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"
|
||||
placeholder="Nome do material"
|
||||
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||
placeholder="Digite o nome completo do material"
|
||||
bind:value={nome}
|
||||
required
|
||||
/>
|
||||
@@ -547,11 +569,11 @@
|
||||
|
||||
<!-- Descrição -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Descrição</span>
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea textarea-bordered"
|
||||
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"
|
||||
@@ -559,13 +581,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Categoria -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text font-bold">Categoria *</span>
|
||||
<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"
|
||||
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||
list="categorias"
|
||||
placeholder="Ex: Escritório"
|
||||
bind:value={categoria}
|
||||
@@ -579,118 +601,163 @@
|
||||
</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>
|
||||
<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" bind:value={unidadeMedida} required>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Estoque Mínimo -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text font-bold">Estoque Mínimo *</span>
|
||||
<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"
|
||||
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 md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text">Estoque Máximo</span>
|
||||
<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"
|
||||
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||
min="0"
|
||||
bind:value={estoqueMaximo}
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Opcional</span>
|
||||
<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 md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text">Estoque Inicial</span>
|
||||
<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"
|
||||
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||
min="0"
|
||||
bind:value={estoqueAtual}
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Quantidade inicial em estoque</span>
|
||||
<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 md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text">Localização</span>
|
||||
<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"
|
||||
placeholder="Ex: Prateleira A-01"
|
||||
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>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Fornecedor -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Fornecedor</span>
|
||||
<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"
|
||||
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>
|
||||
|
||||
<!-- Imagem do Produto -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Imagem do Produto</span>
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<h2 class="text-2xl font-bold text-base-content">Registrar Entrada de Material</h2>
|
||||
</div>
|
||||
Registrar Entrada de Material
|
||||
</h2>
|
||||
<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" />
|
||||
{/if}
|
||||
Registrar Entrada
|
||||
{/if}
|
||||
</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>
|
||||
<h2 class="text-2xl font-bold text-base-content">Registrar Saída de Material</h2>
|
||||
</div>
|
||||
Registrar Saída de Material
|
||||
</h2>
|
||||
<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" />
|
||||
{/if}
|
||||
Registrar Saída
|
||||
{/if}
|
||||
</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" />
|
||||
{/if}
|
||||
Ajustar Estoque
|
||||
{/if}
|
||||
</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}
|
||||
{#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}
|
||||
<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>
|
||||
{#if mov.tipo === 'entrada'}
|
||||
{#if isMovimentacao}
|
||||
{#if item.tipoMovimentacao === 'entrada'}
|
||||
<span class="badge badge-success badge-lg">Entrada</span>
|
||||
{:else if mov.tipo === 'saida'}
|
||||
{: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="font-bold">{mov.quantidade}</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="text-base-content/70">{mov.quantidadeAnterior}</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>
|
||||
<span class="font-semibold">{mov.quantidadeNova}</span>
|
||||
{#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>
|
||||
|
||||
@@ -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="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-bold text-success">{movimentacoesMes.entrada}</span>
|
||||
<span class="font-semibold text-base-content">Entradas</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>
|
||||
<span class="text-2xl font-bold text-success">{movimentacoesMes.entrada}</span>
|
||||
</div>
|
||||
<span class="font-bold text-error">{movimentacoesMes.saida}</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>
|
||||
<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>
|
||||
<span class="font-semibold text-base-content">Saídas</span>
|
||||
</div>
|
||||
<span class="font-bold text-warning">{movimentacoesMes.ajuste}</span>
|
||||
<span class="text-2xl font-bold text-error">{movimentacoesMes.saida}</span>
|
||||
</div>
|
||||
<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="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>
|
||||
</div>
|
||||
{#if estoqueBaixo.length > 10}
|
||||
<p class="text-sm text-base-content/70 mt-2">
|
||||
E mais {estoqueBaixo.length - 10} materiais...
|
||||
<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}
|
||||
</div>
|
||||
{: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>
|
||||
|
||||
@@ -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,24 +394,52 @@
|
||||
</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="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>
|
||||
<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="grid grid-cols-1 gap-4 md:grid-cols-2 mt-4">
|
||||
<!-- 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="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-bold">Solicitante *</span>
|
||||
<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" bind:value={novaRequisicaoSolicitanteId} required>
|
||||
<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}
|
||||
@@ -401,13 +447,19 @@
|
||||
{/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 font-bold">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 class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={novaRequisicaoSetorId} required>
|
||||
<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}
|
||||
@@ -415,19 +467,30 @@
|
||||
{/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>
|
||||
|
||||
<div class="divider">Itens da Requisição</div>
|
||||
<!-- 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-4 md:grid-cols-3 mb-4">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3 mb-6">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Material</span>
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Material</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={novoItemMaterialId}>
|
||||
<option value="">Selecione</option>
|
||||
<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}>
|
||||
@@ -439,50 +502,60 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Quantidade</span>
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Quantidade</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={novoItemQuantidade}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Ações</span>
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Ações</span>
|
||||
</label>
|
||||
<button type="button" class="btn btn-primary" onclick={adicionarItem}>
|
||||
<Plus class="h-4 w-4" />
|
||||
Adicionar
|
||||
<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-4">
|
||||
<table class="table table-sm">
|
||||
<div class="overflow-x-auto mb-6 rounded-lg border border-base-300">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Material</th>
|
||||
<th>Quantidade</th>
|
||||
<th>Ações</th>
|
||||
<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>
|
||||
</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>
|
||||
<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"
|
||||
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>
|
||||
@@ -492,31 +565,54 @@
|
||||
</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 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>
|
||||
</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>
|
||||
Criando...
|
||||
{:else}
|
||||
<Plus class="h-5 w-5" />
|
||||
{/if}
|
||||
Criar Requisição
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,6 +165,101 @@ export const listarMovimentacoes = query({
|
||||
}
|
||||
});
|
||||
|
||||
export const listarMovimentacoesComHistorico = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) return [];
|
||||
|
||||
try {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'almoxarifado',
|
||||
acao: 'listar'
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Buscar movimentações de estoque
|
||||
const movimentacoes = await ctx.db
|
||||
.query('movimentacoesEstoque')
|
||||
.withIndex('by_data')
|
||||
.collect();
|
||||
|
||||
// Buscar histórico de alterações de materiais (criação, edição, exclusão)
|
||||
const historicoMateriais = await ctx.db
|
||||
.query('historicoAlteracoes')
|
||||
.withIndex('by_tipoEntidade', (q) => q.eq('tipoEntidade', 'material'))
|
||||
.collect();
|
||||
|
||||
// Buscar todos os usuários únicos para enriquecer os dados
|
||||
const usuarioIds = new Set<Id<'usuarios'>>();
|
||||
for (const mov of movimentacoes) {
|
||||
usuarioIds.add(mov.usuarioId);
|
||||
}
|
||||
for (const hist of historicoMateriais) {
|
||||
usuarioIds.add(hist.usuarioId);
|
||||
}
|
||||
|
||||
// Buscar informações dos usuários
|
||||
const usuariosMap = new Map<Id<'usuarios'>, Doc<'usuarios'>>();
|
||||
for (const userId of usuarioIds) {
|
||||
const usuario = await ctx.db.get(userId);
|
||||
if (usuario) {
|
||||
usuariosMap.set(userId, usuario);
|
||||
}
|
||||
}
|
||||
|
||||
// Transformar movimentações em formato unificado
|
||||
const movimentacoesFormatadas = movimentacoes.map((mov) => {
|
||||
const usuario = usuariosMap.get(mov.usuarioId);
|
||||
return {
|
||||
id: mov._id,
|
||||
tipo: 'movimentacao' as const,
|
||||
tipoMovimentacao: mov.tipo,
|
||||
materialId: mov.materialId,
|
||||
data: mov.data,
|
||||
quantidade: mov.quantidade,
|
||||
quantidadeAnterior: mov.quantidadeAnterior,
|
||||
quantidadeNova: mov.quantidadeNova,
|
||||
motivo: mov.motivo,
|
||||
funcionarioId: mov.funcionarioId,
|
||||
usuarioId: mov.usuarioId,
|
||||
usuarioNome: usuario?.nome || 'Usuário desconhecido',
|
||||
observacoes: mov.observacoes
|
||||
};
|
||||
});
|
||||
|
||||
// Transformar histórico de alterações em formato unificado
|
||||
const historicoFormatado = historicoMateriais.map((hist) => {
|
||||
const usuario = usuariosMap.get(hist.usuarioId);
|
||||
return {
|
||||
id: hist._id,
|
||||
tipo: 'alteracao' as const,
|
||||
tipoAlteracao: hist.acao, // 'criacao', 'edicao', 'exclusao'
|
||||
materialId: hist.entidadeId as Id<'materiais'>, // Converter string para Id
|
||||
data: hist.timestamp,
|
||||
quantidade: undefined,
|
||||
quantidadeAnterior: undefined,
|
||||
quantidadeNova: undefined,
|
||||
motivo: hist.observacoes || hist.acao,
|
||||
funcionarioId: undefined,
|
||||
usuarioId: hist.usuarioId,
|
||||
usuarioNome: usuario?.nome || 'Usuário desconhecido',
|
||||
observacoes: hist.observacoes,
|
||||
dadosAnteriores: hist.dadosAnteriores,
|
||||
dadosNovos: hist.dadosNovos
|
||||
};
|
||||
});
|
||||
|
||||
// Combinar e ordenar por data (mais recente primeiro)
|
||||
const todos = [...movimentacoesFormatadas, ...historicoFormatado];
|
||||
todos.sort((a, b) => b.data - a.data);
|
||||
|
||||
return todos;
|
||||
}
|
||||
});
|
||||
|
||||
export const listarRequisicoes = query({
|
||||
args: {
|
||||
status: v.optional(requisicaoStatus),
|
||||
|
||||
Reference in New Issue
Block a user