Posts

From Mrityunjoy Chowdhury: Clone Recurring Shift along with Shift Recurrence Pattern:

Preface

In Spring ’22 Salesforce Release – Scheduler Product introduced the Manage Service Resource Availability by Using Shifts feature which gave flexibility for service resources to work flexible hours instead of the Service Territory Operating Hours. This feature was extended in Winter ’23 with the flexibility to create Recurring Shifts – which gave an easy way for
Territory Managers and Service Resources to create multiple shifts with a single record data entry.

This was made possible by capturing the recurring pattern of the shift during data entry.

And Salesforce saves this recurring shift as a single record with Type field as “Recurring”.

Salesforce Scheduler saves the recurrence pattern when the original recurrence shift record is saved. The information is saved in “RecurrencePattern” field on the same Shift record. An example pattern as saved on the record

RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,FR;COUNT=4;

This information corelates to what gets saved as shown in the screenshot – Weekly recurring Event which occurs every Monday, Wednesday and Friday for the next 4 occurrences and repeats every week.

NOTE: Salesforce Scheduler considers the recurrence period in the service territory’s timezone. The period of recurrence is limited to 120 days. For example, you create a recurring shift on July 1, 2022. You can choose an end date that’s either less than or equal to 120 days from July 1, 2022, or the number of occurrences that are repeated within 120 days from July 1, 2022.

Cloning Shift Records

Scheduler product also gave the option to clone shift records. When a user tries to clone a Shift record with the Type field value set to Recurring, the new record creation screen that opens, Salesforce prepopulates the Start and End Times of the Shift, Status, Service Territory, Service Resource and the Time Slot Type fields. You will notice, it does NOT prepopulate the recurring Pattern (at this point of time).

This is a limitation in the current scheduler product, until this feature gets included in future (Roadmap).

In this blog we are going to look at an option to clone the Shift along with its recurring Pattern using Apex

Cloning Shift Records with Recurring Pattern

We want to keep it simple here in this blog. This is just a quick and dirty work around, but feel free to take it to the next level. As you have noticed when using the Clone button on the Shift (with type as recurring) – the new shift screen that opens has all the information from the previous record copied, except for the Recurrence pattern.

The copying of recurrence pattern is handled in the trigger which will copy it from the Original Shift where we are cloning the record from.

Use the sample trigger code below on the Shift Object to copy the recurrence pattern

trigger CloningRecurringShift on Shift (Before insert) {
for(Shift sh : Trigger.New){
if(sh.Type =='Recurring' && sh.IsClone()==true && sh.RecurrencePattern=='RRULE:INTERVAL=1;'){
String sourceId = sh.getCloneSourceId();
Shift orginalShift = [Select RecurrencePattern from shift where ID=:sh.getCloneSourceId()];
System.debug('Orginal Shift recurring patternt ' + orginalShift.RecurrencePattern);
sh.put('RecurrencePattern',orginalShift.RecurrencePattern);

}
}
}

Note: If updating the other fields (not the recurrence type section) that were copied over when you tried to Clone, the saved record will consider the new updated values.

Demo recording

Coming soon

References
https://help.salesforce.com/s/articleView?id=sf.ls_create_shifts.htm&type=5

By Mrityunjoy Chowdhury: Bulk Shift upload with recurring Shift and Add multiple topic and channel

Bulk Upload Shift for Recurring Shift

Follow the previous steps to know how to upload shift in bulk.

https://help.salesforce.com/s/articleView?id=sf.ls_create_shifts_in_bulk_from_a_csv_file.htm&type=5


Bulk Recurring Shift recording

Bulk Shift Topic and Channel recording

In recurring shift we have added 2 new fields which will allow users to upload daily weekly and monthly shift. A sample request payload is given below for the steps 7 in the above doc.

Recurring Shift request payload 

StartTime,EndTime,ServiceResourceId,ServiceTerritoryId,Status,TimeSlotType,WorkTypeGroupId,WorkTypeId,Type,RecurrencePattern

2022-07-08T03:30:00.000Z,2022-07-08T09:30:00.000Z,0Hnx000000007mGCAQ,0Hhx00000002LzGCAU,Confirmed,Normal,,,Recurring,RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=20221120T000000Z

New Fields are added 

  • Type: Recurring /Regular
  • RecurrencePattern : recurring pattern for daily monthly and weekly. Sample 

Bulk shift upload for multiple Topic (Work type Groups) when health Cloud is disabled.

  1. Upload Appointment topic(Work Type Groups) to Shift
  1. Create the Shift and get the Shift Id for which the user want to add multple topic
  2. Get all the Work Type Group id which user wants want to add to shift
  3. Create the CSV file like below 
  1. Conver this to CSV file format 

ShiftId,WorkTypeId,WorkTypeGroupId,AreAllTopicsSupported

0a0x000000003Q6AAI,,,TRUE

0a0x000000005kuAAA,,,TRUE

0a0x000000003QBAAY,,0VSx00000002UgzGAE,

0a0x000000003QBAAY,,0VSx00000002Uh4GAE,

  1. Call bulk API below

a.

URI/services/data/<version number>/jobs/ingestExample: /services/data/v54.0/jobs/ingest
HTTP MethodPOST
HeadersContent-Type: application/json; charset=UTF-8Accept: application/json
Request Body{“object”:”ShiftWorkTopic”,”contentType”:”CSV”,”operation”:”insert”,”lineEnding”:”CRLF”}

B.

RI/services/data/<version number>/jobs/ingest/<job Id>/batchesUse the contentUrl from the POST method and append a forward slash (/) at its beginning.Example:/services/data/v54.0/jobs/ingest/7505j000005JqZrAAK/batches
HTTP MethodPUT
HeadersContent-Type: text/csvAccept: application/json
Request BodyThe contents of the CSV file. Example:ShiftId,WorkTypeId,WorkTypeGroupId,AreAllTopicsSupported0a0x000000003Q6AAI,,,TRUE0a0x000000005kuAAA,,,TRUE0a0x000000003QBAAY,,0VSx00000002UgzGAE,0a0x000000003QBAAY,,0VSx00000002Uh4GAE,

C. Rest same from steps number 8 from above reference doc

Bulk shift upload for multiple channels and multiple topic

  1. Upload Channel to Shift
  1. Create the Shift and get the Shift Id for which the user want to add multple chanel
  2. Get all the channel Id which user wants want to add to shift
  3. Create the CSV file like below 

Conver this to CSV file format 

ShiftId,EngagementChannelTypeId,AreAllEngmtChnlSupported

0a0x000000005vZAAQ,0eFx00000000006EAA,

0a0x000000005vaAAA,0eFx00000000006EAA,

0a0x000000005vaAAA,0eFx0000000000BEAQ,

0a0x000000005vbAAA,,TRUE

0a0x000000005vcAAA,,TRUE

URI/services/data/<version number>/jobs/ingestExample: /services/data/v54.0/jobs/ingest
HTTP MethodPOST
HeadersContent-Type: application/json; charset=UTF-8Accept: application/json
Request Body{“object”:”ShiftEngagementChannel”,”contentType”:”CSV”,”operation”:”insert”,”lineEnding”:”CRLF”}
URI/services/data/<version number>/jobs/ingest/<job Id>/batchesUse the contentUrl from the POST method and append a forward slash (/) at its beginning.Example:/services/data/v54.0/jobs/ingest/7505j000005JqZrAAK/batches
HTTP MethodPUT
HeadersContent-Type: text/csvAccept: application/json
Request BodyThe contents of the CSV file. Example:ShiftId,EngagementChannelTypeId,AreAllEngmtChnlSupported0a0x000000005vZAAQ,0eFx00000000006EAA,0a0x000000005vaAAA,0eFx00000000006EAA,0a0x000000005vaAAA,0eFx0000000000BEAQ,0a0x000000005vbAAA,,TRUE0a0x000000005vcAAA,,TRUE

Rest same from steps number 8 from above reference doc

From Adwait & Mrityunjoy Chowdhury: Reverse Map Service Appointment to the appropriate shift

Shift is one of the major additions made in Salesforce Scheduler, It provides the user with enormous flexibility and makes the entire process of appointment scheduling less cumbersome. The diagram below shows the interaction between Shift and various other entities involved in scheduling a Service Appointment.

Blank diagram2.jpg

A Service Territory Member can have multiple Shifts associated with it and as a result the time slot selected for the Service Appointment can fall between more than one overlapping shifts (considering both the required and primary Service Resources).

In the following sections we will figure out a way to reverse map all the associated Shifts with corresponding Service Appointment.

Creating a custom object

For completing the purpose of reverse mapping Shift with Service Appointment a custom object(Service appointment shift) is used as a junction object. It comprises of the following custom fields:-

  • ServiceAppointnmentId – Referring to a Service Appointment.
  • ShiftId – Referring to a Shift whose startTime and endTime completely encloses the Service Appointment’s time slot.
  • Modified – A boolean flag to help backtrack all the updates made to Service Appointment.Only the records with the flag value false should be considered as reliable entries.
  • Canceled – A boolean flag which is set to true only when the associated Service Appointment is canceled.
Blank diagram.jpeg

As evident from the design proposed whenever a Service Appointment is scheduled we need to obtain all the Shifts associated with the Service Appointment.For each associated Shift, one record comprising of ServiceAppointmentId,ShiftId

and Modified(initially false) must be made.

For achieving our goal of reverse mapping we will be writing triggers on two entities:

  • Service Appointment
  • Assigned Resource

Creating a Service Appointment

We must do the reverse mapping the moment a Service Appointment is created.Whenever a Service Appointment is created necessary inserts are made in Assigned Resource and this will invoke the trigger that we will be writing for Assigned Resource .

Pseudocode

Let us first discuss the pseudocode for triggers when new Service Appointments are created.

  • Create a trigger which runs after inserts are made in Assigned Resource.
  • Fetch all the linked Service Appointments using the just inserted Assigned Resource records.
  • Check for any previous entries for obtained Service Appointments in the custom object
    and for all those records set the Modified as true.
  • Design a map of Service Appointment and all the required resources for that appointment.
  • Loop through all the Service Appointments in the map and obtain the associated Shifts of the Service Resource.
  • Do the inserts for Service Appointment and Shift mapping records.

Updating a Service Appointment

Whenever a Service Appointment is modified we can make changes in the time slot and the required resources of the Service Appointment.Once a Service Appointment is updated we must set the Modified field for all the records of this Service Appointment in the custom object to true and obtain new associated Shifts. Let’s have a look at the possible scenarios and the way we are handling it.

  • Deletion of required SR from Appointment(will invoke an after delete trigger on Assigned Resource)
  • Addition of required SR to Appointment(will invoke an after insert trigger on Assigned Resource)
  • Moving of SR from required to optional resource(will invoke an after update trigger on Assigned Resource)
  • Moving of SR from optional to required resource(will invoke an after update trigger on Assigned Resource)
  • Change in Appointment start time and/or end time
  • Change in Appointment WTG/WT (Appointment duration)
  • Changing the time slot at the same time altering the Assigned Resources.

For these cases we will be using one update and a delete trigger on Assigned Resource wherein we implement a similar logic as mentioned above.

Disclaimer: The following code is meant to be verbose and easily understandable from a Salesforce Developer perspective. Given a choice between performance vs readability I have strived for the latter. It is a proof of concept to demonstrate the feature and should be modified and tested thoroughly as per different data shapes and existing code in the org.

trigger getassociatedshifts on AssignedResource (after insert,after update,after delete) {

   /* fetching all the linked service appointments from the trigger */

   List<ServiceAppointment> AllAppointments = new List<ServiceAppointment>();

   if(Trigger.isDelete) {

       List<String> AffectedAppointments = new List<String>();

       For(AssignedResource a:Trigger.old) {

           AffectedAppointments.add(a.ServiceAppointmentId); 

       }

       AllAppointments = [Select Id,ServiceTerritoryId,SchedStartTime,SchedEndTime,WorkTypeId from ServiceAppointment where Id In :AffectedAppointments];

   } else {

       AllAppointments = [Select Id,ServiceTerritoryId,SchedStartTime,SchedEndTime,WorkTypeId from ServiceAppointment where Id In (Select ServiceAppointmentId from AssignedResource where Id In :Trigger.New)];

   }

   /*

     check for any previous entries for obtained service appointments in the custom object

     and for all those records set the modified flag as true

  */

   List<Service_appointment_shift__c> ServiceAppointmentShiftToUpdate =new List<Service_appointment_shift__c>();

   For(Service_appointment_shift__c all:[Select id,modified__c from Service_appointment_shift__c where Service_Appointment__c In :AllAppointments]) {

       all.Modified__c = true;

       ServiceAppointmentShiftToUpdate.add(all);

   }

   update ServiceAppointmentShiftToUpdate;

   /* Making call to helper class for necessary inserts */

   ServiceAppointmentShiftMapping.InsertNewRecords(AllAppointments);

}

Helper class

public class ServiceAppointmentShiftMapping {

   public static void InsertNewRecords(List<ServiceAppointment> AllAppointments) {

       /*creating a map of ServiceAppointmentId and List<ServiceResourceId> */

       Map<Id,List<Id> > ServiceAppointmentToAllServiceResources = new Map<Id,List<Id> >();

       /*for creating the map we need list of required resource and service appointment */

       List<AssignedResource> ServiceResourcetoServiceAppointment =new List<AssignedResource>();

       ServiceResourceToServiceAppointment = [Select ServiceResourceId,ServiceAppointmentId from AssignedResource where ServiceAppointmentId in:AllAppointments

                AND  IsRequiredResource = True];

       for(AssignedResource temp:ServiceResourceToServiceAppointment) {

           /* check if the key already exists or not*/

           if(ServiceAppointmentToAllServiceResources.containsKey(temp.ServiceAppointmentId)) {

               List<Id> ResourceList = ServiceAppointmentToAllServiceResources.get(temp.ServiceAppointmentId);

               ResourceList.add(temp.ServiceResourceId);

               /*insert the key value pair*/

               ServiceAppointmentToAllServiceResources.put(temp.ServiceAppointmentId,ResourceList);

           } else {

               ServiceAppointmentToAllServiceResources.put(temp.ServiceAppointmentId,new List<Id>{temp.ServiceResourceId});

           }

       }

       /*All the new records for custom object will be stored in this*/ 

       List<Service_appointment_shift__c> AllNewEntries =new List<Service_appointment_shift__c>(); 

       For(ServiceAppointment current:AllAppointments) {

           List<Id> ResourceId = new List<Id>();

           /*obtain the list of resources from the map */

           ResourceId = ServiceAppointmentToAllServiceResources.get(current.Id);

           //fetch associated shits with wtg and without wtg

           List<Shift> probableShifts= new List<Shift>();

           probableShifts =[Select Id from Shift where ServiceResourceId IN :ResourceId

                            AND ServiceTerritoryId = :current.ServiceTerritoryId

                            AND StartTime <= :current.SchedStartTime

                            AND EndTime >= :current.SchedEndTime

                            AND Status =’Confirmed’

                            AND WorkTypeGroupId IN (Select WorkTypeGroupId from WorkTypeGroupMember where WorkTypeId =:current.WorkTypeId)

                          ];

           List<Shift> probableShiftsNoWtg= new List<Shift>();

           probableShiftsNoWtg =[Select Id from Shift where ServiceResourceId IN :ResourceId

                                 AND ServiceTerritoryId = :current.ServiceTerritoryId

                                 AND StartTime <= :current.SchedStartTime

                                 AND EndTime >= :current.SchedEndTime

                                 AND Status =’Confirmed’

                                 AND WorkTypeGroupId =”

                            ]; 

           /* add all the records to be inserted in the custom object in a list*/

           for(Shift a:probableShifts) {

               Service_appointment_shift__c next = new Service_appointment_shift__c(Service_Appointment__c = current.Id,Shift__c = a.Id,Modified__c = false);

               AllNewEntries.add(next);

           }

           for(Shift a:probableShiftsNoWtg) {

               Service_appointment_shift__c next = new Service_appointment_shift__c(Service_Appointment__c = current.Id,Shift__c = a.Id,Modified__c = false);

               AllNewEntries.add(next);

           }

        }

        Insert AllNewEntries;

   }

}

  • Service Appointment is canceled

trigger cancelServiceAppointment on ServiceAppointment (after update)

{

   /*

     This trigger will be called whenever a service appointment is update(time slot is

     changed,assigned resources are changed,status is changed etc)

     but the only purpose of this trigger is to handle the case when the service appointment

     is canceled

   */

   /*

      All the Service Appointments that are canceled.

   */

   List<ServiceAppointment> changed = new List<ServiceAppointment>();

   For(Integer i=0;i<Trigger.New.size();++i) {

       ServiceAppointment justUpdated = Trigger.New[i];

       /*

          checking the status of updated Service Appointment

       */

       if(justUpdated.Status == ‘Canceled’) {

           changed.add(justUpdated);

       }

   }

   /*

       Setting the canceled flag and modified flag of the obtained service Appointment in custom object

   */

   List<Service_appointment_shift__c> ServiceAppointmentShiftToUpdate =new List<Service_appointment_shift__c>();

   For(Service_appointment_shift__c all:[Select id,modified__c from Service_appointment_shift__c where Service_Appointment__c In :changed]) {

       all.Canceled__c =true;

       all.Modified__c = true;

       ServiceAppointmentShiftToUpdate.add(all);

   }

   /* updating the records */

   update ServiceAppointmentShiftToUpdate;

}