feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -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();