feat: Implement granular permission-based status transitions for pedidos.

This commit is contained in:
2025-12-05 19:34:22 -03:00
parent 80e9b76649
commit ff91d8a3ab
2 changed files with 341 additions and 43 deletions

View File

@@ -956,28 +956,91 @@ export const updateItem = mutation({
}
});
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(),
export const getPermissions = query({
args: { pedidoId: v.id('pedidos') },
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');
if (!pedido || !user.funcionarioId) {
return {
canSendToAcceptance: false,
canStartAnalysis: false,
canConclude: false,
canRequestAdjustments: false,
canCancel: false
};
}
// Check Compras Sector
let isInComprasSector = false;
const config = await ctx.db.query('config').first();
if (config && config.comprasSetorId) {
const funcionarioSetores = await ctx.db
.query('funcionarioSetores')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
.first();
isInComprasSector = !!funcionarioSetores;
}
// Check if user has added items
// Optimized: Check if ANY item in this order was added by this user
const userItem = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
.first();
const hasAddedItems = !!userItem;
const isCreator = pedido.criadoPor === user._id;
return {
canSendToAcceptance:
(pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes') && hasAddedItems,
canStartAnalysis: pedido.status === 'aguardando_aceite' && isInComprasSector,
canConclude: pedido.status === 'em_analise' && isInComprasSector,
canRequestAdjustments: pedido.status === 'em_analise' && isInComprasSector,
canCancel:
pedido.status !== 'cancelado' &&
pedido.status !== 'concluido' &&
(isCreator || isInComprasSector)
};
}
});
export const enviarParaAceite = mutation({
args: { pedidoId: v.id('pedidos') },
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 pedido = await ctx.db.get(args.pedidoId);
if (!pedido) throw new Error('Pedido não encontrado.');
if (pedido.status !== 'em_rascunho' && pedido.status !== 'precisa_ajustes') {
throw new Error('Status inválido para envio.');
}
// Check if user has added items
const userItem = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
.first();
if (!userItem) {
throw new Error(
'Você precisa ter adicionado ao menos um item ao pedido para enviá-lo para aceite.'
);
}
const oldStatus = pedido.status;
const newStatus = 'aguardando_aceite';
await ctx.db.patch(args.pedidoId, {
status: args.novoStatus,
status: newStatus,
atualizadoEm: Date.now()
});
@@ -985,15 +1048,219 @@ export const updateStatus = mutation({
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: args.novoStatus }),
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
data: Date.now()
});
// Trigger Notifications
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus: args.novoStatus,
newStatus,
actorId: user._id
});
}
});
export const iniciarAnalise = mutation({
args: { pedidoId: v.id('pedidos') },
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 não encontrado.');
if (pedido.status !== 'aguardando_aceite') {
throw new Error('O pedido não está aguardando aceite.');
}
// Security Check: Must be in Compras Sector
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 não autorizado (Setor de Compras).');
const oldStatus = pedido.status;
const newStatus = 'em_analise';
await ctx.db.patch(args.pedidoId, {
status: newStatus,
aceitoPor: user.funcionarioId, // Also mark as accepted by this user
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
data: Date.now()
});
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id
});
}
});
export const concluirPedido = mutation({
args: { pedidoId: v.id('pedidos') },
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 não encontrado.');
if (pedido.status !== 'em_analise') {
throw new Error('O pedido deve estar em análise para ser concluído.');
}
// Security Check: Must be in Compras Sector
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 não autorizado (Setor de Compras).');
const oldStatus = pedido.status;
const newStatus = 'concluido';
await ctx.db.patch(args.pedidoId, {
status: newStatus,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
data: Date.now()
});
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id
});
}
});
export const solicitarAjustes = mutation({
args: { pedidoId: v.id('pedidos') },
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 não encontrado.');
if (pedido.status !== 'em_analise') {
throw new Error('O pedido deve estar em análise para solicitar ajustes.');
}
// Security Check: Must be in Compras Sector
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 não autorizado (Setor de Compras).');
const oldStatus = pedido.status;
const newStatus = 'precisa_ajustes';
await ctx.db.patch(args.pedidoId, {
status: newStatus,
aceitoPor: undefined, // Clear accepted by since it's back to adjustments
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
data: Date.now()
});
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id
});
}
});
export const cancelarPedido = mutation({
args: { pedidoId: v.id('pedidos') },
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 não encontrado.');
if (pedido.status === 'concluido' || pedido.status === 'cancelado') {
throw new Error('Pedido já finalizado.');
}
// Anyone involved (creator or compras) can cancel? Or just creator?
// Logic: If it's creator OR Compras.
const isCreator = pedido.criadoPor === user._id;
let isCompras = false;
const config = await ctx.db.query('config').first();
if (config && config.comprasSetorId && user.funcionarioId) {
const fs = await ctx.db
.query('funcionarioSetores')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
.first();
isCompras = !!fs;
}
if (!isCreator && !isCompras) {
throw new Error('Permissão negada para cancelar este pedido.');
}
const oldStatus = pedido.status;
const newStatus = 'cancelado';
await ctx.db.patch(args.pedidoId, {
status: newStatus,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
data: Date.now()
});
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id
});
}