Pular para o conteúdo principal

Seleção e Navegação a partir do Cliente

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

Visão Geral

Tocar em um cliente na carteira abre um modal de opções com todas as ações que podem partir daquele cliente. É esse modal, e as funções que ele aciona no controller, que fazem da carteira um hub de navegação em vez de uma tela isolada. A skill do projeto documenta três regras para qualquer ação nova nessa área: preservar selectedClient, fechar modais ou bottom sheets antes de navegar, e serializar os parâmetros de rota de forma consistente.

Arquivos-chave

ArquivoResponsabilidade
features/client/client.controller.tsselectedClient, handleClientCardPress e todas as ações de navegação a partir do cliente
features/client/components/client-options.tsx (e afins)Renderiza o modal de opções com os botões montados pelo controller
storage/catalog-preferences.storage.tsGuarda o cliente selecionado para o catálogo ler ao abrir

Selecionar um cliente

selectedClient é estado de componente React puro, não é persistido em storage. Tocar no card de um cliente na lista chama handleClientCardPress, que se comporta de dois jeitos diferentes dependendo de como a tela de carteira foi aberta:

// client.controller.ts
const handleClientCardPress = (client: Client) => {
if (options?.props?.isSelectClient) {
options?.props?.onSelect?.(client);
options?.props?.onCancelSelect?.();
return;
}
setSelectedClient(client);
setIsClientOptionsModalOpen(true);
};

No uso normal, a carteira guarda o cliente em selectedClient e abre o modal de opções. Mas a mesma tela também é reaproveitada como um seletor de cliente em outros fluxos, por exemplo ao duplicar um pedido para um cliente diferente (documentado em Edição de Pedido Existente). Nesse modo, isSelectClient é true, e tocar num cliente não abre o modal de opções, apenas devolve a escolha através de onSelect/onCancelSelect, sem passar por nenhuma das ações abaixo.

O modal de opções tem dois grupos de botões

O controller monta duas listas de botões separadas, ambas filtradas por flags de configuração: renderButtonsClientOptions (o grupo principal) e renderSubButtonsClientOptions (um grupo secundário, com "Atendimento Promotor", "Análise de Cliente" e "Engajamento do Cliente"). Cada botão só existe na lista se a flag correspondente estiver ativa:

// client.controller.ts
const renderButtonsClientOptions = [
...(isClientDetailsEnabled ? [{ label: "Dados\nCadastrais", onPress: () => { /* ... */ } }] : []),
...(isCatalogEnabled ? [{ label: "Catálogo\nde Produtos", onPress: handleOpenCatalogFromSelectedClient }] : []),
...(isClientDirectionsEnabled ? [{ label: "Como\nChegar", onPress: handleOpenMapsFromSelectedClient }] : []),
// ...
];

Nem todo botão está de fato implementado

Ao ler essa lista, vale separar o que navega de fato do que ainda é um placeholder. Hoje, "Registrar Atividades", "Financeiro Pendente" e "Relatórios do Cliente" só têm um console.log no onPress, e "Duplicar Último Pedido" está explicitamente disabled: true. As demais ações abaixo são as que realmente navegam ou executam algo.

AçãoFlagEstado
Dados Cadastraiscfg-ativardadoscadastraisclienteFuncional, navega para detalhes
Catálogo de Produtoscfg-ativarcatalogodeprodutosFuncional
Como Chegarcfg-ativarcomochegarclienteFuncional, abre mapa nativo
Registrar Atividadescfg-ativaratividadesPlaceholder, só loga no console
Duplicar Último Pedidocfg-ativarduplicarultimopedidoclienteDesabilitado explicitamente
Financeiro Pendentecfg-ativarfinanceiropendentePlaceholder
Últimas Comprascfg-ativarultimascomprasclienteFuncional
Relatórios do Clientecfg-ativarengajamentoPlaceholder
Novo Pedidocfg-ativariniciarvendaFuncional, cria rascunho de venda
Importar Pedido (IA)cfg-ativarimportacaopedidoiaFuncional
Sugestão de Pedido (IA)cfg-ativarsugestaopedidoiaFuncional
Atendimento Promotorcfg-ativarfuncionalidadespromotorPlaceholder
Análise de Clientecfg-ativaranalisedeclienteFuncional
Engajamento do Clientecfg-ativarengajamentoFuncional

Vale notar que cfg-ativarengajamento controla dois botões ao mesmo tempo, em grupos diferentes: "Relatórios do Cliente" (placeholder, no grupo principal) e "Engajamento do Cliente" (funcional, no grupo secundário). Ativar essa flag não liga só uma funcionalidade, liga as duas ao mesmo tempo, mesmo que uma delas não faça nada ainda.

Abrindo o catálogo com o cliente já filtrado

Essa é a ação com a lógica mais completa: o cliente selecionado é gravado tanto no storage de preferências do catálogo quanto nos parâmetros de rota, para que a tela de catálogo consiga lê-lo de qualquer um dos dois jeitos:

// client.controller.ts
const handleOpenCatalogFromSelectedClient = useCallback(() => {
if (!selectedClient) return;

const currentCatalogFilter = getCatalogFilterState();

setCatalogFilterState({
...DEFAULT_CATALOG_FILTER,
...currentCatalogFilter,
client: selectedClient,
nomeCliente: selectedClient.nomeFantasia || selectedClient.razaoSocial || selectedClient.idExterno || null,
});

setIsClientOptionsModalOpen(false);
router.push({
pathname: "/(tabs)/catalogo",
params: {
selectedClient: JSON.stringify(selectedClient),
selectedClientAt: Date.now().toString(),
},
} as any);
}, [selectedClient]);

Iniciando uma venda a partir do cliente

Essa ação não monta a navegação sozinha, ela delega inteiramente para o mesmo serviço de rascunho de venda documentado em Edição de Pedido Existente:

const handleCreateSaleFromSelectedClient = useCallback(async () => {
if (!selectedClient) return;

try {
await saleDraftService.startNewSaleAndNavigate({
push: pushToSaleDraftRoute,
client: selectedClient,
});
setIsClientOptionsModalOpen(false);
} catch (error) {
Alert.alert("Erro ao criar pedido", "Nao foi possivel iniciar um novo pedido para este cliente.");
}
}, [pushToSaleDraftRoute, selectedClient]);

startNewSaleAndNavigate cria um rascunho novo com esse cliente já preenchido e navega para /nova-venda, exatamente o mesmo caminho usado ao duplicar um pedido.

Abrindo o mapa nativo do dispositivo ("Como Chegar")

Diferente da aba Mapa interna do app (ver Mapa de Clientes), essa ação sai do app e abre o aplicativo de mapas do próprio dispositivo, montando uma URL específica por plataforma a partir das coordenadas de entrega do cliente, com um fallback para o endereço em texto quando não há coordenadas:

function buildMapsAppUrl(client?: Client | null): string | null {
const latitude = parseCoordinate(client?.latitudeEntrega);
const longitude = parseCoordinate(client?.longitudeEntrega);

if (latitude !== null && longitude !== null) {
if (Platform.OS === "ios") return `maps:0,0?q=${label}&ll=${latitude},${longitude}`;
if (Platform.OS === "android") return `geo:0,0?q=${latitude},${longitude}(${label})`;
return `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
}

const address = buildClientAddressCandidates(client)[0];
if (!address) return null;
// ... mesma lógica de URL por plataforma, usando o endereço em texto
}

Antes de abrir, o controller confirma que existe um app capaz de abrir essa URL (Linking.canOpenURL), e mostra um alerta amigável se não houver endereço válido ou se nenhum app de navegação estiver disponível no aparelho, em vez de deixar Linking.openURL falhar silenciosamente.

Últimas compras, detalhes e engajamento seguem o mesmo padrão

Essas três ações navegam para telas que recebem o cliente inteiro serializado como parâmetro de rota, sem passar por nenhum storage intermediário:

router.push({
pathname: "/detalhes.cliente",
params: {
client: JSON.stringify(selectedClient),
clientTab: "UltimasCompras", // omitido para "Dados Cadastrais"
},
} as any);

A ação de engajamento segue o mesmo formato, mas envolve o cliente com withClientEngagementMetricsPreview antes de serializar, para que a tela de destino já receba métricas de engajamento pré-calculadas junto com os dados do cliente.

Armadilhas conhecidas

Sempre feche isClientOptionsModalOpen antes ou junto da navegação, nunca depois. A maioria das ações já segue esse padrão, mas vale conferir ao adicionar uma nova, já que deixar o modal aberto por trás de outra tela é exatamente o risco que a skill do projeto lista explicitamente. Antes de assumir que um botão do modal de opções está funcional, confirme o onPress no controller: vários deles ainda são placeholders com console.log, mesmo aparecendo normalmente na UI quando a flag de configuração está ativa.