feat: Implement initial pedido (order) management, product catalog, and TI configuration features.
This commit is contained in:
8
packages/backend/convex/_generated/api.d.ts
vendored
8
packages/backend/convex/_generated/api.d.ts
vendored
@@ -8,6 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as acoes from "../acoes.js";
|
||||
import type * as actions_email from "../actions/email.js";
|
||||
import type * as actions_linkPreview from "../actions/linkPreview.js";
|
||||
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
|
||||
@@ -21,6 +22,7 @@ import type * as auth_utils from "../auth/utils.js";
|
||||
import type * as chamadas from "../chamadas.js";
|
||||
import type * as chamados from "../chamados.js";
|
||||
import type * as chat from "../chat.js";
|
||||
import type * as config from "../config.js";
|
||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||
import type * as configuracaoJitsi from "../configuracaoJitsi.js";
|
||||
import type * as configuracaoPonto from "../configuracaoPonto.js";
|
||||
@@ -43,9 +45,11 @@ import type * as logsAcesso from "../logsAcesso.js";
|
||||
import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as pedidos from "../pedidos.js";
|
||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||
import type * as pontos from "../pontos.js";
|
||||
import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
|
||||
import type * as produtos from "../produtos.js";
|
||||
import type * as pushNotifications from "../pushNotifications.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as saldoFerias from "../saldoFerias.js";
|
||||
@@ -67,6 +71,7 @@ import type {
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
acoes: typeof acoes;
|
||||
"actions/email": typeof actions_email;
|
||||
"actions/linkPreview": typeof actions_linkPreview;
|
||||
"actions/pushNotifications": typeof actions_pushNotifications;
|
||||
@@ -80,6 +85,7 @@ declare const fullApi: ApiFromModules<{
|
||||
chamadas: typeof chamadas;
|
||||
chamados: typeof chamados;
|
||||
chat: typeof chat;
|
||||
config: typeof config;
|
||||
configuracaoEmail: typeof configuracaoEmail;
|
||||
configuracaoJitsi: typeof configuracaoJitsi;
|
||||
configuracaoPonto: typeof configuracaoPonto;
|
||||
@@ -102,9 +108,11 @@ declare const fullApi: ApiFromModules<{
|
||||
logsAtividades: typeof logsAtividades;
|
||||
logsLogin: typeof logsLogin;
|
||||
monitoramento: typeof monitoramento;
|
||||
pedidos: typeof pedidos;
|
||||
permissoesAcoes: typeof permissoesAcoes;
|
||||
pontos: typeof pontos;
|
||||
preferenciasNotificacao: typeof preferenciasNotificacao;
|
||||
produtos: typeof produtos;
|
||||
pushNotifications: typeof pushNotifications;
|
||||
roles: typeof roles;
|
||||
saldoFerias: typeof saldoFerias;
|
||||
|
||||
56
packages/backend/convex/acoes.ts
Normal file
56
packages/backend/convex/acoes.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('acoes').collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
tipo: v.union(v.literal('projeto'), v.literal('lei'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
return await ctx.db.insert('acoes', {
|
||||
...args,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id('acoes'),
|
||||
nome: v.string(),
|
||||
tipo: v.union(v.literal('projeto'), v.literal('lei'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
nome: args.nome,
|
||||
tipo: args.tipo
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: {
|
||||
id: v.id('acoes')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
}
|
||||
});
|
||||
38
packages/backend/convex/config.ts
Normal file
38
packages/backend/convex/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const getComprasSetor = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('config').first();
|
||||
}
|
||||
});
|
||||
|
||||
export const updateComprasSetor = mutation({
|
||||
args: {
|
||||
setorId: v.id('setores')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
// Check if user has permission (e.g., admin or TI) - For now, assuming any auth user can set it,
|
||||
// but in production should be restricted.
|
||||
|
||||
const existingConfig = await ctx.db.query('config').first();
|
||||
|
||||
if (existingConfig) {
|
||||
await ctx.db.patch(existingConfig._id, {
|
||||
comprasSetorId: args.setorId,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert('config', {
|
||||
comprasSetorId: args.setorId,
|
||||
criadoPor: user._id,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
596
packages/backend/convex/pedidos.ts
Normal file
596
packages/backend/convex/pedidos.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
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';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
return user;
|
||||
}
|
||||
|
||||
// ========== QUERIES ==========
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidos'),
|
||||
_creationTime: v.number(),
|
||||
numeroSei: v.optional(v.string()),
|
||||
status: v.union(
|
||||
v.literal('em_rascunho'),
|
||||
v.literal('aguardando_aceite'),
|
||||
v.literal('em_analise'),
|
||||
v.literal('precisa_ajustes'),
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('pedidos').collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const get = query({
|
||||
args: { id: v.id('pedidos') },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('pedidos'),
|
||||
_creationTime: v.number(),
|
||||
numeroSei: v.optional(v.string()),
|
||||
status: v.union(
|
||||
v.literal('em_rascunho'),
|
||||
v.literal('aguardando_aceite'),
|
||||
v.literal('em_analise'),
|
||||
v.literal('precisa_ajustes'),
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
}
|
||||
});
|
||||
|
||||
export const getItems = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoItems'),
|
||||
_creationTime: v.number(),
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
valorEstimado: v.string(),
|
||||
valorReal: v.optional(v.string()),
|
||||
quantidade: v.number(),
|
||||
adicionadoPor: v.id('funcionarios'),
|
||||
adicionadoPorNome: v.string(),
|
||||
criadoEm: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
// Get employee names
|
||||
const itemsWithNames = await Promise.all(
|
||||
items.map(async (item) => {
|
||||
const funcionario = await ctx.db.get(item.adicionadoPor);
|
||||
return {
|
||||
...item,
|
||||
adicionadoPorNome: funcionario?.nome || 'Desconhecido'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return itemsWithNames;
|
||||
}
|
||||
});
|
||||
|
||||
export const getHistory = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
handler: async (ctx, args) => {
|
||||
const history = await ctx.db
|
||||
.query('historicoPedidos')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.order('desc')
|
||||
.collect();
|
||||
|
||||
// Get user names
|
||||
const historyWithNames = await Promise.all(
|
||||
history.map(async (entry) => {
|
||||
const usuario = await ctx.db.get(entry.usuarioId);
|
||||
return {
|
||||
_id: entry._id,
|
||||
_creationTime: entry._creationTime,
|
||||
pedidoId: entry.pedidoId,
|
||||
usuarioId: entry.usuarioId,
|
||||
usuarioNome: usuario?.nome || 'Desconhecido',
|
||||
acao: entry.acao,
|
||||
detalhes: entry.detalhes,
|
||||
data: entry.data
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return historyWithNames;
|
||||
}
|
||||
});
|
||||
|
||||
export const checkExisting = query({
|
||||
args: {
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
numeroSei: v.optional(v.string()),
|
||||
produtoIds: v.optional(v.array(v.id('produtos')))
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidos'),
|
||||
_creationTime: v.number(),
|
||||
numeroSei: v.optional(v.string()),
|
||||
status: v.union(
|
||||
v.literal('em_rascunho'),
|
||||
v.literal('aguardando_aceite'),
|
||||
v.literal('em_analise'),
|
||||
v.literal('precisa_ajustes'),
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
matchingItems: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
produtoId: v.id('produtos'),
|
||||
quantidade: v.number()
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) return [];
|
||||
|
||||
const openStatuses: Array<
|
||||
'em_rascunho' | 'aguardando_aceite' | 'em_analise' | 'precisa_ajustes'
|
||||
> = ['em_rascunho', 'aguardando_aceite', 'em_analise', 'precisa_ajustes'];
|
||||
|
||||
// 1) Buscar todos os pedidos "abertos" usando o índice by_status
|
||||
let pedidosAbertos: Doc<'pedidos'>[] = [];
|
||||
for (const status of openStatuses) {
|
||||
const partial = await ctx.db
|
||||
.query('pedidos')
|
||||
.withIndex('by_status', (q) => q.eq('status', status))
|
||||
.collect();
|
||||
pedidosAbertos = pedidosAbertos.concat(partial);
|
||||
}
|
||||
|
||||
// 2) Filtros opcionais: acaoId e 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
|
||||
const resultados = [];
|
||||
|
||||
for (const pedido of pedidosAbertos) {
|
||||
let include = true;
|
||||
let matchingItems: { produtoId: Id<'produtos'>; quantidade: number }[] = [];
|
||||
|
||||
// Se houver filtro de produtos, verificamos se o pedido tem ALGUM dos produtos
|
||||
if (args.produtoIds && args.produtoIds.length > 0) {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.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));
|
||||
|
||||
if (matching.length > 0) {
|
||||
matchingItems = matching.map((i) => ({
|
||||
produtoId: i.produtoId,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (include) {
|
||||
resultados.push({
|
||||
_id: pedido._id,
|
||||
_creationTime: pedido._creationTime,
|
||||
numeroSei: pedido.numeroSei,
|
||||
status: pedido.status,
|
||||
acaoId: pedido.acaoId,
|
||||
criadoPor: pedido.criadoPor,
|
||||
criadoEm: pedido.criadoEm,
|
||||
atualizadoEm: pedido.atualizadoEm,
|
||||
matchingItems: matchingItems.length > 0 ? matchingItems : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resultados;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== MUTATIONS ==========
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
numeroSei: v.optional(v.string()),
|
||||
acaoId: v.optional(v.id('acoes'))
|
||||
},
|
||||
returns: v.id('pedidos'),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
|
||||
// 1. Check Config
|
||||
const config = await ctx.db.query('config').first();
|
||||
if (!config || !config.comprasSetorId) {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
});
|
||||
|
||||
// 4. Create History
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'criacao',
|
||||
detalhes: JSON.stringify({ numeroSei: args.numeroSei, acaoId: args.acaoId }),
|
||||
data: Date.now()
|
||||
});
|
||||
|
||||
return pedidoId;
|
||||
}
|
||||
});
|
||||
|
||||
export const updateSeiNumber = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
numeroSei: v.string()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
const pedido = await ctx.db.get(args.pedidoId);
|
||||
if (!pedido) throw new Error('Pedido not found');
|
||||
|
||||
// Check if SEI number is already taken by another order
|
||||
const existing = await ctx.db
|
||||
.query('pedidos')
|
||||
.filter((q) =>
|
||||
q.and(q.eq(q.field('numeroSei'), args.numeroSei), q.neq(q.field('_id'), args.pedidoId))
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Este número SEI já está em uso por outro pedido.');
|
||||
}
|
||||
|
||||
const oldSei = pedido.numeroSei;
|
||||
|
||||
await ctx.db.patch(args.pedidoId, {
|
||||
numeroSei: args.numeroSei,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'atualizacao_sei',
|
||||
detalhes: JSON.stringify({ de: oldSei, para: args.numeroSei }),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const addItem = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
valorEstimado: v.string(),
|
||||
quantidade: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
|
||||
// Ensure user has a funcionarioId linked
|
||||
if (!user.funcionarioId) {
|
||||
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()
|
||||
});
|
||||
|
||||
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'),
|
||||
novaQuantidade: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
|
||||
if (!user.funcionarioId) {
|
||||
throw new Error('Usuário não vinculado a um funcionário.');
|
||||
}
|
||||
|
||||
const item = await ctx.db.get(args.itemId);
|
||||
if (!item) throw new Error('Item não encontrado.');
|
||||
|
||||
const quantidadeAnterior = item.quantidade;
|
||||
|
||||
// Check permission: only item owner can decrease 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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Update quantity
|
||||
await ctx.db.patch(args.itemId, { quantidade: args.novaQuantidade });
|
||||
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
||||
|
||||
// Create history entry
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: item.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'alteracao_quantidade',
|
||||
detalhes: JSON.stringify({
|
||||
produtoId: item.produtoId,
|
||||
quantidadeAnterior,
|
||||
novaQuantidade: args.novaQuantidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const removeItem = mutation({
|
||||
args: {
|
||||
itemId: v.id('pedidoItems')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
|
||||
const item = await ctx.db.get(args.itemId);
|
||||
if (!item) throw new Error('Item not found');
|
||||
|
||||
await ctx.db.delete(args.itemId);
|
||||
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: item.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'remocao_item',
|
||||
detalhes: JSON.stringify({ produtoId: item.produtoId, valor: item.valorEstimado }),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const updateStatus = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
novoStatus: v.union(
|
||||
v.literal('em_rascunho'),
|
||||
v.literal('aguardando_aceite'),
|
||||
v.literal('em_analise'),
|
||||
v.literal('precisa_ajustes'),
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
)
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getUsuarioAutenticado(ctx);
|
||||
const pedido = await ctx.db.get(args.pedidoId);
|
||||
if (!pedido) throw new Error('Pedido not found');
|
||||
|
||||
const oldStatus = pedido.status;
|
||||
|
||||
await ctx.db.patch(args.pedidoId, {
|
||||
status: args.novoStatus,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'alteracao_status',
|
||||
detalhes: JSON.stringify({ de: oldStatus, para: args.novoStatus }),
|
||||
data: Date.now()
|
||||
});
|
||||
|
||||
// Trigger Notifications
|
||||
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
||||
pedidoId: args.pedidoId,
|
||||
oldStatus,
|
||||
newStatus: args.novoStatus,
|
||||
actorId: user._id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== INTERNAL (NOTIFICATIONS) ==========
|
||||
|
||||
export const notifyStatusChange = internalMutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
oldStatus: v.string(),
|
||||
newStatus: v.string(),
|
||||
actorId: v.id('usuarios')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const pedido = await ctx.db.get(args.pedidoId);
|
||||
if (!pedido) return;
|
||||
|
||||
const actor = await ctx.db.get(args.actorId);
|
||||
const actorName = actor ? actor.nome : 'Alguém';
|
||||
|
||||
const recipients = new Set<string>(); // Set of User IDs
|
||||
|
||||
// 1. If status is "aguardando_aceite", notify Purchasing Sector
|
||||
if (args.newStatus === 'aguardando_aceite') {
|
||||
const config = await ctx.db.query('config').first();
|
||||
if (config && config.comprasSetorId) {
|
||||
// Find all employees in this sector
|
||||
const funcionarioSetores = await ctx.db
|
||||
.query('funcionarioSetores')
|
||||
.withIndex('by_setorId', (q) => q.eq('setorId', config.comprasSetorId!))
|
||||
.collect();
|
||||
|
||||
const funcionarioIds = funcionarioSetores.map((fs) => fs.funcionarioId);
|
||||
|
||||
// Find users linked to these employees
|
||||
for (const fId of funcionarioIds) {
|
||||
const user = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', fId))
|
||||
.first();
|
||||
if (user) recipients.add(user._id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Notify "Involved" users (Creator + Item Adders)
|
||||
// Always notify creator (unless they are the actor)
|
||||
if (pedido.criadoPor !== args.actorId) {
|
||||
recipients.add(pedido.criadoPor);
|
||||
}
|
||||
|
||||
// Notify item adders
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
for (const item of items) {
|
||||
const user = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', item.adicionadoPor))
|
||||
.first();
|
||||
if (user && user._id !== args.actorId) {
|
||||
recipients.add(user._id);
|
||||
}
|
||||
}
|
||||
|
||||
// Send Notifications
|
||||
for (const recipientId of recipients) {
|
||||
const recipientIdTyped = recipientId as Id<'usuarios'>;
|
||||
|
||||
// 1. In-App Notification
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: recipientIdTyped,
|
||||
tipo: 'alerta_seguranca', // Using alerta_seguranca as the closest match for system notifications
|
||||
titulo: `Pedido ${pedido.numeroSei || 'sem número SEI'} atualizado`,
|
||||
descricao: `Status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
remetenteId: args.actorId
|
||||
});
|
||||
|
||||
// 2. Email Notification (Async)
|
||||
const recipientUser = await ctx.db.get(recipientIdTyped);
|
||||
if (recipientUser && recipientUser.email) {
|
||||
// Using enfileirarEmail directly
|
||||
await ctx.scheduler.runAfter(0, api.email.enfileirarEmail, {
|
||||
destinatario: recipientUser.email,
|
||||
destinatarioId: recipientIdTyped,
|
||||
assunto: `Atualização no Pedido ${pedido.numeroSei || 'sem número SEI'}`,
|
||||
corpo: `O pedido ${pedido.numeroSei || 'sem número SEI'} teve seu status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
|
||||
enviadoPor: args.actorId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -395,6 +395,100 @@ const PERMISSOES_BASE = {
|
||||
recurso: 'fluxos_documentos',
|
||||
acao: 'excluir',
|
||||
descricao: 'Excluir documentos de fluxos'
|
||||
},
|
||||
// Pedidos
|
||||
{
|
||||
nome: 'pedidos.listar',
|
||||
recurso: 'pedidos',
|
||||
acao: 'listar',
|
||||
descricao: 'Listar pedidos'
|
||||
},
|
||||
{
|
||||
nome: 'pedidos.criar',
|
||||
recurso: 'pedidos',
|
||||
acao: 'criar',
|
||||
descricao: 'Criar novos pedidos'
|
||||
},
|
||||
{
|
||||
nome: 'pedidos.ver',
|
||||
recurso: 'pedidos',
|
||||
acao: 'ver',
|
||||
descricao: 'Visualizar detalhes de pedidos'
|
||||
},
|
||||
{
|
||||
nome: 'pedidos.editar_status',
|
||||
recurso: 'pedidos',
|
||||
acao: 'editar_status',
|
||||
descricao: 'Alterar status de pedidos'
|
||||
},
|
||||
{
|
||||
nome: 'pedidos.adicionar_item',
|
||||
recurso: 'pedidos',
|
||||
acao: 'adicionar_item',
|
||||
descricao: 'Adicionar itens ao pedido'
|
||||
},
|
||||
{
|
||||
nome: 'pedidos.remover_item',
|
||||
recurso: 'pedidos',
|
||||
acao: 'remover_item',
|
||||
descricao: 'Remover itens do pedido'
|
||||
},
|
||||
// Produtos
|
||||
{
|
||||
nome: 'produtos.listar',
|
||||
recurso: 'produtos',
|
||||
acao: 'listar',
|
||||
descricao: 'Listar produtos'
|
||||
},
|
||||
{
|
||||
nome: 'produtos.criar',
|
||||
recurso: 'produtos',
|
||||
acao: 'criar',
|
||||
descricao: 'Criar novos produtos'
|
||||
},
|
||||
{
|
||||
nome: 'produtos.editar',
|
||||
recurso: 'produtos',
|
||||
acao: 'editar',
|
||||
descricao: 'Editar produtos'
|
||||
},
|
||||
{
|
||||
nome: 'produtos.excluir',
|
||||
recurso: 'produtos',
|
||||
acao: 'excluir',
|
||||
descricao: 'Excluir produtos'
|
||||
},
|
||||
// Ações
|
||||
{
|
||||
nome: 'acoes.listar',
|
||||
recurso: 'acoes',
|
||||
acao: 'listar',
|
||||
descricao: 'Listar ações'
|
||||
},
|
||||
{
|
||||
nome: 'acoes.criar',
|
||||
recurso: 'acoes',
|
||||
acao: 'criar',
|
||||
descricao: 'Criar novas ações'
|
||||
},
|
||||
{
|
||||
nome: 'acoes.editar',
|
||||
recurso: 'acoes',
|
||||
acao: 'editar',
|
||||
descricao: 'Editar ações'
|
||||
},
|
||||
{
|
||||
nome: 'acoes.excluir',
|
||||
recurso: 'acoes',
|
||||
acao: 'excluir',
|
||||
descricao: 'Excluir ações'
|
||||
},
|
||||
// Configuração Compras
|
||||
{
|
||||
nome: 'config.compras.gerenciar',
|
||||
recurso: 'config',
|
||||
acao: 'gerenciar_compras',
|
||||
descricao: 'Gerenciar configurações de compras'
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
||||
69
packages/backend/convex/produtos.ts
Normal file
69
packages/backend/convex/produtos.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('produtos').collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const search = query({
|
||||
args: { query: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query('produtos')
|
||||
.withSearchIndex('search_nome', (q) => q.search('nome', args.query))
|
||||
.take(10);
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
valorEstimado: v.string(),
|
||||
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
return await ctx.db.insert('produtos', {
|
||||
...args,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id('produtos'),
|
||||
nome: v.string(),
|
||||
valorEstimado: v.string(),
|
||||
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
nome: args.nome,
|
||||
valorEstimado: args.valorEstimado,
|
||||
tipo: args.tipo
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: {
|
||||
id: v.id('produtos')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user