feat: Implement order acceptance and analysis workflows with new pages, sidebar navigation, and backend queries for filtering and permissions.

This commit is contained in:
2025-12-04 17:10:06 -03:00
parent 68475f549a
commit 29577b8e63
7 changed files with 509 additions and 7 deletions

View File

@@ -6,11 +6,22 @@
// Reactive queries
const pedidosQuery = useQuery(api.pedidos.list, {});
const myItemsQuery = useQuery(api.pedidos.listByItemCreator, {});
const acoesQuery = useQuery(api.acoes.list, {});
let pedidos = $derived(pedidosQuery.data || []);
let loading = $derived(pedidosQuery.isLoading || acoesQuery.isLoading);
let error = $derived(pedidosQuery.error?.message || acoesQuery.error?.message || null);
let activeTab = $state<'all' | 'my_items'>('all');
let pedidos = $derived(activeTab === 'all' ? pedidosQuery.data || [] : myItemsQuery.data || []);
let loading = $derived(
(activeTab === 'all' ? pedidosQuery.isLoading : myItemsQuery.isLoading) || acoesQuery.isLoading
);
let error = $derived(
(activeTab === 'all' ? pedidosQuery.error?.message : myItemsQuery.error?.message) ||
acoesQuery.error?.message ||
null
);
function formatStatus(status: string) {
switch (status) {
@@ -67,10 +78,33 @@
</a>
</div>
<div role="tablist" class="tabs tabs-bordered mb-6">
<button
role="tab"
class="tab {activeTab === 'all' ? 'tab-active' : ''}"
onclick={() => (activeTab = 'all')}
>
Todos os Pedidos
</button>
<button
role="tab"
class="tab {activeTab === 'my_items' ? 'tab-active' : ''}"
onclick={() => (activeTab = 'my_items')}
>
Pedidos com meus itens
</button>
</div>
{#if loading}
<p>Carregando...</p>
<div class="flex flex-col gap-4">
{#each Array(3) as _, i (i)}
<div class="skeleton h-16 w-full rounded-lg"></div>
{/each}
</div>
{:else if error}
<p class="text-red-600">{error}</p>
<div class="alert alert-error">
<span>{error}</span>
</div>
{:else}
<div class="overflow-hidden rounded-lg bg-white shadow-md">
<table class="min-w-full divide-y divide-gray-200">
@@ -84,6 +118,10 @@
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Status</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Criado Por</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Data de Criação</th
@@ -113,6 +151,9 @@
{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>
@@ -130,7 +171,7 @@
{#if pedidos.length === 0}
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500"
>Nenhum pedido cadastrado.</td
>Nenhum pedido encontrado.</td
>
</tr>
{/if}

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { CheckCircle, Clock, FileText, User } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
const client = useConvexClient();
const ordersQuery = useQuery(api.pedidos.listForAcceptance, {});
let acceptingId = $state<string | null>(null);
async function handleAccept(pedidoId: Id<'pedidos'>) {
if (!confirm('Tem certeza que deseja aceitar este pedido para análise?')) return;
acceptingId = pedidoId;
try {
await client.mutation(api.pedidos.acceptOrder, { pedidoId });
toast.success('Pedido aceito com sucesso!');
} catch (error) {
console.error('Erro ao aceitar pedido:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao aceitar pedido';
toast.error(errorMessage);
} finally {
acceptingId = null;
}
}
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-primary text-2xl font-bold tracking-tight">Pedidos para Aceite</h1>
<p class="text-base-content/70 mt-1">
Lista de pedidos aguardando análise do setor de compras.
</p>
</div>
</div>
{#if ordersQuery.isLoading}
<div class="flex flex-col gap-4">
{#each Array(3) as _, i (i)}
<div class="skeleton h-24 w-full rounded-lg"></div>
{/each}
</div>
{:else if ordersQuery.error}
<div class="alert alert-error">
<span>Erro ao carregar pedidos: {ordersQuery.error.message}</span>
</div>
{: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">
<CheckCircle class="text-base-content/30 h-8 w-8" />
</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}
<div class="grid gap-4">
{#each ordersQuery.data as pedido (pedido._id)}
<div
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="space-y-1">
<div class="flex items-center gap-2">
<span class="badge badge-warning gap-1 font-medium">
<Clock class="h-3 w-3" />
Aguardando Aceite
</span>
<span class="text-base-content/40 text-xs">
#{pedido._id.slice(-6)}
</span>
</div>
<h3 class="text-lg font-bold">
{pedido.numeroSei ? `SEI: ${pedido.numeroSei}` : 'Sem número SEI'}
</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>{new Date(pedido.criadoEm).toLocaleDateString()}</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<a href="/pedidos/{pedido._id}" class="btn btn-ghost btn-sm">
<FileText class="mr-2 h-4 w-4" />
Ver Detalhes
</a>
<button
class="btn btn-primary btn-sm"
disabled={acceptingId === pedido._id}
onclick={() => handleAccept(pedido._id)}
>
{#if acceptingId === pedido._id}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<CheckCircle class="mr-2 h-4 w-4" />
{/if}
Aceitar Pedido
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { ClipboardList, Clock, FileText, User, Search } from 'lucide-svelte';
const ordersQuery = useQuery(api.pedidos.listMyAnalysis, {});
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-primary text-2xl font-bold tracking-tight">Minhas Análises</h1>
<p class="text-base-content/70 mt-1">Pedidos que você aceitou e está analisando.</p>
</div>
</div>
{#if ordersQuery.isLoading}
<div class="flex flex-col gap-4">
{#each Array(3) as _, i (i)}
<div class="skeleton h-24 w-full rounded-lg"></div>
{/each}
</div>
{:else if ordersQuery.error}
<div class="alert alert-error">
<span>Erro ao carregar análises: {ordersQuery.error.message}</span>
</div>
{: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>
<h3 class="text-lg font-medium">Nenhuma análise em andamento</h3>
<p class="text-base-content/60 mt-1 max-w-sm">
Você não possui pedidos sob sua responsabilidade no momento. Vá para "Pedidos para Aceite"
para pegar novos pedidos.
</p>
</div>
{:else}
<div class="grid gap-4">
{#each ordersQuery.data as pedido (pedido._id)}
<div
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="space-y-1">
<div class="flex items-center gap-2">
<span class="badge badge-info gap-1 font-medium">
<Search class="h-3 w-3" />
Em Análise
</span>
<span class="text-base-content/40 text-xs">
#{pedido._id.slice(-6)}
</span>
</div>
<h3 class="text-lg font-bold">
{pedido.numeroSei ? `SEI: ${pedido.numeroSei}` : 'Sem número SEI'}
</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 class="flex items-center gap-2">
<a href="/pedidos/{pedido._id}" class="btn btn-primary btn-sm">
<FileText class="mr-2 h-4 w-4" />
Continuar Análise
</a>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>