Working With Services - Merge Two Service Results in One Table

I wanted to post this example as it contains some complex use cases and show cases some of the more powerful things you can do with services.  There are a few issues that I will deal with in this example:

1.  A table can only be populated by a single service

2. Services are asynchronous, but in this example I will be demonstrating how to wait for a service to complete before moving on.

Example Description:

The example is a Time Reconciliation system where records from one form are compared with records from another system and then any records that do not match are rendered in a third form.

SETimeSheet.nitro_s

This application contains 3 forms: Event Form, HR Form and Reconcile form.

Challenges:

1. The list of employees to reconcile are returned from a service that queries the Event Form.  We need to loop through each employee and query both the Event Form and the HR Form to get both times to compare.  We cannot use a simple loop here since the services used for getting the Event and HR data are asynchronous.

2. Need to customize what data is shown in the table and be able to add programmatically.

3. Only want to render the table once all the records are added

Implementation Details:

The Event and HR Forms are simple data entry with the unique key for each form being the name of the employee.  All of the heavy lifting is being performed by the Reconcile Form.

So what is the task?  Let's define it in plain English first.  I need to iterate through each employee record, retrieve the event time and the hr time, compare the total hours worked and if they are different then add the information to the reconcile table. 

So what does this mean?  In my form I need a place to store the list of employees, the employee name I am searching and two fields to contain the hours returned from the event and hr forms.  The list of employees will be retrieved from a service when the form is loaded.  Then we will have to loop through the employees put the current name in the temp field, call the two services to get the hours worked, then once both services have returned compare the values and push them into the table if they are different.

The Reconcile Form has 3 services:

1. Get all the employees

This service is a search (SE Time Sheet / Event Time / Search) all the Event Time Records and returns the employees found.  The service has no inputs defined and the employee name is linked to a dropdown for the outputs.  The dropdown options will be loaded by calling this service - select the properties, click the Edit button under Set Options and then select Use a Service and choose the Get Employees service.  All the work will be triggered once this service completes.

2. Get an employees event hours

This service is a retrieve (SE Time Sheet / Event Time / Retrieve) and the inputs link a field with the Employee name and the outputs links the total hours worked to another temporary field.

3. Get an employees HR hours.

This service is a retrieve (SE Time Sheet / HR Time / Retrieve) and the inputs link a field with the Employee name and the outputs links the total hours worked to another temporary field.

Lets look at how the heavy lifting works.  All of the following code exists in the onStart on the Settings tab.

This section defines a listener so that when the employee service is complete we will then perform some additional work. We are going to use the JavaScript API to do some cool things.


//after the employees are loaded then start looping through each employee to get time data
var srv = form.getServiceConfiguration("SC_getEmployees");
  srv.connectEvent('onCallFinished', function(success)
   {
    if(success) {
      var form = app.getForm('F_Reconcile');
      app.getSharedData().empsList = form.getPage('P_NewPage').F_PickEmployee.getOptions();  //1
      app.getSharedData().num = app.getSharedData().empsList.length;  //2

      //start looping through the results
      app.getSharedData().getEmpInfo();
    }
});


//1 - Within every FEB application there is a global sandbox where you can create functions and variables that you can reference anywhere within your application.  This sandbox is accessed by app.getSharedData().  Storing the list of employees in a global array to be accessed later by other pieces.

//2 - Storing the number of employees in a global variable to help control how many times we loop.

The next thing that happens is that we start looping through the employees as defined by the function getEmpInfo():

app.getSharedData().getEmpInfo = function () {

  var empName = get(app.getSharedData().empsList, app.getSharedData().lastProcessed).title;  //3
  form.getBO().F_RecID.setValue(empName);  //4

  //get event and HR time and put into temp form fields
  form.getServiceConfiguration('SC_getEmpEventHours').callService();  //5
  form.getServiceConfiguration('SC_getEmpHRHours').callService();      //6
}


//3 and //4 - Both of these lines are to pull the employee name out of the array and store it in the temporary field on the form.  We have to put it in a temporary field so that it can be used in a service call.  Arrays from a dropdown contain attributes "title" and "value" so we are using the get function to retrieve the employee (defined by the global variable that is keeping track of the last processed - which starts with a value of 0).

//5 and //6 - This initiates the service calls to get the event and hr time.  Since the services are not directly dependent on each other we can call them at the same time.

We can call the services at the same time but we have to make sure that both have completed before we move on to the next part of this process.  In order to do that we need to create listeners for each service.  The structure is exactly the same as we saw for the employee service we just have to change the ID of the service we are referencing.  This is the magic, right here - before we can compare the event hours to the HR hours we have to make sure that both services have completed.  To accomplish this when both services complete I check the value of both fields, if the other field is not empty then that means the current service finished last.  Only the last service will call the evaluateData function

var srv1 = form.getServiceConfiguration("SC_getEmpEventHours");
  srv1.connectEvent('onCallFinished', function(success)
   {
    if(success) {
      var form = app.getForm('F_Reconcile');
      var eventHrs = form.getBO().F_Number.getValue();       //7
      var hrHrs = form.getBO().F_Number0.getValue();

      //if hr was filled in then I am the last one to complete
      if(hrHrs !== "") {
        app.getSharedData.evaluateData("event");                   //8
      }
    }
});

//once we have the event hours get the
var srv2 = form.getServiceConfiguration("SC_getEmpHRHours");
  srv2.connectEvent('onCallFinished', function(success)
   {
    if(success) {
      var form = app.getForm('F_Reconcile');
      var eventHrs = form.getBO().F_Number.getValue();      //7
      var hrHrs = form.getBO().F_Number0.getValue();

      //if event was filled in then I am the last one to complete
      if(eventHrs !== "") {
        app.getSharedData.evaluateData("hr");                         //8
      }
    }
});

//7 - Get the value of the event and hr hours which are populated by the service calls

//8 - Only the service that finished last will cause us to move to the next stage which is to compare the times returned.

Now let's look at how the comparison is performed and the data pushed into the table.

app.getSharedData.evaluateData = function(str) {
  //if their times do not match then add them to the table
  var empName = form.getBO().F_RecID.getValue();
  var eventHrs = form.getBO().F_Number.getValue();
  var hrHrs = form.getBO().F_Number0.getValue();
  var diff = parseFloat(eventHrs) - parseFloat(hrHrs)
  if(diff !== 0) {                                                                                            //9
    var row = form.getBO().F_Table.createNew();
    row.F_EmployeeNameTable.setValue(empName);
    row.F_EventTotalHours.setValue(eventHrs);                                 //10
    row.F_HRTotalHours.setValue(hrHrs);
    row.F_Difference.setValue(diff);
    form.getBO().F_Table.add(row);
  }

  //reset for next emp                                                                               //11
  app.getSharedData().lastProcessed = app.getSharedData().lastProcessed + 1;
  form.getBO().F_RecID.setValue("");
  form.getBO().F_Number.setValue("");
  form.getBO().F_Number0.setValue("");
 
  //get the next employee                                                                         //12
  if(app.getSharedData().lastProcessed < app.getSharedData().num) {
    app.getSharedData().getEmpInfo();
  }

  //if we are processing the last row then set the table to visible     //13
  if(app.getSharedData().lastProcessed === app.getSharedData().num) {
      form.getPage('P_NewPage').F_Table.setVisible(true);
      form.getPage('P_NewPage').F_Text0.setVisible(false);
  }
}

//9 - In these lines we get all the temp field values and calculate the difference between the hours.  If there is a difference then we will push the values into the table.

//10 - This represents how to interact with a table and push values into it.  You create a "template" row, then set all the values of the individual fields and then add that row to the table.

//11 - Once the employee has been pushed into the table (or ignored because there was no difference) then we need to increment the number that keeps track of the employee we are processing and clear the temporary fields

//12 - Here is where we call the getEmpInfo function to start the process all over again with the next employee in the list.

//13 - When the processing is complete we will set the table visible.