Pular para o conteúdo principal

Edição de Pedido Existente

Última atualização: 14 de julho de 2026

Visão Geral

Existem três caminhos diferentes que levam de volta à mesma tela /nova-venda, dependendo da origem do pedido. Todos terminam no mesmo lugar (prepareDraftForEditing + navegação), mas a preparação prévia é bem diferente:

OrigemPonto de entradaO que acontece antes de navegar
Rascunho local já salvo (offline, nunca enviado)saleDraftService.prepareDraftForEditing(saleDraftId)Restaura cabeçalho/carrinho do próprio registro local; se fizer parte de um pedido dividido, reconstrói a sessão combinada
Editar pedido remoto (venda online já enviada)saleDraftService.prepareRemoteOrderForEditing(order)Adapta o OrderHeader remoto para um SaleDraftRecord local preservando orderHeaderId/idExterno (edição vira PUT no envio)
Duplicar pedido (local ou remoto)saleDraftService.buildDuplicatePayloadFromRemoteOrder(order) + startNewSaleAndNavigateGera um rascunho novo, sem vínculo com o pedido de origem (envio vira POST)

Arquivos-chave

ArquivoResponsabilidade
services/sale/sale-draft/sale-draft-edit/edit.sale-draft.service.tsprepareDraftForEditing, prepareRemoteOrderForEditing, startNewSaleAndNavigate, clearDraft
services/sale/sale-draft/adapters/order-header-to-draft.adapter.tsConverte um OrderHeader remoto em SaleDraftRecord + itens, em modo "edit" ou "duplicate"
features/sale/hooks/useSaleActions.tsPontos de entrada chamados pela tela de lista de vendas (editar/duplicar)

Caminho 1 — Editar rascunho local salvo

// features/new-sale/new-sale.tsx
useEffect(() => {
if (!initialSaleDraftId) return;
if (hasInitialHeaderSnapshot || hasInitialClientSnapshot) return;
void saleDraftService.prepareDraftForEditing(initialSaleDraftId);
}, [hasInitialClientSnapshot, hasInitialHeaderSnapshot, initialSaleDraftId]);

prepareDraftForEditing restaura o cabeçalho e o carrinho do SaleDraftRecord salvo para o storage runtime que a tela lê. Se o rascunho não tem vinculoId, é simples: mescla o headerData persistido no header runtime e restaura os itens no carrinho SQLite:

if (savedDraft.vinculoId) {
await this.reconstructLinkedDraftEditSession(saleDraftId, savedDraft);
} else {
const storageHeader = this.deps.getHeaderData(saleDraftId)?.data ?? {};
const persistedHeader = savedDraft.headerData ?? {};
if (Object.keys(persistedHeader).length > 0) {
this.deps.setHeaderData(saleDraftId, { ...storageHeader, ...persistedHeader });
}
await this.restoreCartFromDraftRecord(saleDraftId, savedDraft);
}

Reconstruindo a edição de um pedido dividido

Se o rascunho tem vinculoId, ou seja, se foi salvo com separação por tipo de operação, como visto em Separação por Tipo de Operação, a edição precisa remontar tudo num único carrinho editável, já que a Fase 1 de montagem trabalha com um saleDraftId só:

private async reconstructLinkedDraftEditSession(saleDraftId, draft) {
const linkedDrafts = await this.draftRepo.findByVinculoId(draft.vinculoId);
if (linkedDrafts.length <= 1) return;

// Identifica o registro "principal" (o que guardou _separatedOpTypes no headerData)
const mainDraft = linkedDrafts.find((item) =>
Array.isArray((item.headerData as Record<string, unknown>)?._separatedOpTypes),
) ?? [...linkedDrafts].sort((l, r) => l.createdAt.localeCompare(r.createdAt))[0]!;

this.deps.setHeaderData(saleDraftId, mainDraft.headerData);
// ... restaura separatedOpTypes a partir de _separatedOpTypes (ou reconstruído a partir dos sub-registros)

for (const linkedDraft of linkedDrafts) {
if (linkedDraft.saleDraftId === saleDraftId) continue;
const linkedItems = await this.getPreferredDraftItems(linkedDraft.saleDraftId, linkedDraft);
// ... reinsere esses itens no carrinho do saleDraftId sendo editado,
// com id recalculado para esse saleDraftId (mantendo o codTipOper original do item)
}
}

Ou seja: editar um pedido dividido junta de volta, temporariamente, todos os sub-carrinhos num só saleDraftId de edição, exatamente o inverso do que saveDraftWithSplit fez ao salvar. Se o usuário salvar de novo sem tocar na separação, saveDraft vai rodar o split de novo e recriar os sub-registros (reaproveitando o vinculoId, como visto em Separação por Tipo de Operação).

Caminho 2 — Editar pedido remoto (venda online já enviada)

async prepareRemoteOrderForEditing(order: OrderHeader): Promise<string> {
const { draft, items } = await this.remoteAdapter.adapt(order, { mode: "edit" });
await this.draftRepo.upsert({ ...draft, items });
return this.prepareDraftForEditing(draft.saleDraftId).then(() => draft.saleDraftId);
}

O adaptador (OrderHeaderToDraftAdapter.adapt, modo "edit") monta um SaleDraftRecord local a partir do OrderHeader da API, com duas diferenças importantes em relação a um rascunho comum:

const saleDraftId = mode === "edit" ? order.id : generateDuplicateId();
// ...
orderHeaderId: mode === "edit" ? order.id : null,
idExterno: mode === "edit" ? toStringValue(order.idExterno) || null : null,
  • saleDraftId local = order.id remoto, ou seja, não é gerado um id novo. Assim, reabrir a edição do mesmo pedido sempre cai no mesmo registro local.
  • orderHeaderId/idExterno preenchidos: é isso que faz SaveSaleDraftService decidir enviar via PUT (updateFullOrderHeader) em vez de POST ao salvar, como visto em Salvar e Enviar.

Os itens também preservam o id remoto quando em modo de edição, para que a atualização no ERP identifique corretamente qual item está sendo alterado em vez de tratar como um item novo:

const remoteItemId = mode === "edit" ? toStringValue(item.id) : "";
// ...
id: remoteItemId || ["sale_item", saleDraftId, produtoId, produtoIdExterno, sequencia].join(":"),

Depois de adaptado e salvo localmente, o restante do caminho é idêntico ao Caminho 1. prepareDraftForEditing é chamado sobre esse novo registro local, então tudo que vale para edição de rascunho local também vale aqui a partir desse ponto.

Caminho 3 — Duplicar pedido

Duplicar, seja um pedido local ou remoto, nunca reaproveita identidade: gera sempre um rascunho novo, sem orderHeaderId:

async buildDuplicatePayloadFromRemoteOrder(order: OrderHeader) {
const { draft, items } = await this.remoteAdapter.adapt(order, { mode: "duplicate" });
return { client: draft.client, headerData: draft.headerData, cartItems: items };
}

Em modo "duplicate", o adaptador gera um id local descartável (sale_draft_<timestamp>_<random>, usado só para montar os objetos em memória) e não preenche orderHeaderId/idExterno. O resultado não é persistido diretamente. É passado para startNewSaleAndNavigate, que cria uma sessão de rascunho de fato nova, via startOrResumeDraft sem parâmetro, e só então grava cliente, cabeçalho e itens nela:

async startNewSaleAndNavigate(params: {
push: SaleDraftRouterPush;
client: Client;
headerData?: SaleDraftHeaderData;
cartItems?: SaleDraftItem[];
}): Promise<string> {
const session = this.deps.startOrResumeDraft(); // sempre um saleDraftId novo
const saleDraftId = session.saleDraftId;

if (params.headerData) this.deps.setHeaderData(saleDraftId, params.headerData);
await this.deps.setClientData(saleDraftId, params.client);
if (params.cartItems?.length) {
// ... recalcula ids determinísticos para o novo saleDraftId e faz upsert em lote
}

params.push({ pathname: "/nova-venda", params: { saleDraftId } });
return saleDraftId;
}

Antes de duplicar, a UI (useSaleActions.ts) sempre pergunta se o vendedor quer manter o cliente original ou escolher outro. Duplicar um pedido para um cliente diferente é um caso de uso suportado, não um acidente:

const selectedOption = await appAlert.show({
title: "Duplicar venda",
message: "Deseja manter o cliente original ou escolher um novo cliente para a venda duplicada?",
options: [
{ label: "Cancelar", style: "cancel" },
{ label: "Manter cliente" },
{ label: "Escolher novo cliente" },
],
});

Descartando um rascunho em edição

clearDraft, chamado ao descartar a venda na tela ou após um envio bem-sucedido, também precisa cuidar de pedidos divididos. Apaga todos os sub-registros vinculados junto com o principal, não só o saleDraftId que a tela conhece:

async clearDraft(saleDraftId: string): Promise<void> {
const draft = await this.draftRepo.findById(saleDraftId);
if (draft?.vinculoId) {
const linkedDrafts = await this.draftRepo.findByVinculoId(draft.vinculoId);
for (const linked of linkedDrafts) {
if (linked.saleDraftId === saleDraftId) continue;
await this.cartRepo.clearItemsByDraft(linked.saleDraftId);
await this.draftRepo.deleteById(linked.saleDraftId);
}
}
await this.deps.clearCart(saleDraftId);
await this.draftRepo.deleteById(saleDraftId);
clearSaleDraftHeader(saleDraftId);
clearSaleDraftSession(saleDraftId);
clearActiveDraftOpType(saleDraftId);
clearSeparatedDraftOpTypes(saleDraftId);
}

Armadilhas conhecidas

  • Não confunda "editar" com "duplicar" ao estender esses fluxos. A diferença inteira do comportamento de envio, PUT ou POST, depende só de orderHeaderId/idExterno estarem preenchidos ou não no SaleDraftRecord.
  • Editar um pedido dividido junta tudo num carrinho só durante a edição. Não assuma que o número de SaleDraftRecord no banco reflete o que está sendo editado na tela naquele momento.
  • Ao editar um pedido remoto, os itens preservam o id remoto, não o id determinístico local, especificamente para que o PUT no ERP identifique corretamente cada item. Não regenere esse id ao tocar nesse caminho.
  • clearDraft deve sempre limpar o grupo vinculado inteiro. Remover só o saleDraftId principal deixaria sub-registros órfãos no SQLite.