diff --git a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte index 76263d2..f9bbafb 100644 --- a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte +++ b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte @@ -286,15 +286,6 @@ } } - async function handleSolicitarAjustes() { - if (!confirm('Solicitar ajustes?')) return; - try { - await client.mutation(api.pedidos.solicitarAjustes, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } - } - async function handleCancelar() { if (!confirm('Cancelar pedido?')) return; try { @@ -600,6 +591,40 @@ alert('Erro ao dividir pedido: ' + (e as Error).message); } } + + // Adjustment Modal State + let showRequestAdjustmentsModal = $state(false); + let adjustmentDescription = $state(''); + + function openRequestAdjustmentsModal() { + adjustmentDescription = ''; + showRequestAdjustmentsModal = true; + } + + async function confirmRequestAdjustments() { + if (!adjustmentDescription.trim()) { + alert('Por favor, informe a descrição dos ajustes necessários.'); + return; + } + try { + await client.mutation(api.pedidos.solicitarAjustes, { + pedidoId, + descricao: adjustmentDescription + }); + showRequestAdjustmentsModal = false; + } catch (e) { + alert('Erro: ' + (e as Error).message); + } + } + + async function handleConcluirAjustes() { + if (!confirm('Concluir ajustes e retornar para análise?')) return; + try { + await client.mutation(api.pedidos.concluirAjustes, { pedidoId }); + } catch (e) { + alert('Erro: ' + (e as Error).message); + } + }
@@ -660,6 +685,14 @@ ⚠️ Este pedido não possui número SEI. Adicione um número SEI quando disponível.

{/if} + {#if pedido.status === 'precisa_ajustes' && pedido.descricaoAjuste} +
+

+ Ajustes Solicitados: +

+

{pedido.descricaoAjuste}

+
+ {/if}
@@ -692,13 +725,22 @@ {#if permissions?.canRequestAdjustments} {/if} + {#if permissions?.canCompleteAdjustments} + + {/if} + {#if permissions?.canCancel}
{/if} + + {#if showRequestAdjustmentsModal} +
+
+

Solicitar Ajustes

+

+ Descreva os ajustes necessários para este pedido. O status será alterado para "Precisa de + Ajustes". +

+ +
+ + +
+
+
+ {/if} diff --git a/packages/backend/convex/pedidos.ts b/packages/backend/convex/pedidos.ts index 581daf9..2630a35 100644 --- a/packages/backend/convex/pedidos.ts +++ b/packages/backend/convex/pedidos.ts @@ -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}.

${args.customMessage ? `Detalhes: ${args.customMessage}` : ''}`, enviadoPor: args.actorId }); } diff --git a/packages/backend/convex/tables/pedidos.ts b/packages/backend/convex/tables/pedidos.ts index f5862c2..c50f586 100644 --- a/packages/backend/convex/tables/pedidos.ts +++ b/packages/backend/convex/tables/pedidos.ts @@ -15,6 +15,7 @@ export const pedidosTables = { // acaoId removed criadoPor: v.id('usuarios'), aceitoPor: v.optional(v.id('funcionarios')), + descricaoAjuste: v.optional(v.string()), // Required when status is 'precisa_ajustes' criadoEm: v.number(), atualizadoEm: v.number() })