feat: Implement pedido adjustment workflow with description field and dedicated mutations.

This commit is contained in:
2025-12-08 18:58:40 -03:00
parent ff91d8a3ab
commit e92b10668e
3 changed files with 189 additions and 30 deletions

View File

@@ -32,6 +32,8 @@ export const list = query({
),
// acaoId removed from return
criadoPor: v.id('usuarios'),
aceitoPor: v.optional(v.id('funcionarios')),
descricaoAjuste: v.optional(v.string()),
criadoEm: v.number(),
atualizadoEm: v.number()
})
@@ -58,6 +60,8 @@ export const get = query({
),
acaoId: v.optional(v.id('acoes')),
criadoPor: v.id('usuarios'),
aceitoPor: v.optional(v.id('funcionarios')),
descricaoAjuste: v.optional(v.string()),
criadoEm: v.number(),
atualizadoEm: v.number()
}),
@@ -175,6 +179,8 @@ export const checkExisting = query({
),
// acaoId removed
criadoPor: v.id('usuarios'),
aceitoPor: v.optional(v.id('funcionarios')),
descricaoAjuste: v.optional(v.string()),
criadoEm: v.number(),
atualizadoEm: v.number(),
matchingItems: v.optional(
@@ -259,6 +265,8 @@ export const checkExisting = query({
numeroSei: pedido.numeroSei,
status: pedido.status,
criadoPor: pedido.criadoPor,
aceitoPor: pedido.aceitoPor,
descricaoAjuste: pedido.descricaoAjuste,
criadoEm: pedido.criadoEm,
atualizadoEm: pedido.atualizadoEm,
matchingItems: matchingItems.length > 0 ? matchingItems : undefined
@@ -318,6 +326,8 @@ export const listForAcceptance = query({
status: o.status,
criadoPor: o.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido',
aceitoPor: o.aceitoPor,
descricaoAjuste: o.descricaoAjuste,
criadoEm: o.criadoEm,
atualizadoEm: o.atualizadoEm
};
@@ -337,6 +347,7 @@ export const listMyAnalysis = query({
criadoPor: v.id('usuarios'),
criadoPorNome: v.string(),
aceitoPor: v.optional(v.id('funcionarios')),
descricaoAjuste: v.optional(v.string()),
criadoEm: v.number(),
atualizadoEm: v.number()
})
@@ -383,6 +394,7 @@ export const listMyAnalysis = query({
criadoPor: o.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido',
aceitoPor: o.aceitoPor,
descricaoAjuste: o.descricaoAjuste,
criadoEm: o.criadoEm,
atualizadoEm: o.atualizadoEm
};
@@ -434,6 +446,8 @@ export const listByItemCreator = query({
status: o!.status,
criadoPor: o!.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido',
aceitoPor: o!.aceitoPor,
descricaoAjuste: o!.descricaoAjuste,
criadoEm: o!.criadoEm,
atualizadoEm: o!.atualizadoEm
};
@@ -1000,7 +1014,11 @@ export const getPermissions = query({
(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,
canRequestAdjustments:
pedido.status === 'em_analise' &&
isInComprasSector &&
pedido.aceitoPor === user.funcionarioId,
canCompleteAdjustments: pedido.status === 'precisa_ajustes' && hasAddedItems,
canCancel:
pedido.status !== 'cancelado' &&
pedido.status !== 'concluido' &&
@@ -1161,7 +1179,10 @@ export const concluirPedido = mutation({
});
export const solicitarAjustes = mutation({
args: { pedidoId: v.id('pedidos') },
args: {
pedidoId: v.id('pedidos'),
descricao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const user = await getUsuarioAutenticado(ctx);
@@ -1172,32 +1193,29 @@ export const solicitarAjustes = mutation({
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).');
// Security Check: Only the user who accepted the order can request adjustments
if (pedido.aceitoPor !== user.funcionarioId) {
throw new Error('Apenas o funcionário que aceitou o pedido pode solicitar ajustes.');
}
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
descricaoAjuste: args.descricao,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'alteracao_status',
detalhes: JSON.stringify({ de: oldStatus, para: newStatus }),
acao: 'solicitacao_ajuste',
detalhes: JSON.stringify({
de: oldStatus,
para: newStatus,
descricao: args.descricao
}),
data: Date.now()
});
@@ -1205,7 +1223,65 @@ export const solicitarAjustes = mutation({
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id
actorId: user._id,
customMessage: args.descricao
});
}
});
export const concluirAjustes = 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 !== 'precisa_ajustes') {
throw new Error('O pedido não está aguardando ajustes.');
}
// Security Check: Must have items in the order
const requestorItem = 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 (!requestorItem) {
throw new Error('Você deve ter itens no pedido para concluir os ajustes.');
}
const oldStatus = pedido.status;
const newStatus = 'em_analise';
const descricaoAnterior = pedido.descricaoAjuste;
await ctx.db.patch(args.pedidoId, {
status: newStatus,
descricaoAjuste: undefined, // Clear the adjustment description from the active field
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'conclusao_ajustes',
detalhes: JSON.stringify({
de: oldStatus,
para: newStatus,
descricaoResolvida: descricaoAnterior
}),
data: Date.now()
});
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
pedidoId: args.pedidoId,
oldStatus,
newStatus,
actorId: user._id,
customMessage: 'Ajustes concluídos. O pedido retornou para análise.'
});
}
});
@@ -1273,7 +1349,8 @@ export const notifyStatusChange = internalMutation({
pedidoId: v.id('pedidos'),
oldStatus: v.string(),
newStatus: v.string(),
actorId: v.id('usuarios')
actorId: v.id('usuarios'),
customMessage: v.optional(v.string())
},
returns: v.null(),
handler: async (ctx, args) => {
@@ -1330,6 +1407,11 @@ export const notifyStatusChange = internalMutation({
}
}
let description = `Status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`;
if (args.customMessage) {
description += `\n\nDetalhes: ${args.customMessage}`;
}
// Send Notifications
for (const recipientId of recipients) {
const recipientIdTyped = recipientId as Id<'usuarios'>;
@@ -1339,7 +1421,7 @@ export const notifyStatusChange = internalMutation({
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}.`,
descricao: description,
lida: false,
criadaEm: Date.now(),
remetenteId: args.actorId
@@ -1353,7 +1435,7 @@ export const notifyStatusChange = internalMutation({
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}.`,
corpo: `O pedido ${pedido.numeroSei || 'sem número SEI'} teve seu status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.<br/><br/>${args.customMessage ? `<strong>Detalhes:</strong> ${args.customMessage}` : ''}`,
enviadoPor: args.actorId
});
}