Service and Support Blog - August 2008
-
The Contention-Proof Case Accept Button
Marco Casalaina Aug 21, 2008The case accept button is a handy thing. It shows up on queue views to allow members of that queue to mass-accept cases (actually, in the Agent Console it shows up in the Mass Action dropdown rather than as a button, but it works the same).
If you have a large queue, though, with lots of agents accepting cases at the same time, they may sometimes step on each others' toes. If two agents accept the same cases at almost the same time, then the last one will win, and the first agent will not actually own the cases he thinks he just accepted.
Here's some custom button code I wrote which addresses this issue. It only accepts cases that are still assigned to a queue; if the cases have been accepted by a user already, those cases will remain owned by the users who accepted them.
To add this code, go to Setup->Cases->Buttons and Links and make a new custom button called Accept Cases (or whatever label you'd like to use for this). Its Display Type should be set to List Button, its Behavior to Execute JavaScript, and its Content Source to OnClick JavaScript. Paste the code from this link into the OnClick JavaScript field.
Now you'll have to add this button to the Case list view. To do this, go to Setup->Cases->Search Layouts. Click Edit next to the Cases List View entry, and add your new button from the Available Buttons section to the Selected Buttons section.
So how did I do this?
Quite easily really. Perhaps the most important part is the line that invokes {!GETRECORDIDS($ObjectType.Case)}. This little-known merge field allows you to get a list of all the IDs that are selected in a list view or related list – really handy when you're trying to make buttons that act upon a large number of records.
Once I have the IDs of the selected records, I run them through the also little-known retrieve function to get more information about them. The key to that retrieve call is getting Owner.Type – that will return a string for each case containing either "Queue" or "User" depending on what the type of each case's owner is. A good lesson here is that GETRECORDIDS plus retrieve makes for a quick and powerful combination.
Now I divide the cases into two piles: the "accept" pile for cases that are currently owned by a queue that we're going to allow this user to accept, and the "reject" pile of cases that have already been accepted by other users. If there are any cases in the "accept" pile I call update on it to update those cases, and then I give some feedback to the user of the changes that were made (and the changes that were rejected).
And there we have it: the contention-proof case accept button. Note that with just a couple of minor changes you can adapt this same code for use with leads and custom objects – any object that is ownable by a queue.
-
An Even Better Way To Pop Visualforce Pages From CTI
Marco Casalaina Aug 14, 2008In a previous blog entry I posted a means of popping a Visualforce page from a CTI adapter. That method was somewhat complicated and required an adapter writer to modify some of the core CTI Toolkit library code.
Digging into it a little further, I recently came up with a much simpler way to pop a Visualforce page from CTI. There is one caveat here: using this method, your CTI adapter will always pop this Visualforce page – it won't try to do any other kind of search. As long as you're OK with having your Visualforce page do any searching required, then this method is for you. Here's how to do it.
- Make a Visualforce page that takes in ANI and a DNIS as parameters on the URL. You may also want to make it take in other parameters that might be attached to the call.
- Whenever you call CCTIUserInterface::OnCallRinging to set the adapter to its "call ringing" mode, set the third parameter to false. This disables the native adapter search mechanism.
- After every call to OnCallRinging add this code (replacing the "YourPageHere" with the actual name of your page):
CCTIParty* pParty = pLine->GetPartyByANI(mapInfoFields[KEY_ANI]);
if (pParty) {
RelObjSetList relObjs;
CCTIRelatedObjectSet* pSet =
new CCTIRelatedObjectSet(L"Visualforce",L"Caller Info",L"Caller Info");
pSet->SetVisible(true);
relObjs.push_back(pSet);
std::wstring apexUrl = L"apex/YourPageHere?";
apexUrl += L"ANI=" + mapInfoFields[KEY_ANI];
apexUrl += L"&DNIS=" + mapInfoFields[KEY_DNIS];
CCTIRelatedObject* pObject = new CCTIRelatedObject(apexUrl,L"Click To View");
pObject->SetVisible(true);
pSet->AddRelatedObject(pObject);
pParty->AddRelatedObjectSets(relObjs);
}
UIRefresh();This code verbatim added to your adapter just after you call OnCallRinging will cause it to pop to the Visualforce page. The Visualforce page can figure out what it should search on based on the ANI and DNIS that's passed in via the URL.
To take this a step further, you can also append all the members of mapAttachedData to the URL, which will enable you to pass any attached data (say, caller entered digits in the IVR) to your Visualforce page.
One more thing, though: usually, when the CTI adapter pops an object, it tries to add that object to the call log. However, a Visualforce page cannot be added to the call log, and if you try to add one, it will cause the call log to fail upon saving. So you'll want to override the function that adds objects to the call log to ignore Visualforce pages. You can do so by overriding UIAddLogObject in your subclass of CCTIUserInterface, like this (replacing the CDemoUserInterface with the name of your subclass, of course):
void CDemoUserInterface::UIAddLogObject(PARAM_MAP& parameters) {
std::wstring sId = parameters[KEY_ID];
if (sId.find(L"apex") == std::wstring::npos) {
CCTIUserInterface::UIAddLogObject(parameters);
}
}Obviously you'll have to add a corresponding method header to your .h file also.
To genericize this, you might consider adding a parameter to your Call Center Definition File (that XML file you distribute to your customers) that specifies the Visualforce page to pop to. If the customer specifies this page in the setup, then always pop to it; otherwise take the normal route of searching for a Salesforce.com object and popping what you find.
And by the way:
Let's say you want your Visualforce page to add an object to the log. As I mentioned earlier you can't put the Visualforce page itself in the log, but you can send a message to the CTI adapter to have it add an object to the log.
All you have to do is have your Visualforce page call the following Javascript function:
sendCTIMessage('ADD_LOG_OBJECT?ID='+entityId+'&OBJECT_LABEL=' + escape(entityLabel)+'&ENTITY_NAME='+entityDevName+'&OBJECT_NAME=' + escape(entityName));
where:
entityId = the Salesforce.com ID of the object you want to add to the log
entityLabel = the name of the object you want to add to the log (like "Lead" or "My Custom Object")
entityDevName = the API name of the object you're adding (like "Lead" or "My_Custom_Object__c") -
Case Age In Business Hours
Marco Casalaina Aug 7, 2008I am pleased to announce that we have released an AppExchange application called Case Age In Business Hours (click link for the AppExchange listing) under the Salesforce Labs banner. In this post I'll explain what it does, how it does it, and how you can modify the code if you so desire to make it do more.
So first, what does it do? More than its name suggests, actually. First, and most obviously, it calculates the Case Age In Business Hours for your closed cases. Fancy that. It also has another useful function, though: it includes two more fields called Time With Support and Time With Customer that distinguish between time spent by your support team and time spent awaiting the customer. Note that these new fields will only be accurate for closed cases -- I'll explain why in a bit.
It sounds like it might be complex, doesn't it? A few years ago, back in my full-time developer days, I wrote some code that calculated time differences in business hours, taking into account time zone differences, daylight savings, and all that jazz. It was hundreds and hundreds of lines of code, and it took me days to get it right. My pinkies still twitch at the thought of it.
Thankfully, some of the capabilities we recently added to the Force.com platform make the Case Age In Business Hours package a cinch. It does everything my old hunk of code did and more, but if you look at the trigger which forms the basis of this package, there are only 55 lines of code, and half the lines are comments or whitespace.
What made it all possible are the BusinessHours methods in Apex, particularly BusinessHours.diff. You pass BusinessHours.diff two times and a set of Business Hours (and you can define as many sets of Business Hours as you want at Setup->Company Info->Business Hours) -- and it spits back at you the time difference in business hours! It does all the time zone translation and daylight savings nastiness for you, and all you have to do is call the method. That's it!
Now for the interesting part, though: how about those Time With Support and Time With Customer fields? How do we figure that out?
Well, if you install the package, you'll notice that it also installs a custom object and tab called "Stop Statuses." To set it up, you access this tab and add the names of the case statuses you'd use when you're waiting for your customer. Some people have "Awaiting Customer Response" or "Awaiting Customer Validation" -- some just have "On Hold." Whatever your statuses may be, add them all in the Stop Statuses section. Then, whatever time your case spends in one of those statuses will be tallied to the Time With Customer field -- and any time your case spends not in a stop status (and not in a closed status, of course) will be added to to the Time With Support field.
Now what if you want to take it to the next level -- let's say you want to add a bucket and measure Time With Tier 1, Time With Tier 2, and Time With Customer? Well, that wouldn't be so hard -- you'd just have to factor in the owner as well as the status when deciding what times go in what buckets.
Before we begin, remember that because the Business Hours functionality is new, this Apex will only compile against the 13.0 API and higher (13.0 being the latest as of the Summer '08 release). If you haven't got the latest Force.com IDE plugin, now would be a wonderful time to download it. It makes Apex development much easier.
First, we'll have to add some fields to Case to store this bucket. Let's call them Time With Tier 1 and Time With Tier 2 (with corresponding API names Time_With_Tier_1__c and Time_With_Tier_2__c respectively).
Now we'll modify the trigger a little to accommodate these new buckets. First, we'll need to figure out if the current user is a Tier 1 or a Tier 2 agent. Let's assume (for the sake of simplicity) that we have two queues, Tier 1 and Tier 2, and that the current case owner will either be in one or the other. It is admittedly a simplistic example, but this is a blog post, not a book.
All you really need to do is add some code after this line:
Double timeSinceLastStatus = BusinessHours.diff(hoursToUse, updatedCase.Last_Status_Change__c, System.now())/3600000.0;
That's the code that actually figures out how many business hours have passed since the last status change. At this point, you have the time you need to add to a bucket -- now all you have to do is find the right bucket to add it to.
In the package, the bucketing code is pretty simple -- either the case spent the time since the last status change in a stop status, or it didn't:
if (stopStatusSet.contains(oldCase.Status)) {
updatedCase.Time_With_Customer__c += timeSinceLastStatus;
} else {
updatedCase.Time_With_Support__c += timeSinceLastStatus;
}We'll have to modify this statement to figure out whether the case owner is in tier 1, and then assign the time to the proper bucket. First, let's find out if the owner in tier 1:
Boolean ownerIsTier1 = false;
//Search the Tier 1 queue for this user
GroupMember groupMember = [Select Id from GroupMember g where g.Group.Name='Tier 1' and
g.UserOrGroupId=:updatedCase.OwnerId];
if (groupMember != null || updatedCase.Owner.Name == 'Tier 1') {
//We found the user in the Tier 1 queue!
ownerIsTier1 = true;
}The variable ownerIsTier1 will now be true if either the owner was found in the Tier 1 queue or if the owner is the Tier 1 queue itself. Now that we've figured that out, we can put the time in the right bucket:
//We decide which bucket to add it to based on whether it was in a stop status before
if (stopStatusSet.contains(oldCase.Status)) {
//This still goes in the customer bucket!
updatedCase.Time_With_Customer__c += timeSinceLastStatus;
} else {
//This is time with support -- let's see which tier's bucket we put it in
if (ownerIsTier1) {
updatedCase.Time_With_Tier_1__c += timeSinceLastStatus;
} else {
updatedCase.Time_With_Tier_2__c += timeSinceLastStatus;
}
}Finally, we change the Case Age In Business Hours total code a little:
if (closedStatusSet.contains(updatedCase.Status)) {
updatedCase.Case_Age_In_Business_Hours__c =
updatedCase.Time_With_Customer__c +
updatedCase.Time_With_Tier_1__c +
updatedCase.Time_With_Tier_2__c;
}And there we have it. Extending this example, you can calculate business hours times for all kinds of buckets. Just imagine the reporting possibilities? I can hear call center managers around the globe rubbing their hands together in glee.
Here's the finished trigger modified per the example given here. I'll leave it to you to write the test cases for it.
A little earlier I noted that these fields are only accurate for closed cases. Why do you suppose that is?
Back in high school, my creative writing teacher used to say that if you want to hold your reader's attention, you have to leave an unresolved mystery until the end. Well, if you're still reading this, I guess it worked -- thanks Mrs. T!
The answer to today's unresolved mystery is that these fields don't measure the difference between the time the case was opened and now -- they measure the difference between the time the case was opened and the last time the case status changed. That's because only a change to the case can trigger the Apex which updates these fields, and I deemed a status change the most appropriate change to perform this sort of recalculation. Once the case is closed, these fields are frozen -- even if you change it from one closed state to another, they won't budge (if you reopen the case, on the other hand, they'll start tallying again). The fact of the matter is, then, that only once the case is closed will you have a final tally of Time With Customer, Time With Support, and Case Age In Business Hours.
