Posts

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;

}