feat: Implement item request/approval workflow for pedidos in analysis mode, adding conditional item modifications and new request management APIs.
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
const objetosQuery = $derived.by(() => useQuery(api.objetos.list, {}));
|
const objetosQuery = $derived.by(() => useQuery(api.objetos.list, {}));
|
||||||
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
|
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
|
||||||
const permissionsQuery = $derived.by(() => useQuery(api.pedidos.getPermissions, { pedidoId }));
|
const permissionsQuery = $derived.by(() => useQuery(api.pedidos.getPermissions, { pedidoId }));
|
||||||
|
const requestsQuery = $derived.by(() => useQuery(api.pedidos.getItemRequests, { pedidoId }));
|
||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
let pedido = $derived(pedidoQuery.data);
|
let pedido = $derived(pedidoQuery.data);
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
let objetos = $derived(objetosQuery.data || []);
|
let objetos = $derived(objetosQuery.data || []);
|
||||||
let acoes = $derived(acoesQuery.data || []);
|
let acoes = $derived(acoesQuery.data || []);
|
||||||
let permissions = $derived(permissionsQuery.data);
|
let permissions = $derived(permissionsQuery.data);
|
||||||
|
let requests = $derived(requestsQuery.data || []);
|
||||||
|
|
||||||
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
|
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
|
||||||
|
|
||||||
@@ -116,8 +118,10 @@
|
|||||||
itemsQuery.isLoading ||
|
itemsQuery.isLoading ||
|
||||||
historyQuery.isLoading ||
|
historyQuery.isLoading ||
|
||||||
objetosQuery.isLoading ||
|
objetosQuery.isLoading ||
|
||||||
|
objetosQuery.isLoading ||
|
||||||
acoesQuery.isLoading ||
|
acoesQuery.isLoading ||
|
||||||
permissionsQuery.isLoading
|
permissionsQuery.isLoading ||
|
||||||
|
requestsQuery.isLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
let error = $derived(
|
let error = $derived(
|
||||||
@@ -207,7 +211,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handleAddItem() {
|
async function handleAddItem() {
|
||||||
if (!newItem.objetoId || !newItem.valorEstimado) return;
|
if (!pedido || !newItem.objetoId || !newItem.valorEstimado) return;
|
||||||
addingItem = true;
|
addingItem = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.pedidos.addItem, {
|
await client.mutation(api.pedidos.addItem, {
|
||||||
@@ -228,6 +232,9 @@
|
|||||||
ataId: ''
|
ataId: ''
|
||||||
};
|
};
|
||||||
showAddItem = false;
|
showAddItem = false;
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
alert('Solicitação de adição enviada para análise.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao adicionar item: ' + (e as Error).message);
|
alert('Erro ao adicionar item: ' + (e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -236,6 +243,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdateQuantity(itemId: Id<'objetoItems'>, novaQuantidade: number) {
|
async function handleUpdateQuantity(itemId: Id<'objetoItems'>, novaQuantidade: number) {
|
||||||
|
if (!pedido) return;
|
||||||
|
|
||||||
if (novaQuantidade < 1) {
|
if (novaQuantidade < 1) {
|
||||||
alert('Quantidade deve ser pelo menos 1.');
|
alert('Quantidade deve ser pelo menos 1.');
|
||||||
return;
|
return;
|
||||||
@@ -245,15 +254,22 @@
|
|||||||
itemId,
|
itemId,
|
||||||
novaQuantidade
|
novaQuantidade
|
||||||
});
|
});
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
alert('Solicitação de alteração de quantidade enviada para análise.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao atualizar quantidade: ' + (e as Error).message);
|
alert('Erro ao atualizar quantidade: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveItem(itemId: Id<'objetoItems'>) {
|
async function handleRemoveItem(itemId: Id<'objetoItems'>) {
|
||||||
|
if (!pedido) return;
|
||||||
if (!confirm('Remover este item?')) return;
|
if (!confirm('Remover este item?')) return;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.pedidos.removeItem, { itemId });
|
await client.mutation(api.pedidos.removeItem, { itemId });
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
alert('Solicitação de remoção enviada para análise.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao remover item: ' + (e as Error).message);
|
alert('Erro ao remover item: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
@@ -625,6 +641,32 @@
|
|||||||
alert('Erro: ' + (e as Error).message);
|
alert('Erro: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleApproveRequest(requestId: Id<'solicitacoesItens'>) {
|
||||||
|
if (!confirm('Aprovar esta solicitação?')) return;
|
||||||
|
try {
|
||||||
|
await client.mutation(api.pedidos.approveItemRequest, { requestId });
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao aprovar: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRejectRequest(requestId: Id<'solicitacoesItens'>) {
|
||||||
|
if (!confirm('Rejeitar esta solicitação?')) return;
|
||||||
|
try {
|
||||||
|
await client.mutation(api.pedidos.rejectItemRequest, { requestId });
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao rejeitar: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRequestData(json: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
@@ -752,6 +794,87 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Requests Section -->
|
||||||
|
{#if requests.length > 0}
|
||||||
|
<div class="mb-6 overflow-hidden rounded-lg border-l-4 border-yellow-400 bg-white shadow-md">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between border-b border-gray-200 bg-yellow-50 px-6 py-4"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-semibold text-yellow-800">Solicitações Pendentes</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-sm text-gray-500">
|
||||||
|
<thead class="bg-gray-50 text-xs font-medium text-gray-500 uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2">Tipo</th>
|
||||||
|
<th class="px-4 py-2">Solicitante</th>
|
||||||
|
<th class="px-4 py-2">Detalhes</th>
|
||||||
|
<th class="px-4 py-2 text-right">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{#each requests as req (req._id)}
|
||||||
|
{@const data = parseRequestData(req.dados)}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-2 font-medium">
|
||||||
|
{#if req.tipo === 'adicao'}
|
||||||
|
<span class="text-green-600">Adição</span>
|
||||||
|
{:else if req.tipo === 'alteracao_quantidade'}
|
||||||
|
<span class="text-blue-600">Alteração Qtd</span>
|
||||||
|
{:else if req.tipo === 'exclusao'}
|
||||||
|
<span class="text-red-600">Exclusão</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2">{req.solicitadoPorNome}</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{#if req.tipo === 'adicao'}
|
||||||
|
{getObjetoName(data.objetoId)} - {data.quantidade}x ({data.modalidade})
|
||||||
|
{:else if req.tipo === 'alteracao_quantidade'}
|
||||||
|
{#if data.itemId}
|
||||||
|
{@const item = items.find((i) => i._id === data.itemId)}
|
||||||
|
{item ? getObjetoName(item.objetoId) : 'Item desconhecido'} (Nova Qtd: {data.novaQuantidade})
|
||||||
|
{:else}
|
||||||
|
Qtd: {data.novaQuantidade}
|
||||||
|
{/if}
|
||||||
|
{:else if req.tipo === 'exclusao'}
|
||||||
|
{#if data.itemId}
|
||||||
|
{@const item = items.find((i) => i._id === data.itemId)}
|
||||||
|
Remover: {item ? getObjetoName(item.objetoId) : 'Item desconhecido'}
|
||||||
|
{:else}
|
||||||
|
Remover Item
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-right">
|
||||||
|
{#if permissions?.canManageRequests}
|
||||||
|
<button
|
||||||
|
onclick={() => handleApproveRequest(req._id)}
|
||||||
|
class="mr-2 rounded bg-green-100 p-1 text-green-700 hover:bg-green-200"
|
||||||
|
title="Aprovar"
|
||||||
|
>
|
||||||
|
<CheckCircle size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => handleRejectRequest(req._id)}
|
||||||
|
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
|
||||||
|
title="Rejeitar"
|
||||||
|
>
|
||||||
|
<XCircle size={16} />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs text-gray-400">Aguardando Análise</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Items Section -->
|
<!-- Items Section -->
|
||||||
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
||||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||||
@@ -998,7 +1121,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise'}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -1110,7 +1233,7 @@
|
|||||||
>
|
>
|
||||||
<Eye size={18} />
|
<Eye size={18} />
|
||||||
</button>
|
</button>
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise'}
|
||||||
<button
|
<button
|
||||||
onclick={() => handleRemoveItem(item._id)}
|
onclick={() => handleRemoveItem(item._id)}
|
||||||
class="text-red-600 hover:text-red-900"
|
class="text-red-600 hover:text-red-900"
|
||||||
|
|||||||
@@ -616,20 +616,49 @@ export const addItem = mutation({
|
|||||||
throw new Error('Usuário não vinculado a um funcionário.');
|
throw new Error('Usuário não vinculado a um funcionário.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if item already exists with same parameters (user, object, action, modalidade)
|
const pedido = await ctx.db.get(args.pedidoId);
|
||||||
const existingItem = await ctx.db
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
// --- CHECK ANALYSIS MODE ---
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
|
pedidoId: args.pedidoId,
|
||||||
|
tipo: 'adicao',
|
||||||
|
dados: JSON.stringify(args),
|
||||||
|
status: 'pendente',
|
||||||
|
solicitadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CHECK DUPLICATES (Same Product + User, Different Config) ---
|
||||||
|
// Get all items of this product added by this user in this order
|
||||||
|
const userProductItems = await ctx.db
|
||||||
.query('objetoItems')
|
.query('objetoItems')
|
||||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||||
.filter((q) =>
|
.filter((q) =>
|
||||||
q.and(
|
q.and(
|
||||||
q.eq(q.field('objetoId'), args.objetoId),
|
q.eq(q.field('objetoId'), args.objetoId),
|
||||||
q.eq(q.field('adicionadoPor'), user.funcionarioId),
|
q.eq(q.field('adicionadoPor'), user.funcionarioId)
|
||||||
q.eq(q.field('acaoId'), args.acaoId),
|
|
||||||
q.eq(q.field('ataId'), args.ataId),
|
|
||||||
q.eq(q.field('modalidade'), args.modalidade)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.first();
|
.collect();
|
||||||
|
|
||||||
|
const conflict = userProductItems.find(
|
||||||
|
(i) => i.modalidade !== args.modalidade || i.ataId !== args.ataId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
throw new Error(
|
||||||
|
'Você já adicionou este item com outra modalidade ou ata. Não é permitido adicionar o mesmo produto com configurações diferentes neste pedido.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item already exists with SAME parameters (exact match) to increment
|
||||||
|
const existingItem = userProductItems.find(
|
||||||
|
(i) => i.acaoId === args.acaoId && i.ataId === args.ataId && i.modalidade === args.modalidade
|
||||||
|
);
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
// Increment quantity
|
// Increment quantity
|
||||||
@@ -697,6 +726,22 @@ export const updateItemQuantity = mutation({
|
|||||||
const item = await ctx.db.get(args.itemId);
|
const item = await ctx.db.get(args.itemId);
|
||||||
if (!item) throw new Error('Item não encontrado.');
|
if (!item) throw new Error('Item não encontrado.');
|
||||||
|
|
||||||
|
const pedido = await ctx.db.get(item.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
// --- CHECK ANALYSIS MODE ---
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
|
pedidoId: item.pedidoId,
|
||||||
|
tipo: 'alteracao_quantidade',
|
||||||
|
dados: JSON.stringify({ itemId: args.itemId, novaQuantidade: args.novaQuantidade }),
|
||||||
|
status: 'pendente',
|
||||||
|
solicitadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const quantidadeAnterior = item.quantidade;
|
const quantidadeAnterior = item.quantidade;
|
||||||
|
|
||||||
// Check permission: only item owner can change quantity
|
// Check permission: only item owner can change quantity
|
||||||
@@ -736,6 +781,23 @@ export const removeItem = mutation({
|
|||||||
const item = await ctx.db.get(args.itemId);
|
const item = await ctx.db.get(args.itemId);
|
||||||
if (!item) throw new Error('Item not found');
|
if (!item) throw new Error('Item not found');
|
||||||
|
|
||||||
|
const pedido = await ctx.db.get(item.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
// --- CHECK ANALYSIS MODE ---
|
||||||
|
if (pedido.status === 'em_analise') {
|
||||||
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
|
pedidoId: item.pedidoId,
|
||||||
|
tipo: 'exclusao',
|
||||||
|
dados: JSON.stringify({ itemId: args.itemId }),
|
||||||
|
status: 'pendente',
|
||||||
|
solicitadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.delete(args.itemId);
|
await ctx.db.delete(args.itemId);
|
||||||
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
||||||
|
|
||||||
@@ -1023,7 +1085,8 @@ export const getPermissions = query({
|
|||||||
isInComprasSector &&
|
isInComprasSector &&
|
||||||
pedido.aceitoPor === user.funcionarioId,
|
pedido.aceitoPor === user.funcionarioId,
|
||||||
canCompleteAdjustments: pedido.status === 'precisa_ajustes' && hasAddedItems,
|
canCompleteAdjustments: pedido.status === 'precisa_ajustes' && hasAddedItems,
|
||||||
canCancel: pedido.status !== 'cancelado' && pedido.status !== 'concluido' && isCreator
|
canCancel: pedido.status !== 'cancelado' && pedido.status !== 'concluido' && isCreator,
|
||||||
|
canManageRequests: pedido.status === 'em_analise' && isInComprasSector
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1443,3 +1506,207 @@ export const notifyStatusChange = internalMutation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// ========== REQUESTS MANAGEMENT ==========
|
||||||
|
|
||||||
|
export const getItemRequests = query({
|
||||||
|
args: { pedidoId: v.id('pedidos') },
|
||||||
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
_id: v.id('solicitacoesItens'),
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
tipo: v.union(v.literal('adicao'), v.literal('alteracao_quantidade'), v.literal('exclusao')),
|
||||||
|
dados: v.string(),
|
||||||
|
status: v.union(v.literal('pendente'), v.literal('aprovado'), v.literal('rejeitado')),
|
||||||
|
solicitadoPor: v.id('funcionarios'),
|
||||||
|
solicitadoPorNome: v.string(),
|
||||||
|
criadoEm: v.number()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const requests = await ctx.db
|
||||||
|
.query('solicitacoesItens')
|
||||||
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||||
|
.filter((q) => q.eq(q.field('status'), 'pendente')) // Only pending for now? Or history? User said "manter um historico"
|
||||||
|
// Actually the table has status. I should probably return all, or just pending for the main list.
|
||||||
|
// The prompt says "quando aceita... exclua as solicitacoes" (or "manter um historico"?).
|
||||||
|
// Prompt: "quando aceita... adicione os items automaticamente, e exclua as solicitacoes. deve tambem manter um historico."
|
||||||
|
// "Exclua as solicitacoes" implies deleting the row from 'solicitacoesItens' OR just marking as processed.
|
||||||
|
// "Manter um historico" might refer to 'historicoPedidos' (the central history).
|
||||||
|
// I'll stick to marking as approved/rejected in 'solicitacoesItens' OR deleting and logging to 'historicoPedidos'.
|
||||||
|
// But for now, let's just return pending ones for the UI to be clean.
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Enrich names
|
||||||
|
return await Promise.all(
|
||||||
|
requests.map(async (r) => {
|
||||||
|
const func = await ctx.db.get(r.solicitadoPor);
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
solicitadoPorNome: func?.nome || 'Desconhecido'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const approveItemRequest = mutation({
|
||||||
|
args: {
|
||||||
|
requestId: v.id('solicitacoesItens')
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
const request = await ctx.db.get(args.requestId);
|
||||||
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
|
||||||
|
const pedido = await ctx.db.get(request.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
// Security: Must be Compras Sector AND accepted by this user (or just in sector?)
|
||||||
|
// Usually Analysis rights.
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) throw new Error('Setor de compras não configurado.');
|
||||||
|
|
||||||
|
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('Acesso negado.');
|
||||||
|
|
||||||
|
// Apply the change
|
||||||
|
const data = JSON.parse(request.dados);
|
||||||
|
|
||||||
|
if (request.tipo === 'adicao') {
|
||||||
|
// Reuse addItem logic (simplified: insert or increment)
|
||||||
|
// We trust the request data structure matches addItem args
|
||||||
|
const newItem = data;
|
||||||
|
// Note: We MUST use the original requester's ID (request.solicitadoPor) as addedBy?
|
||||||
|
// Or should we attribute it to the requester? YES.
|
||||||
|
// BUT `addItem` logic usually checks if `existingItem.adicionadoPor === user`.
|
||||||
|
// Since we want to merge with the requester's items, we should check `request.solicitadoPor`.
|
||||||
|
|
||||||
|
const userProductItems = await ctx.db
|
||||||
|
.query('objetoItems')
|
||||||
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', request.pedidoId))
|
||||||
|
.filter((q) =>
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field('objetoId'), newItem.objetoId),
|
||||||
|
q.eq(q.field('adicionadoPor'), request.solicitadoPor)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Duplicate check (should have been caught at request time? Not necessarily if state changed, but let's re-verify or just proceed)
|
||||||
|
// If conflict exists now, we might error or just merge.
|
||||||
|
// Let's assume strict check:
|
||||||
|
const conflict = userProductItems.find(
|
||||||
|
(i) => i.modalidade !== newItem.modalidade || i.ataId !== newItem.ataId
|
||||||
|
);
|
||||||
|
if (conflict) {
|
||||||
|
// Reject strictly? Or throw error?
|
||||||
|
throw new Error('Conflito detectado: Item com configuração diferente já existe.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItem = userProductItems.find(
|
||||||
|
(i) =>
|
||||||
|
i.acaoId === newItem.acaoId &&
|
||||||
|
i.ataId === newItem.ataId &&
|
||||||
|
i.modalidade === newItem.modalidade
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
await ctx.db.patch(existingItem._id, {
|
||||||
|
quantidade: existingItem.quantidade + newItem.quantidade
|
||||||
|
});
|
||||||
|
// Logs attached to REQUESTER or APPROVER? Usually Approver took action, but it was on behalf of requester.
|
||||||
|
// Let's log 'aprovacao_solicitacao'
|
||||||
|
} else {
|
||||||
|
await ctx.db.insert('objetoItems', {
|
||||||
|
pedidoId: request.pedidoId,
|
||||||
|
objetoId: newItem.objetoId,
|
||||||
|
ataId: newItem.ataId,
|
||||||
|
acaoId: newItem.acaoId,
|
||||||
|
modalidade: newItem.modalidade,
|
||||||
|
valorEstimado: newItem.valorEstimado,
|
||||||
|
quantidade: newItem.quantidade,
|
||||||
|
adicionadoPor: request.solicitadoPor, // Important: Attribute to requester
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (request.tipo === 'alteracao_quantidade') {
|
||||||
|
const { itemId, novaQuantidade } = data;
|
||||||
|
const item = await ctx.db.get(itemId);
|
||||||
|
if (item) {
|
||||||
|
await ctx.db.patch(itemId, { quantidade: novaQuantidade });
|
||||||
|
}
|
||||||
|
} else if (request.tipo === 'exclusao') {
|
||||||
|
const { itemId } = data;
|
||||||
|
const item = await ctx.db.get(itemId);
|
||||||
|
if (item) {
|
||||||
|
await ctx.db.delete(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update request status
|
||||||
|
await ctx.db.patch(request._id, { status: 'aprovado' });
|
||||||
|
// OR delete it? "exclua as solicitacoes".
|
||||||
|
// I'll delete it to keep table clean as requested.
|
||||||
|
await ctx.db.delete(request._id);
|
||||||
|
|
||||||
|
// History
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: request.pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'aprovacao_solicitacao',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
tipo: request.tipo,
|
||||||
|
solicitadoPor: request.solicitadoPor,
|
||||||
|
dados: data
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.patch(request.pedidoId, { atualizadoEm: Date.now() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rejectItemRequest = mutation({
|
||||||
|
args: {
|
||||||
|
requestId: v.id('solicitacoesItens')
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
const request = await ctx.db.get(args.requestId);
|
||||||
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) throw new Error('Config Error');
|
||||||
|
|
||||||
|
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('Acesso negado.');
|
||||||
|
|
||||||
|
// Delete request
|
||||||
|
await ctx.db.delete(args.requestId);
|
||||||
|
|
||||||
|
// History
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: request.pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'rejeicao_solicitacao',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
tipo: request.tipo,
|
||||||
|
solicitadoPor: request.solicitadoPor
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ export const pedidosTables = {
|
|||||||
.index('by_adicionadoPor', ['adicionadoPor'])
|
.index('by_adicionadoPor', ['adicionadoPor'])
|
||||||
.index('by_acaoId', ['acaoId']),
|
.index('by_acaoId', ['acaoId']),
|
||||||
|
|
||||||
|
solicitacoesItens: defineTable({
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
tipo: v.union(v.literal('adicao'), v.literal('alteracao_quantidade'), v.literal('exclusao')),
|
||||||
|
dados: v.string(), // JSON string with details
|
||||||
|
status: v.union(v.literal('pendente'), v.literal('aprovado'), v.literal('rejeitado')),
|
||||||
|
solicitadoPor: v.id('funcionarios'),
|
||||||
|
criadoEm: v.number()
|
||||||
|
})
|
||||||
|
.index('by_pedidoId', ['pedidoId'])
|
||||||
|
.index('by_status', ['status']),
|
||||||
|
|
||||||
historicoPedidos: defineTable({
|
historicoPedidos: defineTable({
|
||||||
pedidoId: v.id('pedidos'),
|
pedidoId: v.id('pedidos'),
|
||||||
usuarioId: v.id('usuarios'),
|
usuarioId: v.id('usuarios'),
|
||||||
|
|||||||
Reference in New Issue
Block a user