feat: add DFD number management to pedidos, including editing functionality and validation for sending to acceptance, enhancing data integrity and user feedback

This commit is contained in:
2025-12-16 14:58:35 -03:00
parent f90b27648f
commit fbf00c824e
3 changed files with 196 additions and 42 deletions

View File

@@ -385,6 +385,11 @@
let seiValue = $state(''); let seiValue = $state('');
let updatingSei = $state(false); let updatingSei = $state(false);
// Edit DFD State
let editingDfd = $state(false);
let dfdValue = $state('');
let updatingDfd = $state(false);
// Item Details Modal State // Item Details Modal State
let showDetailsModal = $state(false); let showDetailsModal = $state(false);
let selectedObjeto = $state<Doc<'objetos'> | null>(null); let selectedObjeto = $state<Doc<'objetos'> | null>(null);
@@ -820,6 +825,36 @@
seiValue = ''; seiValue = '';
} }
async function handleUpdateDfd() {
if (!dfdValue.trim()) {
toast.error('O número DFD não pode estar vazio.');
return;
}
updatingDfd = true;
try {
await client.mutation(api.pedidos.updateDfdNumber, {
pedidoId,
numeroDfd: dfdValue
});
editingDfd = false;
toast.success('Número DFD atualizado com sucesso!');
} catch (e) {
toast.error('Erro ao atualizar número DFD: ' + (e as Error).message);
} finally {
updatingDfd = false;
}
}
function startEditingDfd() {
dfdValue = pedido?.numeroDfd || '';
editingDfd = true;
}
function cancelEditingDfd() {
editingDfd = false;
dfdValue = '';
}
function getStatusColor(status: string) { function getStatusColor(status: string) {
switch (status) { switch (status) {
case 'em_rascunho': case 'em_rascunho':
@@ -853,6 +888,8 @@
return '🔄'; return '🔄';
case 'atualizacao_sei': case 'atualizacao_sei':
return '📋'; return '📋';
case 'atualizacao_dfd':
return '📋';
case 'edicao_item': case 'edicao_item':
return '✏️'; return '✏️';
case 'solicitacao_ajuste': case 'solicitacao_ajuste':
@@ -898,7 +935,9 @@
case 'alteracao_status': case 'alteracao_status':
return `${entry.usuarioNome} alterou o status para "${formatStatus(detalhes.novoStatus)}"`; return `${entry.usuarioNome} alterou o status para "${formatStatus(detalhes.novoStatus)}"`;
case 'atualizacao_sei': case 'atualizacao_sei':
return `${entry.usuarioNome} atualizou o número SEI para "${detalhes.numeroSei}"`; return `${entry.usuarioNome} atualizou o número SEI para "${detalhes.para}"`;
case 'atualizacao_dfd':
return `${entry.usuarioNome} atualizou o número DFD para "${detalhes.para}"`;
case 'edicao_item': { case 'edicao_item': {
const objeto = objetos.find((o) => o._id === detalhes.objetoId); const objeto = objetos.find((o) => o._id === detalhes.objetoId);
const nomeObjeto = objeto?.nome || 'Objeto desconhecido'; const nomeObjeto = objeto?.nome || 'Objeto desconhecido';
@@ -1111,53 +1150,102 @@
<div class="mb-6 flex items-start justify-between"> <div class="mb-6 flex items-start justify-between">
<div> <div>
<h1 class="flex items-center gap-3 text-2xl font-bold"> <h1 class="flex items-center gap-3 text-2xl font-bold">
{#if editingSei} <div class="flex items-center gap-4">
<div class="flex items-center gap-2"> {#if editingSei}
<input <div class="flex items-center gap-2">
type="text" <input
bind:value={seiValue} type="text"
class="rounded border px-2 py-1 text-sm" bind:value={seiValue}
placeholder="Número SEI" class="rounded border px-2 py-1 text-sm"
disabled={updatingSei} placeholder="Número SEI"
/> disabled={updatingSei}
<button />
onclick={handleUpdateSei}
disabled={updatingSei}
class="rounded bg-green-600 p-1 text-white hover:bg-green-700 disabled:opacity-50"
title="Salvar"
>
<Save size={16} />
</button>
<button
onclick={cancelEditingSei}
disabled={updatingSei}
class="rounded bg-gray-400 p-1 text-white hover:bg-gray-500 disabled:opacity-50"
title="Cancelar"
>
<X size={16} />
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>Pedido {pedido.numeroSei || 'sem número SEI'}</span>
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
<button <button
onclick={startEditingSei} onclick={handleUpdateSei}
class="rounded bg-blue-100 p-1 text-blue-600 hover:bg-blue-200" disabled={updatingSei}
title="Editar número SEI" class="rounded bg-green-600 p-1 text-white hover:bg-green-700 disabled:opacity-50"
title="Salvar"
> >
<Edit size={16} /> <Save size={16} />
</button> </button>
{/if} <button
</div> onclick={cancelEditingSei}
{/if} disabled={updatingSei}
class="rounded bg-gray-400 p-1 text-white hover:bg-gray-500 disabled:opacity-50"
title="Cancelar"
>
<X size={16} />
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>SEI: {pedido.numeroSei || 'sem número SEI'}</span>
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
<button
onclick={startEditingSei}
class="rounded bg-blue-100 p-1 text-blue-600 hover:bg-blue-200"
title="Editar número SEI"
>
<Edit size={16} />
</button>
{/if}
</div>
{/if}
{#if editingDfd}
<div class="flex items-center gap-2">
<input
type="text"
bind:value={dfdValue}
class="rounded border px-2 py-1 text-sm"
placeholder="Número DFD"
disabled={updatingDfd}
/>
<button
onclick={handleUpdateDfd}
disabled={updatingDfd}
class="rounded bg-green-600 p-1 text-white hover:bg-green-700 disabled:opacity-50"
title="Salvar"
>
<Save size={16} />
</button>
<button
onclick={cancelEditingDfd}
disabled={updatingDfd}
class="rounded bg-gray-400 p-1 text-white hover:bg-gray-500 disabled:opacity-50"
title="Cancelar"
>
<X size={16} />
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>DFD: {pedido.numeroDfd || 'sem número DFD'}</span>
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
<button
onclick={startEditingDfd}
class="rounded bg-blue-100 p-1 text-blue-600 hover:bg-blue-200"
title="Editar número DFD"
>
<Edit size={16} />
</button>
{/if}
</div>
{/if}
</div>
<span class="rounded-full px-3 py-1 text-sm font-medium {getStatusColor(pedido.status)}"> <span class="rounded-full px-3 py-1 text-sm font-medium {getStatusColor(pedido.status)}">
{formatStatus(pedido.status)} {formatStatus(pedido.status)}
</span> </span>
</h1> </h1>
{#if !pedido.numeroSei} {#if !pedido.numeroSei}
<p class="mt-1 text-sm text-amber-600"> <p class="mt-1 text-sm text-amber-600">
⚠️ Este pedido não possui número SEI. Adicione um número SEI quando disponível. ⚠️ Este pedido não possui número SEI. Um número SEI deve ser informado antes de enviar
para aceite.
</p>
{/if}
{#if !pedido.numeroDfd}
<p class="mt-1 text-sm text-amber-600">
⚠️ Este pedido não possui número DFD. Um número DFD deve ser informado antes de enviar
para aceite.
</p> </p>
{/if} {/if}
{#if pedido.status === 'precisa_ajustes' && pedido.descricaoAjuste} {#if pedido.status === 'precisa_ajustes' && pedido.descricaoAjuste}
@@ -1174,7 +1262,11 @@
{#if permissions?.canSendToAcceptance} {#if permissions?.canSendToAcceptance}
<button <button
onclick={handleEnviarParaAceite} onclick={handleEnviarParaAceite}
class="flex items-center gap-2 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700" disabled={!pedido.numeroSei || !pedido.numeroDfd}
class={[
'flex items-center gap-2 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700',
!pedido.numeroSei || !pedido.numeroDfd ? 'cursor-not-allowed opacity-50' : ''
]}
> >
<Send size={18} /> Enviar para Aceite <Send size={18} /> Enviar para Aceite
</button> </button>

View File

@@ -409,6 +409,7 @@ export const list = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: pedidoStatusValidator, status: pedidoStatusValidator,
// acaoId removed from return // acaoId removed from return
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
@@ -433,6 +434,7 @@ export const list = query({
_id: p._id, _id: p._id,
_creationTime: p._creationTime, _creationTime: p._creationTime,
numeroSei: p.numeroSei, numeroSei: p.numeroSei,
numeroDfd: p.numeroDfd,
status: p.status, status: p.status,
criadoPor: p.criadoPor, criadoPor: p.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido', criadoPorNome: creator?.nome || 'Desconhecido',
@@ -455,6 +457,7 @@ export const get = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: pedidoStatusValidator, status: pedidoStatusValidator,
acaoId: v.optional(v.id('acoes')), acaoId: v.optional(v.id('acoes')),
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
@@ -568,6 +571,7 @@ export const checkExisting = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: v.union( status: v.union(
v.literal('em_rascunho'), v.literal('em_rascunho'),
v.literal('aguardando_aceite'), v.literal('aguardando_aceite'),
@@ -662,6 +666,7 @@ export const checkExisting = query({
_id: pedido._id, _id: pedido._id,
_creationTime: pedido._creationTime, _creationTime: pedido._creationTime,
numeroSei: pedido.numeroSei, numeroSei: pedido.numeroSei,
numeroDfd: pedido.numeroDfd,
status: pedido.status, status: pedido.status,
criadoPor: pedido.criadoPor, criadoPor: pedido.criadoPor,
aceitoPor: pedido.aceitoPor, aceitoPor: pedido.aceitoPor,
@@ -684,6 +689,7 @@ export const listForAcceptance = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: v.string(), status: v.string(),
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
criadoPorNome: v.string(), criadoPorNome: v.string(),
@@ -722,6 +728,7 @@ export const listForAcceptance = query({
_id: o._id, _id: o._id,
_creationTime: o._creationTime, _creationTime: o._creationTime,
numeroSei: o.numeroSei, numeroSei: o.numeroSei,
numeroDfd: o.numeroDfd,
status: o.status, status: o.status,
criadoPor: o.criadoPor, criadoPor: o.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido', criadoPorNome: creator?.nome || 'Desconhecido',
@@ -742,6 +749,7 @@ export const listMyAnalysis = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: v.string(), status: v.string(),
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
criadoPorNome: v.string(), criadoPorNome: v.string(),
@@ -789,6 +797,7 @@ export const listMyAnalysis = query({
_id: o._id, _id: o._id,
_creationTime: o._creationTime, _creationTime: o._creationTime,
numeroSei: o.numeroSei, numeroSei: o.numeroSei,
numeroDfd: o.numeroDfd,
status: o.status, status: o.status,
criadoPor: o.criadoPor, criadoPor: o.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido', criadoPorNome: creator?.nome || 'Desconhecido',
@@ -816,6 +825,7 @@ export const listByItemCreator = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: pedidoStatusValidator, status: pedidoStatusValidator,
aceitoPor: v.optional(v.id('funcionarios')), aceitoPor: v.optional(v.id('funcionarios')),
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
@@ -855,6 +865,7 @@ export const listByItemCreator = query({
_id: o._id, _id: o._id,
_creationTime: o._creationTime, _creationTime: o._creationTime,
numeroSei: o.numeroSei, numeroSei: o.numeroSei,
numeroDfd: o.numeroDfd,
status: o.status, status: o.status,
criadoPor: o.criadoPor, criadoPor: o.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido', criadoPorNome: creator?.nome || 'Desconhecido',
@@ -901,6 +912,7 @@ export const gerarRelatorio = query({
_id: v.id('pedidos'), _id: v.id('pedidos'),
_creationTime: v.number(), _creationTime: v.number(),
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: pedidoStatusValidator, status: pedidoStatusValidator,
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
criadoPorNome: v.string(), criadoPorNome: v.string(),
@@ -1111,6 +1123,7 @@ export const gerarRelatorio = query({
_id: p._id, _id: p._id,
_creationTime: p._creationTime, _creationTime: p._creationTime,
numeroSei: p.numeroSei, numeroSei: p.numeroSei,
numeroDfd: p.numeroDfd,
status: p.status, status: p.status,
criadoPor: p.criadoPor, criadoPor: p.criadoPor,
criadoPorNome: creator?.nome || 'Desconhecido', criadoPorNome: creator?.nome || 'Desconhecido',
@@ -1228,7 +1241,8 @@ export const acceptOrder = mutation({
export const create = mutation({ export const create = mutation({
args: { args: {
numeroSei: v.optional(v.string()) numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string())
// acaoId removed // acaoId removed
}, },
returns: v.id('pedidos'), returns: v.id('pedidos'),
@@ -1246,6 +1260,7 @@ export const create = mutation({
// 3. Create Order // 3. Create Order
const pedidoId = await ctx.db.insert('pedidos', { const pedidoId = await ctx.db.insert('pedidos', {
numeroSei: args.numeroSei, numeroSei: args.numeroSei,
numeroDfd: args.numeroDfd,
status: 'em_rascunho', status: 'em_rascunho',
criadoPor: user._id, criadoPor: user._id,
criadoEm: Date.now(), criadoEm: Date.now(),
@@ -1305,6 +1320,41 @@ export const updateSeiNumber = mutation({
} }
}); });
export const updateDfdNumber = mutation({
args: {
pedidoId: v.id('pedidos'),
numeroDfd: v.string()
},
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.');
// Verificar se o pedido está em um status que permite edição
if (pedido.status !== 'em_rascunho' && pedido.status !== 'precisa_ajustes') {
throw new Error(
'Apenas pedidos em rascunho ou que precisam de ajustes podem ter o número DFD atualizado.'
);
}
const oldDfd = pedido.numeroDfd;
await ctx.db.patch(args.pedidoId, {
numeroDfd: args.numeroDfd,
atualizadoEm: Date.now()
});
await ctx.db.insert('historicoPedidos', {
pedidoId: args.pedidoId,
usuarioId: user._id,
acao: 'atualizacao_dfd',
detalhes: JSON.stringify({ de: oldDfd, para: args.numeroDfd }),
data: Date.now()
});
}
});
export const addItem = mutation({ export const addItem = mutation({
args: { args: {
pedidoId: v.id('pedidos'), pedidoId: v.id('pedidos'),
@@ -1635,7 +1685,8 @@ export const splitPedido = mutation({
args: { args: {
pedidoId: v.id('pedidos'), pedidoId: v.id('pedidos'),
itemIds: v.array(v.id('objetoItems')), itemIds: v.array(v.id('objetoItems')),
numeroSei: v.optional(v.string()) numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string())
}, },
returns: v.id('pedidos'), returns: v.id('pedidos'),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -1679,6 +1730,7 @@ export const splitPedido = mutation({
const novoPedidoId = await ctx.db.insert('pedidos', { const novoPedidoId = await ctx.db.insert('pedidos', {
numeroSei: args.numeroSei, numeroSei: args.numeroSei,
numeroDfd: args.numeroDfd,
status: 'em_rascunho', status: 'em_rascunho',
criadoPor: user._id, criadoPor: user._id,
criadoEm: Date.now(), criadoEm: Date.now(),
@@ -1936,6 +1988,14 @@ export const enviarParaAceite = mutation({
); );
} }
// Validar que numeroSei e numeroDfd estão preenchidos
if (!pedido.numeroSei || !pedido.numeroSei.trim()) {
throw new Error('O número SEI deve ser informado antes de enviar o pedido para aceite.');
}
if (!pedido.numeroDfd || !pedido.numeroDfd.trim()) {
throw new Error('O número DFD deve ser informado antes de enviar o pedido para aceite.');
}
const oldStatus = pedido.status; const oldStatus = pedido.status;
const newStatus = 'aguardando_aceite'; const newStatus = 'aguardando_aceite';

View File

@@ -4,6 +4,7 @@ import { v } from 'convex/values';
export const pedidosTables = { export const pedidosTables = {
pedidos: defineTable({ pedidos: defineTable({
numeroSei: v.optional(v.string()), numeroSei: v.optional(v.string()),
numeroDfd: v.optional(v.string()),
status: v.union( status: v.union(
v.literal('em_rascunho'), v.literal('em_rascunho'),
v.literal('aguardando_aceite'), v.literal('aguardando_aceite'),
@@ -21,6 +22,7 @@ export const pedidosTables = {
atualizadoEm: v.number() atualizadoEm: v.number()
}) })
.index('by_numeroSei', ['numeroSei']) .index('by_numeroSei', ['numeroSei'])
.index('by_numeroDfd', ['numeroDfd'])
.index('by_status', ['status']) .index('by_status', ['status'])
.index('by_criadoPor', ['criadoPor']) .index('by_criadoPor', ['criadoPor'])
.index('by_aceitoPor', ['aceitoPor']) .index('by_aceitoPor', ['aceitoPor'])