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:
2025-12-18 12:02:57 -03:00
parent 230be4db61
commit f008610b26
13 changed files with 1430 additions and 1155 deletions

View File

@@ -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>