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
| Arquivo | Responsabilidade |
|---|---|
features/client/client.controller.ts | selectedClient, 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.ts | Guarda 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ção | Flag | Estado |
|---|---|---|
| Dados Cadastrais | cfg-ativardadoscadastraiscliente | Funcional, navega para detalhes |
| Catálogo de Produtos | cfg-ativarcatalogodeprodutos | Funcional |
| Como Chegar | cfg-ativarcomochegarcliente | Funcional, abre mapa nativo |
| Registrar Atividades | cfg-ativaratividades | Placeholder, só loga no console |
| Duplicar Último Pedido | cfg-ativarduplicarultimopedidocliente | Desabilitado explicitamente |
| Financeiro Pendente | cfg-ativarfinanceiropendente | Placeholder |
| Últimas Compras | cfg-ativarultimascomprascliente | Funcional |
| Relatórios do Cliente | cfg-ativarengajamento | Placeholder |
| Novo Pedido | cfg-ativariniciarvenda | Funcional, cria rascunho de venda |
| Importar Pedido (IA) | cfg-ativarimportacaopedidoia | Funcional |
| Sugestão de Pedido (IA) | cfg-ativarsugestaopedidoia | Funcional |
| Atendimento Promotor | cfg-ativarfuncionalidadespromotor | Placeholder |
| Análise de Cliente | cfg-ativaranalisedecliente | Funcional |
| Engajamento do Cliente | cfg-ativarengajamento | Funcional |
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.