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çalho → Produtos → Carrinho). 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
| Arquivo | Responsabilidade |
|---|---|
features/new-sale/new-sale-header/new-sale-header.tsx | Renderiza os campos visíveis usando Form.Input/Form.Select/Form.DatePicker/Form.CheckBox |
features/new-sale/new-sale-header/new-sale-header.controller.ts | Resolve badges do cliente e liga o form ao componente |
features/new-sale/new-sale-header/new-sale-header.form.ts | useNewSaleHeaderForm, monta layout, react-hook-form, persistência campo a campo, validação Zod |
features/new-sale/new-sale-header/new-sale-header.constants.ts | buildLayoutDefault, 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.ts | startOrResumeDraft, validateHeaderBeforeSave, getHeaderRelatedData |
services/sale/sale-draft/sale-draft-shared/sale-draft.helpers.ts | buildMissingHeaderDefaults, 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:
- 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 chaveselected<NomeDoCampo>(viatoSelectedKey), ex.:selectedCodEmp. Esse objeto é usado depois para exibir nome/descrição sem precisar buscar de novo na lista de opções. - 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:
- No próprio formulário (
validateForm, emnew-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". - No service, antes de salvar (
SaleDraftHeaderService.validateHeaderData, chamado porvalidateHeaderBeforeSave): roda de novo sobre o layout e os dados persistidos, e adiciona uma regra extra. Se a configuraçãocfg-ativatranportadorapedidosestiver 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
codEmpoucodTipVendarecalcula preços do carrinho. Se adicionar um novo campo que também deveria disparar recálculo, replique esseshouldRecalculatePricesexplicitamente, 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 emvalidateHeaderBeforeSave.