← Back to list

VBS: Opportunities Layout — Custom Sales Stage Popup

DISCLAIMER: The views expressed in this story are my own and do not necessarily reflect the views of Oracle.

Steve Zebib in Oracle VB Studio and JET · 2024-12-03 01:43 · 0 claps · 5.5 min read
#oracle-cloud #oracle-visual-builder
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📰 · Journalism & News

VBS: Opportunities Layout — Custom Sales Stage Popup

DISCLAIMER: The views expressed in this story are my own and do not necessarily reflect the views of Oracle.

Overview

This article describes how to create and enable a custom sales stage popup on the opportunities details page in Visual Builder Studio. The setup includes the following:

  • Fragment: Create sales stage popup
  • Opportunities Details Page: Add the fragment
  • Opportunities Header Layout: Add an event to open the fragment when sales stage badge is clicked

Setup

Fragment (Sales Stage Popup)

  • Create a fragment called “sales-stage-popup”.
  • The sales stage popup will show all sales stages and status for each stage (CURRENT, PENDING, COMPLETED).
  • Select current sales stage by clicking on sales stage.
  • NOTE: The input parameters for the fragment which need to be passed from from the container (opportunities layout) include: optyNumber, currentStgId, and salesStagesList.
  • In the fragment, copy all the code shown below including: Fragment JSON, HTML, JS, and Action Chains (JS).

JSON

{
  "description": "",
  "eventListeners": {
    "divClick": {
      "chains": [
        {
          "chain": "updateSalesStageActionChain",
          "parameters": {
            "stgId": "[[ $current.data.StgId ]]"
          }
        }
      ]
    },
    "divClick2": {
      "chains": [
        {
          "chain": "updateSalesStageActionChain",
          "parameters": {
            "stgId": "[[ $current.data.StgId ]]"
          }
        }
      ]
    },
    "divClick3": {
      "chains": [
        {
          "chain": "updateSalesStageActionChain",
          "parameters": {
            "stgId": "[[ $current.data.StgId ]]"
          }
        }
      ]
    },
    "vbEnter": {
      "chains": [
        {
          "chain": "updateSalesStageActionChain",
          "parameters": {
            "stgId": "{{ $variables.currentStgId }}"
      }
        }
      ]
    }
  },
  "events": {
    "updateSalesStage": {
      "propagationBehavior": "container",
      "description": "Emits to container",
      "payloadType": {
        "OptyNumber": "string",
        "StgId": "number"
      }
    }
  },
  "imports": {
    "components": {
      "oj-popup": {
        "path": "ojs/ojpopup"
      }
    }
  },
  "types": {
    "salesStageType": {
      "Name": "string",
      "StgId": "number",
      "StgOrder": "number"
    }
  },
  "variables": {
    "currentSalesStage": {
      "type": "salesStageType",
      "required": false,
      "input": "none"
    },
    "currentStgId": {
      "type": "number",
      "required": true,
      "input": "fromCaller"
    },
    "dataReady": {
      "type": "boolean",
      "defaultValue": false
    },
    "optyNumber": {
      "type": "string",
      "required": true,
      "input": "fromCaller"
    },
    "salesStagesList": {
      "type": "salesStageType[]",
      "required": true,
      "input": "fromCaller"

    },
    "salesStageStatusList": {
      "required": false,
      "type": "any[]",
      "input": "none"
    },
    "salesStageStatusListADP": {
      "type": "vb/ArrayDataProvider2",
      "defaultValue": {
        "data": "{{ $variables.salesStageStatusList }}",
        "itemType": "any"
      }
    }

  }
}

HTML

<style>
  .sales-stage {
    width: 170px;
    height: 150px;
    font-size: 14px;
    text-align: center;
    color: #FFF;
    margin: 4px;
    padding: 4px 4px 4px 16px;
    float: left;
    position: relative;
    background-color: var(--oj-core-neutral-1);
    cursor: pointer;
  }

  .sales-stage:after,
  .sales-stage:before {
    content: " ";
    position: absolute;
    top: 0;
    right: -16px;
    width: 0;
    height: 0;
    border-top: 76px solid transparent;
    border-bottom: 82px solid transparent;
    border-left: 17px solid var(--oj-core-neutral-1);
    z-index: 2;
  }

  .sales-stage:before {
    right: auto;
    left: 0;
    border-left: 16px solid #fff;
    z-index: 0;
  }

  .sales-stage:first-child:before,
  .sales-stage:last-child:after {
    border: none;
  }

  .sales-stage:first-child {
    border-top-left-radius: 24px;
    border-bottom-left-radius: 24px;
  }

  .sales-stage:last-child {
    border-top-right-radius: 24px;
    border-bottom-right-radius: 24px;
  }

  .sales-stage.current {
    color: #fff;
    background-color: var(--oj-core-info-1);
  }

  .sales-stage.current:after {
    border-left: 17px solid var(--oj-core-info-1);
  }

  .sales-stage.completed {
    color: #fff;
    background-color: var(--oj-core-success-1);
  }

  .sales-stage.completed:after {
    border-left: 17px solid var(--oj-core-success-1);
  }
</style>

<oj-popup id="popup-sales-stage">
  <div class="oj-flex oj-sm-align-items-center oj-sm-margin-6x">
    <oj-bind-if test="[[ $variables.dataReady ]]">
      <oj-bind-for-each data="[[ $variables.salesStageStatusListADP ]]">
        <template>
          <oj-bind-if test='[[ $current.data.Status === "COMPLETED" ]]'>
            <div class="sales-stage completed" on-click="[[$listeners.divClick]]">
              <div
                class="oj-flex oj-sm-align-items-center oj-sm-justify-content-center oj-sm-padding-2x" style="width: 100%; height: 100%;">
                <img :src="[[ $extension.path + $fragment.functions.getSalesStageImageUrl($current.data.StgOrder) ]]" width="100" height="100" style="border-radius: 50%;">
                <span class="oj-sm-margin-1x-top"><oj-bind-text value="[[ $current.data.Name ]]"></oj-bind-text></span>
              </div>
            </div>
          </oj-bind-if>
          <oj-bind-if test='[[ $current.data.Status === "CURRENT" ]]'>
            <div class="sales-stage current" on-click="[[$listeners.divClick2]]">
              <div
                class="oj-flex oj-sm-align-items-center oj-sm-justify-content-center oj-sm-padding-2x" style="width: 100%; height: 100%;">
                <img :src="[[ $extension.path + $fragment.functions.getSalesStageImageUrl($current.data.StgOrder) ]]" width="100" height="100" style="border-radius: 50%;">
                <span class="oj-sm-margin-1x-top"><oj-bind-text value="[[ $current.data.Name ]]"></oj-bind-text></span>
              </div>
            </div>
          </oj-bind-if>
          <oj-bind-if test='[[ $current.data.Status === "PENDING" ]]'>
            <div class="sales-stage" on-click="[[$listeners.divClick3]]">
              <div
                class="oj-flex oj-sm-align-items-center oj-sm-justify-content-center oj-sm-padding-2x" style="width: 100%; height: 100%;">
                <img :src="[[ $extension.path + $fragment.functions.getSalesStageImageUrl($current.data.StgOrder) ]]" width="100" height="100" style="border-radius: 50%;">
                <span class="oj-sm-margin-1x-top"><oj-bind-text value="[[ $current.data.Name ]]"></oj-bind-text></span>
              </div>
            </div>
          </oj-bind-if>
        </template>
      </oj-bind-for-each>
    </oj-bind-if>
  </div>
</oj-popup>

JS

define([], () => {
  'use strict';

  class FragmentModule {
  }

  FragmentModule.prototype.processCurrentSalesStage = function (salesStagesList, stgId) {
    let salesStagesListCopy = JSON.parse(JSON.stringify(salesStagesList));
    let currentSalesStage;

    for (let salesStage of salesStagesListCopy) {
      if (salesStage.StgId === stgId) {
        currentSalesStage = salesStage;
        break;
      }
    }

    return currentSalesStage;
  };

  FragmentModule.prototype.updateSalesStagesStatusList = function (salesStagesList, currentSalesStage) {
    let salesStagesListCopy = JSON.parse(JSON.stringify(salesStagesList));
    let currentSalesStageCopy = JSON.parse(JSON.stringify(currentSalesStage));

    for (let salesStage of salesStagesListCopy) {
      if (salesStage.StgOrder < currentSalesStageCopy.StgOrder) {
        salesStage.Status = "COMPLETED";
      } else if (salesStage.StgOrder > currentSalesStageCopy.StgOrder) {
        salesStage.Status = "PENDING";
      } else if (salesStage.StgOrder === currentSalesStageCopy.StgOrder) {
        salesStage.Status = "CURRENT";
      }
    }

    return salesStagesListCopy;
  };

  // NOTE: Add sales stages images in resources directory. You can use a different image for each sales stage based on StgOrder value.
  FragmentModule.prototype.getSalesStageImageUrl = function (stgOrder) {
    return "resources/images/o_tag.png";
  };

  return FragmentModule;
});

Action Chain (updateSalesStageActionChain)

define([
  'vb/action/actionChain',
  'vb/action/actions',
  'vb/action/actionUtils',
], (
  ActionChain,
  Actions,
  ActionUtils
) => {
  'use strict';

  class updateSalesStageActionChain extends ActionChain {

    /**
     * @param {Object} context
     * @param {Object} params
     * @param {number} params.stgId 
     */
    async run(context, { stgId }) {
      const { $fragment, $extension, $global, $functions, $variables } = context;

      $variables.dataReady = false;

      const processCurrentSalesStage = await $functions.processCurrentSalesStage($variables.salesStagesList, stgId);

      $fragment.variables.currentSalesStage = processCurrentSalesStage;

      await Actions.fireEvent(context, {
        event: 'updateSalesStage',
        payload: {
          OptyNumber: $variables.optyNumber,
          StgId: stgId,
        },
      });

      const callFunctionResult = await $fragment.functions.updateSalesStagesStatusList($fragment.variables.salesStagesList, $fragment.variables.currentSalesStage);

      $fragment.variables.salesStageStatusList = callFunctionResult;

      await Actions.fireDataProviderEvent(context, {
        refresh: null,
        target: $fragment.variables.salesStageStatusListADP,
      });

      $fragment.variables.dataReady = true;
    }
  }

  return updateSalesStageActionChain;
});

Opportunities Details Page

  • Go to App UIs > Sales > opportunities > opportunities-detail
  • Duplicate the default layout and a section (e.g. salesStagePopupTemplate) which will contain the fragment.
  • Add the fragment (sales-stage-popup) to the template as shown below.
<template id="salesStagePopupTemplate">
  <oj-vb-fragment bridge="[[vbBridge]]" name="sales-stage-popup">
    <oj-vb-fragment-param name="optyNumber" value="[[ $base.variables.puid ]]"></oj-vb-fragment-param>
    <oj-vb-fragment-param name="currentStgId" value="[[ $variables.currentStgId ]]"></oj-vb-fragment-param>
    <oj-vb-fragment-param name="salesStagesList" value="[[ $variables.salesStagesList ]]"></oj-vb-fragment-param>
  </oj-vb-fragment>
</template>
  • Note the input parameters that need to be passed to the fragment include: optyNumber (puid), currentStgId (number), and salesStagesList (array of salesStageType). For example:
optyNumber = "10001";
currentStgId = 300000000323740;
salesStagesList = [
    {
        "Name": "01 - Qualification",
        "StgId": 300000000323741,
        "StgOrder": 1
    },
    {
        "Name": "02 - Discovery",
        "StgId": 300000000323740,
        "StgOrder": 2
    },
    {
        "Name": "03 - Building Vision",
        "StgId": 300000000323742,
        "StgOrder": 3
    },
    {
        "Name": "04 - Presentation",
        "StgId": 300000000323739,
        "StgOrder": 4
    },
    {
        "Name": "05 - Agreement",
        "StgId": 300000000323736,
        "StgOrder": 5
    },
    {
        "Name": "06 - Negotiation",
        "StgId": 300000000323738,
        "StgOrder": 6
    },
    {
        "Name": "07 - Closed",
        "StgId": 300000000323737,
        "StgOrder": 7
    }
]

Opportunities Header Layout

  • Go to Layout > Sales > Opportunities > Header Layout
  • Duplicate the default layout and make sure the SalesStage field is set as the badgeFieldName.
  • Add the opportunitiesOnLoadHeaderEvent which triggers an action chain (opportunitiesOnLoadHeaderEventListener) as shown below. This action chain will add a click handler on the SalesStage field to open the sales stage popup. Make sure the ID of popup (e.g. popup-sales-stage) is the same as the ID used in the fragment.

Action Chain (opportunitiesOnLoadHeaderEventListener)

define([
  'vb/action/actionChain',
  'vb/action/actions',
  'vb/action/actionUtils',
], (
  ActionChain,
  Actions,
  ActionUtils
) => {
  'use strict';

  class opportunitiesOnLoadHeaderEventListener extends ActionChain {

    /**
     * @param {Object} context
     */
    async run(context) {
      const { $layout, $base, $extension, $responsive, $user } = context;

      const elements = document.getElementsByClassName("cx-badge-item");

      if (elements && elements.length > 0) {
        let element = elements.item(0);
        element.addEventListener('click', function() {
            const elementPopup = document.getElementById("popup-sales-stage");
            elementPopup.open();
        });
      }
    }
  }

  return opportunitiesOnLoadHeaderEventListener;
});

Test

  • Run the application and you should see the following:


메타데이터
post_id
8c98bc2e564f
slug
vbs-opportunities-layout-custom-sales-stage-popup-8c98bc2e564f
url
https://medium.com/oracle-jet/vbs-opportunities-layout-custom-sales-stage-popup-8c98bc2e564f
canonical_url
https://medium.com/oracle-jet/vbs-opportunities-layout-custom-sales-stage-popup-8c98bc2e564f
author_url
https://medium.com/@mzebib
status
ok
fetched_at
2026-06-11 15:16:29