feat: Implement granular permission-based status transitions for pedidos.
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user