O passo a passo para criar contratos visuais enriquecidos, estruturar séries temporais na Business Logic e exibir gráficos de desempenho nos Cockpit Cards sem depender de conexão com a nuvem.

No universo da execução de varejo para bens de consumo embalados (CPG/FMCG), a velocidade de interpretação de dados dita o sucesso comercial no ponto de venda (PDV).

Quando um promotor ou representante de vendas entra na loja de um grande supermercado, ele dispõe de poucos minutos para auditar gôndolas, verificar a conformidade de contratos de exibição e negociar pedidos adicionais com o gerente do estabelecimento.

Nesse cenário de correria e pressão, entregar para o profissional tabelas estáticas repletas de dezenas de linhas de números brutos é uma falha grave de design. O cérebro humano interpreta padrões visuais de forma instantânea:

  • Um gráfico de barras comparando a participação de gôndola da sua marca contra os principais concorrentes (Share of Shelf).
  • Um gráfico de rosca ilustrando o percentual atingido da meta de vendas do mês.
  • Um velocímetro de execução indicando o cumprimento dos itens obrigatórios do plano de visita (Planogram Compliance).

Contudo, no Salesforce Consumer Goods Cloud (CGC) Modeler (Mv2), os controles visuais padrão disponibilizados pela camada de interface (.userinterface.xml) limitam-se a listas de itens (<CockpitList>, <HierarchicalList>), formulários de entrada e botões de ação.

Além disso, como o aplicativo opera sob a premissa de Offline-First, soluções web comuns — como incorporar bibliotecas de gráficos baseadas em CDN dentro de iFrames — simplesmente não funcionam: elas quebram o isolamento do runtime móvel e travam silenciosamente quando o aparelho perde o sinal de internet no subsolo ou no depósito de uma loja.

Para atender a essa demanda analítica com aceleração nativa e funcionamento 100% desconectado, a arquitetura do Modeler oferece o contrato UIPluginV2.

O UIPluginV2 atua como uma ponte de extensibilidade de interface que permite instanciar plug-ins de renderização visual avançada diretamente dentro dos Cockpit Cards do dashboard móvel, consumindo dados do banco relacional SQLite local (app.db3).

Neste guia prático, vamos destrinchar toda a esteira de engenharia para implementar gráficos offline no Modeler: desde a criação do contrato .uipluginv2.xml, passando pelo registro de rótulos globais em globallabel.xml, a estruturação das séries de dados na Business Logic (.bl.js), até a amarração final no arquivo de interface e na máquina de estados do ProcessFlow.


1. A Topologia do UIPluginV2: Como os Dados se Transformam em Gráficos

A renderização de um gráfico no CG Cloud Mobile obedece a um fluxo desacoplado de cinco etapas:

Esteira de Renderização do UIPluginV2:
1. SQLite Local (app.db3): Contém registros de auditoria, vendas e metas
   │
2. Business Logic (.bl.js): Extrai registros via ListObject ou DataSource
   │  └──> Agrupa, calcula percentuais e gera um JSON estruturado de gráfico
   │
3. ProcessContext: Armazena o JSON na variável vinculada ao card
   │
4. UI Contract (.userinterface.xml): Declara o <UIPluginV2> dentro do <CardContainer>
   │  └──> Vincula a propriedade de dados através de um Binding ONE_WAY
   │
5. Casca Nativa do App Mobile: Lê o contrato .uipluginv2.xml + globallabel.xml
   └──> Renderiza o gráfico vetorial com aceleração nativa na tela do tablet/smartphone

2. A Esteira dos Quatro Arquivos Obrigatórios

Para implementar um gráfico de acompanhamento de participação de mercado da concorrência (Competitor Rate Chart) dentro de um card do Store Cockpit, precisamos sincronizar quatro artefatos essenciais.

Arquivo 1: O Contrato do Plugin (.uipluginv2.xml)

Crie o arquivo de definição do componente visual no diretório de interface correspondente (src/UI Plugins/).

Regra de Validação do Modeler (Code 03112682 / 03112687): No Salesforce CGC Modeler, qualquer contrato customizado de UIPluginV2 deve obrigatoriamente iniciar com o prefixo My (tanto no nome do arquivo quanto no atributo name do nó raiz, como MyCompetitorRateChartUiPluginV2.uipluginv2.xml). Ignorar essa convenção faz com que o comando sf modeler workspace validate aborte o build imediatamente.

<?xml version="1.0" encoding="UTF-8"?>
<!--
    Data: 2026-10-13
    Objetivo da Customização: Contrato UIPluginV2 para renderização de gráficos offline de concorrência nos Cockpit Cards
-->
<UIPluginV2 name="MyCompetitorRateChartUiPluginV2">
  <Interface>
    <Property id="maximizedMode" />
    <Property id="chartTitle" />
    <Property id="chartData" />
    <Property id="showLegend" />
    <Events>
      <Event name="onChartRefreshed" />
      <Event name="onBrandSelected" />
    </Events>
  </Interface>
  <Labels>
    <Label id="CompetitorRate_Title" />
    <Label id="CompetitorRate_Subtitle" />
  </Labels>
  <Libraries>
  </Libraries>
  <UIComponentHTML name="UIComponentHTML"><![CDATA[
    <div class="competitor-chart-wrapper">
      <div class="card-header">
        <span class="card-title">Auditoria de Gôndola: Share of Shelf</span>
        <span class="card-badge">🟢 SQLite Ready</span>
      </div>
      <div class="canvas-container">
        <canvas id="competitorCanvas" width="560" height="280"></canvas>
      </div>
    </div>
  ]]></UIComponentHTML>
  <UIComponentCSS name="UIComponentCSS"><![CDATA[
    .competitor-chart-wrapper { background: #FFF; border-radius: 8px; padding: 16px; }
  ]]></UIComponentCSS>
  <UIComponentJS name="UIComponentJS"><![CDATA[
    function connectCallback() { drawChart(); }
    function renderCallback() { drawChart(); }
  ]]></UIComponentJS>
</UIPluginV2>

Arquivo 2: O Registro de Rótulos de Localização

Uma das particularidades do compilador do Modeler reside na resolução de referências de internacionalização: todo rótulo referenciado em <Labels> deve constar nos dicionários de localização (src/Locale/en.locale.xml e br.locale.xml).

<!-- No en.locale.xml e br.locale.xml -->
<Label id="CompetitorRate_Title" text="Auditoria de Gôndola: Share of Shelf" translationStatus="7"/>
<Label id="CompetitorRate_Subtitle" text="Comparativo de Presença no PDV" translationStatus="7"/>
<Label id="CompetitorRate_OurBrand" text="Nossa Marca (Líder)" translationStatus="7"/>
<Label id="CompetitorRate_CompetitorA" text="Concorrente Alpha" translationStatus="7"/>
<Label id="CompetitorRate_CompetitorB" text="Concorrente Beta" translationStatus="7"/>
<Label id="CompetitorRate_Others" text="Outros" translationStatus="7"/>

Aviso de Trincheira: Além dos dicionários de locale, certifique-se de que nenhum label fique órfão. Quando o plugin for referenciado em telas ou menus, o compilador exige correspondência estrita nos arquivos de locale para evitar erros de compilação.


Arquivo 3: Preparação dos Dados do Gráfico na Business Logic (.bl.js)

Os motores de gráficos nativos da Salesforce esperam uma estrutura JSON padronizada, contendo categorias (labels), séries numéricas e configurações visuais.

No controlador do cockpit (como BoStoreCockpitHelper ou em um método dedicado de ListObject), implementamos o método que consulta o banco local SQLite (app.db3), calcula os totais e monta a string JSON formatada:

/* Data: 2026-10-12; Objetivo da Customização: Preparar dados agregados em JSON para o UIPluginV2 nos Cockpit Cards */
/**
 * @function prepareCompetitorRateChartData
 * @this BoStoreCockpitHelper
 * @kind businessobject
 * @namespace Customer
 * @param {String} customerPKey Chave primária do cliente em visita.
 * @returns {Promise} Promessa contendo a string JSON pronta para consumo pelo gráfico.
 */
function prepareCompetitorRateChartData(customerPKey) {
    var me = this;

    // 1. Monta os parâmetros para consultar auditorias de concorrentes no SQLite
    var jsonQuery = {
        params: [
            { "field": "customerPKey", "value": customerPKey }
        ]
    };

    // 2. Carrega a coleção de itens via BoFactory
    return BoFactory.loadObjectByParamsAsync("LoCompetitorAudits", jsonQuery)
        .then(function (loAudits) {
            var items = loAudits.getAllItems();

            var categories = [];
            var seriesData = [];
            var colorPalette = ["#0176D3", "#D4504C", "#E07400", "#7F8C8D"];

            // 3. Itera sobre os registros locais agregando métricas de gôndola
            items.forEach(function (auditItem, index) {
                categories.push(auditItem.getBrandName());
                seriesData.push({
                    name: auditItem.getBrandName(),
                    value: Number(auditItem.getSharePercentage()) || 0,
                    color: colorPalette[index % colorPalette.length]
                });
            });

            // 4. Monta o contrato JSON esperado pelo plug-in nativo
            var chartPayload = {
                type: "bar",
                title: Localization.localize("CompetitorChart_Title", "text"),
                xAxis: {
                    categories: categories
                },
                yAxis: {
                    title: "% de Gôndola",
                    min: 0,
                    max: 100
                },
                series: [
                    {
                        name: "Share de Gôndola (%)",
                        data: seriesData
                    }
                ]
            };

            var serializedPayload = JSON.stringify(chartPayload);
            AppLog.info("Dados do gráfico de concorrência gerados com sucesso: " + serializedPayload);

            return serializedPayload;
        }).catch(function (error) {
            AppLog.error("Falha ao gerar dados do gráfico de concorrência.", error);
            // Retorna um gráfico vazio defensivo para evitar quebra de interface
            return JSON.stringify({ type: "bar", series: [] });
        });
}

Arquivo 4: Incorporação Visual na Interface (.userinterface.xml)

Com os dados calculados, posicionamos o UIPluginV2 dentro de um CardContainer no layout do Cockpit da Visita (StoreCockpitUI.userinterface.xml).

O container do card utiliza o padrão de Lazy Loading: o card começa vazio e, assim que fica visível na rolagem da tela, dispara o evento que busca e formata os dados do gráfico:

<CardContainer name="CardCompetitorChart">
  <Bindings>
    <!-- Amarração obrigatória para decidir o subcomponente visual -->
    <Binding target="DisplayedSubcomponentName" type="Text"
             binding="ProcessContext::CardCompetitorChart_DisplayedSubcomponentName"
             bindingMode="ONE_WAY" />
    <Binding target="IsReadyToLoad" type="DomBool"
             binding="ProcessContext::CardCompetitorChart_DataLoaded"
             bindingMode="ONE_WAY" />
    <Resource target="title" type="Label" id="CompetitorCardTitleId"
              defaultLabel="Auditoria da Concorrência" />
  </Bindings>

  <Events>
    <!-- Evento disparado pelo runtime quando o card entra na tela -->
    <LoadContainerData event="CardCompetitorChart_loadData" />
  </Events>

  <!-- O Plug-in Visual Gráfico instanciado nativamente -->
  <UIPluginV2 name="CompetitorRateChartPlugin"
              pluginName="CompetitorRateChartPlugin">
    <Bindings>
      <Binding target="chartData" type="Text"
               binding="ProcessContext::CardCompetitorChart_SerializedData"
               bindingMode="ONE_WAY" />
      <Binding target="showLegend" type="DomBool"
               binding="ProcessContext::CardCompetitorChart_ShowLegend"
               bindingMode="ONE_WAY" />
    </Bindings>
  </UIPluginV2>

  <!-- Mensagem de Empty-State caso não haja dados auditados -->
  <NoDataMessage name="CardNoDataMessageUiPlugin">
    <Bindings>
      <Resource target="Text" type="Label" id="NoCompetitorDataId"
                defaultLabel="Nenhuma auditoria registrada para este cliente." />
    </Bindings>
  </NoDataMessage>
</CardContainer>

3. Orquestração de Eventos no ProcessFlow (.processflow.xml)

Para que tudo funcione de forma reativa, o arquivo de processo gerencia o ciclo de vida e a injeção das variáveis no ProcessContext:

<!-- No StoreCockpitProcess.processflow.xml -->
<ProcessContext>
  <Declarations>
    <Declaration name="CardCompetitorChart_SerializedData" type="String" />
    <Declaration name="CardCompetitorChart_DataLoaded" type="DomBool" />
    <Declaration name="CardCompetitorChart_DisplayedSubcomponentName" type="String" />
    <Declaration name="CardCompetitorChart_ShowLegend" type="DomBool" />
  </Declarations>
</ProcessContext>

<!-- Ação disparada pelo evento LoadContainerData da UI -->
<Action name="CardCompetitorChart_LoadData" actionType="LOGIC"
        call="ProcessContext::StoreCockpitHelper.prepareCompetitorRateChartData">
  <Parameters>
    <Input name="customerPKey" value="ProcessContext::customerPKey" />
  </Parameters>
  <Return name="ProcessContext::CardCompetitorChart_SerializedData" />
  <TransitionTo action="CardCompetitorChart_SetDisplayMode" />
</Action>

<!-- Define se exibe o gráfico ou a mensagem de sem dados -->
<Action name="CardCompetitorChart_SetDisplayMode" actionType="LOGIC"
        call="ProcessContext::StoreCockpitHelper.getChartDisplayedSubcomponentName">
  <Parameters>
    <Input name="chartData" value="ProcessContext::CardCompetitorChart_SerializedData" />
    <Input name="pluginName" value="CompetitorRateChartPlugin" type="Literal" />
  </Parameters>
  <Return name="ProcessContext::CardCompetitorChart_DisplayedSubcomponentName" />
</Action>

4. Comparativo Arquitetural: Abordagens de Relatórios no CG Cloud Mobile

Dimensão de ComparaçãoListas Padrão do ModelerEncapsulamento com iFrame / WebContrato Nativo UIPluginV2
Experiência VisualApenas textos e linhas tabulares.Rica, baseada em bibliotecas web (D3/Chart.js).Rica e vetorial com aceleração gráfica nativa.
Operação 100% OfflineTotal (lê do SQLite local).Falha se depender de assets em nuvem ou CDN.Total (compilado localmente no shell móvel).
Impacto de Memória RAMMuito baixo.Muito alto (instancia navegadores secundários).Controlado (destrói nós ao rolar para fora).
Acoplamento com o ModelerNativo.Frágil e não homologado pela Salesforce.100% Declarativo e validado por XSD.
Internacionalização (i18n)Suporte completo via locale.Exige tratamento manual em JavaScript.Integrado nativamente via globallabel.xml.

5. Armadilhas Comuns no Build e Como Evitá-las

Durante o desenvolvimento com UIPluginV2, fique atento aos seguintes pontos críticos:

  1. Omissão no globallabel.xml: É a falha número um em projetos novos. Se a chave UIPluginV2_{Nome} não estiver declarada no arquivo de labels globais, a validação acusa erro ou o gráfico simplesmente não renderiza em tela.
  2. JSON Malformado: O motor nativo de gráficos exige strings JSON perfeitamente formatadas. Sempre monte seu objeto como estrutura JavaScript nativa e serialize com JSON.stringify() na Business Logic. Fuja de concatenações manuais de aspas e vírgulas.
  3. Correspondência Exata no DisplayedSubcomponentName: O CardContainer compara strings de forma literal. O valor de DisplayedSubcomponentName precisa ser idêntico ao atributo name do <UIPluginV2> (ex: "CompetitorRateChartPlugin") ou ao "CardNoDataMessageUiPlugin" quando a lista estiver vazia.

Conclusão

A arquitetura do Salesforce Consumer Goods Cloud Modeler mostra sua flexibilidade ao conectar a precisão transacional das suas camadas declarativas com o impacto visual de relatórios corporativos modernos.

Com o contrato UIPluginV2, o aplicativo mobile une o melhor de dois mundos: a resiliência do SQLite local (app.db3) funcionando sem sinal no subsolo de uma loja com a velocidade de gráficos acelerados por hardware.

Ao estruturar a cadeia correta entre contratos XML, rótulos globais e preparação em JavaScript, você entrega aos promotores e representantes comerciais o poder de transformar números brutos de prateleira em decisões de vendas instantâneas.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *