Wednesday, March 16, 2011

How to 'Queue' sObjects based on SLA and Priority

The other day I was given the task of creating a button on the Case sObject that gives the User the next Case from the Queue's that they belong to. It would be based on SLA time and priority, which would be generated from a custom setting and formula field on Case sObject. The Custom setting would hold priority ratings and SLA times for each Queue, and the formula field would show the age of the Case in hours from last-mod date. The end result is that it will take  the oldest Case with the highest Priority if SLA time is meet, otherwise it will take the first Case out of SLA based on Priority. The custom setting can be a list or a hierarchy, it just depends on if you want to be able to control it down to the user level, where as the list is more of an org-wide default. This can also be set up for any sObject that can have a Queue instead of just the Case sObject like in my example.

So on the Case page it would look like this:
and when they clicked the 'Take Next Ticket' link it would return the next ticket by SLA and priority, and automatically assigns the user as the owner of the Case. Then once they close the case and click the 'Take Next Ticket' link it will all repeat over and over. Ensuring that no one has Cases waiting to in their personal Queues and that the oldest/highest priority gets handled first. 

Custom Setting Setup:

Example Items:
NamePriority(1-10)SLA(min)
HighPriorityQueue1012
AnotherQueueName524
LowPriorityQueue136
    

Formula Field Setup:

The field needs to be a number and the formula to calculate the age of the Case in mins:

Now the VF page:

The page itself will be blank, unless you want to show errors if anything fails, like no more tickets in the Queues that the rep belongs to. 

<apex:page standardController="Case" extensions="TakeNextTicketCon" standardStylesheets="false" tabstyle="Case" action="{!TakeNextTicket}">
<style>
      #messageWrap{width:50%margin:autofont-size:15px;}
      #linkWrap{text-align:center;}
      ul li{color:redtext-decoration:nonelist-style:nonepadding:0pxmargin:0text-align:center;}
</style>
      <apex:form >
            <div id="messageWrap">
                  <apex:messages id="messagesblock" />
            </div>
            <div id="linkWrap">
                  <apex:outputlink value="/{!Case.id}">Go Back</apex:outputlink>
                  &nbsp;|&nbsp;
                  <apex:commandLink action="{!TakeNextTicket}">Take-Next-Ticket</apex:commandLink>
            </div>
      </apex:form>
</apex:page>

Now the fun stuff, the Classes. In mine i split the classes up into the main controller (TakeNextTicketCon.cls) and a helper class (TakeNextTicketHelper.cls) and then finally the test Class (TakeNextTicketHelper). I tired to set up the classes so that all the info and action methods are in the Main class, and the everything else is in the helper. It was difficult to get the info required for this due to the way that salesforce has the Queue's set up in the schema. 

To get the Queues that a user belongs to for a particular sObject, you need to first query GroupMemeber where UserOrGroupId = User Id
GroupMember[] gm = [Select Id, GroupId From GroupMember where UserOrGroupId =:sInUserId limit 50];

Then you need to take that list and convert it to a string list of GroupId's and use it to query the Group sObject where type = Queue and the id is in the list of id's that we just create from the GroupMemeber sObject. 
Group[] g = [select id, Name from Group where Type = 'Queue' and id IN :lstInGroupId limit 50];

Then once you get that list and again convert it to a string list of Id's and use it in the next query of the QueueSobject where sobjectType = Case and QueueId is in the list of Group id's.
QueueSobject[] q = [Select Id, QueueId, Queue.Name From QueueSobject where SobjectType = 'Case' and QueueId IN:lstInGroupIdlimit 50];

fun right? If you look at the sObject in schema, you'll see why i did it that way, but you'll see another way to traverse the sObjects in an upcoming post on making the User/Queue edit more manageable. But lets take a look at the classes i used to power this little link of wonders, just note that I will not be explaining the classes, just showing them. If you need help understanding what or how I did something feel free to contact me and I will brake it down further.

TakeNextTicketCon:

public with sharing class TakeNextTicketCon {
      
      private final Case cse;
      
      public TakeNextTicketCon(ApexPages.StandardController stdController)
      {
        this.cse = (Case)stdController.getRecord();
    }
   
      public Case NextTicket{get;set;} 
      
      public List<QueuePriority__c> objQueueOrder
      {
            get{returnTakeNextTicketHelper.Priority(TakeNextTicketHelper.FindQueues(TakeNextTicketHelper.CurrentUserGroups(TakeNextTicketHelper.FindGroupIdsFromMember(UserInfo.getUserId()))));}
            set;
      }
      /// <summary>
    /// GETS THE NEXT TICKET BASED ON QUEUE PRIORITY AND LAST MOD OF TICKETS IN QUEUE
    /// </summary>
    /// <returns>NEXT TICKET /CURRENT TICKET IF NONE TO GET</returns>
      public pageReference TakeNextTicket()
      {
            NextTicket = TakeNextTicketHelper.NextCase(TakeNextTicketHelper.NextCaseList(objQueueOrder), objQueueOrder);
            
            TakeNextTicketHelper.ChangeOwnerOnCase(UserInfo.getUserId(),NextTicket);
            if(NextTicket != null)
            {
                  PageReference ref = new PageReference('/' + NextTicket.id);
                  ref.setRedirect(true);
                  return ref; 
            }
            ApexPages.Message msg = new ApexPages.Message( ApexPages.Severity.ERROR,'Opps.... All queues are empty... but not for long I am sure :-)');
            ApexPages.addMessage(msg);
            return null;
      }
}

TakeNextTicketHelper:

public with sharing class TakeNextTicketHelper {
      
      /// <summary>
    /// CREATES AN ID LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> AddToIdListFromGM(List<GroupMember> lstInSobject)
      {
            List<String> idList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(GroupMember obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.GroupId, idList))
                              {
                                    idList.add(obj.GroupId);
                              }
                        }
                  }
                  return idList;
            }
            return null;
      }
      /// <summary>
    /// CREATES AN ID LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> AddToIdListFromQS(List<QueueSobject> lstInSobject)
      {
            List<String> nameList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(QueueSobject obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.Queue.Name, nameList))
                              {
                                    nameList.add(obj.Queue.Name);
                              }
                        }
                  }
                  return nameList;
            }
            return null;
      }
      /// <summary>
    /// CREATES AN NAME LIST FROM PASSED SOBJECT
    /// </summary>
    /// <param name="lstInSobject">LIST TO DEDUP AND ADD</param>
    /// <returns>STRING NAME LIST</returns>
      public static List<String> AddToNameList(List<QueuePriority__c> lstInSobject)
      {
            List<String> nameList = new List<string>();
            if(GlobalHelper.CheckForNotNull(lstInSobject))
            {
                  for(QueuePriority__c obj: lstInSobject)
                  {
                        if(obj != null)
                        {
                              if(!GlobalHelper.ContainsItem(obj.Name, nameList))
                              {
                                    nameList.add(obj.Name);
                              }
                        }
                  }
                  return nameList;
            }
            return null;
      }
      /// <summary>
    /// FINDS GROUP IDS BASED ON ID LIST PASSED
    /// </summary>
    /// <param name="lstInGroupId">LIST TO SELECT GROUP IDS FROM</param>
    /// <returns>STRING ID LIST</returns>
      public static List<string> CurrentUserGroups(List<String> lstInGroupId)
      {
            if(GlobalHelper.CheckForNotNull(lstInGroupId))
            {
                  try
                  {
                        Group[] g = [select id, Name from Group where Type = 'Queue' and id IN :lstInGroupId limit 50];
                              return GlobalHelper.AddItemsToIdList(g);
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - CurrentUserGroups - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS QUEUES BASED ON ID LIST PASSED
    /// </summary>
    /// <param name="lstInGroupId">LIST TO SELECT GROUP IDS FROM</param>
    /// <returns>SOBJECT LIST OF QUEUES</returns>
      public static List<QueueSobject> FindQueues(List<String> lstInGroupId)
      {
            if(GlobalHelper.CheckForNotNull(lstInGroupId))
            {
                  try
                  {
                        QueueSobject[] q = [Select Id, QueueId, Queue.Name From QueueSobject where SobjectType = 'Case' and QueueIdIN:lstInGroupId limit 50];
                        return q;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - FindQueues - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS GROUP IDS BASED ON ID FROM USER
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <returns>STRING ID LIST</returns>
      public static List<String> FindGroupIdsFromMember(string sInUserId)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId))
            {
                  try
                  {
                        GroupMember[] gm = [Select Id, GroupId From GroupMember where UserOrGroupId =:sInUserId limit 50];
                        if(GlobalHelper.CheckForNotNull(gm))
                        {
                              return TakeNextTicketHelper.AddToIdListFromGM(gm);
                        }
                        ApexPages.Message msg = new ApexPages.Message( ApexPages.Severity.ERROR,'Oops... no queues are assigned to you... please contact your manager and they will assign you to one.');
                        ApexPages.addMessage(msg);
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - FindGroupIdsFromMember - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS PRIORITY BASED ON NAME LIST PASSED
    /// </summary>
    /// <param name="lstInQueueNames">QUEUE NAME SOBJECT LIST</param>
    /// <returns>SOBJECT LIST OF QUEUE PRIORITY</returns>
      public static List<QueuePriority__c> Priority(List<QueueSobject> lstInQueueNames){
            if(GlobalHelper.CheckForNotNull(lstInQueueNames))
            {
                  try
                  {
                        QueuePriority__c[] qp = [select id, name, Priority__c, SLA_mins__c from QueuePriority__c where NameIN:TakeNextTicketHelper.AddToIdListFromQS(lstInQueueNames) order by Priority__c DESC limit 50];
                        if(GlobalHelper.CheckForNotNull(qp))
                        {
                              return qp;
                        }
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - Priority - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS NEXT CASES 
    /// </summary>
    /// <param name="lstInPriority">LIST OF QUEUE PRIORITY</param>
    /// <returns>SOBJECT LIST OF CASES</returns>
      public static List<Case> NextCaseList(List<QueuePriority__c> lstInPriority)
      {
            if(GlobalHelper.CheckForNotNull(lstInPriority))
            {     
                  try


                  {
                        Case[] c = [select id, caseNumber, Owner.Name, Time_in_Queue__c from Case where Owner.NameIn:TakeNextTicketHelper.AddToNameList(lstInPriority) and status != 'Closed' order by Time_in_Queue__c DESC limit 200];
                        return c;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - NextCaseList - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS NEXT CASE 
    /// </summary>
    /// <param name="lstInCase">LIST OF CASES</param>
    /// <param name="lstInPriority">LIST OF QUEUE PRIORITY</param>
    /// <returns>CASES SOBJECT</returns>
      public static Case NextCase(List<Case> lstInCase, List<QueuePriority__c> lstInPriority)
      {
            if(GlobalHelper.CheckForNotNull(lstInPriority))
            {
                  for(QueuePriority__c qp : lstInPriority)
                  {
                        for(Case c : lstInCase)
                        {
                              if(c.Owner.Name == qp.Name)
                              {
                                    if(c.Time_in_Queue__c > qp.SLA_mins__c)
                                    {
                                          return c;
                                    }
                              }
                        }
                  }
                  for(QueuePriority__c qp : lstInPriority)
                  {
                        for(Case c : lstInCase)
                        {
                              if(c.Owner.Name == qp.Name)
                              {
                                    return c;
                              }
                        }
                  }
                  
            }
            return null;
      }
      /// <summary>
    /// CHANGES CASE OWNER TO CURRENT USER 
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <param name="objInCase">CASE OBJ</param>
      public static void ChangeOwnerOnCase(String sInUserId, Case objInCase)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId) && objInCase != null)
            {
                  try
                  {
                        objInCase.OwnerId = sInUserId;
                        update objInCase;
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... TakeNextTicketHelper - ChangeOwnerOnCase - Error: ' + e);
                  }
            }
      }
}

The Test class i will leave up to you, in my org, we try to get as close to 100% coverage as possible, and i recommend the same for your org as well. If you don't, and just go for 75% coverage, you will find issues down the line with not being fully covered. Remember the rule of 75% coverage is an org standard not per class, so if a few classes bring the average down then you need to go back and fix old code. So its best to take the extra hour and try to get the most coverage as possible in the project you are working on.
Once all the classes are done, you'll still need to add it to the case as a link then add the link to the page layout.

Once you add it as a button/link on the case sObject it will be available to be added via the page-layout edit page.
Questions? 
Twitter: @SalesForceGirl  or Facebook  

No comments:

Post a Comment