feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { mutation, query, internalMutation } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
@@ -30,7 +30,7 @@ export const list = query({
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
// acaoId removed from return
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
@@ -72,10 +72,17 @@ export const getItems = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoItems'),
|
||||
_id: v.id('objetoItems'),
|
||||
_creationTime: v.number(),
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
modalidade: v.union(
|
||||
v.literal('dispensa'),
|
||||
v.literal('inexgibilidade'),
|
||||
v.literal('adesao'),
|
||||
v.literal('consumo')
|
||||
),
|
||||
valorEstimado: v.string(),
|
||||
valorReal: v.optional(v.string()),
|
||||
quantidade: v.number(),
|
||||
@@ -86,7 +93,7 @@ export const getItems = query({
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
@@ -137,9 +144,9 @@ export const getHistory = query({
|
||||
|
||||
export const checkExisting = query({
|
||||
args: {
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
acaoId: v.optional(v.id('acoes')), // Used to filter items
|
||||
numeroSei: v.optional(v.string()),
|
||||
produtoIds: v.optional(v.array(v.id('produtos')))
|
||||
objetoIds: v.optional(v.array(v.id('objetos')))
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
@@ -154,14 +161,14 @@ export const checkExisting = query({
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
// acaoId removed
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
matchingItems: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
quantidade: v.number()
|
||||
})
|
||||
)
|
||||
@@ -186,42 +193,47 @@ export const checkExisting = query({
|
||||
pedidosAbertos = pedidosAbertos.concat(partial);
|
||||
}
|
||||
|
||||
// 2) Filtros opcionais: acaoId e numeroSei
|
||||
// 2) Filtros opcionais: numeroSei
|
||||
pedidosAbertos = pedidosAbertos.filter((p) => {
|
||||
if (args.acaoId && p.acaoId !== args.acaoId) return false;
|
||||
if (args.numeroSei && p.numeroSei !== args.numeroSei) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 3) Filtro por produtos (se informado) e coleta de matchingItems
|
||||
// 3) Filtro por acaoId (via items)
|
||||
if (args.acaoId) {
|
||||
// This is expensive, but for now we iterate. Better would be to query items by acaoId first.
|
||||
// Optimization: Query items by acaoId and get unique pedidoIds.
|
||||
const itemsComAcao = await ctx.db
|
||||
.query('objetoItems')
|
||||
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
|
||||
.collect();
|
||||
|
||||
const pedidoIdsComAcao = new Set(itemsComAcao.map((i) => i.pedidoId));
|
||||
pedidosAbertos = pedidosAbertos.filter((p) => pedidoIdsComAcao.has(p._id));
|
||||
}
|
||||
|
||||
// 4) Filtro por objetos (se informado) e coleta de matchingItems
|
||||
const resultados = [];
|
||||
|
||||
for (const pedido of pedidosAbertos) {
|
||||
let include = true;
|
||||
let matchingItems: { produtoId: Id<'produtos'>; quantidade: number }[] = [];
|
||||
let matchingItems: { objetoId: Id<'objetos'>; quantidade: number }[] = [];
|
||||
|
||||
// Se houver filtro de produtos, verificamos se o pedido tem ALGUM dos produtos
|
||||
if (args.produtoIds && args.produtoIds.length > 0) {
|
||||
// Se houver filtro de objetos, verificamos se o pedido tem ALGUM dos objetos
|
||||
if (args.objetoIds && args.objetoIds.length > 0) {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
|
||||
.collect();
|
||||
|
||||
// const pedidoProdutoIds = new Set(items.map((i) => i.produtoId)); // Unused
|
||||
const matching = items.filter((i) => args.produtoIds?.includes(i.produtoId));
|
||||
const matching = items.filter((i) => args.objetoIds?.includes(i.objetoId));
|
||||
|
||||
if (matching.length > 0) {
|
||||
matchingItems = matching.map((i) => ({
|
||||
produtoId: i.produtoId,
|
||||
objetoId: i.objetoId,
|
||||
quantidade: i.quantidade
|
||||
}));
|
||||
} else {
|
||||
// Se foi pedido filtro por produtos e não tem nenhum match, ignoramos este pedido
|
||||
// A MENOS que tenha dado match por numeroSei ou acaoId?
|
||||
// A regra original era: "Filtro por produtos (se informado)"
|
||||
// Se o usuário informou produtos, ele quer ver pedidos que tenham esses produtos.
|
||||
// Mas se ele TAMBÉM informou numeroSei, talvez ele queira ver aquele pedido específico mesmo sem o produto?
|
||||
// Vamos manter a lógica de "E": se informou produtos, tem que ter o produto.
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +244,6 @@ export const checkExisting = query({
|
||||
_creationTime: pedido._creationTime,
|
||||
numeroSei: pedido.numeroSei,
|
||||
status: pedido.status,
|
||||
acaoId: pedido.acaoId,
|
||||
criadoPor: pedido.criadoPor,
|
||||
criadoEm: pedido.criadoEm,
|
||||
atualizadoEm: pedido.atualizadoEm,
|
||||
@@ -249,8 +260,8 @@ export const checkExisting = query({
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
numeroSei: v.optional(v.string()),
|
||||
acaoId: v.optional(v.id('acoes'))
|
||||
numeroSei: v.optional(v.string())
|
||||
// acaoId removed
|
||||
},
|
||||
returns: v.id('pedidos'),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -262,31 +273,12 @@ export const create = mutation({
|
||||
throw new Error('Setor de Compras não configurado. Contate o administrador.');
|
||||
}
|
||||
|
||||
// 2. Check Existing (Double check)
|
||||
if (args.acaoId) {
|
||||
const existing = await ctx.db
|
||||
.query('pedidos')
|
||||
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
|
||||
.filter((q) =>
|
||||
q.or(
|
||||
q.eq(q.field('status'), 'em_rascunho'),
|
||||
q.eq(q.field('status'), 'aguardando_aceite'),
|
||||
q.eq(q.field('status'), 'em_analise'),
|
||||
q.eq(q.field('status'), 'precisa_ajustes')
|
||||
)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Já existe um pedido em andamento para esta ação.');
|
||||
}
|
||||
}
|
||||
// 2. Check Existing (Double check) - Removed acaoId check here as it's now per item
|
||||
|
||||
// 3. Create Order
|
||||
const pedidoId = await ctx.db.insert('pedidos', {
|
||||
numeroSei: args.numeroSei,
|
||||
status: 'em_rascunho',
|
||||
acaoId: args.acaoId,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
@@ -297,7 +289,7 @@ export const create = mutation({
|
||||
pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'criacao',
|
||||
detalhes: JSON.stringify({ numeroSei: args.numeroSei, acaoId: args.acaoId }),
|
||||
detalhes: JSON.stringify({ numeroSei: args.numeroSei }),
|
||||
data: Date.now()
|
||||
});
|
||||
|
||||
@@ -348,7 +340,14 @@ export const updateSeiNumber = mutation({
|
||||
export const addItem = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
modalidade: v.union(
|
||||
v.literal('dispensa'),
|
||||
v.literal('inexgibilidade'),
|
||||
v.literal('adesao'),
|
||||
v.literal('consumo')
|
||||
),
|
||||
valorEstimado: v.string(),
|
||||
quantidade: v.number()
|
||||
},
|
||||
@@ -361,34 +360,71 @@ export const addItem = mutation({
|
||||
throw new Error('Usuário não vinculado a um funcionário.');
|
||||
}
|
||||
|
||||
await ctx.db.insert('pedidoItems', {
|
||||
pedidoId: args.pedidoId,
|
||||
produtoId: args.produtoId,
|
||||
valorEstimado: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
adicionadoPor: user.funcionarioId,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
// Check if item already exists with same parameters (user, object, action, modalidade)
|
||||
const existingItem = 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('modalidade'), args.modalidade)
|
||||
)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existingItem) {
|
||||
// Increment quantity
|
||||
const novaQuantidade = existingItem.quantidade + args.quantidade;
|
||||
await ctx.db.patch(existingItem._id, { quantidade: novaQuantidade });
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item_incremento',
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: args.objetoId,
|
||||
quantidadeAdicionada: args.quantidade,
|
||||
novaQuantidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
} else {
|
||||
// Insert new item
|
||||
await ctx.db.insert('objetoItems', {
|
||||
pedidoId: args.pedidoId,
|
||||
objetoId: args.objetoId,
|
||||
acaoId: args.acaoId,
|
||||
modalidade: args.modalidade,
|
||||
valorEstimado: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
adicionadoPor: user.funcionarioId,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item',
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: args.objetoId,
|
||||
valor: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
acaoId: args.acaoId,
|
||||
modalidade: args.modalidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item',
|
||||
detalhes: JSON.stringify({
|
||||
produtoId: args.produtoId,
|
||||
valor: args.valorEstimado,
|
||||
quantidade: args.quantidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const updateItemQuantity = mutation({
|
||||
args: {
|
||||
itemId: v.id('pedidoItems'),
|
||||
itemId: v.id('objetoItems'),
|
||||
novaQuantidade: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
@@ -404,14 +440,11 @@ export const updateItemQuantity = mutation({
|
||||
|
||||
const quantidadeAnterior = item.quantidade;
|
||||
|
||||
// Check permission: only item owner can decrease quantity
|
||||
// Check permission: only item owner can change quantity
|
||||
const isOwner = item.adicionadoPor === user.funcionarioId;
|
||||
const isDecreasing = args.novaQuantidade < quantidadeAnterior;
|
||||
|
||||
if (isDecreasing && !isOwner) {
|
||||
throw new Error(
|
||||
'Apenas quem adicionou este item pode diminuir a quantidade. Você pode apenas aumentar.'
|
||||
);
|
||||
if (!isOwner) {
|
||||
throw new Error('Apenas quem adicionou este item pode alterar a quantidade.');
|
||||
}
|
||||
|
||||
// Update quantity
|
||||
@@ -424,7 +457,7 @@ export const updateItemQuantity = mutation({
|
||||
usuarioId: user._id,
|
||||
acao: 'alteracao_quantidade',
|
||||
detalhes: JSON.stringify({
|
||||
produtoId: item.produtoId,
|
||||
objetoId: item.objetoId,
|
||||
quantidadeAnterior,
|
||||
novaQuantidade: args.novaQuantidade
|
||||
}),
|
||||
@@ -435,7 +468,7 @@ export const updateItemQuantity = mutation({
|
||||
|
||||
export const removeItem = mutation({
|
||||
args: {
|
||||
itemId: v.id('pedidoItems')
|
||||
itemId: v.id('objetoItems')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -451,7 +484,10 @@ export const removeItem = mutation({
|
||||
pedidoId: item.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'remocao_item',
|
||||
detalhes: JSON.stringify({ produtoId: item.produtoId, valor: item.valorEstimado }),
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: item.objetoId,
|
||||
valor: item.valorEstimado
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
@@ -550,7 +586,7 @@ export const notifyStatusChange = internalMutation({
|
||||
|
||||
// Notify item adders
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user