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:
| Origem | Ponto de entrada | O 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) + startNewSaleAndNavigate | Gera um rascunho novo, sem vínculo com o pedido de origem (envio vira POST) |
Arquivos-chave
| Arquivo | Responsabilidade |
|---|---|
services/sale/sale-draft/sale-draft-edit/edit.sale-draft.service.ts | prepareDraftForEditing, prepareRemoteOrderForEditing, startNewSaleAndNavigate, clearDraft |
services/sale/sale-draft/adapters/order-header-to-draft.adapter.ts | Converte um OrderHeader remoto em SaleDraftRecord + itens, em modo "edit" ou "duplicate" |
features/sale/hooks/useSaleActions.ts | Pontos 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,
saleDraftIdlocal =order.idremoto, ou seja, não é gerado um id novo. Assim, reabrir a edição do mesmo pedido sempre cai no mesmo registro local.orderHeaderId/idExternopreenchidos: é isso que fazSaveSaleDraftServicedecidir 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/idExternoestarem preenchidos ou não noSaleDraftRecord. - Editar um pedido dividido junta tudo num carrinho só durante a edição. Não assuma que o número de
SaleDraftRecordno banco reflete o que está sendo editado na tela naquele momento. - Ao editar um pedido remoto, os itens preservam o
idremoto, 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. clearDraftdeve sempre limpar o grupo vinculado inteiro. Remover só osaleDraftIdprincipal deixaria sub-registros órfãos no SQLite.