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:
29
apps/web/src/lib/components/layout/Breadcrumbs.svelte
Normal file
29
apps/web/src/lib/components/layout/Breadcrumbs.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export type BreadcrumbItem = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { items, class: className = '' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={['breadcrumbs mb-4 text-sm', className].filter(Boolean)}>
|
||||||
|
<ul>
|
||||||
|
{#each items as item (item.label)}
|
||||||
|
<li>
|
||||||
|
{#if item.href}
|
||||||
|
<a href={item.href} class="text-primary hover:underline">{item.label}</a>
|
||||||
|
{:else}
|
||||||
|
{item.label}
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
50
apps/web/src/lib/components/layout/PageHeader.svelte
Normal file
50
apps/web/src/lib/components/layout/PageHeader.svelte
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
|
||||||
|
icon?: Snippet;
|
||||||
|
actions?: Snippet;
|
||||||
|
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
actions,
|
||||||
|
class: className = ''
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={['mb-6', className].filter(Boolean)}>
|
||||||
|
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
{#if icon}
|
||||||
|
<div class="bg-primary/10 rounded-xl p-3">
|
||||||
|
<div class="text-primary [&_svg]:h-8 [&_svg]:w-8">
|
||||||
|
{@render icon()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-primary text-3xl font-bold">{title}</h1>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="text-base-content/70">{subtitle}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if actions}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{@render actions()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
16
apps/web/src/lib/components/layout/PageShell.svelte
Normal file
16
apps/web/src/lib/components/layout/PageShell.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '', children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class={['container mx-auto flex max-w-7xl flex-col px-4 py-4', className].filter(Boolean)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
34
apps/web/src/lib/components/ui/EmptyState.svelte
Normal file
34
apps/web/src/lib/components/ui/EmptyState.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: Snippet;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, description, icon, class: className = '' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
'bg-base-100 border-base-300 flex flex-col items-center justify-center rounded-lg border py-12 text-center',
|
||||||
|
className
|
||||||
|
].filter(Boolean)}
|
||||||
|
>
|
||||||
|
{#if icon}
|
||||||
|
<div class="bg-base-200 mb-4 rounded-full p-4">
|
||||||
|
<div class="text-base-content/30 [&_svg]:h-8 [&_svg]:w-8">
|
||||||
|
{@render icon()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold">{title}</h3>
|
||||||
|
{#if description}
|
||||||
|
<p class="text-base-content/60 mt-1 max-w-sm text-sm">{description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
22
apps/web/src/lib/components/ui/GlassCard.svelte
Normal file
22
apps/web/src/lib/components/ui/GlassCard.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
bodyClass?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '', bodyClass = '', children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
'card bg-base-100/90 border-base-300 border shadow-xl backdrop-blur-sm',
|
||||||
|
className
|
||||||
|
].filter(Boolean)}
|
||||||
|
>
|
||||||
|
<div class={['card-body', bodyClass].filter(Boolean)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
apps/web/src/lib/components/ui/TableCard.svelte
Normal file
18
apps/web/src/lib/components/ui/TableCard.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
bodyClass?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '', bodyClass = '', children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<GlassCard class={className} bodyClass={['p-0', bodyClass].filter(Boolean).join(' ')}>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
@@ -3,9 +3,14 @@
|
|||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { exportarRelatorioPedidosXLSX } from '$lib/utils/pedidos/relatorioPedidosExcel';
|
import { exportarRelatorioPedidosXLSX } from '$lib/utils/pedidos/relatorioPedidosExcel';
|
||||||
import { gerarRelatorioPedidosPDF } from '$lib/utils/pedidos/relatorioPedidosPDF';
|
import { gerarRelatorioPedidosPDF } from '$lib/utils/pedidos/relatorioPedidosPDF';
|
||||||
|
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 GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||||
|
import TableCard from '$lib/components/ui/TableCard.svelte';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { endOfDay, startOfDay } from 'date-fns';
|
import { endOfDay, startOfDay } from 'date-fns';
|
||||||
import { Eye, Plus } from 'lucide-svelte';
|
import { Eye, FileText, Plus } from 'lucide-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -149,22 +154,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusColor(status: string) {
|
function getStatusBadgeClass(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'em_rascunho':
|
case 'em_rascunho':
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
case 'aguardando_aceite':
|
case 'aguardando_aceite':
|
||||||
return 'bg-yellow-100 text-yellow-800';
|
return 'badge-warning';
|
||||||
case 'em_analise':
|
case 'em_analise':
|
||||||
return 'bg-blue-100 text-blue-800';
|
return 'badge-info';
|
||||||
case 'precisa_ajustes':
|
case 'precisa_ajustes':
|
||||||
return 'bg-orange-100 text-orange-800';
|
return 'badge-secondary';
|
||||||
case 'concluido':
|
case 'concluido':
|
||||||
return 'bg-green-100 text-green-800';
|
return 'badge-success';
|
||||||
case 'cancelado':
|
case 'cancelado':
|
||||||
return 'bg-red-100 text-red-800';
|
return 'badge-error';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,148 +178,150 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<PageShell>
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<Breadcrumbs items={[{ label: 'Dashboard', href: resolve('/') }, { label: 'Pedidos' }]} />
|
||||||
<h1 class="text-2xl font-bold">Pedidos</h1>
|
|
||||||
<div class="flex items-center gap-2">
|
<PageHeader title="Pedidos" subtitle="Cadastro, acompanhamento e relatórios de pedidos">
|
||||||
|
{#snippet icon()}
|
||||||
|
<FileText strokeWidth={2} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet actions()}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-60"
|
class="btn btn-outline"
|
||||||
onclick={gerarPDF}
|
onclick={gerarPDF}
|
||||||
disabled={generatingPDF || generatingXLSX}
|
disabled={generatingPDF || generatingXLSX}
|
||||||
title="Gera relatório completo (PDF) no padrão do sistema"
|
title="Gera relatório completo (PDF) no padrão do sistema"
|
||||||
>
|
>
|
||||||
{#if generatingPDF}
|
{#if generatingPDF}
|
||||||
Gerando PDF...
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
|
||||||
Relatório (PDF)
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{generatingPDF ? 'Gerando PDF...' : 'Relatório (PDF)'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-60"
|
class="btn btn-outline"
|
||||||
onclick={exportarXLSX}
|
onclick={exportarXLSX}
|
||||||
disabled={generatingPDF || generatingXLSX}
|
disabled={generatingPDF || generatingXLSX}
|
||||||
title="Exporta relatório completo em Excel (XLSX)"
|
title="Exporta relatório completo em Excel (XLSX)"
|
||||||
>
|
>
|
||||||
{#if generatingXLSX}
|
{#if generatingXLSX}
|
||||||
Exportando...
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
|
||||||
Excel (XLSX)
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{generatingXLSX ? 'Exportando...' : 'Excel (XLSX)'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={resolve('/pedidos/novo')}
|
href={resolve('/pedidos/novo')}
|
||||||
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<Plus size={20} />
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
||||||
Novo Pedido
|
Novo Pedido
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</PageHeader>
|
||||||
|
|
||||||
<div class="mb-6 rounded-lg bg-white p-4 shadow-md">
|
<GlassCard class="mb-6">
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
|
||||||
<div>
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-5">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_numeroSei"
|
<div class="form-control w-full">
|
||||||
>Número SEI</label
|
<label class="label" for="filtro_numeroSei">
|
||||||
>
|
<span class="label-text font-semibold">Número SEI</span>
|
||||||
<input
|
</label>
|
||||||
id="filtro_numeroSei"
|
<input
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
id="filtro_numeroSei"
|
||||||
type="text"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
placeholder="Buscar..."
|
type="text"
|
||||||
bind:value={filtroNumeroSei}
|
placeholder="Digite para filtrar..."
|
||||||
/>
|
bind:value={filtroNumeroSei}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_criadoPor"
|
<label class="label" for="filtro_criadoPor">
|
||||||
>Criado por</label
|
<span class="label-text font-semibold">Criado por</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="filtro_criadoPor"
|
id="filtro_criadoPor"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={filtroCriadoPor}
|
bind:value={filtroCriadoPor}
|
||||||
>
|
>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos</option>
|
||||||
{#each usuariosQuery.data || [] as u (u._id)}
|
{#each usuariosQuery.data || [] as u (u._id)}
|
||||||
<option value={u._id}>{u.nome}</option>
|
<option value={u._id}>{u.nome}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_aceitoPor"
|
<label class="label" for="filtro_aceitoPor">
|
||||||
>Aceito por</label
|
<span class="label-text font-semibold">Aceito por</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="filtro_aceitoPor"
|
id="filtro_aceitoPor"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={filtroAceitoPor}
|
bind:value={filtroAceitoPor}
|
||||||
>
|
>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos</option>
|
||||||
{#each funcionariosQuery.data || [] as f (f._id)}
|
{#each funcionariosQuery.data || [] as f (f._id)}
|
||||||
<option value={f._id}>{f.nome}</option>
|
<option value={f._id}>{f.nome}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_inicio"
|
<label class="label" for="filtro_inicio">
|
||||||
>Período (início)</label
|
<span class="label-text font-semibold">Período (início)</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="filtro_inicio"
|
id="filtro_inicio"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={filtroInicio}
|
bind:value={filtroInicio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_fim"
|
<label class="label" for="filtro_fim">
|
||||||
>Período (fim)</label
|
<span class="label-text font-semibold">Período (fim)</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="filtro_fim"
|
id="filtro_fim"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={filtroFim}
|
bind:value={filtroFim}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lg:col-span-3">
|
<div class="form-control w-full md:col-span-3 lg:col-span-5">
|
||||||
<div class="mb-2 text-sm font-medium text-gray-700">Status</div>
|
<div class="label">
|
||||||
<div class="flex flex-wrap gap-3">
|
<span class="label-text font-semibold">Status</span>
|
||||||
{#each statusOptions as s (s.value)}
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<div class="flex flex-wrap gap-3">
|
||||||
<input
|
{#each statusOptions as s (s.value)}
|
||||||
type="checkbox"
|
<label class="label cursor-pointer justify-start gap-2 py-1">
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
<input
|
||||||
checked={statusSelected[s.value]}
|
type="checkbox"
|
||||||
onchange={(e) =>
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
(statusSelected[s.value] = (e.currentTarget as HTMLInputElement).checked)}
|
checked={statusSelected[s.value]}
|
||||||
/>
|
onchange={(e) =>
|
||||||
<span>{s.label}</span>
|
(statusSelected[s.value] = (e.currentTarget as HTMLInputElement).checked)}
|
||||||
</label>
|
/>
|
||||||
{/each}
|
<span class="label-text">{s.label}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
<div class="flex items-center justify-between gap-3 sm:min-w-max sm:justify-end">
|
||||||
<button
|
<div class="text-base-content/70 text-sm">{pedidos.length} resultado(s)</div>
|
||||||
type="button"
|
<button type="button" class="btn btn-ghost" onclick={limparFiltros}>Limpar</button>
|
||||||
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
</div>
|
||||||
onclick={limparFiltros}
|
|
||||||
>
|
|
||||||
Limpar filtros
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
|
|
||||||
<div role="tablist" class="tabs tabs-bordered mb-6">
|
<div role="tablist" class="tabs tabs-bordered mb-6">
|
||||||
<button
|
<button
|
||||||
@@ -334,87 +341,82 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex items-center justify-center py-10">
|
||||||
{#each Array(3) as _, i (i)}
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<div class="skeleton h-16 w-full rounded-lg"></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<TableCard>
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="table-zebra table w-full">
|
||||||
<thead class="bg-gray-50">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Número SEI</th
|
>Número SEI</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Status</th
|
>Status</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
>Criado por</th
|
||||||
>Criado Por</th
|
>
|
||||||
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
|
>Data de criação</th
|
||||||
>
|
>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
|
||||||
>Data de Criação</th
|
|
||||||
>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Ações</th
|
>Ações</th
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody>
|
||||||
{#each pedidos as pedido (pedido._id)}
|
|
||||||
<tr class="hover:bg-gray-50">
|
|
||||||
<td class="px-6 py-4 font-medium whitespace-nowrap">
|
|
||||||
{#if pedido.numeroSei}
|
|
||||||
{pedido.numeroSei}
|
|
||||||
{:else}
|
|
||||||
<span class="text-amber-600">Sem número SEI</span>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getStatusColor(
|
|
||||||
pedido.status
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{formatStatus(pedido.status)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500">
|
|
||||||
{pedido.criadoPorNome || 'Desconhecido'}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500">
|
|
||||||
{formatDate(pedido.criadoEm)}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
|
||||||
<a
|
|
||||||
href={resolve(`/pedidos/${pedido._id}`)}
|
|
||||||
class="inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-900"
|
|
||||||
>
|
|
||||||
<Eye size={18} />
|
|
||||||
Visualizar
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{#if pedidos.length === 0}
|
{#if pedidos.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500"
|
<td colspan="5" class="py-12 text-center">
|
||||||
>Nenhum pedido encontrado.</td
|
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
||||||
>
|
<p class="text-lg font-semibold">Nenhum pedido encontrado</p>
|
||||||
|
<p class="text-sm">Ajuste ou limpe os filtros para ver resultados.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{:else}
|
||||||
|
{#each pedidos as pedido (pedido._id)}
|
||||||
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
|
<td class="font-medium whitespace-nowrap">
|
||||||
|
{#if pedido.numeroSei}
|
||||||
|
{pedido.numeroSei}
|
||||||
|
{:else}
|
||||||
|
<span class="text-warning">Sem número SEI</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap">
|
||||||
|
<span class="badge badge-sm {getStatusBadgeClass(pedido.status)}">
|
||||||
|
{formatStatus(pedido.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-base-content/70 whitespace-nowrap">
|
||||||
|
{pedido.criadoPorNome || 'Desconhecido'}
|
||||||
|
</td>
|
||||||
|
<td class="text-base-content/70 whitespace-nowrap">
|
||||||
|
{formatDate(pedido.criadoEm)}
|
||||||
|
</td>
|
||||||
|
<td class="text-right whitespace-nowrap">
|
||||||
|
<a
|
||||||
|
href={resolve(`/pedidos/${pedido._id}`)}
|
||||||
|
class="btn btn-ghost btn-sm gap-2"
|
||||||
|
aria-label="Visualizar pedido"
|
||||||
|
>
|
||||||
|
<Eye class="h-4 w-4" />
|
||||||
|
Visualizar
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</TableCard>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</PageShell>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,12 @@
|
|||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { CheckCircle, Clock, FileText, User } from 'lucide-svelte';
|
import { CheckCircle, Clock, FileText, User } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
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 GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||||
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const ordersQuery = useQuery(api.pedidos.listForAcceptance, {});
|
const ordersQuery = useQuery(api.pedidos.listForAcceptance, {});
|
||||||
@@ -27,42 +33,44 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<PageShell>
|
||||||
<div class="flex items-center justify-between">
|
<Breadcrumbs
|
||||||
<div>
|
items={[
|
||||||
<h1 class="text-primary text-2xl font-bold tracking-tight">Pedidos para Aceite</h1>
|
{ label: 'Dashboard', href: resolve('/') },
|
||||||
<p class="text-base-content/70 mt-1">
|
{ label: 'Pedidos', href: resolve('/pedidos') },
|
||||||
Lista de pedidos aguardando análise do setor de compras.
|
{ label: 'Aceite' }
|
||||||
</p>
|
]}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if ordersQuery.isLoading}
|
<PageHeader
|
||||||
<div class="flex flex-col gap-4">
|
title="Pedidos para Aceite"
|
||||||
{#each Array(3) as _, i (i)}
|
subtitle="Lista de pedidos aguardando análise do setor de compras"
|
||||||
<div class="skeleton h-24 w-full rounded-lg"></div>
|
>
|
||||||
{/each}
|
{#snippet icon()}
|
||||||
</div>
|
<Clock strokeWidth={2} />
|
||||||
|
{/snippet}
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
{#if ordersQuery.isLoading}
|
||||||
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
{:else if ordersQuery.error}
|
{:else if ordersQuery.error}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<span>Erro ao carregar pedidos: {ordersQuery.error.message}</span>
|
<span>Erro ao carregar pedidos: {ordersQuery.error.message}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if !ordersQuery.data || ordersQuery.data.length === 0}
|
{:else if !ordersQuery.data || ordersQuery.data.length === 0}
|
||||||
<div
|
<EmptyState title="Tudo em dia!" description="Não há pedidos aguardando aceite no momento.">
|
||||||
class="bg-base-100 flex flex-col items-center justify-center rounded-lg border py-12 text-center shadow-sm"
|
{#snippet icon()}
|
||||||
>
|
<CheckCircle />
|
||||||
<div class="bg-base-200 mb-4 rounded-full p-4">
|
{/snippet}
|
||||||
<CheckCircle class="text-base-content/30 h-8 w-8" />
|
</EmptyState>
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-medium">Tudo em dia!</h3>
|
|
||||||
<p class="text-base-content/60 mt-1 max-w-sm">Não há pedidos aguardando aceite no momento.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
{#each ordersQuery.data as pedido (pedido._id)}
|
{#each ordersQuery.data as pedido (pedido._id)}
|
||||||
<div
|
<GlassCard class="border-base-300 hover:border-primary/30 transition-all hover:shadow-md">
|
||||||
class="bg-base-100 border-base-200 hover:border-primary/30 group relative overflow-hidden rounded-xl border p-6 shadow-sm transition-all hover:shadow-md"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -90,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="/pedidos/{pedido._id}" class="btn btn-ghost btn-sm">
|
<a href={resolve(`/pedidos/${pedido._id}`)} class="btn btn-ghost btn-sm">
|
||||||
<FileText class="mr-2 h-4 w-4" />
|
<FileText class="mr-2 h-4 w-4" />
|
||||||
Ver Detalhes
|
Ver Detalhes
|
||||||
</a>
|
</a>
|
||||||
@@ -108,8 +116,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
|||||||
@@ -2,82 +2,91 @@
|
|||||||
import { useQuery } from 'convex-svelte';
|
import { useQuery } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { ClipboardList, Clock, FileText, User, Search } from 'lucide-svelte';
|
import { ClipboardList, Clock, FileText, User, Search } from 'lucide-svelte';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
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 GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||||
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
|
|
||||||
const ordersQuery = useQuery(api.pedidos.listMyAnalysis, {});
|
const ordersQuery = useQuery(api.pedidos.listMyAnalysis, {});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<PageShell>
|
||||||
<div class="flex items-center justify-between">
|
<Breadcrumbs
|
||||||
<div>
|
items={[
|
||||||
<h1 class="text-primary text-2xl font-bold tracking-tight">Minhas Análises</h1>
|
{ label: 'Dashboard', href: resolve('/') },
|
||||||
<p class="text-base-content/70 mt-1">Pedidos que você aceitou e está analisando.</p>
|
{ label: 'Pedidos', href: resolve('/pedidos') },
|
||||||
</div>
|
{ label: 'Minhas análises' }
|
||||||
</div>
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if ordersQuery.isLoading}
|
<PageHeader title="Minhas Análises" subtitle="Pedidos que você aceitou e está analisando">
|
||||||
<div class="flex flex-col gap-4">
|
{#snippet icon()}
|
||||||
{#each Array(3) as _, i (i)}
|
<Search strokeWidth={2} />
|
||||||
<div class="skeleton h-24 w-full rounded-lg"></div>
|
{/snippet}
|
||||||
{/each}
|
</PageHeader>
|
||||||
</div>
|
|
||||||
{:else if ordersQuery.error}
|
<div class="space-y-6">
|
||||||
<div class="alert alert-error">
|
{#if ordersQuery.isLoading}
|
||||||
<span>Erro ao carregar análises: {ordersQuery.error.message}</span>
|
<div class="flex items-center justify-center py-10">
|
||||||
</div>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
{:else if !ordersQuery.data || ordersQuery.data.length === 0}
|
|
||||||
<div
|
|
||||||
class="bg-base-100 flex flex-col items-center justify-center rounded-lg border py-12 text-center shadow-sm"
|
|
||||||
>
|
|
||||||
<div class="bg-base-200 mb-4 rounded-full p-4">
|
|
||||||
<ClipboardList class="text-base-content/30 h-8 w-8" />
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium">Nenhuma análise em andamento</h3>
|
{:else if ordersQuery.error}
|
||||||
<p class="text-base-content/60 mt-1 max-w-sm">
|
<div class="alert alert-error">
|
||||||
Você não possui pedidos sob sua responsabilidade no momento. Vá para "Pedidos para Aceite"
|
<span>Erro ao carregar análises: {ordersQuery.error.message}</span>
|
||||||
para pegar novos pedidos.
|
</div>
|
||||||
</p>
|
{:else if !ordersQuery.data || ordersQuery.data.length === 0}
|
||||||
</div>
|
<EmptyState
|
||||||
{:else}
|
title="Nenhuma análise em andamento"
|
||||||
<div class="grid gap-4">
|
description="Você não possui pedidos sob sua responsabilidade no momento. Vá para "Pedidos para Aceite" para pegar novos pedidos."
|
||||||
{#each ordersQuery.data as pedido (pedido._id)}
|
>
|
||||||
<div
|
{#snippet icon()}
|
||||||
class="bg-base-100 border-base-200 hover:border-primary/30 group relative overflow-hidden rounded-xl border p-6 shadow-sm transition-all hover:shadow-md"
|
<ClipboardList />
|
||||||
>
|
{/snippet}
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
</EmptyState>
|
||||||
<div class="space-y-1">
|
{:else}
|
||||||
<div class="flex items-center gap-2">
|
<div class="grid gap-4">
|
||||||
<span class="badge badge-info gap-1 font-medium">
|
{#each ordersQuery.data as pedido (pedido._id)}
|
||||||
<Search class="h-3 w-3" />
|
<GlassCard class="border-base-300 hover:border-primary/30 transition-all hover:shadow-md">
|
||||||
Em Análise
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
</span>
|
<div class="space-y-1">
|
||||||
<span class="text-base-content/40 text-xs">
|
<div class="flex items-center gap-2">
|
||||||
#{pedido._id.slice(-6)}
|
<span class="badge badge-info gap-1 font-medium">
|
||||||
</span>
|
<Search class="h-3 w-3" />
|
||||||
</div>
|
Em Análise
|
||||||
<h3 class="text-lg font-bold">
|
</span>
|
||||||
{pedido.numeroSei ? `SEI: ${pedido.numeroSei}` : 'Sem número SEI'}
|
<span class="text-base-content/40 text-xs">
|
||||||
</h3>
|
#{pedido._id.slice(-6)}
|
||||||
<div class="text-base-content/70 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
</span>
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<User class="h-3.5 w-3.5" />
|
|
||||||
<span>Criado por: {pedido.criadoPorNome}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<h3 class="text-lg font-bold">
|
||||||
<Clock class="h-3.5 w-3.5" />
|
{pedido.numeroSei ? `SEI: ${pedido.numeroSei}` : 'Sem número SEI'}
|
||||||
<span>Aceito em: {new Date(pedido.atualizadoEm).toLocaleDateString()}</span>
|
</h3>
|
||||||
|
<div
|
||||||
|
class="text-base-content/70 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<User class="h-3.5 w-3.5" />
|
||||||
|
<span>Criado por: {pedido.criadoPorNome}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Clock class="h-3.5 w-3.5" />
|
||||||
|
<span>Aceito em: {new Date(pedido.atualizadoEm).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="/pedidos/{pedido._id}" class="btn btn-primary btn-sm">
|
<a href={resolve(`/pedidos/${pedido._id}`)} class="btn btn-primary btn-sm">
|
||||||
<FileText class="mr-2 h-4 w-4" />
|
<FileText class="mr-2 h-4 w-4" />
|
||||||
Continuar Análise
|
Continuar Análise
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</PageShell>
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
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 { 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 { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
@@ -254,71 +259,81 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto max-w-4xl p-6">
|
<PageShell class="max-w-4xl">
|
||||||
<h1 class="mb-6 text-3xl font-bold">Novo Pedido</h1>
|
<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">
|
<div class="space-y-6">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="rounded-lg border border-red-400 bg-red-50 px-4 py-3 text-red-700">
|
<div class="alert alert-error">
|
||||||
<p class="font-semibold">Erro</p>
|
<span>{error}</span>
|
||||||
<p class="text-sm">{error}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form onsubmit={handleSubmit} class="space-y-6">
|
<form onsubmit={handleSubmit} class="space-y-6">
|
||||||
<!-- Section 1: Basic Information -->
|
<GlassCard>
|
||||||
<div class="rounded-lg bg-white p-6 shadow-md">
|
<h2 class="text-lg font-semibold">Informações Básicas</h2>
|
||||||
<h2 class="mb-4 text-lg font-semibold text-gray-800">Informações Básicas</h2>
|
<div class="mt-4">
|
||||||
<div>
|
<label class="label py-0" for="numeroSei">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="numeroSei">
|
<span class="label-text font-semibold">Número SEI (Opcional)</span>
|
||||||
Número SEI (Opcional)
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="numeroSei"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.numeroSei}
|
bind:value={formData.numeroSei}
|
||||||
placeholder="Ex: 12345.000000/2023-00"
|
placeholder="Ex: 12345.000000/2023-00"
|
||||||
onblur={checkExisting}
|
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.
|
Você pode adicionar o número SEI posteriormente.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Section 2: Add Objects -->
|
<GlassCard>
|
||||||
<div class="rounded-lg bg-white p-6 shadow-md">
|
<h2 class="text-lg font-semibold">Adicionar Objetos ao Pedido</h2>
|
||||||
<h2 class="mb-4 text-lg font-semibold text-gray-800">Adicionar Objetos ao Pedido</h2>
|
|
||||||
|
|
||||||
<div class="relative mb-4">
|
<div class="relative mt-4">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="search-objetos">
|
<label class="label py-0" for="search-objetos">
|
||||||
Buscar Objetos
|
<span class="label-text font-semibold">Buscar Objetos</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="search-objetos"
|
id="search-objetos"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Digite o nome do objeto..."
|
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}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if searchQuery.length > 0 && searchResults}
|
{#if searchQuery.length > 0 && searchResults}
|
||||||
<div
|
<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}
|
{#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}
|
{: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)}
|
{#each searchResults as objeto (objeto._id)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onclick={() => openItemModal(objeto)}
|
||||||
>
|
>
|
||||||
<span class="font-medium text-gray-800">{objeto.nome}</span>
|
<span class="font-medium">{objeto.nome}</span>
|
||||||
<Plus size={16} class="text-blue-600" />
|
<Plus class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -328,178 +343,161 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedItems.length > 0}
|
<div class="mt-6">
|
||||||
<div class="mt-6">
|
{#if selectedItems.length > 0}
|
||||||
<h3 class="mb-3 text-sm font-semibold text-gray-700">
|
<h3 class="text-base-content/70 mb-3 text-sm font-semibold">
|
||||||
Itens Selecionados ({selectedItems.length})
|
Itens Selecionados ({selectedItems.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="grid gap-3">
|
||||||
{#each selectedItems as item, index (index)}
|
{#each selectedItems as item, index (index)}
|
||||||
<div
|
<GlassCard class="border-base-300" bodyClass="p-4">
|
||||||
class="rounded-xl border border-gray-200 bg-gray-50 p-4 transition hover:shadow-md"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-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">
|
<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}
|
{#if item.acaoId}
|
||||||
<span
|
<span class="badge badge-info badge-sm"
|
||||||
class="inline-flex items-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800"
|
>Ação: {getAcaoNome(item.acaoId)}</span
|
||||||
>
|
>
|
||||||
Ação: {getAcaoNome(item.acaoId)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-600">
|
<div class="text-base-content/70 mt-1 text-sm">
|
||||||
<span>
|
<span class="font-semibold">Qtd:</span>
|
||||||
<strong>Qtd:</strong>
|
{item.quantidade}
|
||||||
{item.quantidade}
|
{item.objeto.unidade}
|
||||||
{item.objeto.unidade}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onclick={() => openDetails(item)}
|
||||||
aria-label="Ver detalhes"
|
title="Ver detalhes"
|
||||||
>
|
>
|
||||||
<Info size={18} />
|
<Info class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onclick={() => removeItem(index)}
|
||||||
aria-label="Remover item"
|
title="Remover item"
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{:else}
|
<EmptyState
|
||||||
<div class="mt-4 rounded-lg border-2 border-dashed border-gray-300 p-8 text-center">
|
title="Nenhum item adicionado"
|
||||||
<p class="text-sm text-gray-500">
|
description="Use a busca acima para adicionar objetos ao pedido."
|
||||||
Nenhum item adicionado. Use a busca acima para adicionar objetos ao pedido.
|
>
|
||||||
</p>
|
{#snippet icon()}
|
||||||
</div>
|
<Plus />
|
||||||
{/if}
|
{/snippet}
|
||||||
</div>
|
</EmptyState>
|
||||||
|
{/if}
|
||||||
<!-- Warnings Section -->
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
|
||||||
{#if warning}
|
{#if warning}
|
||||||
<div
|
<div class={`alert ${existingPedidos.length > 0 ? 'alert-warning' : 'alert-info'}`}>
|
||||||
class="rounded-lg border border-yellow-400 bg-yellow-50 px-4 py-3 text-sm text-yellow-800"
|
<span>{warning}</span>
|
||||||
>
|
|
||||||
<p class="font-semibold">Aviso</p>
|
|
||||||
<p>{warning}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if checking}
|
{#if checking}
|
||||||
<p class="text-sm text-gray-500">Verificando pedidos existentes...</p>
|
<div class="text-base-content/60 flex items-center gap-2 text-sm">
|
||||||
{/if}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Verificando pedidos existentes...
|
||||||
{#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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
{#if existingPedidos.length > 0}
|
||||||
<div class="flex items-center justify-end gap-3 border-t pt-6">
|
<GlassCard>
|
||||||
<a
|
<h2 class="text-lg font-semibold">Pedidos similares encontrados</h2>
|
||||||
href={resolve('/pedidos')}
|
<div class="mt-4 space-y-2">
|
||||||
class="rounded-lg bg-gray-200 px-6 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
{#each existingPedidos as pedido (pedido._id)}
|
||||||
>
|
<div
|
||||||
Cancelar
|
class="border-base-300 bg-base-100 flex items-start justify-between gap-3 rounded-lg border p-4"
|
||||||
</a>
|
>
|
||||||
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={creating || selectedItems.length === 0}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Item Configuration Modal -->
|
|
||||||
{#if showItemModal && itemConfig.objeto}
|
{#if showItemModal && itemConfig.objeto}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
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="modal-box max-w-lg">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeItemModal}
|
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 modal"
|
||||||
aria-label="Fechar"
|
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</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">
|
<div class="border-base-300 bg-base-200/30 mt-4 rounded-lg border p-4">
|
||||||
<p class="font-semibold text-gray-900">{itemConfig.objeto.nome}</p>
|
<p class="font-semibold">{itemConfig.objeto.nome}</p>
|
||||||
<p class="text-sm text-gray-600">Unidade: {itemConfig.objeto.unidade}</p>
|
<p class="text-base-content/70 text-sm">Unidade: {itemConfig.objeto.unidade}</p>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
<p class="text-base-content/60 mt-1 text-xs">
|
||||||
Valor estimado: {itemConfig.objeto.valorEstimado}
|
Valor estimado: {itemConfig.objeto.valorEstimado}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="mt-4 grid gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="quantidade">
|
<label class="label py-0" for="quantidade">
|
||||||
Quantidade
|
<span class="label-text font-semibold">Quantidade</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="quantidade"
|
id="quantidade"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
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}
|
bind:value={itemConfig.quantidade}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="itemAcao">
|
<label class="label py-0" for="itemAcao">
|
||||||
Ação (Opcional)
|
<span class="label-text font-semibold">Ação (Opcional)</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="itemAcao"
|
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}
|
bind:value={itemConfig.acaoId}
|
||||||
>
|
>
|
||||||
<option value="">Selecione uma ação...</option>
|
<option value="">Selecione uma ação...</option>
|
||||||
@@ -510,59 +508,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeItemModal}>Cancelar</button>
|
||||||
type="button"
|
<button type="button" class="btn btn-primary" onclick={confirmAddItem}
|
||||||
onclick={closeItemModal}
|
>Adicionar Item</button
|
||||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
|
||||||
>
|
>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-backdrop"
|
||||||
|
onclick={closeItemModal}
|
||||||
|
aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Details Modal -->
|
|
||||||
{#if showDetailsModal && detailsItem}
|
{#if showDetailsModal && detailsItem}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
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="modal-box max-w-lg">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeDetails}
|
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 modal"
|
||||||
aria-label="Fechar"
|
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</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="mt-4 space-y-4">
|
||||||
<div class="rounded-lg bg-gray-50 p-4">
|
<div class="border-base-300 bg-base-200/30 rounded-lg border p-4">
|
||||||
<h4 class="mb-2 font-semibold text-gray-800">Objeto</h4>
|
<h4 class="font-semibold">Objeto</h4>
|
||||||
<p class="text-gray-700"><strong>Nome:</strong> {detailsItem.objeto.nome}</p>
|
<p class="text-base-content/70 mt-2 text-sm">
|
||||||
<p class="text-gray-700"><strong>Unidade:</strong> {detailsItem.objeto.unidade}</p>
|
<strong>Nome:</strong>
|
||||||
<p class="text-gray-700">
|
{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>
|
<strong>Valor Estimado:</strong>
|
||||||
{detailsItem.objeto.valorEstimado}
|
{detailsItem.objeto.valorEstimado}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg bg-gray-50 p-4">
|
<div class="border-base-300 bg-base-200/30 rounded-lg border p-4">
|
||||||
<h4 class="mb-2 font-semibold text-gray-800">Pedido</h4>
|
<h4 class="font-semibold">Pedido</h4>
|
||||||
<p class="text-gray-700"><strong>Quantidade:</strong> {detailsItem.quantidade}</p>
|
<p class="text-base-content/70 mt-2 text-sm">
|
||||||
|
<strong>Quantidade:</strong>
|
||||||
|
{detailsItem.quantidade}
|
||||||
|
</p>
|
||||||
|
|
||||||
{#if detailsItem.acaoId}
|
{#if detailsItem.acaoId}
|
||||||
<p class="text-gray-700">
|
<p class="text-base-content/70 text-sm">
|
||||||
<strong>Ação:</strong>
|
<strong>Ação:</strong>
|
||||||
{getAcaoNome(detailsItem.acaoId)}
|
{getAcaoNome(detailsItem.acaoId)}
|
||||||
</p>
|
</p>
|
||||||
@@ -570,16 +571,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeDetails}>Fechar</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeDetails} aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</PageShell>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
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 TableCard from '$lib/components/ui/TableCard.svelte';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { Plus, Eye, X } from 'lucide-svelte';
|
import { ClipboardList, Eye, Plus, X } from 'lucide-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -28,16 +32,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusColor(status: string) {
|
function getStatusBadgeClass(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'rascunho':
|
case 'rascunho':
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
case 'gerado':
|
case 'gerado':
|
||||||
return 'bg-green-100 text-green-800';
|
return 'badge-success';
|
||||||
case 'cancelado':
|
case 'cancelado':
|
||||||
return 'bg-red-100 text-red-800';
|
return 'badge-error';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,165 +98,171 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<PageShell>
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<Breadcrumbs
|
||||||
<h1 class="text-2xl font-bold">Planejamento de Pedidos</h1>
|
items={[
|
||||||
<button
|
{ label: 'Dashboard', href: resolve('/') },
|
||||||
type="button"
|
{ label: 'Pedidos', href: resolve('/pedidos') },
|
||||||
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
{ label: 'Planejamento' }
|
||||||
onclick={openCreate}
|
]}
|
||||||
>
|
/>
|
||||||
<Plus size={20} />
|
|
||||||
Novo planejamento
|
<PageHeader
|
||||||
</button>
|
title="Planejamento de Pedidos"
|
||||||
</div>
|
subtitle="Crie e acompanhe planejamentos antes de gerar pedidos"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ClipboardList strokeWidth={2} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet actions()}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
|
||||||
|
onclick={openCreate}
|
||||||
|
>
|
||||||
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
||||||
|
Novo planejamento
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
{#if planejamentosQuery.isLoading}
|
{#if planejamentosQuery.isLoading}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex items-center justify-center py-10">
|
||||||
{#each Array(3)}
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<div class="skeleton h-16 w-full rounded-lg"></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{:else if planejamentosQuery.error}
|
{:else if planejamentosQuery.error}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<span>{planejamentosQuery.error.message}</span>
|
<span>{planejamentosQuery.error.message}</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<TableCard>
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="table-zebra table w-full">
|
||||||
<thead class="bg-gray-50">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
>Título</th
|
||||||
>
|
>
|
||||||
Título
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
</th>
|
>Data</th
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>
|
>
|
||||||
Data
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
</th>
|
>Responsável</th
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>
|
>
|
||||||
Responsável
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
</th>
|
>Ação</th
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>
|
>
|
||||||
Ação
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
</th>
|
>Status</th
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>
|
>
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
|
||||||
|
>Ações</th
|
||||||
>
|
>
|
||||||
Ações
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody>
|
||||||
{#each planejamentos as p (p._id)}
|
|
||||||
<tr class="hover:bg-gray-50">
|
|
||||||
<td class="px-6 py-4 font-medium whitespace-nowrap">{p.titulo}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{formatDateYMD(p.data)}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{p.responsavelNome}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{p.acaoNome || '-'}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getStatusColor(
|
|
||||||
p.status
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{formatStatus(p.status)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
|
||||||
<a
|
|
||||||
href={resolve(`/pedidos/planejamento/${p._id}`)}
|
|
||||||
class="inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-900"
|
|
||||||
>
|
|
||||||
<Eye size={18} />
|
|
||||||
Abrir
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{#if planejamentos.length === 0}
|
{#if planejamentos.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="px-6 py-4 text-center text-gray-500"
|
<td colspan="6" class="py-12 text-center">
|
||||||
>Nenhum planejamento encontrado.</td
|
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
||||||
>
|
<p class="text-lg font-semibold">Nenhum planejamento encontrado</p>
|
||||||
|
<p class="text-sm">Clique em “Novo planejamento” para criar o primeiro.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{:else}
|
||||||
|
{#each planejamentos as p (p._id)}
|
||||||
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
|
<td class="font-medium whitespace-nowrap">{p.titulo}</td>
|
||||||
|
<td class="text-base-content/70 whitespace-nowrap">{formatDateYMD(p.data)}</td>
|
||||||
|
<td class="text-base-content/70 whitespace-nowrap">{p.responsavelNome}</td>
|
||||||
|
<td class="text-base-content/70 whitespace-nowrap">{p.acaoNome || '-'}</td>
|
||||||
|
<td class="whitespace-nowrap">
|
||||||
|
<span class="badge badge-sm {getStatusBadgeClass(p.status)}">
|
||||||
|
{formatStatus(p.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right whitespace-nowrap">
|
||||||
|
<a
|
||||||
|
href={resolve(`/pedidos/planejamento/${p._id}`)}
|
||||||
|
class="btn btn-ghost btn-sm gap-2"
|
||||||
|
aria-label="Abrir planejamento"
|
||||||
|
>
|
||||||
|
<Eye class="h-4 w-4" />
|
||||||
|
Abrir
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</TableCard>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreate}
|
{#if showCreate}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
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="modal-box max-w-2xl">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeCreate}
|
onclick={closeCreate}
|
||||||
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 modal"
|
||||||
aria-label="Fechar"
|
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 class="mb-4 text-xl font-bold text-gray-900">Novo planejamento</h2>
|
<h3 class="text-lg font-bold">Novo planejamento</h3>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="md:col-span-2">
|
<div class="form-control w-full md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="titulo">Título</label>
|
<label class="label" for="titulo">
|
||||||
|
<span class="label-text font-semibold">Título</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="titulo"
|
id="titulo"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm 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={form.titulo}
|
bind:value={form.titulo}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
<div class="form-control w-full md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="descricao"
|
<label class="label" for="descricao">
|
||||||
>Descrição</label
|
<span class="label-text font-semibold">Descrição</span>
|
||||||
>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="descricao"
|
id="descricao"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
class="textarea textarea-bordered focus:textarea-primary w-full"
|
||||||
rows="4"
|
rows="4"
|
||||||
bind:value={form.descricao}
|
bind:value={form.descricao}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="data">Data</label>
|
<label class="label" for="data">
|
||||||
|
<span class="label-text font-semibold">Data</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="data"
|
id="data"
|
||||||
type="date"
|
type="date"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-gray-700 shadow-sm 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={form.data}
|
bind:value={form.data}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="responsavel"
|
<label class="label" for="responsavel">
|
||||||
>Responsável</label
|
<span class="label-text font-semibold">Responsável</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="responsavel"
|
id="responsavel"
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-gray-700 shadow-sm 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={form.responsavelId}
|
bind:value={form.responsavelId}
|
||||||
disabled={creating || funcionariosQuery.isLoading}
|
disabled={creating || funcionariosQuery.isLoading}
|
||||||
>
|
>
|
||||||
@@ -263,13 +273,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
<div class="form-control w-full md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="acao"
|
<label class="label" for="acao">
|
||||||
>Ação (opcional)</label
|
<span class="label-text font-semibold">Ação (opcional)</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="acao"
|
id="acao"
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-gray-700 shadow-sm 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={form.acaoId}
|
bind:value={form.acaoId}
|
||||||
disabled={creating || acoesQuery.isLoading}
|
disabled={creating || acoesQuery.isLoading}
|
||||||
>
|
>
|
||||||
@@ -281,25 +291,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-2">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeCreate} disabled={creating}
|
||||||
type="button"
|
>Cancelar</button
|
||||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
|
||||||
onclick={closeCreate}
|
|
||||||
disabled={creating}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
onclick={handleCreate}
|
|
||||||
disabled={creating}
|
|
||||||
>
|
>
|
||||||
|
<button type="button" class="btn btn-primary" onclick={handleCreate} disabled={creating}>
|
||||||
|
{#if creating}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
{creating ? 'Criando...' : 'Criar'}
|
{creating ? 'Criando...' : 'Criar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeCreate} aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</PageShell>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
import Breadcrumbs from '$lib/components/layout/Breadcrumbs.svelte';
|
||||||
|
import PageShell from '$lib/components/layout/PageShell.svelte';
|
||||||
|
import GlassCard from '$lib/components/ui/GlassCard.svelte';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
@@ -40,16 +43,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusColor(status: string) {
|
function getStatusBadgeClass(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'rascunho':
|
case 'rascunho':
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
case 'gerado':
|
case 'gerado':
|
||||||
return 'bg-green-100 text-green-800';
|
return 'badge-success';
|
||||||
case 'cancelado':
|
case 'cancelado':
|
||||||
return 'bg-red-100 text-red-800';
|
return 'badge-error';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,22 +75,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPedidoStatusColor(status: string) {
|
function getPedidoBadgeClass(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'em_rascunho':
|
case 'em_rascunho':
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
case 'aguardando_aceite':
|
case 'aguardando_aceite':
|
||||||
return 'bg-yellow-100 text-yellow-800';
|
return 'badge-warning';
|
||||||
case 'em_analise':
|
case 'em_analise':
|
||||||
return 'bg-blue-100 text-blue-800';
|
return 'badge-info';
|
||||||
case 'precisa_ajustes':
|
case 'precisa_ajustes':
|
||||||
return 'bg-orange-100 text-orange-800';
|
return 'badge-secondary';
|
||||||
case 'concluido':
|
case 'concluido':
|
||||||
return 'bg-green-100 text-green-800';
|
return 'badge-success';
|
||||||
case 'cancelado':
|
case 'cancelado':
|
||||||
return 'bg-red-100 text-red-800';
|
return 'badge-error';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-800';
|
return 'badge-ghost';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,40 +325,51 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<PageShell>
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ label: 'Dashboard', href: resolve('/') },
|
||||||
|
{ label: 'Pedidos', href: resolve('/pedidos') },
|
||||||
|
{ label: 'Planejamento', href: resolve('/pedidos/planejamento') },
|
||||||
|
{ label: planejamento?.titulo ?? 'Detalhes' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if planejamentoQuery.isLoading}
|
{#if planejamentoQuery.isLoading}
|
||||||
<p>Carregando...</p>
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
{:else if planejamentoQuery.error}
|
{:else if planejamentoQuery.error}
|
||||||
<p class="text-red-600">{planejamentoQuery.error.message}</p>
|
<div class="alert alert-error">
|
||||||
|
<span>{planejamentoQuery.error.message}</span>
|
||||||
|
</div>
|
||||||
{:else if planejamento}
|
{:else if planejamento}
|
||||||
<div class="mb-6 overflow-hidden rounded-lg bg-white p-6 shadow-md">
|
<GlassCard class="mb-6">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h1 class="truncate text-2xl font-bold text-gray-900">{planejamento.titulo}</h1>
|
<h1 class="text-primary truncate text-2xl font-bold">{planejamento.titulo}</h1>
|
||||||
<span
|
<span class="badge badge-sm {getStatusBadgeClass(planejamento.status)}">
|
||||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getStatusColor(
|
|
||||||
planejamento.status
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{formatStatus(planejamento.status)}
|
{formatStatus(planejamento.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-2 text-sm whitespace-pre-wrap text-gray-700">{planejamento.descricao}</p>
|
<p class="text-base-content/70 mt-2 text-sm whitespace-pre-wrap">
|
||||||
|
{planejamento.descricao}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
<div class="rounded-lg bg-gray-50 p-3">
|
<div class="bg-base-200/50 rounded-lg p-3">
|
||||||
<div class="text-xs font-semibold text-gray-500">Data</div>
|
<div class="text-base-content/60 text-xs font-semibold">Data</div>
|
||||||
<div class="text-sm text-gray-900">{planejamento.data}</div>
|
<div class="text-base-content text-sm">{planejamento.data}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg bg-gray-50 p-3">
|
<div class="bg-base-200/50 rounded-lg p-3">
|
||||||
<div class="text-xs font-semibold text-gray-500">Responsável</div>
|
<div class="text-base-content/60 text-xs font-semibold">Responsável</div>
|
||||||
<div class="text-sm text-gray-900">{planejamento.responsavelNome}</div>
|
<div class="text-base-content text-sm">{planejamento.responsavelNome}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg bg-gray-50 p-3">
|
<div class="bg-base-200/50 rounded-lg p-3">
|
||||||
<div class="text-xs font-semibold text-gray-500">Ação</div>
|
<div class="text-base-content/60 text-xs font-semibold">Ação</div>
|
||||||
<div class="text-sm text-gray-900">{planejamento.acaoNome || '-'}</div>
|
<div class="text-base-content text-sm">{planejamento.acaoNome || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,16 +380,20 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-2 rounded bg-green-600 px-3 py-2 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
class="btn btn-primary btn-sm gap-2"
|
||||||
onclick={saveHeader}
|
onclick={saveHeader}
|
||||||
disabled={savingHeader}
|
disabled={savingHeader}
|
||||||
>
|
>
|
||||||
<Save size={16} />
|
{#if savingHeader}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Save class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
Salvar
|
Salvar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded bg-gray-200 px-3 py-2 text-sm text-gray-800 hover:bg-gray-300"
|
class="btn btn-ghost btn-sm"
|
||||||
onclick={cancelEditHeader}
|
onclick={cancelEditHeader}
|
||||||
disabled={savingHeader}
|
disabled={savingHeader}
|
||||||
>
|
>
|
||||||
@@ -383,12 +401,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button type="button" class="btn btn-ghost btn-sm gap-2" onclick={startEditHeader}>
|
||||||
type="button"
|
<Edit class="h-4 w-4" />
|
||||||
class="flex items-center gap-2 rounded bg-gray-100 px-3 py-2 text-sm text-gray-800 hover:bg-gray-200"
|
|
||||||
onclick={startEditHeader}
|
|
||||||
>
|
|
||||||
<Edit size={16} />
|
|
||||||
Editar
|
Editar
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -397,49 +411,51 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if editingHeader}
|
{#if editingHeader}
|
||||||
<div class="mt-5 rounded-lg border border-gray-200 bg-white p-4">
|
<div class="border-base-300 bg-base-100 mt-5 rounded-lg border p-4">
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_titulo"
|
<label class="label" for="eh_titulo">
|
||||||
>Título</label
|
<span class="label-text font-semibold">Título</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="eh_titulo"
|
id="eh_titulo"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={headerForm.titulo}
|
bind:value={headerForm.titulo}
|
||||||
disabled={savingHeader}
|
disabled={savingHeader}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_descricao"
|
<label class="label" for="eh_descricao">
|
||||||
>Descrição</label
|
<span class="label-text font-semibold">Descrição</span>
|
||||||
>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="eh_descricao"
|
id="eh_descricao"
|
||||||
rows="4"
|
rows="4"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="textarea textarea-bordered focus:textarea-primary w-full"
|
||||||
bind:value={headerForm.descricao}
|
bind:value={headerForm.descricao}
|
||||||
disabled={savingHeader}
|
disabled={savingHeader}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_data">Data</label>
|
<label class="label" for="eh_data">
|
||||||
|
<span class="label-text font-semibold">Data</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="eh_data"
|
id="eh_data"
|
||||||
type="date"
|
type="date"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={headerForm.data}
|
bind:value={headerForm.data}
|
||||||
disabled={savingHeader}
|
disabled={savingHeader}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_resp"
|
<label class="label" for="eh_resp">
|
||||||
>Responsável</label
|
<span class="label-text font-semibold">Responsável</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="eh_resp"
|
id="eh_resp"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={headerForm.responsavelId}
|
bind:value={headerForm.responsavelId}
|
||||||
disabled={savingHeader || funcionariosQuery.isLoading}
|
disabled={savingHeader || funcionariosQuery.isLoading}
|
||||||
>
|
>
|
||||||
@@ -450,12 +466,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700" for="eh_acao"
|
<label class="label" for="eh_acao">
|
||||||
>Ação (opcional)</label
|
<span class="label-text font-semibold">Ação (opcional)</span>
|
||||||
>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="eh_acao"
|
id="eh_acao"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={headerForm.acaoId}
|
bind:value={headerForm.acaoId}
|
||||||
disabled={savingHeader || acoesQuery.isLoading}
|
disabled={savingHeader || acoesQuery.isLoading}
|
||||||
>
|
>
|
||||||
@@ -468,15 +484,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Itens -->
|
<!-- Itens -->
|
||||||
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
<GlassCard class="mb-6" bodyClass="p-0">
|
||||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold">Itens</h2>
|
<h2 class="text-lg font-semibold">Itens</h2>
|
||||||
{#if itensSemDfdCount > 0}
|
{#if itensSemDfdCount > 0}
|
||||||
<p class="mt-1 text-xs text-amber-700">
|
<p class="text-warning mt-1 text-xs">
|
||||||
{itensSemDfdCount} item(ns) sem DFD. Para gerar pedidos, todos os itens precisam ter DFD.
|
{itensSemDfdCount} item(ns) sem DFD. Para gerar pedidos, todos os itens precisam ter DFD.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -487,7 +503,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={openGerarModal}
|
onclick={openGerarModal}
|
||||||
disabled={!canGerar}
|
disabled={!canGerar}
|
||||||
class="rounded bg-indigo-600 px-3 py-2 text-sm text-white hover:bg-indigo-700 disabled:opacity-50"
|
class="btn btn-primary btn-sm"
|
||||||
>
|
>
|
||||||
Gerar pedidos
|
Gerar pedidos
|
||||||
</button>
|
</button>
|
||||||
@@ -496,29 +512,29 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-64 rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="input input-bordered focus:input-primary input-sm w-64"
|
||||||
placeholder="Buscar objetos..."
|
placeholder="Buscar objetos..."
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
{#if searchQuery.length > 0}
|
{#if searchQuery.length > 0}
|
||||||
<div
|
<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 absolute z-10 mt-2 w-full rounded-lg border shadow-xl"
|
||||||
>
|
>
|
||||||
{#if searchResultsQuery.isLoading}
|
{#if searchResultsQuery.isLoading}
|
||||||
<div class="p-3 text-sm text-gray-500">Buscando...</div>
|
<div class="text-base-content/60 p-3 text-sm">Buscando...</div>
|
||||||
{:else if searchResults.length === 0}
|
{:else if searchResults.length === 0}
|
||||||
<div class="p-3 text-sm text-gray-500">Nenhum objeto encontrado.</div>
|
<div class="text-base-content/60 p-3 text-sm">Nenhum objeto encontrado.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="max-h-64 overflow-y-auto">
|
<ul class="max-h-64 overflow-y-auto">
|
||||||
{#each searchResults as o (o._id)}
|
{#each searchResults as o (o._id)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between px-4 py-3 text-left transition hover:bg-blue-50"
|
class="hover:bg-base-200/50 flex w-full items-center justify-between px-4 py-3 text-left transition-colors"
|
||||||
onclick={() => openAddItemModal(o)}
|
onclick={() => openAddItemModal(o)}
|
||||||
>
|
>
|
||||||
<span class="font-medium text-gray-800">{o.nome}</span>
|
<span class="font-medium">{o.nome}</span>
|
||||||
<Plus size={16} class="text-blue-600" />
|
<Plus class="text-primary h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -533,22 +549,24 @@
|
|||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
{#if itemsQuery.isLoading}
|
{#if itemsQuery.isLoading}
|
||||||
<p class="text-sm text-gray-500">Carregando itens...</p>
|
<p class="text-base-content/60 text-sm">Carregando itens...</p>
|
||||||
{:else if items.length === 0}
|
{:else if items.length === 0}
|
||||||
<p class="text-sm text-gray-500">Nenhum item adicionado.</p>
|
<p class="text-base-content/60 text-sm">Nenhum item adicionado.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each grouped as group (group.key)}
|
{#each grouped as group (group.key)}
|
||||||
{@const dfd = group.isSemDfd ? null : group.key}
|
{@const dfd = group.isSemDfd ? null : group.key}
|
||||||
{@const pedidoLink = dfd ? pedidosByDfd[dfd] : null}
|
{@const pedidoLink = dfd ? pedidosByDfd[dfd] : null}
|
||||||
<div class="rounded-lg border border-gray-200">
|
<GlassCard bodyClass="p-0">
|
||||||
<div class="flex items-center justify-between bg-gray-50 px-4 py-3">
|
<div
|
||||||
|
class="border-base-300 bg-base-200/50 flex items-center justify-between border-b px-6 py-4"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div class="font-semibold text-gray-900">{group.label}</div>
|
<div class="font-semibold">{group.label}</div>
|
||||||
{#if pedidoLink?.pedido}
|
{#if pedidoLink?.pedido}
|
||||||
<a
|
<a
|
||||||
href={resolve(`/pedidos/${pedidoLink.pedido._id}`)}
|
href={resolve(`/pedidos/${pedidoLink.pedido._id}`)}
|
||||||
class="text-xs text-blue-700 hover:underline"
|
class="text-primary text-xs hover:underline"
|
||||||
>
|
>
|
||||||
Pedido: {pedidoLink.pedido.numeroSei || pedidoLink.pedido._id} — {formatPedidoStatus(
|
Pedido: {pedidoLink.pedido.numeroSei || pedidoLink.pedido._id} — {formatPedidoStatus(
|
||||||
pedidoLink.pedido.status
|
pedidoLink.pedido.status
|
||||||
@@ -556,45 +574,52 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500">{group.items.length} item(ns)</div>
|
<div class="text-base-content/60 text-xs">{group.items.length} item(ns)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="table-zebra table w-full">
|
||||||
<thead class="bg-white">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
<th
|
||||||
|
class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
>Objeto</th
|
>Objeto</th
|
||||||
>
|
>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
<th
|
||||||
|
class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
>Qtd</th
|
>Qtd</th
|
||||||
>
|
>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
<th
|
||||||
|
class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
>Valor Est.</th
|
>Valor Est.</th
|
||||||
>
|
>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase"
|
<th
|
||||||
|
class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
>DFD</th
|
>DFD</th
|
||||||
>
|
>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase"
|
<th
|
||||||
|
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
|
||||||
>Ações</th
|
>Ações</th
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody>
|
||||||
{#each group.items as it (it._id)}
|
{#each group.items as it (it._id)}
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
<td class="px-4 py-2 text-sm text-gray-900">
|
<td class="whitespace-nowrap">
|
||||||
{it.objetoNome}
|
{it.objetoNome}
|
||||||
{#if it.objetoUnidade}
|
{#if it.objetoUnidade}
|
||||||
<span class="ml-2 text-xs text-gray-400">({it.objetoUnidade})</span>
|
<span class="text-base-content/60 ml-2 text-xs">
|
||||||
|
({it.objetoUnidade})
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-sm text-gray-700">
|
<td class="whitespace-nowrap">
|
||||||
{#if isRascunho}
|
{#if isRascunho}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-24 rounded border px-2 py-1 text-sm"
|
class="input input-bordered input-sm w-24"
|
||||||
value={it.quantidade}
|
value={it.quantidade}
|
||||||
onchange={(e) =>
|
onchange={(e) =>
|
||||||
updateItemField(it._id, {
|
updateItemField(it._id, {
|
||||||
@@ -605,11 +630,11 @@
|
|||||||
{it.quantidade}
|
{it.quantidade}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-sm text-gray-700">
|
<td class="whitespace-nowrap">
|
||||||
{#if isRascunho}
|
{#if isRascunho}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-40 rounded border px-2 py-1 text-sm"
|
class="input input-bordered input-sm w-40"
|
||||||
value={it.valorEstimado}
|
value={it.valorEstimado}
|
||||||
onblur={(e) =>
|
onblur={(e) =>
|
||||||
updateItemField(it._id, { valorEstimado: e.currentTarget.value })}
|
updateItemField(it._id, { valorEstimado: e.currentTarget.value })}
|
||||||
@@ -618,11 +643,11 @@
|
|||||||
{it.valorEstimado}
|
{it.valorEstimado}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-sm text-gray-700">
|
<td class="whitespace-nowrap">
|
||||||
{#if isRascunho}
|
{#if isRascunho}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-32 rounded border px-2 py-1 text-sm"
|
class="input input-bordered input-sm w-32"
|
||||||
value={it.numeroDfd || ''}
|
value={it.numeroDfd || ''}
|
||||||
placeholder="(opcional)"
|
placeholder="(opcional)"
|
||||||
onblur={(e) => {
|
onblur={(e) => {
|
||||||
@@ -634,18 +659,18 @@
|
|||||||
{it.numeroDfd || '-'}
|
{it.numeroDfd || '-'}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-right">
|
<td class="text-right whitespace-nowrap">
|
||||||
{#if isRascunho}
|
{#if isRascunho}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded bg-red-100 p-2 text-red-700 hover:bg-red-200"
|
class="btn btn-ghost btn-sm text-error"
|
||||||
onclick={() => removeItem(it._id)}
|
onclick={() => removeItem(it._id)}
|
||||||
title="Remover item"
|
title="Remover item"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-xs text-gray-400">—</span>
|
<span class="text-base-content/50 text-xs">—</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -653,33 +678,33 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Pedidos -->
|
<!-- Pedidos -->
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<GlassCard bodyClass="p-0">
|
||||||
<div class="border-b border-gray-200 px-6 py-4">
|
<div class="border-base-300 border-b px-6 py-4">
|
||||||
<h2 class="text-lg font-semibold">Pedidos gerados</h2>
|
<h2 class="text-lg font-semibold">Pedidos gerados</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
{#if pedidosQuery.isLoading}
|
{#if pedidosQuery.isLoading}
|
||||||
<p class="text-sm text-gray-500">Carregando pedidos...</p>
|
<p class="text-base-content/60 text-sm">Carregando pedidos...</p>
|
||||||
{:else if pedidosLinks.length === 0}
|
{:else if pedidosLinks.length === 0}
|
||||||
<p class="text-sm text-gray-500">Nenhum pedido gerado ainda.</p>
|
<p class="text-base-content/60 text-sm">Nenhum pedido gerado ainda.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each pedidosLinks as row (row._id)}
|
{#each pedidosLinks as row (row._id)}
|
||||||
<div class="rounded-lg border border-gray-200 p-4">
|
<div class="border-base-300 rounded-lg border p-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="text-sm font-semibold text-gray-900">DFD: {row.numeroDfd}</div>
|
<div class="text-sm font-semibold">DFD: {row.numeroDfd}</div>
|
||||||
{#if row.pedido}
|
{#if row.pedido}
|
||||||
<a
|
<a
|
||||||
class="text-sm text-blue-700 hover:underline"
|
class="text-primary text-sm hover:underline"
|
||||||
href={resolve(`/pedidos/${row.pedido._id}`)}
|
href={resolve(`/pedidos/${row.pedido._id}`)}
|
||||||
>
|
>
|
||||||
Pedido: {row.pedido.numeroSei || row.pedido._id}
|
Pedido: {row.pedido.numeroSei || row.pedido._id}
|
||||||
@@ -687,11 +712,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if row.pedido}
|
{#if row.pedido}
|
||||||
<span
|
<span class="badge badge-sm {getPedidoBadgeClass(row.pedido.status)}">
|
||||||
class="inline-flex rounded-full px-2 py-1 text-xs font-semibold {getPedidoStatusColor(
|
|
||||||
row.pedido.status
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{formatPedidoStatus(row.pedido.status)}
|
{formatPedidoStatus(row.pedido.status)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -699,12 +720,12 @@
|
|||||||
|
|
||||||
{#if row.lastHistory && row.lastHistory.length > 0}
|
{#if row.lastHistory && row.lastHistory.length > 0}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="mb-2 text-xs font-semibold text-gray-500">Últimas ações</div>
|
<div class="text-base-content/60 mb-2 text-xs font-semibold">Últimas ações</div>
|
||||||
<ul class="space-y-1 text-xs text-gray-700">
|
<ul class="text-base-content/70 space-y-1 text-xs">
|
||||||
{#each row.lastHistory as h (h._id)}
|
{#each row.lastHistory as h (h._id)}
|
||||||
<li class="flex items-center justify-between gap-2">
|
<li class="flex items-center justify-between gap-2">
|
||||||
<span class="truncate">{h.usuarioNome}: {h.acao}</span>
|
<span class="truncate">{h.usuarioNome}: {h.acao}</span>
|
||||||
<span class="shrink-0 text-gray-400"
|
<span class="text-base-content/50 shrink-0"
|
||||||
>{new Date(h.data).toLocaleString('pt-BR')}</span
|
>{new Date(h.data).toLocaleString('pt-BR')}</span
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@@ -717,65 +738,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlassCard>
|
||||||
|
|
||||||
<!-- Add item modal -->
|
<!-- Add item modal -->
|
||||||
{#if showAddItemModal && addItemConfig.objeto}
|
{#if showAddItemModal && addItemConfig.objeto}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
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="modal-box max-w-lg">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeAddItemModal}
|
onclick={closeAddItemModal}
|
||||||
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 modal"
|
||||||
aria-label="Fechar"
|
|
||||||
disabled={addingItem}
|
disabled={addingItem}
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 class="mb-4 text-xl font-bold text-gray-900">Adicionar item</h3>
|
<h3 class="text-lg font-bold">Adicionar item</h3>
|
||||||
|
|
||||||
<div class="mb-4 rounded-lg bg-blue-50 p-4">
|
<div class="bg-base-200/50 mt-4 rounded-lg p-4">
|
||||||
<p class="font-semibold text-gray-900">{addItemConfig.objeto.nome}</p>
|
<p class="font-semibold">{addItemConfig.objeto.nome}</p>
|
||||||
<p class="text-sm text-gray-600">Unidade: {addItemConfig.objeto.unidade}</p>
|
<p class="text-base-content/70 text-sm">Unidade: {addItemConfig.objeto.unidade}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="mt-6 space-y-4">
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_qtd"
|
<label class="label" for="ai_qtd">
|
||||||
>Quantidade</label
|
<span class="label-text font-semibold">Quantidade</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ai_qtd"
|
id="ai_qtd"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={addItemConfig.quantidade}
|
bind:value={addItemConfig.quantidade}
|
||||||
disabled={addingItem}
|
disabled={addingItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_valor"
|
<label class="label" for="ai_valor">
|
||||||
>Valor estimado</label
|
<span class="label-text font-semibold">Valor estimado</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ai_valor"
|
id="ai_valor"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={addItemConfig.valorEstimado}
|
bind:value={addItemConfig.valorEstimado}
|
||||||
disabled={addingItem}
|
disabled={addingItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700" for="ai_dfd"
|
<label class="label" for="ai_dfd">
|
||||||
>Número DFD (opcional)</label
|
<span class="label-text font-semibold">Número DFD (opcional)</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ai_dfd"
|
id="ai_dfd"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-lg border border-gray-300 px-4 py-2.5"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={addItemConfig.numeroDfd}
|
bind:value={addItemConfig.numeroDfd}
|
||||||
disabled={addingItem}
|
disabled={addingItem}
|
||||||
placeholder="Ex: 123/2025"
|
placeholder="Ex: 123/2025"
|
||||||
@@ -783,64 +802,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeAddItemModal} disabled={addingItem}>
|
||||||
type="button"
|
|
||||||
onclick={closeAddItemModal}
|
|
||||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 hover:bg-gray-300"
|
|
||||||
disabled={addingItem}
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
onclick={confirmAddItem}
|
onclick={confirmAddItem}
|
||||||
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
disabled={addingItem}
|
disabled={addingItem}
|
||||||
>
|
>
|
||||||
|
{#if addingItem}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
{addingItem ? 'Adicionando...' : 'Adicionar'}
|
{addingItem ? 'Adicionando...' : 'Adicionar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-backdrop"
|
||||||
|
onclick={closeAddItemModal}
|
||||||
|
aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Gerar pedidos modal -->
|
<!-- Gerar pedidos modal -->
|
||||||
{#if showGerarModal}
|
{#if showGerarModal}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
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="modal-box max-w-2xl">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeGerarModal}
|
onclick={closeGerarModal}
|
||||||
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 modal"
|
||||||
aria-label="Fechar"
|
|
||||||
disabled={gerando}
|
disabled={gerando}
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 class="mb-2 text-xl font-bold text-gray-900">Gerar pedidos</h3>
|
<h3 class="text-lg font-bold">Gerar pedidos</h3>
|
||||||
<p class="mb-4 text-sm text-gray-600">
|
<p class="text-base-content/70 mt-2 text-sm">
|
||||||
Será criado <strong>1 pedido por DFD</strong>. Informe o número SEI de cada pedido.
|
Será criado <strong>1 pedido por DFD</strong>. Informe o número SEI de cada pedido.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="mt-6 space-y-3">
|
||||||
{#each dfdsParaGerar as dfd (dfd)}
|
{#each dfdsParaGerar as dfd (dfd)}
|
||||||
<div class="grid gap-3 rounded-lg border border-gray-200 p-4 md:grid-cols-3">
|
<div class="border-base-300 grid gap-3 rounded-lg border p-4 md:grid-cols-3">
|
||||||
<div class="md:col-span-1">
|
<div class="md:col-span-1">
|
||||||
<div class="text-xs font-semibold text-gray-500">DFD</div>
|
<div class="text-base-content/60 text-xs font-semibold">DFD</div>
|
||||||
<div class="text-sm font-semibold text-gray-900">{dfd}</div>
|
<div class="text-sm font-semibold">{dfd}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="mb-1 block text-xs font-semibold text-gray-500" for={`sei_${dfd}`}
|
<label class="label py-0" for={`sei_${dfd}`}>
|
||||||
>Número SEI</label
|
<span class="label-text font-semibold">Número SEI</span>
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={`sei_${dfd}`}
|
id={`sei_${dfd}`}
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
class="input input-bordered focus:input-primary w-full"
|
||||||
bind:value={seiByDfd[dfd]}
|
bind:value={seiByDfd[dfd]}
|
||||||
disabled={gerando}
|
disabled={gerando}
|
||||||
placeholder="Ex: 12345.000000/2025-00"
|
placeholder="Ex: 12345.000000/2025-00"
|
||||||
@@ -850,26 +871,30 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-2">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeGerarModal} disabled={gerando}>
|
||||||
type="button"
|
|
||||||
onclick={closeGerarModal}
|
|
||||||
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 hover:bg-gray-300"
|
|
||||||
disabled={gerando}
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
onclick={confirmarGeracao}
|
onclick={confirmarGeracao}
|
||||||
class="rounded-lg bg-indigo-600 px-5 py-2.5 font-semibold text-white hover:bg-indigo-700 disabled:opacity-50"
|
|
||||||
disabled={gerando}
|
disabled={gerando}
|
||||||
>
|
>
|
||||||
|
{#if gerando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
{gerando ? 'Gerando...' : 'Confirmar e gerar'}
|
{gerando ? 'Gerando...' : 'Confirmar e gerar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-backdrop"
|
||||||
|
onclick={closeGerarModal}
|
||||||
|
aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</PageShell>
|
||||||
|
|||||||
Reference in New Issue
Block a user