Monday, 14 May 2012

Cloudstock London 2012


In an unexpected turn of events, I'm going to be presenting a community session at Cloudstock London 2012.  Unexpected as I hadn't planned on this, but a slot came open at the last minute and I agreed to fill in.

The upside to this is that I didn't have much time to think about what I was committing to.  The downside was that I had to decide on my topic and produce a description in a few hours.  What to speak about then?   It had to be something I was familiar with that wouldn't take a huge amount of time to prepare, as Cloudstock was just over a week away.  Looking at the list of existing community sessions, there was a theme of deep dives into specific technical areas - @wesnolte on Javascript Remoting and MVC Frameworks for example, or @pbatisson on Advanced Force.com Testing Techniques.

It struck me that there was an opportunity here to reach those attendees with little or no experience of Force.com, or those with some experience looking to progress.  There was already a breakout session introducing the Force.com platform, so there was little point in a session with the same technical angle.  Therefore I decided to look at this from a different perspective, and cover starting out as a Force.com developer, what employers are looking for and career progression.

This seemed like a good starting point, but unlikely to fill a 30 minute session unless I spoke very slowly! As followers of this blog know, if there's one thing I'm pretty familiar with that's Salesforce.com Certification. Most Salesforce/Force.com careers will involve gaining certifications - its certainly a key metric for Cloud Alliance Partners - so I'll be talking about the benefits, what the exams involve and how to prepare.  Since I gained the Technical Architect accreditation I've had a lot of interest in the exam, particularly the Review Board, so I'll be covering that too and expecting questions!

So if you are thinking about a job in Force.com development, or looking to gain recognition and advance your career through Certification, join me at Cloudstock from 12:30 to 1pm to find out more.

Wednesday, 2 May 2012

Opportunity Status Chart

An important aspect when working on Opportunities (or anything with a process that a record progresses through) is to knowing how far through the Sales Process a particular Opportunity is.  While some users may be familiar enough with the Sales Process to be able to figure this out immediately from the Stage Name, a visual indicator always helps.

Something I've been working on for a while now is using a line chart for just this purpose.  The key elements of this are:


  • List all of the Stages for the Opportunity - if record types are in use, this should be the Stages applicable to that record type
  • Plot a green line and markers for Stages that have been completed (or skipped, if the Opportunity has jumped into the process at an advanced staged
  • Plot a red line and markers for Stages still to come

The first point had always been a sticking point, but I managed to solve this using back in January by allowing Visualforce to render the specific picklist for the record, and then sending this back to the controller prior to processing the record, as detailed in this post.

It will come as no surprise to regular readers of this blog that I chose to handle the second and third points using the Dojo Charting framework.  I've finally completed the charting code  - below is a screenshot showing the chart embedded into the standard Opportunity record view page:



The controller simply walks the Stage Names and adds them to a complete list until it encounters the current state.   All Stage Names after this go into a todo list - note that both Closed-Won and Closed-Lost appear, and also the default '-- None-- entry - probably not what is desired and left as an exercise for the avid student.  The page is as follows:

<apex:page standardController="Opportunity" extensions="OpportunityStatusChartController" showheader="false" standardstylesheets="false" sidebar="false">
<head>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true,
      modulePaths: { 
          'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 
          'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 
         'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' 
             }
    ">
  </script>
  <link rel="stylesheet"
    href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css" />
</head>
<body class="claro">
  <apex:form id="frm">
    <apex:actionFunction name="reloadWithStages" action="{!reload}" />
    <div id="test1" style="width: 100%; height: 150px;"></div>
    <apex:outputPanel layout="block" id="vals" style="display:none">
      <apex:inputField value="{!Opportunity.StageName}" required="false" id="stages"/>
      <apex:inputText value="{!valsText}" required="false" id="back"/>
    </apex:outputPanel>
  </apex:form>
  <apex:outputField value="{!Opportunity.StageName}" rendered="false"/>

  <script>
  function reload()
  {
     var ele=document.getElementById('{!$Component.frm.stages}');
     var idx=0;
     var valText='';
     for (idx=0; idx<ele.length; idx++)
     {
        valText+=ele.options[idx].text + ':';
     }
   
     var backele=document.getElementById('{!$Component.frm.back}');
     backele.value=valText;
   
     reloadWithStages();
  }

  <apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
    dojo.require("dojox.charting.Chart2D");
    dojo.require("dojox.charting.axis2d.Default");  
    dojo.require("dojox.charting.plot2d.Default");
    dojo.require("dojox.charting.plot2d.StackedLines");
    dojo.require("dojox.charting.plot2d.Columns");
    dojo.require("dojox.charting.plot2d.Bars");
    dojo.require("dojox.charting.plot2d.ClusteredBars");
    dojo.require("dojox.charting.plot2d.StackedBars");
    dojo.require("dojox.charting.plot2d.Bubble");
    dojo.require("dojox.charting.plot2d.Grid");
    dojo.require("dojox.charting.plot2d.Pie");

    dojo.require("dojox.charting.themes.PlotKit.green");

    dojo.require("dojox.charting.action2d.Highlight");
    dojo.require("dojox.charting.action2d.Magnify");
    dojo.require("dojox.charting.action2d.MoveSlice");
    dojo.require("dojox.charting.action2d.Shake");
    dojo.require("dojox.charting.action2d.Tooltip");

    dojo.require("dojox.charting.widget.Legend");

    dojo.require("dojo.colors");
    dojo.require("dojo.fx.easing");
    dojo.require("dojo.date.stamp");
    dojo.require("dojo.date.locale");

    makeCharts = function(){

 var myMap={
  <apex:repeat value="{!labels}" var="label">
      '{!label.idx}':'{!label.text}'<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
  </apex:repeat>
            };
            
 var chart1 = new dojox.charting.Chart2D("test1");
 chart1.setTheme(dojox.charting.themes.PlotKit.green);
 chart1.addPlot("default", {type: "Default", lines: true, markers: true, tension:2});
 chart1.addAxis("x",
 { 
    majorTick: {stroke: "black", length: 3},
    majorTickStep:1, 
  minorTicks: false, 
  microTicks: false,
  min: 0,
  max: {!labelCount},
  rotation:30, 
  font: "6pt Tahoma",
  labels: [
  <apex:repeat value="{!labels}" var="label">
      {value: {!label.idx}, text:'{!label.text}'}<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
  </apex:repeat>
  ]
  
 });
 
 chart1.addSeries("cleared", 
   [
       <apex:repeat value="{!doneStageNumbers}" var="stage">
    {x: {!stage}, y: 2},
    </apex:repeat>
   ]);
 chart1.addSeries("todo", 
   [
       <apex:repeat value="{!todoStageNumbers}" var="stage">
    {x: {!stage}, y: 2},
    </apex:repeat>
   ],
   {plot: "default", stroke: {color:"#FE2E2E"}}
   );
   
 var myMap={
  <apex:repeat value="{!tooltips}" var="tooltip">
   "{!tooltip.idx}": "{!tooltip.text}",
  </apex:repeat>
            };
            
 var anim1a = new dojox.charting.action2d.Magnify(chart1, "default");
 var anim1b = new dojox.charting.action2d.Tooltip(chart1, "default",
 {
     text : function(o) {
         return (myMap[o.x])
                       }
        });
   
 chart1.render();
    };
  </apex:outputPanel>
   dojo.addOnLoad(
     <apex:outputPanel layout="none" rendered="{!loadOnce}">
        reload
     </apex:outputPanel>
     <apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
 makeCharts
     </apex:outputPanel>
      );
  </script>
</body>
</apex:page>

Essentially this page has two distinct functions - the first to render a hidden form containing an input field for the StageName field, and then submit the values back via javascript, while the second is to actually render the chart based on the values.  The completed stages are plotted as a separate series to those that remain, to allow that part of the chart to be rendered in a different colour and markers. I've also thrown in a tooltip on each of my markers so that I can display some help/explanation text about each of the stages.

The controller is quite a simple one:
public class OpportunityStatusChartController
{
 public List<Tuple> labels {get; set;}
 public List<Tuple> tooltips {get; set;}
 public List<Integer> doneStageNumbers {get; set;}
 public List<Integer> todoStageNumbers {get; set;}
 public Integer labelCount {get; set;}
 public Boolean loadOnce {get; set;}
 public String valsText {get; set;}
        private Opportunity opp;
 
 public OpportunityStatusChartController(ApexPages.StandardController std)
 {
  opp=(Opportunity) std.getRecord();
  loadOnce=true;
 }
 
 public PageReference reload()
 {
  init();
  loadOnce=false;
  
  return null;
 }
 
 
 public void init()
 {
  labels=new List<Tuple>();
  tooltips=new List<Tuple>();
  doneStageNumbers=new List<Integer>();
  todoStageNumbers=new List<Integer>();
  
  labelCount=0;
  Boolean done=false;
  labels.add(new Tuple(labelCount++, '.'));
  
  for (String val : valsText.split(':'))
  {
   if (!done)
   {
    doneStageNumbers.add(labelCount);
   }
   else
   {
    todoStageNumbers.add(labelCount);
   }
    
   if (val==opp.StageName)
   {
    done=true;
    todoStageNumbers.add(labelCount);
   }
   labels.add(new Tuple(labelCount, val));
   toolTips.add(new Tuple(labelCount, 'Help for ' + val + ' stage'));
   labelCount++;
  }  
  labels.add(new Tuple(labelCount, '.'));
 }
 
 public class Tuple 
 {
  public Integer idx {get; set;}
  public String text {get; set;}
  
  public Tuple(Integer inIdx, String inText)
  {
   idx=inIdx;
   text=inText;
  }
 }
}

The labels are returned in a containing Tuple class along with an index, which is the x-axis position of the ploy.  Everything is plotted at the same level on the y-axis, as I want a straight line chart.

I can then change the record type of the Opportunity, save it and the chart updates accordingly:


You can see a live example of this on my demo site - simply click on one of the Opportunities to see the progress chart.