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);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeImage(
|
function resizeImage(dataUrl: string, maxWidth: number, maxHeight: number): Promise<string> {
|
||||||
dataUrl: string,
|
|
||||||
maxWidth: number,
|
|
||||||
maxHeight: number
|
|
||||||
): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new window.Image();
|
const img = new window.Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@@ -207,7 +203,8 @@
|
|||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
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') {
|
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
||||||
errorMessage = 'Nenhuma câmera encontrada no dispositivo.';
|
errorMessage = 'Nenhuma câmera encontrada no dispositivo.';
|
||||||
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
} else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
||||||
@@ -280,11 +277,14 @@
|
|||||||
|
|
||||||
// Sincronizar preview com value sempre que value mudar
|
// Sincronizar preview com value sempre que value mudar
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
// Acessar value para criar dependência reativa
|
||||||
|
const currentValue = value;
|
||||||
// Sempre sincronizar quando value mudar
|
// Sempre sincronizar quando value mudar
|
||||||
preview = value;
|
if (currentValue !== preview) {
|
||||||
|
preview = currentValue;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Limpar stream quando o componente for desmontado
|
// Limpar stream quando o componente for desmontado
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -305,7 +305,11 @@
|
|||||||
|
|
||||||
{#if preview}
|
{#if preview}
|
||||||
<div class="relative inline-block">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-error absolute top-2 right-2"
|
class="btn btn-sm btn-circle btn-error absolute top-2 right-2"
|
||||||
@@ -318,7 +322,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div
|
<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}
|
onclick={triggerFileInput}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -329,18 +333,14 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Upload class="h-12 w-12 mx-auto mb-4 text-base-content/40" />
|
<Upload class="text-base-content/40 mx-auto mb-4 h-12 w-12" />
|
||||||
<p class="text-base-content/70 font-medium mb-2">Clique para fazer upload da imagem</p>
|
<p class="text-base-content/70 mb-2 font-medium">Clique para fazer upload da imagem</p>
|
||||||
<p class="text-sm text-base-content/50">
|
<p class="text-base-content/50 text-sm">
|
||||||
PNG, JPG ou GIF até {maxSizeMB}MB
|
PNG, JPG ou GIF até {maxSizeMB}MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider text-sm">ou</div>
|
<div class="divider text-sm">ou</div>
|
||||||
<button
|
<button type="button" class="btn btn-outline btn-primary w-full" onclick={openCamera}>
|
||||||
type="button"
|
|
||||||
class="btn btn-outline btn-primary w-full"
|
|
||||||
onclick={openCamera}
|
|
||||||
>
|
|
||||||
<Camera class="h-5 w-5" />
|
<Camera class="h-5 w-5" />
|
||||||
Capturar da Câmera
|
Capturar da Câmera
|
||||||
</button>
|
</button>
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if preview}
|
{#if preview}
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="mt-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline btn-primary flex-1"
|
class="btn btn-sm btn-outline btn-primary flex-1"
|
||||||
@@ -363,11 +363,7 @@
|
|||||||
<ImageIcon class="h-4 w-4" />
|
<ImageIcon class="h-4 w-4" />
|
||||||
Alterar Imagem
|
Alterar Imagem
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" class="btn btn-sm btn-outline btn-primary flex-1" onclick={openCamera}>
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline btn-primary flex-1"
|
|
||||||
onclick={openCamera}
|
|
||||||
>
|
|
||||||
<Camera class="h-4 w-4" />
|
<Camera class="h-4 w-4" />
|
||||||
Capturar Foto
|
Capturar Foto
|
||||||
</button>
|
</button>
|
||||||
@@ -377,12 +373,15 @@
|
|||||||
|
|
||||||
<!-- Modal da Câmera -->
|
<!-- Modal da Câmera -->
|
||||||
{#if showCamera}
|
{#if showCamera}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80" onclick={closeCamera}>
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
|
||||||
|
onclick={closeCamera}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-base-100 rounded-lg shadow-2xl p-6 max-w-2xl w-full mx-4"
|
class="bg-base-100 mx-4 w-full max-w-2xl rounded-lg p-6 shadow-2xl"
|
||||||
onclick={(e) => e.stopPropagation()}
|
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>
|
<h3 class="text-xl font-bold">Capturar Foto</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -394,19 +393,24 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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}
|
{#if showCamera}
|
||||||
<video
|
<video
|
||||||
bind:this={videoElement}
|
bind:this={videoElement}
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
style="transform: scaleX(-1); opacity: {capturing ? '1' : '0'}; transition: opacity 0.3s;"
|
style="transform: scaleX(-1); opacity: {capturing
|
||||||
|
? '1'
|
||||||
|
: '0'}; transition: opacity 0.3s;"
|
||||||
></video>
|
></video>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !capturing}
|
{#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">
|
<div class="text-center">
|
||||||
<span class="loading loading-spinner loading-lg text-primary mb-2"></span>
|
<span class="loading loading-spinner loading-lg text-primary mb-2"></span>
|
||||||
<p class="text-base-content/70 text-sm">Iniciando câmera...</p>
|
<p class="text-base-content/70 text-sm">Iniciando câmera...</p>
|
||||||
@@ -415,20 +419,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex justify-end gap-2">
|
||||||
<button
|
<button type="button" class="btn btn-ghost" onclick={closeCamera}> Cancelar </button>
|
||||||
type="button"
|
<button type="button" class="btn btn-primary" onclick={capturePhoto} disabled={!capturing}>
|
||||||
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" />
|
<Camera class="h-5 w-5" />
|
||||||
Capturar Foto
|
Capturar Foto
|
||||||
</button>
|
</button>
|
||||||
@@ -442,4 +435,3 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Settings
|
List
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte';
|
import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte';
|
||||||
|
|
||||||
@@ -323,6 +323,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</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
|
<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"
|
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')}
|
onclick={() => goto('/almoxarifado/relatorios')}
|
||||||
@@ -337,21 +352,6 @@
|
|||||||
<p class="text-base-content/70 text-sm">Visualizar relatórios e estatísticas</p>
|
<p class="text-base-content/70 text-sm">Visualizar relatórios e estatísticas</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import type { AlertaStatus, AlertaTipo } from '@sgse-app/backend/convex/tables/almoxarifado';
|
import type { AlertaStatus, AlertaTipo } from '@sgse-app/backend/convex/tables/almoxarifado';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { resolve } from '$app/paths';
|
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';
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -114,12 +114,14 @@
|
|||||||
<!-- Cabeçalho -->
|
<!-- Cabeçalho -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<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} />
|
<AlertTriangle class="h-10 w-10 text-warning" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Alertas de Estoque</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-warning to-warning/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Visualize e gerencie alertas de estoque baixo</p>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,57 +134,74 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h3 class="text-lg font-semibold mb-4">Filtros</h3>
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<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">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Tipo de Alerta</span>
|
<span class="label-text font-semibold">Tipo de Alerta</span>
|
||||||
</label>
|
</label>
|
||||||
<select class="select select-bordered" bind:value={filtroTipo}>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroTipo}>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos os tipos</option>
|
||||||
<option value="estoque_zerado">Estoque Zerado</option>
|
<option value="estoque_zerado">Estoque Zerado</option>
|
||||||
<option value="estoque_minimo">Estoque Mínimo</option>
|
<option value="estoque_minimo">Estoque Mínimo</option>
|
||||||
<option value="reposicao_necessaria">Reposição Necessária</option>
|
<option value="reposicao_necessaria">Reposição Necessária</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Filtre por tipo de alerta</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Status</span>
|
<span class="label-text font-semibold">Status</span>
|
||||||
</label>
|
</label>
|
||||||
<select class="select select-bordered" bind:value={filtroStatus}>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroStatus}>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos os status</option>
|
||||||
<option value="ativo">Ativo</option>
|
<option value="ativo">Ativo</option>
|
||||||
<option value="resolvido">Resolvido</option>
|
<option value="resolvido">Resolvido</option>
|
||||||
<option value="ignorado">Ignorado</option>
|
<option value="ignorado">Ignorado</option>
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de Alertas -->
|
<!-- Lista de Alertas -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<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}
|
{#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>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else if alertasQuery.data && alertasQuery.data.length > 0}
|
{: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">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
<th class="font-semibold">Material</th>
|
<th class="font-bold text-base-content">Material</th>
|
||||||
<th class="font-semibold">Tipo</th>
|
<th class="font-bold text-base-content">Tipo</th>
|
||||||
<th class="font-semibold">Quantidade Atual</th>
|
<th class="font-bold text-base-content">Quantidade Atual</th>
|
||||||
<th class="font-semibold">Quantidade Mínima</th>
|
<th class="font-bold text-base-content">Quantidade Mínima</th>
|
||||||
<th class="font-semibold">Diferença</th>
|
<th class="font-bold text-base-content">Diferença</th>
|
||||||
<th class="font-semibold">Status</th>
|
<th class="font-bold text-base-content">Status</th>
|
||||||
<th class="font-semibold">Data</th>
|
<th class="font-bold text-base-content">Data</th>
|
||||||
<th class="font-semibold">Ações</th>
|
<th class="font-bold text-base-content">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -224,15 +243,17 @@
|
|||||||
{#if alerta.status === 'ativo'}
|
{#if alerta.status === 'ativo'}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success transition-all"
|
||||||
onclick={() => resolverAlerta(alerta._id)}
|
onclick={() => resolverAlerta(alerta._id)}
|
||||||
|
title="Resolver alerta"
|
||||||
>
|
>
|
||||||
<CheckCircle class="h-4 w-4" />
|
<CheckCircle class="h-4 w-4" />
|
||||||
Resolver
|
Resolver
|
||||||
</button>
|
</button>
|
||||||
<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)}
|
onclick={() => abrirModalIgnorar(alerta._id)}
|
||||||
|
title="Ignorar alerta"
|
||||||
>
|
>
|
||||||
<XCircle class="h-4 w-4" />
|
<XCircle class="h-4 w-4" />
|
||||||
Ignorar
|
Ignorar
|
||||||
@@ -245,11 +266,19 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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}
|
{:else}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-16">
|
||||||
<AlertTriangle class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
|
<AlertTriangle class="mx-auto mb-6 h-24 w-24 text-base-content/30" />
|
||||||
<h3 class="text-2xl font-bold mb-2">Nenhum alerta encontrado</h3>
|
<h3 class="text-2xl font-bold mb-3 text-base-content">Nenhum alerta encontrado</h3>
|
||||||
<p class="text-base-content/70 text-lg mb-4">
|
<p class="text-base-content/70 text-lg mb-6">
|
||||||
{#if filtroStatus === 'ativo'}
|
{#if filtroStatus === 'ativo'}
|
||||||
Não há alertas ativos no momento. Todos os materiais estão com estoque adequado!
|
Não há alertas ativos no momento. Todos os materiais estão com estoque adequado!
|
||||||
{:else if filtroStatus || filtroTipo}
|
{:else if filtroStatus || filtroTipo}
|
||||||
@@ -258,11 +287,11 @@
|
|||||||
Ainda não há alertas registrados no sistema.
|
Ainda não há alertas registrados no sistema.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<div class="alert alert-info max-w-2xl mx-auto">
|
<div class="alert alert-info max-w-2xl mx-auto border-info/30 bg-info/10">
|
||||||
<AlertTriangle class="h-6 w-6" />
|
<AlertTriangle class="h-6 w-6 text-info shrink-0" />
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<h4 class="font-bold mb-2">Como os alertas funcionam?</h4>
|
<h4 class="font-bold mb-3 text-base-content">Como os alertas funcionam?</h4>
|
||||||
<ul class="text-sm space-y-1 list-disc list-inside">
|
<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>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>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>
|
<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 { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
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';
|
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -12,7 +12,18 @@
|
|||||||
// Usar useQuery para atualização automática
|
// Usar useQuery para atualização automática
|
||||||
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {});
|
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 filtered = $state<Array<Doc<'materiais'>>>([]);
|
||||||
let filtroBusca = $state('');
|
let filtroBusca = $state('');
|
||||||
let filtroCategoria = $state('');
|
let filtroCategoria = $state('');
|
||||||
@@ -253,15 +264,17 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
<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="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">
|
<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-amber-600" strokeWidth={2.5} />
|
<Package class="h-10 w-10 text-primary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Materiais</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Gerencie o cadastro de materiais do almoxarifado</p>
|
Materiais
|
||||||
|
</h1>
|
||||||
|
<p class="text-base-content/70 text-lg mt-1">Gerencie o cadastro e controle de materiais do almoxarifado</p>
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<Plus class="h-5 w-5" />
|
||||||
Cadastrar Material
|
Cadastrar Material
|
||||||
</button>
|
</button>
|
||||||
@@ -276,41 +289,52 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="mb-6 flex items-center justify-between border-b-2 border-primary/20 pb-4">
|
||||||
<h3 class="text-lg font-semibold">Filtros de Busca</h3>
|
<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
|
<BarcodeScanner
|
||||||
enabled={scannerEnabled}
|
enabled={scannerEnabled}
|
||||||
onScan={handleBarcodeScanned}
|
onScan={handleBarcodeScanned}
|
||||||
onError={(error) => console.error('Erro no scanner:', error)}
|
onError={(error) => console.error('Erro no scanner:', error)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Buscar</span>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<Search class="h-4 w-4" />
|
||||||
|
Buscar
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
|
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Código, nome ou código de barras..."
|
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}
|
bind:value={filtroBusca}
|
||||||
/>
|
/>
|
||||||
{#if buscandoPorCodigoBarras}
|
{#if buscandoPorCodigoBarras}
|
||||||
<span class="loading loading-spinner loading-xs absolute right-3 top-1/2 -translate-y-1/2"></span>
|
<span class="loading loading-spinner loading-xs absolute right-3 top-1/2 -translate-y-1/2"></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Categoria</span>
|
<span class="label-text font-semibold">Categoria</span>
|
||||||
</label>
|
</label>
|
||||||
<select class="select select-bordered" bind:value={filtroCategoria}>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroCategoria}>
|
||||||
<option value="">Todas</option>
|
<option value="">Todas as categorias</option>
|
||||||
{#each categorias as cat}
|
{#each categorias as cat}
|
||||||
<option value={cat}>{cat}</option>
|
<option value={cat}>{cat}</option>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -318,10 +342,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Status</span>
|
<span class="label-text font-semibold">Status</span>
|
||||||
</label>
|
</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="">Todos</option>
|
||||||
<option value={true}>Ativos</option>
|
<option value={true}>Ativos</option>
|
||||||
<option value={false}>Inativos</option>
|
<option value={false}>Inativos</option>
|
||||||
@@ -329,9 +353,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label cursor-pointer justify-start gap-2">
|
<label class="label pb-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={filtroEstoqueBaixo} />
|
<span class="label-text font-semibold">Filtros Adicionais</span>
|
||||||
<span class="label-text">Apenas estoque baixo</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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,30 +366,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabela -->
|
<!-- Tabela -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<div class="overflow-x-auto">
|
<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">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
<th class="font-semibold">Código</th>
|
<th class="font-bold text-base-content">Código</th>
|
||||||
<th class="font-semibold">Nome</th>
|
<th class="font-bold text-base-content">Nome</th>
|
||||||
<th class="font-semibold">Categoria</th>
|
<th class="font-bold text-base-content">Categoria</th>
|
||||||
<th class="font-semibold">Estoque Atual</th>
|
<th class="font-bold text-base-content">Estoque Atual</th>
|
||||||
<th class="font-semibold">Estoque Mínimo</th>
|
<th class="font-bold text-base-content">Estoque Mínimo</th>
|
||||||
<th class="font-semibold">Unidade</th>
|
<th class="font-bold text-base-content">Unidade</th>
|
||||||
<th class="font-semibold">Status</th>
|
<th class="font-bold text-base-content">Status</th>
|
||||||
<th class="font-semibold">Ações</th>
|
<th class="font-bold text-base-content">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#if filtered.length === 0}
|
{#if filtered.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center">
|
<td colspan="8" class="text-center">
|
||||||
<div class="py-12">
|
<div class="py-16">
|
||||||
<Package class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
|
<Package class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
|
||||||
<p class="text-base-content/70 text-lg font-medium">Nenhum material encontrado</p>
|
<p class="text-base-content/80 text-xl font-semibold mb-2">Nenhum material encontrado</p>
|
||||||
<p class="text-base-content/50 text-sm mt-2">Tente ajustar os filtros de busca</p>
|
<p class="text-base-content/60 text-base">Tente ajustar os filtros de busca ou cadastre um novo material</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -405,8 +438,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost hover:btn-primary"
|
class="btn btn-sm btn-ghost hover:btn-primary transition-all"
|
||||||
title="Visualizar"
|
title="Visualizar detalhes"
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
goto(
|
goto(
|
||||||
resolve(
|
resolve(
|
||||||
@@ -418,8 +451,8 @@
|
|||||||
<Eye class="h-4 w-4" />
|
<Eye class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost hover:btn-info"
|
class="btn btn-sm btn-ghost hover:btn-info transition-all"
|
||||||
title="Editar"
|
title="Editar material"
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
goto(
|
goto(
|
||||||
resolve(
|
resolve(
|
||||||
@@ -432,8 +465,8 @@
|
|||||||
<Edit class="h-4 w-4" />
|
<Edit class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-ghost hover:btn-error"
|
class="btn btn-sm btn-ghost hover:btn-error transition-all"
|
||||||
title="Excluir"
|
title="Excluir material"
|
||||||
onclick={() => abrirModalExclusao(material)}
|
onclick={() => abrirModalExclusao(material)}
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="h-4 w-4" />
|
||||||
@@ -448,9 +481,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if filtered.length > 0}
|
{#if filtered.length > 0}
|
||||||
<div class="mt-6 flex items-center justify-between border-t border-base-300 pt-4">
|
<div class="mt-8 flex items-center justify-between border-t-2 border-base-300 pt-6">
|
||||||
<div class="text-sm text-base-content/70">
|
<div class="text-base font-semibold text-base-content/80">
|
||||||
Mostrando <span class="font-semibold text-base-content">{filtered.length}</span> de <span class="font-semibold text-base-content">{materiais.length}</span> materiais
|
Mostrando <span class="text-primary font-bold">{filtered.length}</span> de <span class="text-primary font-bold">{materiais.length}</span> materiais
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -458,29 +491,42 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de Confirmação de Exclusão -->
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
<dialog id="modal-excluir-material" class="modal">
|
<dialog id="modal-excluir-material" class="modal backdrop-blur-sm">
|
||||||
<div class="modal-box">
|
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
|
||||||
<h3 class="text-lg font-bold mb-4">Confirmar Exclusão</h3>
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-error/20 pb-4">
|
||||||
|
<div class="rounded-2xl bg-error/20 p-3">
|
||||||
|
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-bold text-base-content">Confirmar Exclusão</h3>
|
||||||
|
<p class="text-base-content/70 mt-1">Esta ação não pode ser desfeita</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{#if materialParaExcluir}
|
{#if materialParaExcluir}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4 mb-6">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning border-warning/30 bg-warning/10">
|
||||||
<AlertTriangle class="h-5 w-5" />
|
<AlertTriangle class="h-5 w-5 shrink-0 text-warning" />
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<p class="font-semibold">Atenção!</p>
|
<p class="font-semibold text-base-content">Atenção!</p>
|
||||||
<p class="text-sm">
|
<p class="text-sm text-base-content/90 mt-1">
|
||||||
Esta ação não pode ser desfeita. O material será permanentemente excluído.
|
Esta ação não pode ser desfeita. O material será permanentemente excluído do sistema.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-base-200 rounded-lg p-4">
|
<div class="bg-base-200 rounded-lg p-5 border border-base-300">
|
||||||
<p class="text-sm text-base-content/70 mb-2">Material a ser excluído:</p>
|
<p class="text-sm text-base-content/70 mb-3 font-semibold">Material a ser excluído:</p>
|
||||||
<p class="font-semibold text-base-content">{materialParaExcluir.nome}</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">{materialParaExcluir.codigo}</span>
|
Código: <span class="font-mono font-semibold">{materialParaExcluir.codigo}</span>
|
||||||
</p>
|
</p>
|
||||||
|
{#if materialParaExcluir.codigoBarras}
|
||||||
|
<p class="text-sm text-base-content/60 mt-1">
|
||||||
|
Código de Barras: <span class="font-mono font-semibold">{materialParaExcluir.codigoBarras}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
{#if materialParaExcluir.estoqueAtual > 0}
|
{#if materialParaExcluir.estoqueAtual > 0}
|
||||||
<div class="mt-2 alert alert-info py-2">
|
<div class="mt-3 alert alert-info py-2 border-info/30 bg-info/10">
|
||||||
<p class="text-xs">
|
<p class="text-sm font-medium">
|
||||||
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades em estoque.
|
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades em estoque.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,16 +534,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="modal-action">
|
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
onclick={fecharModalExclusao}
|
onclick={fecharModalExclusao}
|
||||||
disabled={excluindo}
|
disabled={excluindo}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-error"
|
class="btn btn-error btn-lg min-w-[180px] shadow-lg hover:shadow-xl"
|
||||||
onclick={confirmarExclusao}
|
onclick={confirmarExclusao}
|
||||||
disabled={excluindo}
|
disabled={excluindo}
|
||||||
>
|
>
|
||||||
@@ -505,8 +551,8 @@
|
|||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Excluindo...
|
Excluindo...
|
||||||
{:else}
|
{:else}
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="h-5 w-5" />
|
||||||
Excluir
|
Excluir Material
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -518,9 +564,9 @@
|
|||||||
|
|
||||||
<!-- Modal de Erro na Exclusão -->
|
<!-- Modal de Erro na Exclusão -->
|
||||||
{#if erroExclusao}
|
{#if erroExclusao}
|
||||||
<dialog id="modal-erro-exclusao" class="modal modal-open">
|
<dialog id="modal-erro-exclusao" class="modal modal-open backdrop-blur-sm">
|
||||||
<div class="modal-box max-w-2xl">
|
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<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">
|
<div class="rounded-2xl bg-error/20 p-3">
|
||||||
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
|
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
@@ -540,21 +586,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if materialParaExcluir}
|
{#if materialParaExcluir}
|
||||||
<div class="bg-base-200 rounded-lg p-4 border border-base-300">
|
<div class="bg-base-200 rounded-lg p-5 border border-base-300">
|
||||||
<p class="text-sm text-base-content/70 mb-2 font-semibold">Material:</p>
|
<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="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>
|
Código: <span class="font-mono font-semibold">{materialParaExcluir.codigo}</span>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
{/if}
|
{/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">
|
<div class="flex gap-3">
|
||||||
<Info class="h-5 w-5 text-info shrink-0 mt-0.5" />
|
<Info class="h-5 w-5 text-info shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-base-content mb-1">Solução recomendada</p>
|
<p class="font-semibold text-base-content mb-2">Solução recomendada</p>
|
||||||
<p class="text-sm text-base-content/80">
|
<p class="text-sm text-base-content/80 leading-relaxed">
|
||||||
{#if erroExclusao.tipo === 'movimentacoes'}
|
{#if erroExclusao.tipo === 'movimentacoes'}
|
||||||
O material possui histórico de movimentações de estoque. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
|
O material possui histórico de movimentações de estoque. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
|
||||||
{:else if erroExclusao.tipo === 'requisicoes'}
|
{:else if erroExclusao.tipo === 'requisicoes'}
|
||||||
@@ -568,9 +619,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action gap-3">
|
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
onclick={fecharModalErro}
|
onclick={fecharModalErro}
|
||||||
disabled={desativando}
|
disabled={desativando}
|
||||||
>
|
>
|
||||||
@@ -578,7 +629,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if erroExclusao.tipo === 'movimentacoes' || erroExclusao.tipo === 'requisicoes'}
|
{#if erroExclusao.tipo === 'movimentacoes' || erroExclusao.tipo === 'requisicoes'}
|
||||||
<button
|
<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}
|
onclick={desativarMaterial}
|
||||||
disabled={desativando}
|
disabled={desativando}
|
||||||
>
|
>
|
||||||
@@ -586,7 +637,7 @@
|
|||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Desativando...
|
Desativando...
|
||||||
{:else}
|
{:else}
|
||||||
<X class="h-4 w-4" />
|
<X class="h-5 w-5" />
|
||||||
Desativar Material
|
Desativar Material
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -601,3 +652,4 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
let estoqueMaximo = $state<number | undefined>(undefined);
|
let estoqueMaximo = $state<number | undefined>(undefined);
|
||||||
let localizacao = $state('');
|
let localizacao = $state('');
|
||||||
let fornecedor = $state('');
|
let fornecedor = $state('');
|
||||||
|
let codigoBarras = $state('');
|
||||||
|
let imagemBase64 = $state<string | null>(null);
|
||||||
let ativo = $state(true);
|
let ativo = $state(true);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let loadingData = $state(true);
|
let loadingData = $state(true);
|
||||||
@@ -69,6 +71,8 @@
|
|||||||
estoqueMaximo = material.estoqueMaximo;
|
estoqueMaximo = material.estoqueMaximo;
|
||||||
localizacao = material.localizacao || '';
|
localizacao = material.localizacao || '';
|
||||||
fornecedor = material.fornecedor || '';
|
fornecedor = material.fornecedor || '';
|
||||||
|
codigoBarras = material.codigoBarras || '';
|
||||||
|
imagemBase64 = material.imagemBase64 || null;
|
||||||
ativo = material.ativo;
|
ativo = material.ativo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao carregar material:', error);
|
console.error('Erro ao carregar material:', error);
|
||||||
@@ -112,6 +116,8 @@
|
|||||||
estoqueMaximo,
|
estoqueMaximo,
|
||||||
localizacao: localizacao.trim() || undefined,
|
localizacao: localizacao.trim() || undefined,
|
||||||
fornecedor: fornecedor.trim() || undefined,
|
fornecedor: fornecedor.trim() || undefined,
|
||||||
|
codigoBarras: codigoBarras.trim() || undefined,
|
||||||
|
imagemBase64: imagemBase64 || undefined,
|
||||||
ativo
|
ativo
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -323,6 +329,33 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 -->
|
<!-- Status Ativo -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label cursor-pointer justify-start gap-2">
|
<label class="label cursor-pointer justify-start gap-2">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { tick } from 'svelte';
|
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 BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||||
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
|
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
fornecedor = dadosExternos.marca;
|
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) {
|
if (dadosExternos.imagemUrl) {
|
||||||
let imagemParaSalvar: string | null = null;
|
let imagemParaSalvar: string | null = null;
|
||||||
const imagemUrlAtual = dadosExternos.imagemUrl;
|
const imagemUrlAtual = dadosExternos.imagemUrl;
|
||||||
@@ -141,16 +141,15 @@
|
|||||||
imagemParaSalvar = imagemUrlAtual;
|
imagemParaSalvar = imagemUrlAtual;
|
||||||
console.log('Imagem já carregada em background, usando diretamente');
|
console.log('Imagem já carregada em background, usando diretamente');
|
||||||
} else {
|
} else {
|
||||||
// Ainda é uma URL, aguardar um pouco para ver se o carregamento em background terminou
|
// Ainda é uma URL, tentar 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, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Verificar novamente se foi carregada em background
|
// Verificar novamente se foi carregada em background
|
||||||
if (dadosExternos.imagemUrl && dadosExternos.imagemUrl.startsWith('data:')) {
|
if (dadosExternos.imagemUrl && dadosExternos.imagemUrl.startsWith('data:')) {
|
||||||
imagemParaSalvar = dadosExternos.imagemUrl;
|
imagemParaSalvar = dadosExternos.imagemUrl;
|
||||||
console.log('Imagem foi carregada em background durante a espera');
|
console.log('Imagem foi carregada em background durante a espera');
|
||||||
} else {
|
} else {
|
||||||
// Ainda é URL, carregar agora
|
// Ainda é URL, carregar agora de forma síncrona
|
||||||
console.log('Carregando imagem da URL:', imagemUrlAtual);
|
console.log('Carregando imagem da URL:', imagemUrlAtual);
|
||||||
try {
|
try {
|
||||||
const imagemBase64Carregada = await carregarImagemDeUrl(imagemUrlAtual);
|
const imagemBase64Carregada = await carregarImagemDeUrl(imagemUrlAtual);
|
||||||
@@ -168,16 +167,22 @@
|
|||||||
|
|
||||||
// Atribuir a imagem após o carregamento completo
|
// Atribuir a imagem após o carregamento completo
|
||||||
if (imagemParaSalvar) {
|
if (imagemParaSalvar) {
|
||||||
|
// Atribuir diretamente ao estado - isso deve atualizar o componente ImageUpload automaticamente
|
||||||
imagemBase64 = imagemParaSalvar;
|
imagemBase64 = imagemParaSalvar;
|
||||||
|
|
||||||
// Aguardar o tick para garantir que o componente ImageUpload detecte a mudança
|
// Aguardar o tick para garantir que o componente ImageUpload detecte a mudança
|
||||||
await tick();
|
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');
|
console.log('Imagem atribuída ao campo imagemBase64, tamanho:', imagemParaSalvar.length, 'caracteres');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Nenhuma imagem disponível para salvar');
|
console.warn('Nenhuma imagem disponível para salvar');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fechar modal
|
// Fechar modal após garantir que tudo foi carregado
|
||||||
modalDadosExternos = false;
|
modalDadosExternos = false;
|
||||||
const dadosTemp = dadosExternos;
|
const dadosTemp = dadosExternos;
|
||||||
dadosExternos = null;
|
dadosExternos = null;
|
||||||
@@ -454,12 +459,14 @@
|
|||||||
>
|
>
|
||||||
<ArrowLeft class="h-5 w-5" />
|
<ArrowLeft class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<div class="rounded-2xl bg-gradient-to-br from-amber-500/20 to-amber-600/30 p-4 shadow-lg">
|
<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-amber-600" strokeWidth={2.5} />
|
<Package class="h-10 w-10 text-primary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Cadastrar Material</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Adicione um novo material ao almoxarifado</p>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -472,225 +479,285 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Formulário -->
|
<!-- Formulário -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||||
<!-- Leitor de Código de Barras -->
|
<!-- Seção: Identificação -->
|
||||||
<div class="mb-6">
|
<div class="mb-10">
|
||||||
<BarcodeScanner
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
|
||||||
enabled={scannerEnabled}
|
<div class="rounded-lg bg-primary/10 p-2.5">
|
||||||
onScan={handleBarcodeScanned}
|
<Box class="h-5 w-5 text-primary" strokeWidth={2.5} />
|
||||||
onError={(error) => mostrarMensagem('error', error)}
|
</div>
|
||||||
/>
|
<h2 class="text-xl font-bold text-base-content">Identificação do Material</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leitor de Código de Barras -->
|
||||||
|
<div class="mb-8 rounded-xl border border-base-300 bg-base-200/50 p-4">
|
||||||
|
<BarcodeScanner
|
||||||
|
enabled={scannerEnabled}
|
||||||
|
onScan={handleBarcodeScanned}
|
||||||
|
onError={(error) => mostrarMensagem('error', error)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<!-- Código -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<Package class="h-4 w-4" />
|
||||||
|
Código <span class="text-error">*</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
placeholder="Ex: MAT-001"
|
||||||
|
bind:value={codigo}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Código único identificador do material</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Código de Barras -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<Barcode class="h-4 w-4" />
|
||||||
|
Código de Barras
|
||||||
|
{#if buscandoProduto || buscandoExterno}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12 {(buscandoProduto || buscandoExterno) ? 'input-info' : ''}"
|
||||||
|
placeholder="EAN-13, UPC, etc."
|
||||||
|
bind:value={codigoBarras}
|
||||||
|
onkeydown={handleBarcodeKeydown}
|
||||||
|
oninput={() => verificarEExecutarBusca(codigoBarras)}
|
||||||
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">
|
||||||
|
{#if buscandoProduto}
|
||||||
|
<span class="text-info">Buscando produto no banco de dados...</span>
|
||||||
|
{:else if buscandoExterno}
|
||||||
|
<span class="text-info">Buscando produto em base externa...</span>
|
||||||
|
{:else}
|
||||||
|
Digite ou use o leitor acima para escanear
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nome -->
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Nome do Material <span class="text-error">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
placeholder="Digite o nome completo do material"
|
||||||
|
bind:value={nome}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descrição -->
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Descrição</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
|
||||||
|
placeholder="Descrição detalhada do material (opcional)"
|
||||||
|
bind:value={descricao}
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categoria -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Categoria <span class="text-error">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
list="categorias"
|
||||||
|
placeholder="Ex: Escritório"
|
||||||
|
bind:value={categoria}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<datalist id="categorias">
|
||||||
|
{#each categoriasComuns as cat}
|
||||||
|
<option value={cat} />
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Unidade de Medida -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Unidade de Medida <span class="text-error">*</span></span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={unidadeMedida} required>
|
||||||
|
{#each unidadesMedida as un}
|
||||||
|
<option value={un}>{un}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<!-- Seção: Estoque -->
|
||||||
<!-- Código -->
|
<div class="mb-10">
|
||||||
<div class="form-control md:col-span-1">
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-success/20 pb-4">
|
||||||
<label class="label">
|
<div class="rounded-lg bg-success/10 p-2.5">
|
||||||
<span class="label-text font-bold">Código *</span>
|
<Warehouse class="h-5 w-5 text-success" strokeWidth={2.5} />
|
||||||
</label>
|
</div>
|
||||||
<input
|
<h2 class="text-xl font-bold text-base-content">Controle de Estoque</h2>
|
||||||
type="text"
|
|
||||||
class="input input-bordered"
|
|
||||||
placeholder="Ex: MAT-001"
|
|
||||||
bind:value={codigo}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text-alt">Código único do material</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Código de Barras -->
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<div class="form-control md:col-span-1">
|
<!-- Estoque Mínimo -->
|
||||||
<label class="label">
|
<div class="form-control">
|
||||||
<span class="label-text">Código de Barras</span>
|
<label class="label pb-2">
|
||||||
{#if buscandoProduto || buscandoExterno}
|
<span class="label-text font-semibold">Estoque Mínimo <span class="text-error">*</span></span>
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
</label>
|
||||||
{/if}
|
<input
|
||||||
</label>
|
type="number"
|
||||||
<input
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
type="text"
|
min="0"
|
||||||
class="input input-bordered {(buscandoProduto || buscandoExterno) ? 'input-info' : ''}"
|
bind:value={estoqueMinimo}
|
||||||
placeholder="EAN-13, UPC, etc."
|
required
|
||||||
bind:value={codigoBarras}
|
/>
|
||||||
onkeydown={handleBarcodeKeydown}
|
<label class="label pt-1">
|
||||||
oninput={() => verificarEExecutarBusca(codigoBarras)}
|
<span class="label-text-alt text-base-content/60">Quantidade mínima para alerta</span>
|
||||||
/>
|
</label>
|
||||||
<label class="label">
|
</div>
|
||||||
<span class="label-text-alt">
|
|
||||||
{#if buscandoProduto}
|
<!-- Estoque Máximo -->
|
||||||
Buscando produto no banco de dados...
|
<div class="form-control">
|
||||||
{:else if buscandoExterno}
|
<label class="label pb-2">
|
||||||
Buscando produto em base externa...
|
<span class="label-text font-semibold">Estoque Máximo</span>
|
||||||
{:else}
|
</label>
|
||||||
Digite ou use o leitor acima para escanear
|
<input
|
||||||
{/if}
|
type="number"
|
||||||
</span>
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
</label>
|
min="0"
|
||||||
|
bind:value={estoqueMaximo}
|
||||||
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Opcional - Capacidade máxima</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estoque Atual -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Estoque Inicial</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
min="0"
|
||||||
|
bind:value={estoqueAtual}
|
||||||
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Quantidade inicial em estoque</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Localização -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Localização Física</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
placeholder="Ex: Prateleira A-01, Setor B"
|
||||||
|
bind:value={localizacao}
|
||||||
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Onde o material está armazenado</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Seção: Informações Adicionais -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
|
||||||
|
<div class="rounded-lg bg-info/10 p-2.5">
|
||||||
|
<ShoppingCart class="h-5 w-5 text-info" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-base-content">Informações Adicionais</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nome -->
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div class="form-control md:col-span-1">
|
<!-- Fornecedor -->
|
||||||
<label class="label">
|
<div class="form-control">
|
||||||
<span class="label-text font-bold">Nome *</span>
|
<label class="label pb-2">
|
||||||
</label>
|
<span class="label-text font-semibold">Fornecedor</span>
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<input
|
||||||
class="input input-bordered"
|
type="text"
|
||||||
placeholder="Nome do material"
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
bind:value={nome}
|
placeholder="Nome do fornecedor (opcional)"
|
||||||
required
|
bind:value={fornecedor}
|
||||||
/>
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Fornecedor principal do material</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Seção: Imagem -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
|
||||||
|
<div class="rounded-lg bg-warning/10 p-2.5">
|
||||||
|
<Image class="h-5 w-5 text-warning" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-base-content">Imagem do Produto</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Descrição -->
|
<div class="form-control">
|
||||||
<div class="form-control md:col-span-2">
|
<label class="label pb-2">
|
||||||
<label class="label">
|
<span class="label-text font-semibold">Upload de Imagem</span>
|
||||||
<span class="label-text">Descrição</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
class="textarea textarea-bordered"
|
|
||||||
placeholder="Descrição detalhada do material (opcional)"
|
|
||||||
bind:value={descricao}
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoria -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-bold">Categoria *</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered"
|
|
||||||
list="categorias"
|
|
||||||
placeholder="Ex: Escritório"
|
|
||||||
bind:value={categoria}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<datalist id="categorias">
|
|
||||||
{#each categoriasComuns as cat}
|
|
||||||
<option value={cat} />
|
|
||||||
{/each}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Unidade de Medida -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-bold">Unidade de Medida *</span>
|
|
||||||
</label>
|
|
||||||
<select class="select select-bordered" bind:value={unidadeMedida} required>
|
|
||||||
{#each unidadesMedida as un}
|
|
||||||
<option value={un}>{un}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estoque Mínimo -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-bold">Estoque Mínimo *</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="input input-bordered"
|
|
||||||
min="0"
|
|
||||||
bind:value={estoqueMinimo}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estoque Máximo -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Estoque Máximo</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="input input-bordered"
|
|
||||||
min="0"
|
|
||||||
bind:value={estoqueMaximo}
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text-alt">Opcional</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estoque Atual -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Estoque Inicial</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="input input-bordered"
|
|
||||||
min="0"
|
|
||||||
bind:value={estoqueAtual}
|
|
||||||
/>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text-alt">Quantidade inicial em estoque</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Localização -->
|
|
||||||
<div class="form-control md:col-span-1">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Localização</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered"
|
|
||||||
placeholder="Ex: Prateleira A-01"
|
|
||||||
bind:value={localizacao}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fornecedor -->
|
|
||||||
<div class="form-control md:col-span-2">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Fornecedor</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered"
|
|
||||||
placeholder="Nome do fornecedor (opcional)"
|
|
||||||
bind:value={fornecedor}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imagem do Produto -->
|
|
||||||
<div class="form-control md:col-span-2">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Imagem do Produto</span>
|
|
||||||
</label>
|
</label>
|
||||||
<ImageUpload bind:value={imagemBase64} />
|
<ImageUpload bind:value={imagemBase64} />
|
||||||
<label class="label">
|
<label class="label pt-1">
|
||||||
<span class="label-text-alt">Upload opcional da imagem do produto</span>
|
<span class="label-text-alt text-base-content/60">Upload opcional da imagem do produto para melhor identificação</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botões -->
|
<!-- 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
onclick={() => goto(resolve('/almoxarifado/materiais'))}
|
onclick={() => goto(resolve('/almoxarifado/materiais'))}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
<ArrowLeft class="h-5 w-5" />
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</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}
|
{#if loading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Cadastrando...
|
||||||
{:else}
|
{:else}
|
||||||
<Save class="h-5 w-5" />
|
<Save class="h-5 w-5" />
|
||||||
|
Cadastrar Material
|
||||||
{/if}
|
{/if}
|
||||||
Cadastrar
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { resolve } from '$app/paths';
|
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();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, { ativo: true });
|
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, { ativo: true });
|
||||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||||
const setoresQuery = useQuery(api.setores.list, {});
|
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
|
// Criar mapa de funcionários para lookup eficiente
|
||||||
const funcionariosMap = $derived.by(() => {
|
const funcionariosMap = $derived.by(() => {
|
||||||
@@ -187,12 +187,14 @@
|
|||||||
<!-- Cabeçalho -->
|
<!-- Cabeçalho -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<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} />
|
<ArrowLeftRight class="h-10 w-10 text-info" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Movimentações de Estoque</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-info to-info/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Registre entradas, saídas e ajustes de estoque</p>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,30 +207,30 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Abas -->
|
<!-- 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
|
<button
|
||||||
class="tab {abaAtiva === 'entrada' ? 'tab-active' : ''} transition-all"
|
class="tab {abaAtiva === 'entrada' ? 'tab-active' : ''} transition-all font-semibold"
|
||||||
onclick={() => (abaAtiva = 'entrada')}
|
onclick={() => (abaAtiva = 'entrada')}
|
||||||
>
|
>
|
||||||
<ArrowDown class="h-5 w-5 mr-2" />
|
<ArrowDown class="h-5 w-5 mr-2" />
|
||||||
Entrada
|
Entrada
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {abaAtiva === 'saida' ? 'tab-active' : ''} transition-all"
|
class="tab {abaAtiva === 'saida' ? 'tab-active' : ''} transition-all font-semibold"
|
||||||
onclick={() => (abaAtiva = 'saida')}
|
onclick={() => (abaAtiva = 'saida')}
|
||||||
>
|
>
|
||||||
<ArrowUp class="h-5 w-5 mr-2" />
|
<ArrowUp class="h-5 w-5 mr-2" />
|
||||||
Saída
|
Saída
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {abaAtiva === 'ajuste' ? 'tab-active' : ''} transition-all"
|
class="tab {abaAtiva === 'ajuste' ? 'tab-active' : ''} transition-all font-semibold"
|
||||||
onclick={() => (abaAtiva = 'ajuste')}
|
onclick={() => (abaAtiva = 'ajuste')}
|
||||||
>
|
>
|
||||||
<Settings class="h-5 w-5 mr-2" />
|
<Settings class="h-5 w-5 mr-2" />
|
||||||
Ajuste
|
Ajuste
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab {abaAtiva === 'historico' ? 'tab-active' : ''} transition-all"
|
class="tab {abaAtiva === 'historico' ? 'tab-active' : ''} transition-all font-semibold"
|
||||||
onclick={() => (abaAtiva = 'historico')}
|
onclick={() => (abaAtiva = 'historico')}
|
||||||
>
|
>
|
||||||
<History class="h-5 w-5 mr-2" />
|
<History class="h-5 w-5 mr-2" />
|
||||||
@@ -238,21 +240,25 @@
|
|||||||
|
|
||||||
<!-- Conteúdo das Abas -->
|
<!-- Conteúdo das Abas -->
|
||||||
{#if abaAtiva === 'entrada'}
|
{#if abaAtiva === 'entrada'}
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h2 class="card-title mb-6 text-xl">
|
<div class="mb-8 flex items-center gap-3 border-b-2 border-success/20 pb-4">
|
||||||
<div class="rounded-lg bg-success/20 p-2">
|
<div class="rounded-lg bg-success/10 p-2.5">
|
||||||
<ArrowDown class="h-6 w-6 text-success" />
|
<ArrowDown class="h-6 w-6 text-success" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
Registrar Entrada de Material
|
<h2 class="text-2xl font-bold text-base-content">Registrar Entrada de Material</h2>
|
||||||
</h2>
|
</div>
|
||||||
<form onsubmit={(e) => { e.preventDefault(); registrarEntrada(); }}>
|
<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">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Material *</span>
|
<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>
|
</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>
|
<option value="">Selecione um material</option>
|
||||||
{#if materiaisQuery.data}
|
{#if materiaisQuery.data}
|
||||||
{#each materiaisQuery.data as material}
|
{#each materiaisQuery.data as material}
|
||||||
@@ -264,51 +270,67 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quantidade -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Quantidade *</span>
|
<span class="label-text font-semibold">Quantidade <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered"
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
min="0.01"
|
min="0.01"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
bind:value={entradaQuantidade}
|
bind:value={entradaQuantidade}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Quantidade a ser adicionada ao estoque</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Documento -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Documento (NF, etc.)</span>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<FileText class="h-4 w-4" />
|
||||||
|
Documento
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Número da nota fiscal"
|
||||||
bind:value={entradaDocumento}
|
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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Motivo -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Motivo *</span>
|
<span class="label-text font-semibold">Motivo <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Ex: Compra, Doação, Devolução"
|
||||||
bind:value={entradaMotivo}
|
bind:value={entradaMotivo}
|
||||||
required
|
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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Observações -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Observações</span>
|
<span class="label-text font-semibold">Observações</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered"
|
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
|
||||||
placeholder="Observações adicionais (opcional)"
|
placeholder="Observações adicionais (opcional)"
|
||||||
bind:value={entradaObservacoes}
|
bind:value={entradaObservacoes}
|
||||||
rows="3"
|
rows="3"
|
||||||
@@ -316,35 +338,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions mt-6 justify-end">
|
<div class="card-actions mt-8 justify-end gap-4 border-t-2 border-base-300 pt-6">
|
||||||
<button type="submit" class="btn btn-primary" disabled={entradaLoading}>
|
<button type="submit" class="btn btn-success btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={entradaLoading}>
|
||||||
{#if entradaLoading}
|
{#if entradaLoading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Registrando...
|
||||||
{:else}
|
{:else}
|
||||||
<ArrowDown class="h-5 w-5" />
|
<ArrowDown class="h-5 w-5" />
|
||||||
|
Registrar Entrada
|
||||||
{/if}
|
{/if}
|
||||||
Registrar Entrada
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if abaAtiva === 'saida'}
|
{:else if abaAtiva === 'saida'}
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h2 class="card-title mb-6 text-xl">
|
<div class="mb-8 flex items-center gap-3 border-b-2 border-error/20 pb-4">
|
||||||
<div class="rounded-lg bg-error/20 p-2">
|
<div class="rounded-lg bg-error/10 p-2.5">
|
||||||
<ArrowUp class="h-6 w-6 text-error" />
|
<ArrowUp class="h-6 w-6 text-error" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
Registrar Saída de Material
|
<h2 class="text-2xl font-bold text-base-content">Registrar Saída de Material</h2>
|
||||||
</h2>
|
</div>
|
||||||
<form onsubmit={(e) => { e.preventDefault(); registrarSaida(); }}>
|
<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">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Material *</span>
|
<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>
|
</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>
|
<option value="">Selecione um material</option>
|
||||||
{#if materiaisQuery.data}
|
{#if materiaisQuery.data}
|
||||||
{#each materiaisQuery.data as material}
|
{#each materiaisQuery.data as material}
|
||||||
@@ -356,25 +383,33 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quantidade -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Quantidade *</span>
|
<span class="label-text font-semibold">Quantidade <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered"
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
min="0.01"
|
min="0.01"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
bind:value={saidaQuantidade}
|
bind:value={saidaQuantidade}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Quantidade a ser retirada do estoque</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Funcionário -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Funcionário</span>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<User class="h-4 w-4" />
|
||||||
|
Funcionário
|
||||||
|
</span>
|
||||||
</label>
|
</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>
|
<option value="">Selecione (opcional)</option>
|
||||||
{#if funcionariosQuery.data}
|
{#if funcionariosQuery.data}
|
||||||
{#each funcionariosQuery.data as funcionario}
|
{#each funcionariosQuery.data as funcionario}
|
||||||
@@ -382,13 +417,20 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Funcionário que receberá o material</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Setor -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Setor</span>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<Building2 class="h-4 w-4" />
|
||||||
|
Setor
|
||||||
|
</span>
|
||||||
</label>
|
</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>
|
<option value="">Selecione (opcional)</option>
|
||||||
{#if setoresQuery.data}
|
{#if setoresQuery.data}
|
||||||
{#each setoresQuery.data as setor}
|
{#each setoresQuery.data as setor}
|
||||||
@@ -396,27 +438,35 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Setor que receberá o material</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Motivo -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Motivo *</span>
|
<span class="label-text font-semibold">Motivo <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Ex: Uso interno, Empréstimo, Descarte"
|
||||||
bind:value={saidaMotivo}
|
bind:value={saidaMotivo}
|
||||||
required
|
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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Observações -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Observações</span>
|
<span class="label-text font-semibold">Observações</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered"
|
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
|
||||||
placeholder="Observações adicionais (opcional)"
|
placeholder="Observações adicionais (opcional)"
|
||||||
bind:value={saidaObservacoes}
|
bind:value={saidaObservacoes}
|
||||||
rows="3"
|
rows="3"
|
||||||
@@ -424,39 +474,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions mt-6 justify-end">
|
<div class="card-actions mt-8 justify-end gap-4 border-t-2 border-base-300 pt-6">
|
||||||
<button type="submit" class="btn btn-primary" disabled={saidaLoading}>
|
<button type="submit" class="btn btn-error btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={saidaLoading}>
|
||||||
{#if saidaLoading}
|
{#if saidaLoading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Registrando...
|
||||||
{:else}
|
{:else}
|
||||||
<ArrowUp class="h-5 w-5" />
|
<ArrowUp class="h-5 w-5" />
|
||||||
|
Registrar Saída
|
||||||
{/if}
|
{/if}
|
||||||
Registrar Saída
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if abaAtiva === 'ajuste'}
|
{:else if abaAtiva === 'ajuste'}
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h2 class="card-title mb-6 text-xl">
|
<div class="mb-8 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
|
||||||
<div class="rounded-lg bg-warning/20 p-2">
|
<div class="rounded-lg bg-warning/10 p-2.5">
|
||||||
<Settings class="h-6 w-6 text-warning" />
|
<Settings class="h-6 w-6 text-warning" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
Ajustar Estoque
|
<h2 class="text-2xl font-bold text-base-content">Ajustar Estoque</h2>
|
||||||
</h2>
|
</div>
|
||||||
<div class="alert alert-warning mb-6 shadow-lg">
|
<div class="alert alert-warning mb-8 shadow-lg border-2 border-warning/30">
|
||||||
<Settings class="h-6 w-6" />
|
<AlertCircle class="h-6 w-6" />
|
||||||
<span class="font-medium">Ajustes de estoque devem ser justificados e são registrados no histórico.</span>
|
<span class="font-semibold">Atenção: Ajustes de estoque devem ser justificados e são registrados permanentemente no histórico.</span>
|
||||||
</div>
|
</div>
|
||||||
<form onsubmit={(e) => { e.preventDefault(); ajustarEstoque(); }}>
|
<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">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Material *</span>
|
<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>
|
</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>
|
<option value="">Selecione um material</option>
|
||||||
{#if materiaisQuery.data}
|
{#if materiaisQuery.data}
|
||||||
{#each materiaisQuery.data as material}
|
{#each materiaisQuery.data as material}
|
||||||
@@ -468,38 +523,47 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<!-- Nova Quantidade -->
|
||||||
<label class="label">
|
<div class="form-control md:col-span-2">
|
||||||
<span class="label-text font-bold">Nova Quantidade *</span>
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Nova Quantidade <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered"
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
min="0"
|
min="0"
|
||||||
bind:value={ajusteQuantidadeNova}
|
bind:value={ajusteQuantidadeNova}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Quantidade correta após o ajuste</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Justificativa -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text font-bold">Justificativa *</span>
|
<span class="label-text font-semibold">Justificativa <span class="text-error">*</span></span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Ex: Inventário físico, Correção de erro, Perda"
|
||||||
bind:value={ajusteMotivo}
|
bind:value={ajusteMotivo}
|
||||||
required
|
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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Observações -->
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Observações</span>
|
<span class="label-text font-semibold">Observações</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered"
|
class="textarea textarea-bordered w-full focus:textarea-primary transition-colors min-h-[100px]"
|
||||||
placeholder="Observações adicionais (opcional)"
|
placeholder="Observações adicionais (opcional)"
|
||||||
bind:value={ajusteObservacoes}
|
bind:value={ajusteObservacoes}
|
||||||
rows="3"
|
rows="3"
|
||||||
@@ -507,74 +571,111 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions mt-6 justify-end">
|
<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" disabled={ajusteLoading}>
|
<button type="submit" class="btn btn-warning btn-lg min-w-[180px] shadow-lg hover:shadow-xl transition-all" disabled={ajusteLoading}>
|
||||||
{#if ajusteLoading}
|
{#if ajusteLoading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Ajustando...
|
||||||
{:else}
|
{:else}
|
||||||
<Settings class="h-5 w-5" />
|
<Settings class="h-5 w-5" />
|
||||||
|
Ajustar Estoque
|
||||||
{/if}
|
{/if}
|
||||||
Ajustar Estoque
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if abaAtiva === 'historico'}
|
{:else if abaAtiva === 'historico'}
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h2 class="card-title mb-6 text-xl">
|
<div class="mb-8 flex items-center gap-3 border-b-2 border-info/20 pb-4">
|
||||||
<div class="rounded-lg bg-info/20 p-2">
|
<div class="rounded-lg bg-info/10 p-2.5">
|
||||||
<History class="h-6 w-6 text-info" />
|
<History class="h-6 w-6 text-info" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
Histórico de Movimentações
|
<h2 class="text-2xl font-bold text-base-content">Histórico de Movimentações</h2>
|
||||||
</h2>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto rounded-lg border border-base-300">
|
||||||
<table class="table table-zebra">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
<th class="font-semibold">Data</th>
|
<th class="font-bold text-base-content">Data</th>
|
||||||
<th class="font-semibold">Material</th>
|
<th class="font-bold text-base-content">Material</th>
|
||||||
<th class="font-semibold">Tipo</th>
|
<th class="font-bold text-base-content">Tipo</th>
|
||||||
<th class="font-semibold">Quantidade</th>
|
<th class="font-bold text-base-content">Quantidade</th>
|
||||||
<th class="font-semibold">Anterior</th>
|
<th class="font-bold text-base-content">Anterior</th>
|
||||||
<th class="font-semibold">Nova</th>
|
<th class="font-bold text-base-content">Nova</th>
|
||||||
<th class="font-semibold">Funcionário</th>
|
<th class="font-bold text-base-content">Funcionário</th>
|
||||||
<th class="font-semibold">Motivo</th>
|
<th class="font-bold text-base-content">Usuário</th>
|
||||||
|
<th class="font-bold text-base-content">Motivo</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#if movimentacoesQuery.data && movimentacoesQuery.data.length > 0}
|
{#if movimentacoesQuery.data && movimentacoesQuery.data.length > 0}
|
||||||
{#each movimentacoesQuery.data.slice(0, 50) as mov}
|
{#each movimentacoesQuery.data.slice(0, 100) as item}
|
||||||
{@const material = materiaisMap.get(mov.materialId)}
|
{@const material = materiaisMap.get(item.materialId)}
|
||||||
{@const funcionario = mov.funcionarioId ? funcionariosMap.get(mov.funcionarioId) : null}
|
{@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">
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<div class="font-medium">{material?.nome || 'Carregando...'}</div>
|
{#if isAlteracao && item.tipoAlteracao === 'exclusao'}
|
||||||
{#if material?.codigo}
|
<div class="font-medium text-base-content/60 italic">
|
||||||
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
|
Material excluído (ID: {item.materialId})
|
||||||
{/if}
|
</div>
|
||||||
</td>
|
{:else if material}
|
||||||
<td>
|
<div class="font-medium">{material.nome}</div>
|
||||||
{#if mov.tipo === 'entrada'}
|
{#if material.codigo}
|
||||||
<span class="badge badge-success badge-lg">Entrada</span>
|
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
|
||||||
{:else if mov.tipo === 'saida'}
|
{/if}
|
||||||
<span class="badge badge-error badge-lg">Saída</span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<span class="badge badge-warning badge-lg">Ajuste</span>
|
<div class="font-medium text-base-content/60">Material não encontrado</div>
|
||||||
|
<div class="text-xs text-base-content/50 font-mono">ID: {item.materialId}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="font-bold">{mov.quantidade}</span>
|
{#if isMovimentacao}
|
||||||
|
{#if item.tipoMovimentacao === 'entrada'}
|
||||||
|
<span class="badge badge-success badge-lg">Entrada</span>
|
||||||
|
{:else if item.tipoMovimentacao === 'saida'}
|
||||||
|
<span class="badge badge-error badge-lg">Saída</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-warning badge-lg">Ajuste</span>
|
||||||
|
{/if}
|
||||||
|
{:else if isAlteracao}
|
||||||
|
{#if item.tipoAlteracao === 'criacao'}
|
||||||
|
<span class="badge badge-info badge-lg">Criação</span>
|
||||||
|
{:else if item.tipoAlteracao === 'edicao'}
|
||||||
|
<span class="badge badge-primary badge-lg">Edição</span>
|
||||||
|
{:else if item.tipoAlteracao === 'exclusao'}
|
||||||
|
<span class="badge badge-error badge-lg">Exclusão</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-neutral badge-lg">{item.tipoAlteracao}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-base-content/70">{mov.quantidadeAnterior}</span>
|
{#if isMovimentacao && item.quantidade !== undefined}
|
||||||
|
<span class="font-bold">{item.quantidade}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/50 text-sm italic">—</span>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="font-semibold">{mov.quantidadeNova}</span>
|
{#if isMovimentacao && item.quantidadeAnterior !== undefined}
|
||||||
|
<span class="text-base-content/70">{item.quantidadeAnterior}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/50 text-sm italic">—</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if isMovimentacao && item.quantidadeNova !== undefined}
|
||||||
|
<span class="font-semibold">{item.quantidadeNova}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/50 text-sm italic">—</span>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#if funcionario}
|
{#if funcionario}
|
||||||
@@ -587,13 +688,31 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center">
|
<td colspan="9" class="text-center">
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<History class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
|
<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>
|
<p class="text-base-content/70 text-lg font-medium">Nenhuma movimentação registrada</p>
|
||||||
|
|||||||
@@ -762,19 +762,28 @@
|
|||||||
<!-- Cabeçalho -->
|
<!-- Cabeçalho -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<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">
|
<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-success" strokeWidth={2.5} />
|
<BarChart3 class="h-10 w-10 text-primary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Relatórios</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Estatísticas e relatórios do almoxarifado</p>
|
Relatórios
|
||||||
|
</h1>
|
||||||
|
<p class="text-base-content/70 text-lg mt-1">Estatísticas e relatórios do almoxarifado</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Estatísticas Gerais -->
|
<!-- Estatísticas Gerais -->
|
||||||
{#if statsQuery.data}
|
{#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 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="card-body">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -838,15 +847,27 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Relatórios Disponíveis -->
|
<!-- Relatórios Disponíveis -->
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div class="mb-8">
|
||||||
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
|
||||||
|
<div class="rounded-lg bg-info/10 p-2.5">
|
||||||
|
<FileText class="h-5 w-5 text-info" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-base-content">Relatórios Disponíveis</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<!-- Relatório de Materiais por Categoria -->
|
<!-- Relatório de Materiais por Categoria -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
|
||||||
<h2 class="card-title">Materiais por Categoria</h2>
|
<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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarPDFMateriaisCategoria}
|
onclick={gerarPDFMateriaisCategoria}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar PDF"
|
title="Gerar PDF"
|
||||||
@@ -859,7 +880,7 @@
|
|||||||
PDF
|
PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarExcelMateriaisCategoria}
|
onclick={gerarExcelMateriaisCategoria}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar Excel"
|
title="Gerar Excel"
|
||||||
@@ -874,38 +895,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if materiaisQuery.data && Object.keys(materiaisPorCategoria).length > 0}
|
{#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]}
|
{#each Object.entries(materiaisPorCategoria) as [categoria, quantidade]}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between p-2 rounded-lg hover:bg-base-200/50 transition-colors">
|
||||||
<span class="font-medium">{categoria}</span>
|
<span class="font-semibold text-base-content">{categoria}</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-32">
|
<div class="w-40">
|
||||||
<div class="h-2 bg-base-300 rounded-full overflow-hidden">
|
<div class="h-3 bg-base-300 rounded-full overflow-hidden shadow-inner">
|
||||||
<div
|
<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}%"
|
style="width: {(quantidade / (materiaisQuery.data?.length || 1)) * 100}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Movimentações do Mês -->
|
<!-- Movimentações do Mês -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
|
||||||
<h2 class="card-title">Movimentações do Mês</h2>
|
<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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarPDFMovimentacoesMes}
|
onclick={gerarPDFMovimentacoesMes}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar PDF"
|
title="Gerar PDF"
|
||||||
@@ -918,7 +947,7 @@
|
|||||||
PDF
|
PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarExcelMovimentacoesMes}
|
onclick={gerarExcelMovimentacoesMes}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar Excel"
|
title="Gerar Excel"
|
||||||
@@ -933,39 +962,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<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-2">
|
<div class="flex items-center gap-3">
|
||||||
<ArrowDown class="h-5 w-5 text-success" />
|
<div class="rounded-lg bg-success/20 p-2">
|
||||||
<span>Entradas</span>
|
<ArrowDown class="h-5 w-5 text-success" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<span class="font-semibold text-base-content">Entradas</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-success">{movimentacoesMes.entrada}</span>
|
<span class="text-2xl font-bold text-success">{movimentacoesMes.entrada}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<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-2">
|
<div class="flex items-center gap-3">
|
||||||
<ArrowUp class="h-5 w-5 text-error" />
|
<div class="rounded-lg bg-error/20 p-2">
|
||||||
<span>Saídas</span>
|
<ArrowUp class="h-5 w-5 text-error" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<span class="font-semibold text-base-content">Saídas</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-error">{movimentacoesMes.saida}</span>
|
<span class="text-2xl font-bold text-error">{movimentacoesMes.saida}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<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-2">
|
<div class="flex items-center gap-3">
|
||||||
<Settings class="h-5 w-5 text-warning" />
|
<div class="rounded-lg bg-warning/20 p-2">
|
||||||
<span>Ajustes</span>
|
<Settings class="h-5 w-5 text-warning" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<span class="font-semibold text-base-content">Ajustes</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-warning">{movimentacoesMes.ajuste}</span>
|
<span class="text-2xl font-bold text-warning">{movimentacoesMes.ajuste}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Materiais com Estoque Baixo -->
|
<!-- Materiais com Estoque Baixo -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
|
||||||
<h2 class="card-title">Materiais com Estoque Baixo</h2>
|
<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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarPDFEstoqueBaixo}
|
onclick={gerarPDFEstoqueBaixo}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar PDF"
|
title="Gerar PDF"
|
||||||
@@ -978,7 +1018,7 @@
|
|||||||
PDF
|
PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarExcelEstoqueBaixo}
|
onclick={gerarExcelEstoqueBaixo}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar Excel"
|
title="Gerar Excel"
|
||||||
@@ -995,40 +1035,42 @@
|
|||||||
{#if materiaisQuery.data}
|
{#if materiaisQuery.data}
|
||||||
{@const estoqueBaixo = materiaisQuery.data.filter(m => m.estoqueAtual <= m.estoqueMinimo)}
|
{@const estoqueBaixo = materiaisQuery.data.filter(m => m.estoqueAtual <= m.estoqueMinimo)}
|
||||||
{#if estoqueBaixo.length > 0}
|
{#if estoqueBaixo.length > 0}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto rounded-lg border border-base-300">
|
||||||
<table class="table table-sm">
|
<table class="table table-zebra table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="bg-base-200">
|
||||||
<th>Material</th>
|
<th class="font-bold text-base-content">Material</th>
|
||||||
<th>Atual</th>
|
<th class="font-bold text-base-content">Atual</th>
|
||||||
<th>Mínimo</th>
|
<th class="font-bold text-base-content">Mínimo</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each estoqueBaixo.slice(0, 10) as material}
|
{#each estoqueBaixo.slice(0, 10) as material}
|
||||||
<tr>
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
<td>
|
<td>
|
||||||
<div class="font-medium">{material.nome}</div>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<span class="font-bold text-error">{material.estoqueAtual}</span>
|
<span class="font-bold text-error">{material.estoqueAtual}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{material.estoqueMinimo}</td>
|
<td>
|
||||||
|
<span class="font-medium">{material.estoqueMinimo}</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{#if estoqueBaixo.length > 10}
|
|
||||||
<p class="text-sm text-base-content/70 mt-2">
|
|
||||||
E mais {estoqueBaixo.length - 10} materiais...
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
{#if estoqueBaixo.length > 10}
|
||||||
|
<p class="text-sm text-base-content/70 mt-4 text-center font-medium">
|
||||||
|
E mais <span class="text-primary font-bold">{estoqueBaixo.length - 10}</span> materiais...
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success border-success/30 bg-success/10">
|
||||||
<CheckCircle class="h-6 w-6" />
|
<CheckCircle class="h-6 w-6 text-success" />
|
||||||
<span>Todos os materiais estão com estoque adequado!</span>
|
<span class="font-medium">Todos os materiais estão com estoque adequado!</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1036,13 +1078,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alertas Recentes -->
|
<!-- Alertas Recentes -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-6 border-b-2 border-base-300 pb-4">
|
||||||
<h2 class="card-title">Alertas Recentes</h2>
|
<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">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarPDFAlertas}
|
onclick={gerarPDFAlertas}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar PDF"
|
title="Gerar PDF"
|
||||||
@@ -1055,7 +1102,7 @@
|
|||||||
PDF
|
PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success shadow-md hover:shadow-lg transition-all"
|
||||||
onclick={gerarExcelAlertas}
|
onclick={gerarExcelAlertas}
|
||||||
disabled={gerandoRelatorio}
|
disabled={gerandoRelatorio}
|
||||||
title="Gerar Excel"
|
title="Gerar Excel"
|
||||||
@@ -1070,21 +1117,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if alertasQuery.data && alertasQuery.data.length > 0}
|
{#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}
|
{#each alertasQuery.data.slice(0, 5) as alerta}
|
||||||
<div class="flex items-center justify-between p-2 bg-warning/10 rounded">
|
<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>
|
<div class="flex-1">
|
||||||
<div class="font-medium text-sm">
|
<div class="font-semibold text-sm text-base-content">
|
||||||
{#if materiaisQuery.data}
|
{#if materiaisQuery.data}
|
||||||
{@const material = materiaisQuery.data.find(m => m._id === alerta.materialId)}
|
{@const material = materiaisQuery.data.find(m => m._id === alerta.materialId)}
|
||||||
{material?.nome || 'Carregando...'}
|
{material?.nome || 'Carregando...'}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-base-content/60">
|
<div class="text-xs text-base-content/60 mt-1 font-mono">
|
||||||
{alerta.quantidadeAtual} / {alerta.quantidadeMinima}
|
Estoque: <span class="font-bold text-error">{alerta.quantidadeAtual}</span> / Mínimo: <span class="font-bold">{alerta.quantidadeMinima}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-warning badge-sm">
|
<span class="badge badge-warning badge-lg ml-3">
|
||||||
{#if alerta.tipo === 'estoque_zerado'}
|
{#if alerta.tipo === 'estoque_zerado'}
|
||||||
Zerado
|
Zerado
|
||||||
{:else}
|
{:else}
|
||||||
@@ -1095,9 +1142,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success border-success/30 bg-success/10">
|
||||||
<CheckCircle class="h-6 w-6" />
|
<CheckCircle class="h-6 w-6 text-success" />
|
||||||
<span>Nenhum alerta ativo</span>
|
<span class="font-medium">Nenhum alerta ativo</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { resolve } from '$app/paths';
|
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';
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -250,15 +250,17 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
<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="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">
|
<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-purple-600" strokeWidth={2.5} />
|
<ClipboardList class="h-10 w-10 text-primary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Requisições de Material</h1>
|
<h1 class="text-4xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
|
||||||
<p class="text-base-content/70 text-lg">Gerencie requisições de material dos funcionários</p>
|
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>
|
||||||
</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" />
|
<Plus class="h-5 w-5" />
|
||||||
Nova Requisição
|
Nova Requisição
|
||||||
</button>
|
</button>
|
||||||
@@ -282,48 +284,62 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 mb-8 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<h3 class="text-lg font-semibold mb-4">Filtros</h3>
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-primary/20 pb-4">
|
||||||
<div class="form-control">
|
<div class="rounded-lg bg-primary/10 p-2.5">
|
||||||
<label class="label">
|
<Filter class="h-5 w-5 text-primary" strokeWidth={2.5} />
|
||||||
<span class="label-text font-medium">Filtrar por Status</span>
|
</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>
|
</label>
|
||||||
<select class="select select-bordered" bind:value={filtroStatus}>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={filtroStatus}>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos os status</option>
|
||||||
<option value="pendente">Pendente</option>
|
<option value="pendente">Pendente</option>
|
||||||
<option value="aprovada">Aprovada</option>
|
<option value="aprovada">Aprovada</option>
|
||||||
<option value="atendida">Atendida</option>
|
<option value="atendida">Atendida</option>
|
||||||
<option value="rejeitada">Rejeitada</option>
|
<option value="rejeitada">Rejeitada</option>
|
||||||
<option value="cancelada">Cancelada</option>
|
<option value="cancelada">Cancelada</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de Requisições -->
|
<!-- Lista de Requisições -->
|
||||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
<div class="card bg-base-100 border border-base-300 shadow-2xl">
|
||||||
<div class="card-body">
|
<div class="card-body p-8">
|
||||||
<div class="overflow-x-auto">
|
<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">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
<th class="font-semibold">Número</th>
|
<th class="font-bold text-base-content">Número</th>
|
||||||
<th class="font-semibold">Solicitante</th>
|
<th class="font-bold text-base-content">Solicitante</th>
|
||||||
<th class="font-semibold">Setor</th>
|
<th class="font-bold text-base-content">Setor</th>
|
||||||
<th class="font-semibold">Status</th>
|
<th class="font-bold text-base-content">Status</th>
|
||||||
<th class="font-semibold">Data</th>
|
<th class="font-bold text-base-content">Data</th>
|
||||||
<th class="font-semibold">Ações</th>
|
<th class="font-bold text-base-content">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#if requisicoes.length === 0}
|
{#if requisicoes.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center">
|
<td colspan="6" class="text-center">
|
||||||
<div class="py-12">
|
<div class="py-16">
|
||||||
<ClipboardList class="mx-auto mb-4 h-16 w-16 text-base-content/30" />
|
<ClipboardList class="mx-auto mb-4 h-20 w-20 text-base-content/30" />
|
||||||
<p class="text-base-content/70 text-lg font-medium">Nenhuma requisição encontrada</p>
|
<p class="text-base-content/80 text-xl font-semibold mb-2">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>
|
<p class="text-base-content/60 text-base">Tente ajustar os filtros ou criar uma nova requisição</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -353,16 +369,18 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#if requisicao.status === 'pendente'}
|
{#if requisicao.status === 'pendente'}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success"
|
class="btn btn-sm btn-success transition-all"
|
||||||
onclick={() => aprovarRequisicao(requisicao._id)}
|
onclick={() => aprovarRequisicao(requisicao._id)}
|
||||||
|
title="Aprovar requisição"
|
||||||
>
|
>
|
||||||
<CheckCircle class="h-4 w-4" />
|
<CheckCircle class="h-4 w-4" />
|
||||||
Aprovar
|
Aprovar
|
||||||
</button>
|
</button>
|
||||||
{:else if requisicao.status === 'aprovada'}
|
{:else if requisicao.status === 'aprovada'}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary transition-all"
|
||||||
onclick={() => atenderRequisicao(requisicao._id)}
|
onclick={() => atenderRequisicao(requisicao._id)}
|
||||||
|
title="Atender requisição"
|
||||||
>
|
>
|
||||||
<Package class="h-4 w-4" />
|
<Package class="h-4 w-4" />
|
||||||
Atender
|
Atender
|
||||||
@@ -376,147 +394,225 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Nova Requisição -->
|
<!-- Modal Nova Requisição -->
|
||||||
{#if showModalNova}
|
{#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">
|
<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">
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-primary/20 pb-4">
|
||||||
<ClipboardList class="h-6 w-6 text-primary" />
|
<div class="rounded-2xl bg-primary/20 p-3">
|
||||||
Nova Requisição de Material
|
<ClipboardList class="h-8 w-8 text-primary" strokeWidth={2.5} />
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 mt-4">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-bold">Solicitante *</span>
|
|
||||||
</label>
|
|
||||||
<select class="select select-bordered" bind:value={novaRequisicaoSolicitanteId} required>
|
|
||||||
<option value="">Selecione um funcionário</option>
|
|
||||||
{#if funcionariosQuery.data}
|
|
||||||
{#each funcionariosQuery.data as funcionario}
|
|
||||||
<option value={funcionario._id}>{funcionario.nome}</option>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div class="form-control">
|
<h3 class="text-2xl font-bold text-base-content">Nova Requisição de Material</h3>
|
||||||
<label class="label">
|
<p class="text-base-content/70 mt-1">Preencha os dados da requisição</p>
|
||||||
<span class="label-text font-bold">Setor *</span>
|
|
||||||
</label>
|
|
||||||
<select class="select select-bordered" bind:value={novaRequisicaoSetorId} required>
|
|
||||||
<option value="">Selecione um setor</option>
|
|
||||||
{#if setoresQuery.data}
|
|
||||||
{#each setoresQuery.data as setor}
|
|
||||||
<option value={setor._id}>{setor.nome}</option>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider">Itens da Requisição</div>
|
<!-- Seção: Informações Básicas -->
|
||||||
|
<div class="mb-8">
|
||||||
<!-- Adicionar Item -->
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-info/20 pb-4">
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 mb-4">
|
<div class="rounded-lg bg-info/10 p-2.5">
|
||||||
<div class="form-control">
|
<FileText class="h-5 w-5 text-info" strokeWidth={2.5} />
|
||||||
<label class="label">
|
</div>
|
||||||
<span class="label-text">Material</span>
|
<h4 class="text-lg font-bold text-base-content">Informações Básicas</h4>
|
||||||
</label>
|
|
||||||
<select class="select select-bordered" bind:value={novoItemMaterialId}>
|
|
||||||
<option value="">Selecione</option>
|
|
||||||
{#if materiaisQuery.data}
|
|
||||||
{#each materiaisQuery.data as material}
|
|
||||||
<option value={material._id}>
|
|
||||||
{material.codigo} - {material.nome}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<label class="label">
|
<div class="form-control">
|
||||||
<span class="label-text">Quantidade</span>
|
<label class="label pb-2">
|
||||||
</label>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
<input
|
<User class="h-4 w-4" />
|
||||||
type="number"
|
Solicitante <span class="text-error">*</span>
|
||||||
class="input input-bordered"
|
</span>
|
||||||
min="0.01"
|
</label>
|
||||||
step="0.01"
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novaRequisicaoSolicitanteId} required>
|
||||||
bind:value={novoItemQuantidade}
|
<option value="">Selecione um funcionário</option>
|
||||||
/>
|
{#if funcionariosQuery.data}
|
||||||
</div>
|
{#each funcionariosQuery.data as funcionario}
|
||||||
|
<option value={funcionario._id}>{funcionario.nome}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Funcionário que está solicitando</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label pb-2">
|
||||||
<span class="label-text">Ações</span>
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
</label>
|
<Building2 class="h-4 w-4" />
|
||||||
<button type="button" class="btn btn-primary" onclick={adicionarItem}>
|
Setor <span class="text-error">*</span>
|
||||||
<Plus class="h-4 w-4" />
|
</span>
|
||||||
Adicionar
|
</label>
|
||||||
</button>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novaRequisicaoSetorId} required>
|
||||||
|
<option value="">Selecione um setor</option>
|
||||||
|
{#if setoresQuery.data}
|
||||||
|
{#each setoresQuery.data as setor}
|
||||||
|
<option value={setor._id}>{setor.nome}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Setor do solicitante</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de Itens -->
|
<!-- Seção: Itens da Requisição -->
|
||||||
{#if novaRequisicaoItens.length > 0}
|
<div class="mb-8">
|
||||||
<div class="overflow-x-auto mb-4">
|
<div class="mb-6 flex items-center gap-3 border-b-2 border-warning/20 pb-4">
|
||||||
<table class="table table-sm">
|
<div class="rounded-lg bg-warning/10 p-2.5">
|
||||||
<thead>
|
<Package class="h-5 w-5 text-warning" strokeWidth={2.5} />
|
||||||
<tr>
|
</div>
|
||||||
<th>Material</th>
|
<h4 class="text-lg font-bold text-base-content">Itens da Requisição</h4>
|
||||||
<th>Quantidade</th>
|
</div>
|
||||||
<th>Ações</th>
|
|
||||||
</tr>
|
<!-- Adicionar Item -->
|
||||||
</thead>
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-3 mb-6">
|
||||||
<tbody>
|
<div class="form-control">
|
||||||
{#each novaRequisicaoItens as item}
|
<label class="label pb-2">
|
||||||
{@const material = materiaisQuery.data?.find(m => m._id === item.materialId)}
|
<span class="label-text font-semibold">Material</span>
|
||||||
<tr>
|
</label>
|
||||||
<td>{material?.nome || 'Carregando...'}</td>
|
<select class="select select-bordered w-full focus:select-primary transition-colors h-12" bind:value={novoItemMaterialId}>
|
||||||
<td>{item.quantidade} {material?.unidadeMedida || ''}</td>
|
<option value="">Selecione um material</option>
|
||||||
<td>
|
{#if materiaisQuery.data}
|
||||||
<button
|
{#each materiaisQuery.data as material}
|
||||||
class="btn btn-sm btn-ghost"
|
<option value={material._id}>
|
||||||
onclick={() => removerItem(item.id)}
|
{material.codigo} - {material.nome}
|
||||||
>
|
</option>
|
||||||
<XCircle class="h-4 w-4" />
|
{/each}
|
||||||
</button>
|
{/if}
|
||||||
</td>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Quantidade</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered w-full focus:input-primary transition-colors h-12"
|
||||||
|
min="0.01"
|
||||||
|
step="0.01"
|
||||||
|
bind:value={novoItemQuantidade}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold">Ações</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="btn btn-primary w-full h-12 shadow-lg hover:shadow-xl transition-all" onclick={adicionarItem}>
|
||||||
|
<Plus class="h-5 w-5" />
|
||||||
|
Adicionar Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de Itens -->
|
||||||
|
{#if novaRequisicaoItens.length > 0}
|
||||||
|
<div class="overflow-x-auto mb-6 rounded-lg border border-base-300">
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-200">
|
||||||
|
<th class="font-bold text-base-content">Material</th>
|
||||||
|
<th class="font-bold text-base-content">Quantidade</th>
|
||||||
|
<th class="font-bold text-base-content">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{#each novaRequisicaoItens as item}
|
||||||
</div>
|
{@const material = materiaisQuery.data?.find(m => m._id === item.materialId)}
|
||||||
{/if}
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
|
<td>
|
||||||
<div class="form-control mt-4">
|
<div class="font-medium">{material?.nome || 'Carregando...'}</div>
|
||||||
<label class="label">
|
{#if material?.codigo}
|
||||||
<span class="label-text">Observações</span>
|
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
|
||||||
</label>
|
{/if}
|
||||||
<textarea
|
</td>
|
||||||
class="textarea textarea-bordered"
|
<td>
|
||||||
placeholder="Observações gerais da requisição (opcional)"
|
<span class="font-semibold">{item.quantidade}</span>
|
||||||
bind:value={novaRequisicaoObservacoes}
|
<span class="text-sm text-base-content/60 ml-1">{material?.unidadeMedida || ''}</span>
|
||||||
rows="3"
|
</td>
|
||||||
></textarea>
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-ghost hover:btn-error transition-all"
|
||||||
|
onclick={() => removerItem(item.id)}
|
||||||
|
title="Remover item"
|
||||||
|
>
|
||||||
|
<XCircle class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-info mb-6 border-info/30 bg-info/10">
|
||||||
|
<Package class="h-5 w-5 text-info" />
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-base-content">Nenhum item adicionado</p>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">Adicione pelo menos um item à requisição</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action">
|
<!-- Seção: Observações -->
|
||||||
<button class="btn btn-ghost" onclick={fecharModalNova} disabled={criandoRequisicao}>
|
<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
|
Cancelar
|
||||||
</button>
|
</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}
|
{#if criandoRequisicao}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
Criando...
|
||||||
<Plus class="h-5 w-5" />
|
{:else}
|
||||||
{/if}
|
<Plus class="h-5 w-5" />
|
||||||
Criar Requisição
|
Criar Requisição
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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({
|
export const listarRequisicoes = query({
|
||||||
args: {
|
args: {
|
||||||
status: v.optional(requisicaoStatus),
|
status: v.optional(requisicaoStatus),
|
||||||
|
|||||||
Reference in New Issue
Block a user