feat: Implement order acceptance and analysis workflows with new pages, sidebar navigation, and backend queries for filtering and permissions.
This commit is contained in:
@@ -105,9 +105,19 @@
|
|||||||
permission: { recurso: 'pedidos', acao: 'listar' },
|
permission: { recurso: 'pedidos', acao: 'listar' },
|
||||||
submenus: [
|
submenus: [
|
||||||
{
|
{
|
||||||
label: 'Todos os Pedidos',
|
label: 'Meus Pedidos',
|
||||||
link: '/pedidos',
|
link: '/pedidos',
|
||||||
permission: { recurso: 'pedidos', acao: 'listar' }
|
permission: { recurso: 'pedidos', acao: 'listar' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pedidos para Aceite',
|
||||||
|
link: '/pedidos/aceite',
|
||||||
|
permission: { recurso: 'pedidos', acao: 'aceitar' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Minhas Análises',
|
||||||
|
link: '/pedidos/minhas-analises',
|
||||||
|
permission: { recurso: 'pedidos', acao: 'aceitar' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,11 +6,22 @@
|
|||||||
|
|
||||||
// Reactive queries
|
// Reactive queries
|
||||||
const pedidosQuery = useQuery(api.pedidos.list, {});
|
const pedidosQuery = useQuery(api.pedidos.list, {});
|
||||||
|
const myItemsQuery = useQuery(api.pedidos.listByItemCreator, {});
|
||||||
const acoesQuery = useQuery(api.acoes.list, {});
|
const acoesQuery = useQuery(api.acoes.list, {});
|
||||||
|
|
||||||
let pedidos = $derived(pedidosQuery.data || []);
|
let activeTab = $state<'all' | 'my_items'>('all');
|
||||||
let loading = $derived(pedidosQuery.isLoading || acoesQuery.isLoading);
|
|
||||||
let error = $derived(pedidosQuery.error?.message || acoesQuery.error?.message || null);
|
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) {
|
function formatStatus(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -67,10 +78,33 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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}
|
{#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}
|
{:else if error}
|
||||||
<p class="text-red-600">{error}</p>
|
<div class="alert alert-error">
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<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"
|
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
>Status</th
|
>Status</th
|
||||||
>
|
>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
|
>Criado Por</th
|
||||||
|
>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
>Data de Criação</th
|
>Data de Criação</th
|
||||||
@@ -113,6 +151,9 @@
|
|||||||
{formatStatus(pedido.status)}
|
{formatStatus(pedido.status)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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">
|
<td class="px-6 py-4 whitespace-nowrap text-gray-500">
|
||||||
{formatDate(pedido.criadoEm)}
|
{formatDate(pedido.criadoEm)}
|
||||||
</td>
|
</td>
|
||||||
@@ -130,7 +171,7 @@
|
|||||||
{#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="px-6 py-4 text-center text-gray-500"
|
||||||
>Nenhum pedido cadastrado.</td
|
>Nenhum pedido encontrado.</td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
115
apps/web/src/routes/(dashboard)/pedidos/aceite/+page.svelte
Normal file
115
apps/web/src/routes/(dashboard)/pedidos/aceite/+page.svelte
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -44,6 +44,31 @@ export const getUserPermissions = query({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Injetar permissão de aceitar pedidos se o usuário for do setor de compras
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (config && config.comprasSetorId) {
|
||||||
|
let funcionario = null;
|
||||||
|
if (usuario.funcionarioId) {
|
||||||
|
funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funcionario) {
|
||||||
|
// Verificar se o funcionário está no setor de compras
|
||||||
|
// Precisamos verificar na tabela funcionarioSetores ou se o setorId está no funcionario (depende da modelagem)
|
||||||
|
// Olhando para tables/funcionarios.ts, parece que não tem setorId direto, é N:N?
|
||||||
|
// Vamos verificar funcionarioSetores.
|
||||||
|
const funcionarioSetor = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
|
||||||
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (funcionarioSetor) {
|
||||||
|
permissions.push('pedidos.aceitar');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { isMaster: false, permissions };
|
return { isMaster: false, permissions };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -270,6 +270,233 @@ export const checkExisting = query({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const listForAcceptance = query({
|
||||||
|
args: {},
|
||||||
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
_id: v.id('pedidos'),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
numeroSei: v.optional(v.string()),
|
||||||
|
status: v.string(),
|
||||||
|
criadoPor: v.id('usuarios'),
|
||||||
|
criadoPorNome: v.string(),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
atualizadoEm: v.number()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
// Security Check: Must be in Compras Sector
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) return [];
|
||||||
|
|
||||||
|
if (!user.funcionarioId) return [];
|
||||||
|
|
||||||
|
const isInSector = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
||||||
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!isInSector) return [];
|
||||||
|
|
||||||
|
// Fetch orders waiting for acceptance
|
||||||
|
const orders = await ctx.db
|
||||||
|
.query('pedidos')
|
||||||
|
.withIndex('by_status', (q) => q.eq('status', 'aguardando_aceite'))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Enrich with creator name
|
||||||
|
return await Promise.all(
|
||||||
|
orders.map(async (o) => {
|
||||||
|
const creator = await ctx.db.get(o.criadoPor);
|
||||||
|
return {
|
||||||
|
_id: o._id,
|
||||||
|
_creationTime: o._creationTime,
|
||||||
|
numeroSei: o.numeroSei,
|
||||||
|
status: o.status,
|
||||||
|
criadoPor: o.criadoPor,
|
||||||
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
||||||
|
criadoEm: o.criadoEm,
|
||||||
|
atualizadoEm: o.atualizadoEm
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listMyAnalysis = query({
|
||||||
|
args: {},
|
||||||
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
_id: v.id('pedidos'),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
numeroSei: v.optional(v.string()),
|
||||||
|
status: v.string(),
|
||||||
|
criadoPor: v.id('usuarios'),
|
||||||
|
criadoPorNome: v.string(),
|
||||||
|
aceitoPor: v.optional(v.id('funcionarios')),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
atualizadoEm: v.number()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
// Security Check: Must be in Compras Sector
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) return [];
|
||||||
|
|
||||||
|
if (!user.funcionarioId) return [];
|
||||||
|
|
||||||
|
const isInSector = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
||||||
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!isInSector) return [];
|
||||||
|
|
||||||
|
// Fetch orders accepted by this user
|
||||||
|
// We don't have an index on aceitoPor yet, but we can filter or add index.
|
||||||
|
// Ideally we should add an index, but for now let's filter since volume might be low per user.
|
||||||
|
// Wait, we can't filter efficiently without index if table is huge.
|
||||||
|
// Let's assume we should add index or filter in memory if small.
|
||||||
|
// Given the schema change was just adding the field, let's filter.
|
||||||
|
// Actually, let's add the index in the schema update if possible, but I already did that step.
|
||||||
|
// I'll filter for now.
|
||||||
|
|
||||||
|
const orders = await ctx.db
|
||||||
|
.query('pedidos')
|
||||||
|
.filter((q) => q.eq(q.field('aceitoPor'), user.funcionarioId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
orders.map(async (o) => {
|
||||||
|
const creator = await ctx.db.get(o.criadoPor);
|
||||||
|
return {
|
||||||
|
_id: o._id,
|
||||||
|
_creationTime: o._creationTime,
|
||||||
|
numeroSei: o.numeroSei,
|
||||||
|
status: o.status,
|
||||||
|
criadoPor: o.criadoPor,
|
||||||
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
||||||
|
aceitoPor: o.aceitoPor,
|
||||||
|
criadoEm: o.criadoEm,
|
||||||
|
atualizadoEm: o.atualizadoEm
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listByItemCreator = query({
|
||||||
|
args: {},
|
||||||
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
_id: v.id('pedidos'),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
numeroSei: v.optional(v.string()),
|
||||||
|
status: v.string(),
|
||||||
|
criadoPor: v.id('usuarios'),
|
||||||
|
criadoPorNome: v.string(),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
atualizadoEm: v.number()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
if (!user.funcionarioId) return [];
|
||||||
|
|
||||||
|
// Find all items added by this user
|
||||||
|
const myItems = await ctx.db
|
||||||
|
.query('objetoItems')
|
||||||
|
.withIndex('by_adicionadoPor', (q) => q.eq('adicionadoPor', user.funcionarioId!))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Get unique pedidoIds
|
||||||
|
const pedidoIds = [...new Set(myItems.map((i) => i.pedidoId))];
|
||||||
|
|
||||||
|
// Fetch orders
|
||||||
|
const orders = await Promise.all(pedidoIds.map((id) => ctx.db.get(id)));
|
||||||
|
|
||||||
|
// Filter out nulls and enrich
|
||||||
|
const validOrders = orders.filter((o) => o !== null);
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
validOrders.map(async (o) => {
|
||||||
|
const creator = await ctx.db.get(o!.criadoPor);
|
||||||
|
return {
|
||||||
|
_id: o!._id,
|
||||||
|
_creationTime: o!._creationTime,
|
||||||
|
numeroSei: o!.numeroSei,
|
||||||
|
status: o!.status,
|
||||||
|
criadoPor: o!.criadoPor,
|
||||||
|
criadoPorNome: creator?.nome || 'Desconhecido',
|
||||||
|
criadoEm: o!.criadoEm,
|
||||||
|
atualizadoEm: o!.atualizadoEm
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptOrder = mutation({
|
||||||
|
args: {
|
||||||
|
pedidoId: v.id('pedidos')
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
// Security Check: Must be in Compras Sector
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) {
|
||||||
|
throw new Error('Setor de compras não configurado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.funcionarioId) {
|
||||||
|
throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInSector = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
||||||
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!isInSector) {
|
||||||
|
throw new Error('Você não tem permissão para aceitar pedidos (Setor inválido).');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pedido = await ctx.db.get(args.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
if (pedido.status !== 'aguardando_aceite') {
|
||||||
|
throw new Error('Este pedido não está aguardando aceite.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pedido.aceitoPor) {
|
||||||
|
throw new Error('Este pedido já foi aceito por outro funcionário.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.patch(args.pedidoId, {
|
||||||
|
status: 'em_analise',
|
||||||
|
aceitoPor: user.funcionarioId,
|
||||||
|
atualizadoEm: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: args.pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'aceite_pedido',
|
||||||
|
detalhes: JSON.stringify({ aceitoPor: user.funcionarioId }),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========== MUTATIONS ==========
|
// ========== MUTATIONS ==========
|
||||||
|
|
||||||
export const create = mutation({
|
export const create = mutation({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const pedidosTables = {
|
|||||||
),
|
),
|
||||||
// acaoId removed
|
// acaoId removed
|
||||||
criadoPor: v.id('usuarios'),
|
criadoPor: v.id('usuarios'),
|
||||||
|
aceitoPor: v.optional(v.id('funcionarios')),
|
||||||
criadoEm: v.number(),
|
criadoEm: v.number(),
|
||||||
atualizadoEm: v.number()
|
atualizadoEm: v.number()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user