Pular para o conteúdo principal

Cabeçalho da Venda

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

Visão Geral

O cabeçalho é a aba inicial da tela de nova venda (CabeçalhoProdutosCarrinho). Ele é um formulário dinâmico: os campos exibidos, obrigatoriedade, opções e dependências não são fixos no componente. Eles vêm de um layout construído a partir de dados carregados localmente, como empresas, tipos de operação e formas de pagamento. Isso permite adicionar/alterar campos de cabeçalho sem alterar código de UI.

Arquivos-chave

ArquivoResponsabilidade
features/new-sale/new-sale-header/new-sale-header.tsxRenderiza os campos visíveis usando Form.Input/Form.Select/Form.DatePicker/Form.CheckBox
features/new-sale/new-sale-header/new-sale-header.controller.tsResolve badges do cliente e liga o form ao componente
features/new-sale/new-sale-header/new-sale-header.form.tsuseNewSaleHeaderForm, monta layout, react-hook-form, persistência campo a campo, validação Zod
features/new-sale/new-sale-header/new-sale-header.constants.tsbuildLayoutDefault, monta a lista de campos a partir de empresas/tipos de operação/formas de pagamento
services/sale/sale-draft/sale-draft-header/header.sale-draft.service.tsstartOrResumeDraft, validateHeaderBeforeSave, getHeaderRelatedData
services/sale/sale-draft/sale-draft-shared/sale-draft.helpers.tsbuildMissingHeaderDefaults, isHeaderFieldVisible, getHeaderLayoutSnapshot

De onde vêm os campos do cabeçalho

buildLayoutDefault({ companies, operationTypes, paymentMethods }) monta a lista de campos disponíveis (empresa, tipo de operação, condição de pagamento, transportadora, observação, etc.), cada um com metadados: tipo (text/number/date/checkbox/opções), se é visivel, requerido, editavel, valor padrão (valorPadrao) e dependências de exibição (depedencia).

A tela filtra apenas os campos visíveis:

// new-sale-header.form.ts
const fields = useMemo(() => {
return layoutDefault.filter((item) => item.visivel === "S");
}, [layoutDefault]);

E, dentro dos visíveis, ainda existe uma segunda checagem de visibilidade condicional por dependência (ex.: um campo "cidade da transportadora" só aparece se "transportadora" estiver preenchida):

const isFieldVisible = (field: LayoutField): boolean => {
const dependencies =
"depedencia" in field && Array.isArray(field.depedencia)
? (field.depedencia as Array<{ campo: string; valor: unknown }>)
: [];

if (!dependencies.length) return true;

return dependencies.every((dep) => {
const currentValue = watchedValues?.[dep.campo];
return String(currentValue) === String(dep.valor);
});
};

Defaults aplicados automaticamente ao abrir o rascunho

Ao criar ou retomar um rascunho, SaleDraftHeaderService.startOrResumeDraft aplica os valores padrão de todos os campos visíveis que ainda não têm valor, sem que nenhum componente precise saber disso:

// header.sale-draft.service.ts
startOrResumeDraft(saleDraftId?: string): SaleDraftSession {
const session = ensureSaleDraftSession(saleDraftId);
const currentHeader = getSaleDraftHeader(session.saleDraftId)?.data ?? {};
const defaultsPartial = buildMissingHeaderDefaults(currentHeader);
if (Object.keys(defaultsPartial).length > 0) {
mergeSaleDraftHeader(session.saleDraftId, defaultsPartial);
}
return session;
}

buildMissingHeaderDefaults (em sale-draft.helpers.ts) percorre o mesmo layout usado na tela e resolve o valor inicial de cada campo, inclusive datas com valor especial "HOJE", que são convertidas para a data atual formatada dd/mm/aaaa:

export function buildMissingHeaderDefaults(
headerData: SaleDraftHeaderData,
): SaleDraftHeaderData {
const current = (headerData ?? {}) as Record<string, unknown>;
const partial: SaleDraftHeaderData = {};
const layoutDefault = getHeaderLayoutSnapshot();

for (const field of layoutDefault.filter((item) => item.visivel === "S")) {
const fieldName = field.field.nome;
if (hasValue(current[fieldName])) continue;
const initialValue = coerceHeaderInitialValue(field);
if (!hasValue(initialValue)) continue;
partial[fieldName] = initialValue;
// ...
}

return partial;
}

Regra crítica (SKILL.md): nunca replicar essa lógica de defaults manualmente dentro de um componente. Qualquer novo campo de cabeçalho com valor padrão deve ser resolvido através do layout (buildLayoutDefault + buildMissingHeaderDefaults), nunca hardcoded na tela.

Persistência campo a campo

Cada alteração de campo grava direto no rascunho via mergeHeaderData. Não existe um "salvar cabeçalho" em lote, porque cada campo já fica persistido a cada mudança:

// new-sale-header.form.ts
const persistFieldValue = (layoutField: LayoutField, value: unknown) => {
const fieldName = layoutField.field.nome;
const options = (layoutField.Opcoes ?? []) as LayoutOption[];

const normalizedValue = value ?? null;
const previousValue = headerDataRecord[fieldName];
const shouldRecalculatePrices =
fieldName === "codEmp" || fieldName === "codTipVenda";
const hasRelevantValueChanged =
String(previousValue ?? "") !== String(normalizedValue ?? "");

const partial: Record<string, unknown> = {
[fieldName]: normalizedValue,
};

if (options.length > 0) {
const selectedOption = options.find(
(option) => String(option.valor ?? "") === String(value ?? ""),
);
partial[toSelectedKey(fieldName)] = selectedOption ?? null;
}

mergeHeaderData(partial);

if (shouldRecalculatePrices && hasRelevantValueChanged) {
void saleDraftService.recalculateItensPrice({ saleDraftId });
}
};

Dois pontos importantes aqui:

  1. Convenção selected<Campo>: para campos com opções (selects), além do valor primitivo (codEmp, codTipVenda, codTipOper, ...) o rascunho também guarda o objeto completo da opção escolhida sob a chave selected<NomeDoCampo> (via toSelectedKey), ex.: selectedCodEmp. Esse objeto é usado depois para exibir nome/descrição sem precisar buscar de novo na lista de opções.
  2. Trocar empresa ou condição de pagamento recalcula os preços do carrinho (saleDraftService.recalculateItensPrice). Ver Descontos e Validação Comercial para o que esse recálculo afeta.

Validação

Existem duas validações independentes, com o mesmo conjunto de regras (campos visíveis + obrigatoriedade + máscara), mas em momentos diferentes:

  1. No próprio formulário (validateForm, em new-sale-header.form.ts): monta um schema Zod dinâmico a partir dos campos visíveis e roda no cliente ao tocar em "Confirmar Cabeçalho".
  2. No service, antes de salvar (SaleDraftHeaderService.validateHeaderData, chamado por validateHeaderBeforeSave): roda de novo sobre o layout e os dados persistidos, e adiciona uma regra extra. Se a configuração cfg-ativatranportadorapedidos estiver ativa ("S"), o campo Transportadora passa a ser obrigatório mesmo que o layout não marque isso:
// header.sale-draft.service.ts
if (settings.get("cfg-ativatranportadorapedidos") === "S") {
const carrierValue =
headerData?.codTransportadora ?? headerData?.selectedCodTransportadora;
if (!hasValue(carrierValue)) {
return "Campo obrigatório: Transportadora";
}
}

Essa segunda validação é a que efetivamente bloqueia o envio do pedido: new-sale.tsx chama saleDraftService.validateHeaderBeforeSave(saleDraftId) antes de abrir o bottom sheet de opções de envio. A validação do formulário é só uma barreira de UX, pensada para não deixar o vendedor avançar de aba com campos obrigatórios em branco.

Dados relacionados ao cabeçalho

getHeaderRelatedData resolve, a partir do headerData bruto salvo no rascunho, as entidades completas de cliente, empresa, condição de pagamento e tipo de operação, usadas tanto para exibição quanto para cálculo de preço:

async getHeaderRelatedData(input: { saleDraftId: string }): Promise<{
client: Client | null;
company?: Company;
condicaoPagamento?: PaymentMethod;
tipoOperacao?: OperationType;
}>

Ele busca os candidatos a partir tanto do valor primitivo (headerData.codEmp) quanto do objeto selected* salvo (selectedCodEmp.idExterno, .id, .valor, .value), e casa contra as listas carregadas de companyService, paymentMethodService e operationTypeService.

Armadilhas conhecidas

  • Não reimplemente cálculo de defaults de campo no componente. Use sempre buildMissingHeaderDefaults/getHeaderLayoutSnapshot.
  • Trocar codEmp ou codTipVenda recalcula preços do carrinho. Se adicionar um novo campo que também deveria disparar recálculo, replique esse shouldRecalculatePrices explicitamente, já que isso não é automático para qualquer campo.
  • A obrigatoriedade de Transportadora não vem do layout. É uma regra extra amarrada à setting cfg-ativatranportadorapedidos, verificada apenas em validateHeaderBeforeSave.