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:
2025-12-09 11:03:49 -03:00
parent 09af2c796b
commit 090298659e
3 changed files with 413 additions and 12 deletions

View File

@@ -616,20 +616,49 @@ export const addItem = mutation({
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 existingItem = await ctx.db
const pedido = await ctx.db.get(args.pedidoId);
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')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.filter((q) =>
q.and(
q.eq(q.field('objetoId'), args.objetoId),
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)
q.eq(q.field('adicionadoPor'), user.funcionarioId)
)
)
.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) {
// Increment quantity
@@ -697,6 +726,22 @@ export const updateItemQuantity = mutation({
const item = await ctx.db.get(args.itemId);
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;
// Check permission: only item owner can change quantity
@@ -736,6 +781,23 @@ export const removeItem = mutation({
const item = await ctx.db.get(args.itemId);
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.patch(item.pedidoId, { atualizadoEm: Date.now() });
@@ -1023,7 +1085,8 @@ export const getPermissions = query({
isInComprasSector &&
pedido.aceitoPor === user.funcionarioId,
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()
});
}
});

View File

@@ -45,6 +45,17 @@ export const pedidosTables = {
.index('by_adicionadoPor', ['adicionadoPor'])
.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({
pedidoId: v.id('pedidos'),
usuarioId: v.id('usuarios'),