Pular para o conteúdo principal

Cadastro e Edição de Cliente

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

Visão Geral

Cadastro e edição de cliente compartilham a mesma tela e o mesmo formulário, assim como acontece em nova venda com criar e editar um pedido. A diferença entre os dois modos vem inteiramente dos parâmetros de rota: chegar com mode=edit e um client serializado transforma a tela num formulário de edição pré-preenchido, e chegar sem esses parâmetros abre um cadastro em branco.

Arquivos-chave

ArquivoResponsabilidade
app/cliente/registrar-cliente.tsxRota, decide se redireciona com base na flag de cadastro e no modo
features/client/client-register/client-register.client.controller.tsLayout dinâmico, rascunho, busca de CEP e submissão
features/client/client-register/client-register.client.constants.tslayoutDefault, a definição estática de todos os campos e abas do formulário
storage/client/client.storage.tsPersistência do rascunho de cadastro em andamento
services/utils/utils.service.tsfetchAddressByCep, usado pela busca automática de endereço

Criar é gated por flag, editar não

A rota só bloqueia a criação de um cliente novo quando a flag está desligada. Editar um cliente já existente sempre é permitido, independentemente da flag:

// app/cliente/registrar-cliente.tsx
export default function RegistrarCliente() {
const settings = useSettings();
const params = useLocalSearchParams<{ mode?: string | string[] }>();
const mode = Array.isArray(params.mode) ? params.mode[0] : params.mode;

if (mode !== "edit" && settings.get("cfg-ativarcadastrocliente") !== "S") {
return <Redirect href="/(tabs)/home" />;
}

return <ClientRegisterScreen />;
}

A entrada em modo de edição vem da sub-aba "Dados Cadastrais" dos detalhes do cliente (ver Detalhes do Cliente), que empurra a rota com { mode: "edit", client: JSON.stringify(client) }.

Layout dinâmico, mas estático no código

Assim como o cabeçalho de venda documentado em Cabeçalho da Venda, o formulário de cliente é dirigido por um layout com o mesmo formato: cada campo tem visivel, requerido, editavel, dependências de exibição (depedencia) e, quando aplicável, uma lista de opções. A diferença é a origem desse layout: no cabeçalho de venda ele é construído em tempo de execução a partir de empresas, tipos de operação e formas de pagamento carregados localmente; aqui ele é um array estático, layoutDefault, definido diretamente no código:

// client-register.client.constants.ts
export const layoutDefault = [
{
fieldLayoutId: "FL-BAS-01",
requerido: "S",
editavel: "S",
visivel: "S",
valorPadrao: "J",
aba: "Dados Básicos",
field: { id: "FB-001", nome: "tipoPessoa", descricao: "Tipo de Pessoa", entidade: "Cliente", tipo: "string" },
Opcoes: [
{ id: "O-001", nome: "Jurídica", valor: "J" },
{ id: "O-002", nome: "Física", valor: "F" },
],
},
{
fieldLayoutId: "FL-BAS-02",
requerido: "S",
editavel: "S",
visivel: "S",
aba: "Dados Básicos",
depedencia: [{ campo: "tipoPessoa", valor: "J" }],
field: { id: "FB-002", nome: "cnpj", descricao: "CNPJ", entidade: "Cliente", tipo: "string", mascara: "cnpj" },
Opcoes: [],
},
// ...
];

O campo CNPJ, nesse exemplo, só aparece quando tipoPessoa for "J" (jurídica), usando exatamente o mesmo mecanismo de dependência (depedencia) visto no cabeçalho de venda.

As abas do formulário vêm do próprio layout

O campo aba de cada item do layout também define em que aba do formulário ele aparece. O controller monta a lista de abas dinamicamente, na ordem em que encontra valores novos de aba percorrendo o layout visível:

const tabs = useMemo(() => {
const map = new Map<string, string>();
visibleLayout.forEach((item) => {
if (!map.has(item.aba)) {
map.set(item.aba, normalizeKey(item.aba));
}
});
// ...
}, [visibleLayout]);

Rascunho salvo automaticamente

Qualquer alteração em qualquer campo do formulário é gravada no storage local, através de uma assinatura no próprio react-hook-form, sem debounce:

useEffect(() => {
const subscription = form.watch((values) => {
setClientRegisterDraft(values as FormValues);
});
return () => subscription.unsubscribe();
}, [form]);

Além desse salvamento automático e silencioso, existe um botão explícito de "Salvar Rascunho" que faz a mesma gravação, mas mostra um toast confirmando ao usuário:

const handleSaveDraft = useCallback(() => {
setClientRegisterDraft(form.getValues() as FormValues);
toast.success("Rascunho salvo", "As informações foram salvas localmente.");
}, [form]);

Como o salvamento automático roda em toda mudança de campo, sem debounce, qualquer novo efeito colateral adicionado a esse watch deve ser leve. Um efeito custoso ali rodaria a cada tecla digitada em qualquer campo do formulário inteiro, não só no que o usuário está editando.

Busca automática de endereço por CEP

Ao digitar um CEP completo (8 dígitos) diferente do último já resolvido, o formulário busca o endereço automaticamente e preenche os campos correspondentes, com um pequeno debounce de 300ms:

const handleCepLookup = useCallback(async () => {
const currentCep = onlyDigits(form.getValues("cep"));
if (currentCep.length !== 8 || currentCep === lastResolvedCep) return;

setIsFetchingCep(true);
try {
const address = await utilsService.fetchAddressByCep(currentCep);
if (!address) {
toast.warning("CEP não encontrado", "Informe um CEP válido.");
return;
}
setFieldValueIfExists(["uf"], address.uf);
setFieldValueIfExists(["nomeCidade"], address.nomeCidade);
setFieldValueIfExists(["nomeBai", "nomeBairro"], address.nomeBairro);
setFieldValueIfExists(["rua", "nomeLogradouro", "logradouro"], address.nomeLogradouro);
setLastResolvedCep(currentCep);
} finally {
setIsFetchingCep(false);
}
}, [form, lastResolvedCep, setFieldValueIfExists]);

O complemento do endereço só é sobrescrito pela busca automática se o campo estiver vazio, para não apagar algo que o próprio usuário já tenha digitado.

Salvar: online-first com fallback offline

Salvar um cliente segue uma ordem clara de tentativas: primeiro confirma se há conexão, tenta salvar remotamente se houver, e só cai para o armazenamento puramente local se a conexão falhar ou já não existir:

const connectionStatus = await internetService.checkConnection();
let savedOffline = false;

if (connectionStatus.apiAvailable) {
try {
const remoteClient = isEditMode
? await clientService.updateClient(effectiveId, onlinePayload)
: await clientService.registerClient(onlinePayload);

await clientService.save({ ...clientData, ...remoteClient, isOffline: "0" });
} catch (error) {
const isConnectionError = /* ... detecta erro de rede na mensagem ... */;
if (isConnectionError) {
await clientService.save(clientData);
savedOffline = true;
} else {
throw error;
}
}
} else {
await clientService.save(clientData);
savedOffline = true;
}

O toast final muda de acordo com o resultado: "Cliente salvo com sucesso" ou "Cliente atualizado com sucesso" quando foi para o servidor, e "Cliente salvo localmente e pendente de sincronização" quando ficou só no dispositivo, marcado com isOffline: "1". Depois de salvar, o rascunho é limpo (clearClientRegisterDraft), o formulário é resetado, e a tela volta para a carteira.

Armadilhas conhecidas

ClientService tem os métodos fetchCepRemote e fetchCnpjRemote, mas nenhum dos dois é chamado por este controller. A busca de endereço por CEP realmente usada é utilsService.fetchAddressByCep, um serviço diferente. Não assuma que alterar ClientService.fetchCepRemote vai mudar o comportamento da busca de CEP no cadastro, ela não passa por ali.