|
|
|
|
@@ -47,6 +47,61 @@ async function ensurePedidoModalidadeAtaConsistency(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function isFuncionarioInComprasSector(
|
|
|
|
|
ctx: QueryCtx | MutationCtx,
|
|
|
|
|
funcionarioId: Id<'funcionarios'>
|
|
|
|
|
) {
|
|
|
|
|
const config = await ctx.db.query('config').first();
|
|
|
|
|
if (!config || !config.comprasSetorId) return false;
|
|
|
|
|
|
|
|
|
|
const isInSector = await ctx.db
|
|
|
|
|
.query('funcionarioSetores')
|
|
|
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
|
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
return !!isInSector;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function isUsuarioEnvolvidoNoPedido(
|
|
|
|
|
ctx: QueryCtx | MutationCtx,
|
|
|
|
|
pedidoId: Id<'pedidos'>,
|
|
|
|
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
|
|
|
|
) {
|
|
|
|
|
const pedido = await ctx.db.get(pedidoId);
|
|
|
|
|
if (!pedido) return false;
|
|
|
|
|
|
|
|
|
|
// Criador do pedido (por usuarioId)
|
|
|
|
|
if (pedido.criadoPor === user._id) return true;
|
|
|
|
|
|
|
|
|
|
// Envolvimento por itens (requer funcionarioId)
|
|
|
|
|
if (!user.funcionarioId) return false;
|
|
|
|
|
|
|
|
|
|
const hasItem = await ctx.db
|
|
|
|
|
.query('objetoItems')
|
|
|
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedidoId))
|
|
|
|
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
return !!hasItem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assertPodeGerenciarDocumentosDoPedido(
|
|
|
|
|
ctx: QueryCtx | MutationCtx,
|
|
|
|
|
pedidoId: Id<'pedidos'>,
|
|
|
|
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
|
|
|
|
) {
|
|
|
|
|
const isEnvolvido = await isUsuarioEnvolvidoNoPedido(ctx, pedidoId, user);
|
|
|
|
|
if (isEnvolvido) return;
|
|
|
|
|
|
|
|
|
|
if (user.funcionarioId) {
|
|
|
|
|
const isCompras = await isFuncionarioInComprasSector(ctx, user.funcionarioId);
|
|
|
|
|
if (isCompras) return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error('Acesso negado.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== QUERIES ==========
|
|
|
|
|
|
|
|
|
|
export const list = query({
|
|
|
|
|
@@ -1652,6 +1707,8 @@ export const approveItemRequest = mutation({
|
|
|
|
|
returns: v.null(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
const funcionarioId = user.funcionarioId;
|
|
|
|
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
|
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
|
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
|
@@ -1666,12 +1723,18 @@ export const approveItemRequest = mutation({
|
|
|
|
|
|
|
|
|
|
const isInSector = await ctx.db
|
|
|
|
|
.query('funcionarioSetores')
|
|
|
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
|
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
|
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!isInSector) throw new Error('Acesso negado.');
|
|
|
|
|
|
|
|
|
|
// Documentos anexados à solicitação (se houver) devem migrar para o pedido ao aprovar.
|
|
|
|
|
const solicitacaoDocs = await ctx.db
|
|
|
|
|
.query('solicitacoesItensDocumentos')
|
|
|
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Apply the change
|
|
|
|
|
const data = JSON.parse(request.dados);
|
|
|
|
|
|
|
|
|
|
@@ -1790,15 +1853,37 @@ export const approveItemRequest = mutation({
|
|
|
|
|
// I'll delete it to keep table clean as requested.
|
|
|
|
|
await ctx.db.delete(request._id);
|
|
|
|
|
|
|
|
|
|
// Migrar docs: cria em pedidoDocumentos e remove os registros da solicitacao
|
|
|
|
|
const documentosMigrados: Array<{ nome: string; descricao: string }> = [];
|
|
|
|
|
if (solicitacaoDocs.length > 0) {
|
|
|
|
|
for (const doc of solicitacaoDocs) {
|
|
|
|
|
await ctx.db.insert('pedidoDocumentos', {
|
|
|
|
|
pedidoId: request.pedidoId,
|
|
|
|
|
descricao: doc.descricao,
|
|
|
|
|
nome: doc.nome,
|
|
|
|
|
storageId: doc.storageId,
|
|
|
|
|
tipo: doc.tipo,
|
|
|
|
|
tamanho: doc.tamanho,
|
|
|
|
|
criadoPor: doc.criadoPor,
|
|
|
|
|
criadoEm: doc.criadoEm,
|
|
|
|
|
origemSolicitacaoId: request._id
|
|
|
|
|
});
|
|
|
|
|
documentosMigrados.push({ nome: doc.nome, descricao: doc.descricao });
|
|
|
|
|
await ctx.db.delete(doc._id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// History
|
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
|
|
|
pedidoId: request.pedidoId,
|
|
|
|
|
usuarioId: user._id,
|
|
|
|
|
acao: 'aprovacao_solicitacao',
|
|
|
|
|
detalhes: JSON.stringify({
|
|
|
|
|
requestId: request._id,
|
|
|
|
|
tipo: request.tipo,
|
|
|
|
|
solicitadoPor: request.solicitadoPor,
|
|
|
|
|
dados: data
|
|
|
|
|
dados: data,
|
|
|
|
|
documentosMigrados
|
|
|
|
|
}),
|
|
|
|
|
data: Date.now()
|
|
|
|
|
});
|
|
|
|
|
@@ -1814,6 +1899,8 @@ export const rejectItemRequest = mutation({
|
|
|
|
|
returns: v.null(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
const funcionarioId = user.funcionarioId;
|
|
|
|
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
|
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
|
|
|
|
|
|
@@ -1822,12 +1909,25 @@ export const rejectItemRequest = mutation({
|
|
|
|
|
|
|
|
|
|
const isInSector = await ctx.db
|
|
|
|
|
.query('funcionarioSetores')
|
|
|
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
|
|
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
|
|
|
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
|
|
|
|
.first();
|
|
|
|
|
|
|
|
|
|
if (!isInSector) throw new Error('Acesso negado.');
|
|
|
|
|
|
|
|
|
|
// Remover documentos anexados à solicitação (para não deixar órfãos no storage)
|
|
|
|
|
const solicitacaoDocs = await ctx.db
|
|
|
|
|
.query('solicitacoesItensDocumentos')
|
|
|
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
const documentosRemovidos: Array<{ nome: string; descricao: string }> = [];
|
|
|
|
|
for (const doc of solicitacaoDocs) {
|
|
|
|
|
documentosRemovidos.push({ nome: doc.nome, descricao: doc.descricao });
|
|
|
|
|
await ctx.storage.delete(doc.storageId);
|
|
|
|
|
await ctx.db.delete(doc._id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete request
|
|
|
|
|
await ctx.db.delete(args.requestId);
|
|
|
|
|
|
|
|
|
|
@@ -1837,10 +1937,230 @@ export const rejectItemRequest = mutation({
|
|
|
|
|
usuarioId: user._id,
|
|
|
|
|
acao: 'rejeicao_solicitacao',
|
|
|
|
|
detalhes: JSON.stringify({
|
|
|
|
|
requestId: request._id,
|
|
|
|
|
tipo: request.tipo,
|
|
|
|
|
solicitadoPor: request.solicitadoPor
|
|
|
|
|
solicitadoPor: request.solicitadoPor,
|
|
|
|
|
documentosRemovidos
|
|
|
|
|
}),
|
|
|
|
|
data: Date.now()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ========== DOCUMENTOS (PEDIDO / SOLICITAÇÃO) ==========
|
|
|
|
|
|
|
|
|
|
export const generatePedidoUploadUrl = mutation({
|
|
|
|
|
args: { pedidoId: v.id('pedidos') },
|
|
|
|
|
returns: v.string(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
|
|
|
return await ctx.storage.generateUploadUrl();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const addPedidoDocumento = mutation({
|
|
|
|
|
args: {
|
|
|
|
|
pedidoId: v.id('pedidos'),
|
|
|
|
|
descricao: v.string(),
|
|
|
|
|
nome: v.string(),
|
|
|
|
|
storageId: v.id('_storage'),
|
|
|
|
|
tipo: v.string(),
|
|
|
|
|
tamanho: v.number(),
|
|
|
|
|
origemSolicitacaoId: v.optional(v.id('solicitacoesItens'))
|
|
|
|
|
},
|
|
|
|
|
returns: v.id('pedidoDocumentos'),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
|
|
|
|
|
|
|
|
if (!user.funcionarioId) {
|
|
|
|
|
throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Garantir que o pedido existe
|
|
|
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
|
|
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
|
|
|
|
|
|
|
|
|
return await ctx.db.insert('pedidoDocumentos', {
|
|
|
|
|
pedidoId: args.pedidoId,
|
|
|
|
|
descricao: args.descricao,
|
|
|
|
|
nome: args.nome,
|
|
|
|
|
storageId: args.storageId,
|
|
|
|
|
tipo: args.tipo,
|
|
|
|
|
tamanho: args.tamanho,
|
|
|
|
|
criadoPor: user.funcionarioId,
|
|
|
|
|
criadoEm: Date.now(),
|
|
|
|
|
origemSolicitacaoId: args.origemSolicitacaoId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const listPedidoDocumentos = query({
|
|
|
|
|
args: { pedidoId: v.id('pedidos') },
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
|
|
|
|
|
|
|
|
|
const docs = await ctx.db
|
|
|
|
|
.query('pedidoDocumentos')
|
|
|
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
|
|
|
.order('desc')
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
return await Promise.all(
|
|
|
|
|
docs.map(async (doc) => {
|
|
|
|
|
const url = await ctx.storage.getUrl(doc.storageId);
|
|
|
|
|
const func = await ctx.db.get(doc.criadoPor);
|
|
|
|
|
return {
|
|
|
|
|
...doc,
|
|
|
|
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
|
|
|
|
url
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const removePedidoDocumento = mutation({
|
|
|
|
|
args: { id: v.id('pedidoDocumentos') },
|
|
|
|
|
returns: v.null(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
|
|
|
|
|
const doc = await ctx.db.get(args.id);
|
|
|
|
|
if (!doc) throw new Error('Documento não encontrado.');
|
|
|
|
|
|
|
|
|
|
// Pode remover se for o autor OU se for envolvido/compras
|
|
|
|
|
if (!user.funcionarioId || doc.criadoPor !== user.funcionarioId) {
|
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, doc.pedidoId, user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.storage.delete(doc.storageId);
|
|
|
|
|
await ctx.db.delete(doc._id);
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const generateSolicitacaoUploadUrl = mutation({
|
|
|
|
|
args: { requestId: v.id('solicitacoesItens') },
|
|
|
|
|
returns: v.string(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
|
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
|
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
|
if (request.tipo !== 'adicao') {
|
|
|
|
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
|
|
|
|
}
|
|
|
|
|
if (request.status !== 'pendente') {
|
|
|
|
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
|
|
|
|
}
|
|
|
|
|
if (request.solicitadoPor !== user.funcionarioId) {
|
|
|
|
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await ctx.storage.generateUploadUrl();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const addSolicitacaoDocumento = mutation({
|
|
|
|
|
args: {
|
|
|
|
|
requestId: v.id('solicitacoesItens'),
|
|
|
|
|
descricao: v.string(),
|
|
|
|
|
nome: v.string(),
|
|
|
|
|
storageId: v.id('_storage'),
|
|
|
|
|
tipo: v.string(),
|
|
|
|
|
tamanho: v.number()
|
|
|
|
|
},
|
|
|
|
|
returns: v.id('solicitacoesItensDocumentos'),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
|
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
|
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
|
if (request.tipo !== 'adicao') {
|
|
|
|
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
|
|
|
|
}
|
|
|
|
|
if (request.status !== 'pendente') {
|
|
|
|
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
|
|
|
|
}
|
|
|
|
|
if (request.solicitadoPor !== user.funcionarioId) {
|
|
|
|
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await ctx.db.insert('solicitacoesItensDocumentos', {
|
|
|
|
|
requestId: args.requestId,
|
|
|
|
|
pedidoId: request.pedidoId,
|
|
|
|
|
descricao: args.descricao,
|
|
|
|
|
nome: args.nome,
|
|
|
|
|
storageId: args.storageId,
|
|
|
|
|
tipo: args.tipo,
|
|
|
|
|
tamanho: args.tamanho,
|
|
|
|
|
criadoPor: user.funcionarioId,
|
|
|
|
|
criadoEm: Date.now()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const listSolicitacaoDocumentos = query({
|
|
|
|
|
args: { requestId: v.id('solicitacoesItens') },
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
const request = await ctx.db.get(args.requestId);
|
|
|
|
|
if (!request) return [];
|
|
|
|
|
|
|
|
|
|
await assertPodeGerenciarDocumentosDoPedido(ctx, request.pedidoId, user);
|
|
|
|
|
|
|
|
|
|
const docs = await ctx.db
|
|
|
|
|
.query('solicitacoesItensDocumentos')
|
|
|
|
|
.withIndex('by_requestId', (q) => q.eq('requestId', args.requestId))
|
|
|
|
|
.order('desc')
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
return await Promise.all(
|
|
|
|
|
docs.map(async (doc) => {
|
|
|
|
|
const url = await ctx.storage.getUrl(doc.storageId);
|
|
|
|
|
const func = await ctx.db.get(doc.criadoPor);
|
|
|
|
|
return {
|
|
|
|
|
...doc,
|
|
|
|
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
|
|
|
|
url
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const removeSolicitacaoDocumento = mutation({
|
|
|
|
|
args: { id: v.id('solicitacoesItensDocumentos') },
|
|
|
|
|
returns: v.null(),
|
|
|
|
|
handler: async (ctx, args) => {
|
|
|
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
|
|
|
|
|
|
|
|
|
const doc = await ctx.db.get(args.id);
|
|
|
|
|
if (!doc) throw new Error('Documento não encontrado.');
|
|
|
|
|
|
|
|
|
|
const request = await ctx.db.get(doc.requestId);
|
|
|
|
|
if (!request) throw new Error('Solicitação não encontrada.');
|
|
|
|
|
if (request.tipo !== 'adicao') {
|
|
|
|
|
throw new Error('Apenas solicitações de adição permitem documentos.');
|
|
|
|
|
}
|
|
|
|
|
if (request.status !== 'pendente') {
|
|
|
|
|
throw new Error('Não é possível remover documentos de uma solicitação não pendente.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (doc.criadoPor !== user.funcionarioId || request.solicitadoPor !== user.funcionarioId) {
|
|
|
|
|
throw new Error('Apenas quem criou a solicitação pode remover documentos.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.storage.delete(doc.storageId);
|
|
|
|
|
await ctx.db.delete(doc._id);
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|