refactor: enhance pedidos UI by integrating reusable components for layout and styling, improving code maintainability and user experience across various pages
This commit is contained in:
@@ -2,7 +2,12 @@
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { Plus, Trash2, X, Info } from 'lucide-svelte';
|
||||
import Breadcrumbs from '$lib/components/layout/Breadcrumbs.svelte';
|
||||
import PageHeader from '$lib/components/layout/PageHeader.svelte';
|
||||
import PageShell from '$lib/components/layout/PageShell.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||
import { Info, Plus, Trash2, X } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
@@ -254,71 +259,81 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto max-w-4xl p-6">
|
||||
<h1 class="mb-6 text-3xl font-bold">Novo Pedido</h1>
|
||||
<PageShell class="max-w-4xl">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Dashboard', href: resolve('/') },
|
||||
{ label: 'Pedidos', href: resolve('/pedidos') },
|
||||
{ label: 'Novo' }
|
||||
]}
|
||||
/>
|
||||
|
||||
<PageHeader title="Novo Pedido" subtitle="Crie um pedido e adicione objetos.">
|
||||
{#snippet icon()}
|
||||
<Plus class="h-6 w-6" />
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-red-400 bg-red-50 px-4 py-3 text-red-700">
|
||||
<p class="font-semibold">Erro</p>
|
||||
<p class="text-sm">{error}</p>
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-6">
|
||||
<!-- Section 1: Basic Information -->
|
||||
<div class="rounded-lg bg-white p-6 shadow-md">
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-800">Informações Básicas</h2>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="numeroSei">
|
||||
Número SEI (Opcional)
|
||||
<GlassCard>
|
||||
<h2 class="text-lg font-semibold">Informações Básicas</h2>
|
||||
<div class="mt-4">
|
||||
<label class="label py-0" for="numeroSei">
|
||||
<span class="label-text font-semibold">Número SEI (Opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
class="focus:shadow-outline w-full appearance-none rounded-lg border border-gray-300 px-4 py-2.5 leading-tight text-gray-700 shadow-sm transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
id="numeroSei"
|
||||
type="text"
|
||||
bind:value={formData.numeroSei}
|
||||
placeholder="Ex: 12345.000000/2023-00"
|
||||
onblur={checkExisting}
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
<p class="text-base-content/60 mt-2 text-xs">
|
||||
Você pode adicionar o número SEI posteriormente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Section 2: Add Objects -->
|
||||
<div class="rounded-lg bg-white p-6 shadow-md">
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-800">Adicionar Objetos ao Pedido</h2>
|
||||
<GlassCard>
|
||||
<h2 class="text-lg font-semibold">Adicionar Objetos ao Pedido</h2>
|
||||
|
||||
<div class="relative mb-4">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="search-objetos">
|
||||
Buscar Objetos
|
||||
<div class="relative mt-4">
|
||||
<label class="label py-0" for="search-objetos">
|
||||
<span class="label-text font-semibold">Buscar Objetos</span>
|
||||
</label>
|
||||
<input
|
||||
id="search-objetos"
|
||||
type="text"
|
||||
placeholder="Digite o nome do objeto..."
|
||||
class="focus:shadow-outline w-full appearance-none rounded-lg border border-gray-300 px-4 py-2.5 leading-tight text-gray-700 shadow-sm transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
|
||||
{#if searchQuery.length > 0 && searchResults}
|
||||
<div
|
||||
class="absolute z-10 mt-2 w-full rounded-lg border border-gray-200 bg-white shadow-xl"
|
||||
class="border-base-300 bg-base-100 rounded-box absolute z-20 mt-2 w-full overflow-hidden border shadow"
|
||||
>
|
||||
{#if searchResults.length === 0}
|
||||
<div class="p-4 text-sm text-gray-500">Nenhum objeto encontrado.</div>
|
||||
<div class="text-base-content/60 p-4 text-sm">Nenhum objeto encontrado.</div>
|
||||
{:else}
|
||||
<ul class="max-h-64 overflow-y-auto">
|
||||
<ul class="menu max-h-64 overflow-y-auto p-2">
|
||||
{#each searchResults as objeto (objeto._id)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left transition hover:bg-blue-50"
|
||||
class="flex items-center justify-between"
|
||||
onclick={() => openItemModal(objeto)}
|
||||
>
|
||||
<span class="font-medium text-gray-800">{objeto.nome}</span>
|
||||
<Plus size={16} class="text-blue-600" />
|
||||
<span class="font-medium">{objeto.nome}</span>
|
||||
<Plus class="h-4 w-4" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -328,178 +343,161 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedItems.length > 0}
|
||||
<div class="mt-6">
|
||||
<h3 class="mb-3 text-sm font-semibold text-gray-700">
|
||||
<div class="mt-6">
|
||||
{#if selectedItems.length > 0}
|
||||
<h3 class="text-base-content/70 mb-3 text-sm font-semibold">
|
||||
Itens Selecionados ({selectedItems.length})
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-3">
|
||||
{#each selectedItems as item, index (index)}
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-gray-50 p-4 transition hover:shadow-md"
|
||||
>
|
||||
<GlassCard class="border-base-300" bodyClass="p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="font-semibold text-gray-900">{item.objeto.nome}</p>
|
||||
<p class="font-semibold">{item.objeto.nome}</p>
|
||||
{#if item.acaoId}
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800"
|
||||
<span class="badge badge-info badge-sm"
|
||||
>Ação: {getAcaoNome(item.acaoId)}</span
|
||||
>
|
||||
Ação: {getAcaoNome(item.acaoId)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-600">
|
||||
<span>
|
||||
<strong>Qtd:</strong>
|
||||
{item.quantidade}
|
||||
{item.objeto.unidade}
|
||||
</span>
|
||||
<div class="text-base-content/70 mt-1 text-sm">
|
||||
<span class="font-semibold">Qtd:</span>
|
||||
{item.quantidade}
|
||||
{item.objeto.unidade}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-blue-600 transition hover:bg-blue-50"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
onclick={() => openDetails(item)}
|
||||
aria-label="Ver detalhes"
|
||||
title="Ver detalhes"
|
||||
>
|
||||
<Info size={18} />
|
||||
<Info class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-red-600 transition hover:bg-red-50"
|
||||
class="btn btn-ghost btn-sm btn-square text-error"
|
||||
onclick={() => removeItem(index)}
|
||||
aria-label="Remover item"
|
||||
title="Remover item"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-4 rounded-lg border-2 border-dashed border-gray-300 p-8 text-center">
|
||||
<p class="text-sm text-gray-500">
|
||||
Nenhum item adicionado. Use a busca acima para adicionar objetos ao pedido.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Warnings Section -->
|
||||
{:else}
|
||||
<EmptyState
|
||||
title="Nenhum item adicionado"
|
||||
description="Use a busca acima para adicionar objetos ao pedido."
|
||||
>
|
||||
{#snippet icon()}
|
||||
<Plus />
|
||||
{/snippet}
|
||||
</EmptyState>
|
||||
{/if}
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
{#if warning}
|
||||
<div
|
||||
class="rounded-lg border border-yellow-400 bg-yellow-50 px-4 py-3 text-sm text-yellow-800"
|
||||
>
|
||||
<p class="font-semibold">Aviso</p>
|
||||
<p>{warning}</p>
|
||||
<div class={`alert ${existingPedidos.length > 0 ? 'alert-warning' : 'alert-info'}`}>
|
||||
<span>{warning}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if checking}
|
||||
<p class="text-sm text-gray-500">Verificando pedidos existentes...</p>
|
||||
{/if}
|
||||
|
||||
{#if existingPedidos.length > 0}
|
||||
<div class="rounded-lg border border-yellow-300 bg-yellow-50 p-4">
|
||||
<p class="mb-3 font-semibold text-yellow-900">Pedidos similares encontrados:</p>
|
||||
<ul class="space-y-2">
|
||||
{#each existingPedidos as pedido (pedido._id)}
|
||||
<li class="flex flex-col rounded-lg bg-white px-4 py-3 shadow-sm">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
Pedido {pedido.numeroSei || 'sem número SEI'} — {formatStatus(pedido.status)}
|
||||
</p>
|
||||
|
||||
{#if getMatchingInfo(pedido)}
|
||||
<p class="mt-1 text-xs text-blue-700">
|
||||
{getMatchingInfo(pedido)}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href={resolve(buildPedidoHref(pedido))}
|
||||
class="text-sm font-medium text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Abrir
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="text-base-content/60 flex items-center gap-2 text-sm">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Verificando pedidos existentes...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-end gap-3 border-t pt-6">
|
||||
<a
|
||||
href={resolve('/pedidos')}
|
||||
class="rounded-lg bg-gray-200 px-6 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
||||
>
|
||||
Cancelar
|
||||
</a>
|
||||
{#if existingPedidos.length > 0}
|
||||
<GlassCard>
|
||||
<h2 class="text-lg font-semibold">Pedidos similares encontrados</h2>
|
||||
<div class="mt-4 space-y-2">
|
||||
{#each existingPedidos as pedido (pedido._id)}
|
||||
<div
|
||||
class="border-base-300 bg-base-100 flex items-start justify-between gap-3 rounded-lg border p-4"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium">
|
||||
Pedido {pedido.numeroSei || 'sem número SEI'} — {formatStatus(pedido.status)}
|
||||
</p>
|
||||
{#if getMatchingInfo(pedido)}
|
||||
<p class="text-info mt-1 text-xs">{getMatchingInfo(pedido)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<a href={resolve(buildPedidoHref(pedido))} class="btn btn-ghost btn-sm">Abrir</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</GlassCard>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<a href={resolve('/pedidos')} class="btn">Cancelar</a>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || selectedItems.length === 0}
|
||||
class="rounded-lg bg-blue-600 px-6 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{creating ? 'Criando...' : 'Criar Pedido'}
|
||||
{#if creating}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Criar Pedido
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Item Configuration Modal -->
|
||||
{#if showItemModal && itemConfig.objeto}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||
>
|
||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-lg">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||
onclick={closeItemModal}
|
||||
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Fechar"
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
<X size={24} />
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<h3 class="mb-4 text-xl font-bold text-gray-900">Configurar Item</h3>
|
||||
<h3 class="text-lg font-bold">Configurar Item</h3>
|
||||
|
||||
<div class="mb-6 rounded-lg bg-blue-50 p-4">
|
||||
<p class="font-semibold text-gray-900">{itemConfig.objeto.nome}</p>
|
||||
<p class="text-sm text-gray-600">Unidade: {itemConfig.objeto.unidade}</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<div class="border-base-300 bg-base-200/30 mt-4 rounded-lg border p-4">
|
||||
<p class="font-semibold">{itemConfig.objeto.nome}</p>
|
||||
<p class="text-base-content/70 text-sm">Unidade: {itemConfig.objeto.unidade}</p>
|
||||
<p class="text-base-content/60 mt-1 text-xs">
|
||||
Valor estimado: {itemConfig.objeto.valorEstimado}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="mt-4 grid gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="quantidade">
|
||||
Quantidade
|
||||
<label class="label py-0" for="quantidade">
|
||||
<span class="label-text font-semibold">Quantidade</span>
|
||||
</label>
|
||||
<input
|
||||
id="quantidade"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
bind:value={itemConfig.quantidade}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="itemAcao">
|
||||
Ação (Opcional)
|
||||
<label class="label py-0" for="itemAcao">
|
||||
<span class="label-text font-semibold">Ação (Opcional)</span>
|
||||
</label>
|
||||
<select
|
||||
id="itemAcao"
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||
class="select select-bordered focus:select-primary w-full"
|
||||
bind:value={itemConfig.acaoId}
|
||||
>
|
||||
<option value="">Selecione uma ação...</option>
|
||||
@@ -510,59 +508,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeItemModal}
|
||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeItemModal}>Cancelar</button>
|
||||
<button type="button" class="btn btn-primary" onclick={confirmAddItem}
|
||||
>Adicionar Item</button
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmAddItem}
|
||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700"
|
||||
>
|
||||
Adicionar Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="modal-backdrop"
|
||||
onclick={closeItemModal}
|
||||
aria-label="Fechar modal"
|
||||
></button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Details Modal -->
|
||||
{#if showDetailsModal && detailsItem}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||
>
|
||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-lg">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||
onclick={closeDetails}
|
||||
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||
aria-label="Fechar"
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
<X size={24} />
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<h3 class="mb-4 text-xl font-bold text-gray-900">Detalhes do Item</h3>
|
||||
<h3 class="text-lg font-bold">Detalhes do Item</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg bg-gray-50 p-4">
|
||||
<h4 class="mb-2 font-semibold text-gray-800">Objeto</h4>
|
||||
<p class="text-gray-700"><strong>Nome:</strong> {detailsItem.objeto.nome}</p>
|
||||
<p class="text-gray-700"><strong>Unidade:</strong> {detailsItem.objeto.unidade}</p>
|
||||
<p class="text-gray-700">
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="border-base-300 bg-base-200/30 rounded-lg border p-4">
|
||||
<h4 class="font-semibold">Objeto</h4>
|
||||
<p class="text-base-content/70 mt-2 text-sm">
|
||||
<strong>Nome:</strong>
|
||||
{detailsItem.objeto.nome}
|
||||
</p>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
<strong>Unidade:</strong>
|
||||
{detailsItem.objeto.unidade}
|
||||
</p>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
<strong>Valor Estimado:</strong>
|
||||
{detailsItem.objeto.valorEstimado}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-gray-50 p-4">
|
||||
<h4 class="mb-2 font-semibold text-gray-800">Pedido</h4>
|
||||
<p class="text-gray-700"><strong>Quantidade:</strong> {detailsItem.quantidade}</p>
|
||||
<div class="border-base-300 bg-base-200/30 rounded-lg border p-4">
|
||||
<h4 class="font-semibold">Pedido</h4>
|
||||
<p class="text-base-content/70 mt-2 text-sm">
|
||||
<strong>Quantidade:</strong>
|
||||
{detailsItem.quantidade}
|
||||
</p>
|
||||
|
||||
{#if detailsItem.acaoId}
|
||||
<p class="text-gray-700">
|
||||
<p class="text-base-content/70 text-sm">
|
||||
<strong>Ação:</strong>
|
||||
{getAcaoNome(detailsItem.acaoId)}
|
||||
</p>
|
||||
@@ -570,16 +571,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeDetails}
|
||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700"
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeDetails}>Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="modal-backdrop" onclick={closeDetails} aria-label="Fechar modal"
|
||||
></button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PageShell>
|
||||
|
||||
Reference in New Issue
Block a user