Friday, April 4, 2014

Autosave ePerfromance Take III

Since we are working on our tools upgrade to 8.53, I was forced to readdress the auto save functionality that I had previously blogged about.  In my initial posting I was auto saving when the user navigated to the EP_APPR_EDIT5 or EP_APPR_M_EDIT5 pages.  The auto save functionality was no matching buffer issues if the document did not already have a goal attached.  I also attempted to auto save from the two pages above, but it to generated the same error messages.  For now I am leaving them out, and commenting out the JavaScript that calls the save prior to navigating to these pages.  You are welcome to uncomment this code out and play around at your own risk as always. Back to the method I went with:

First, create a derived work record and add the fields HTMLAREA and EP_STORE_PB





Second,  add the fields from step one to the EP_APPR_MAIN1 and EP_APPR_BASE1.




 *Make sure you set the EP_STORE_PB to invisible and modifiable via JavaScript and give it a page field name of ZZ_EP_STORE.

Third, add the following PeopleCode to the work records.



Add some FieldChange PeopleCode to the EP_STORE_PB to save the Document.  This can be copied from the Delivered Save Logic.



/*******************************************
RITM0054746 8.53 TOOLS UPGRADE 
UMBMOD
KCW002

Autosave code changed to for new timout logic
in 8.53
*****************************************/

import EP_FUNCTIONS:EP_Utilities;

Declare Function TransferToDocumentSelection PeopleCode FUNCLIB_EP.EP_APPRAISAL_ID FieldFormula;

Global Record &EP_GBLKEYS_WRK;
Component string &PushButton;



Evaluate %Component
When = Component.EP_APPR_BASE
   &ReturnCode = "RETURN_EST_SAVE";
   &TransferPg = "Page.EP_APPR_BASE1";
   Break;
When = Component.EP_APPR_MAIN
   &ReturnCode = "RETURN_EVL_SAVE";
   &TransferPg = "Page.EP_APPR_MAIN1";
   Break;
When-Other
   Exit;
   Break;
End-Evaluate;

/* Clear error messages, if any, and Save */
EP_BTN_LINK_WRK.EP_HTMLAREA_ERROR1.SetDefault();
EP_BTN_LINK_WRK.EP_HTMLAREA_ERROR2.SetDefault();
EP_BTN_LINK_WRK.EP_HTMLAREA_ERROR1.Visible = False;
EP_BTN_LINK_WRK.EP_HTMLAREA_ERROR2.Visible = False;
&PushButton = "STORE";
DoSaveNow();

Rem: Set Return Status to display a confirmation message;
&EP_GBLKEYS_WRK.EP_RETURN_STATUS.Value = &ReturnCode;
Rem: Set the Return Status HTML Area;
Local EP_FUNCTIONS:EP_Utilities &EP_Utilities = create EP_FUNCTIONS:EP_Utilities();
&EP_Utilities.SetReturnStatusHtmlArea();
Rem: Set focus to the top of the page for the return status message;

If ZZ_DERIVED_EP.EP_STORE_PB.Value = "T" Then
   
   REM DO TIMEOUT;
   ZZ_DERIVED_EP.HTMLAREA.Value = GetHTMLText(HTML.ZZ_EP_TIMEOUT);
Else
   
   ZZ_DERIVED_EP.EP_STORE_PB.Value = "";
   
End-If;

Also add some PeopleCode to the RowInit of the HTMLAREA.

/*******************************************
RITM0054746 8.53 TOOLS UPGRADE 
UMBMOD
KCW002

Autosave code changed to for new timout logic
in 8.53

Setup JavaScript for UMB Autosave timeout
*****************************************/

ZZ_DERIVED_EP.HTMLAREA.Value = GetHTMLText(HTML.ZZ_EP_AUTOSAVE);
 

Fourth, create two HTML Objects.

HTML.ZZ_EP_AUTOSAVE below:
<input type="hidden" name="ZZ_EP_STORE" id="ZZ_EP_STORE" value=""/>

<script type="text/javascript">

threadLock = false;
var xStore = document.getElementById("ZZ_EP_STORE");

function user_function()
{
//alert('starting process');
//debugger;

 if (!threadLock)
  {
   threadLock = true;
   if ("%page" == "EP_APPR_MAIN1" || "%page" == "EP_APPR_BASE1" )
   {


    xStore.value = 'T';
    //alert(xStore.value);
    document.%formname.submit();

   }
  }
}

</script>
<script type="text/javascript" language="javascript">

window.onbeforeunload = function(e) { 
var changes = checkFormChanged(document.%formname);
if (changes && !threadLock)
{
 threadLock = true; 
 if ("%page" == "EP_APPR_MAIN1" || "%page" == "EP_APPR_BASE1" )
 {
            return "You have unsaved changes, please save them."
    };
}
}
</script>

<script type="text/javascript">
function adaptLinks() {
    var links = document.getElementsByTagName('a');
    for (i = 0; i != links.length; i++) {
        links[i].onclick = (function () {
            var origOnClick = links[i].onclick;
            return function (e) { 
                 //add logic to save if the user is adding and item to the document                
                 //linkId = this.id.split('$');                 
                 //if (linkId[0] == 'EP_BTN_LINK_WRK_EP_ADD_ITEM_PB')
                 // {
                 //   xStore.value = 'Z'; 
                 // }
                 //end mod to auto save if the user is adding an item to the document
                 window.onbeforeunload = function(){};
                if (origOnClick != null && !origOnClick()) {
                    return false;
                }                
            }
        })();
    }
}
(function() {
  var originaldisplayTimeoutMsg = displayTimeoutMsg2;
  displayTimeoutMsg2 = function() {
    window.onbeforeunload = function(){};
    
    user_function();
  
  }
})();

adaptLinks();

</script> 
 
 
HTML.ZZ_EP_TIMEOUT below:
<input type="hidden" name="TIMEOUT" value=""/>

<script type="text/javascript">

self.location=timeOutURL; ;

</script> 
 
 

Test it out.

 When the user timesout, the user function will be called instead of the delivered timout logic, this will set the hidden ZZ_EP_STORE field value to 'T'.  This will trigger the fieldchange Peoplecode to trigger and this will save the document and set the HTMLAREA to the ZZ_EP_TIMEOUT.  This will inject JavaScript into the page that will log the user out and present them with the timeout message.  The adaptLinks function will prevent the user from navigating away from the current page when the form is changed and will prompt them to stay and save the information, for example when the user hits the Back button.  But it will also change the value of the hidden ZZ_EP_STORE to 'Z' if the link clicked is one to navigate the user to add something to the document, this will also trigger the FieldChange PeopleCode and it will save the document, but it won't timeout the user.    

Tuesday, April 1, 2014

Mobile Approvals using Integration Broker and Node.js

If you read this blog then you know I have already blogged about using our ESB, DataPower, to process mobile approvals. The concept is to have integration broker server up really simple HTML forms and use a web server or ESB to send the request to Integration broker.  Integration broker then responds with some HTML that looks like this:


Much of the code that generates this HTML is shown with the previous post,  however I omitted the HTML objects.  So looking at how the HTML gets generated and then wrapped in an xml document, this is the code that creates the form for the mobile user to act on.

&ResponseXML = CreateXmlDoc(GetHTMLText(HTML.ZZ_MYAPPROVAL_XML, GetHTMLText(HTML.ZZ_MYAPPROVAL_WRAPPER, %This.postURL, GenToken(), GetHTMLText(HTML.ZZ_MYAPPROVAL_ABS, &processId, &bgn_dt, &emplid, &empl_rcd, &end_dt, &pin_take_num, &transaction_id, &transaction_nbr, &eoawdefn_id, &punch_type, &zz_job_opening_id, &bgn_dt, &end_dt, &gp_pin.DESCR.Value, &reason, &GP_ABS_EVENT.DURATION_ABS.Value, "8", &Util.getUserNamebyEmplid(&emplid)))));

 Starting with the inner most HTML object we find the HTML.ZZ_MYAPPROVAL_ABS:
         

<h2>Absence Request</h2>
<h4>%Bind(:18)</h4>
<br>
     
Start Date: <input type="text" name="StDt"  value="%bind(:12)" readonly><br>
End Date: <input type="text" name="EndDt"  value="%bind(:13)" readonly><br>
Absence Name: <input type="text" name="Name"  value="%Bind(:14)" readonly><br>
Duration: <input type="text" name="Dur"  value="%bind(:17) Hours**" readonly><br>


<br><br>

<input type="submit" value="Approve"  onclick="submitAction(document.approval,'%bind(:1)','%bind(:2)','%bind(:3)','%bind(:4)','%bind(:5)','%bind(:6)','%bind(:7)','%bind(:8)','%bind(:9)','%bind(:10)','%bind(:11)', 'Approve')" >
<input type="submit" value="Deny"  onclick="submitAction(document.approval,'%bind(:1)','%bind(:2)','%bind(:3)','%bind(:4)','%bind(:5)','%bind(:6)','%bind(:7)','%bind(:8)','%bind(:9)','%bind(:10)','%bind(:11)','Deny')" >
<input type="submit" value="Worklist"  onclick="submitAction(document.approval,' ','%bind(:2)','%bind(:3)','%bind(:4)','%bind(:5)','%bind(:6)','%bind(:7)','%bind(:8)','%bind(:9)','%bind(:10)','%bind(:11)','')" >

<p>If you click the "Submit" button, the form-data will be sent to Integration Broker.</p> 


The HTML above then gets wrapped by the HTML.ZZ_MYAPPROVAL_WRAPPER.


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="application/x-www-form-urlencoded;charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.css" />
//<link rel="stylesheet" href="http://shell.acmelabs.io/~kweaver/css/umb-custom-theme.css" />
//<link rel="stylesheet" href="http://www.shell.acmelabs.io/~kweaver/css/umb-custom-theme.css" />
<link rel="stylesheet" href="http://www.connectedchef.com/umb-custom-theme.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.js">

</head>
<script> 
function submitAction(form,  processid, bgn_dt, emplid, empl_rcd, end_dt, pin_take_num, transactionid, transactionnbr, eoawdefn_id, punch_type, zz_job_opening_id, userValue)
{
//alert('starting process');
// set form values


form.processid_e.value = processid;
form.bgn_dt_e.value = bgn_dt;
form.emplid_e.value = emplid;
form.empl_rcd_e.value = empl_rcd;
form.end_dt_e.value = end_dt;
form.pin_take_num_e.value = pin_take_num;
form.transactionid_e.value = transactionid;
form.transaction_nbr_e.value = transactionnbr;
form.eoawdefn_id_e.value = eoawdefn_id;
form.punch_type_e.value = punch_type;
form.zz_job_opening_id_e.value = zz_job_opening_id;
form.useraction_e.value = userValue;
// form.Submit.disabled=true;
form.submit();
}

</script>

<body>
<div id="container_form" data-role="page"  data-theme="a">
<header data-role="header" data-theme="a">
<h1>myApprovals</h1>
 </header>

<form   action="%BIND(:1)" data-ajax="false"  id="approval" name="approval" method="Post" > 
<input type="hidden" name="pstoken_e" value="%BIND(:2)">

<input type="hidden" name="processid_e" value=" ">
<input type="hidden" name="bgn_dt_e" value=" ">
<input type="hidden" name="emplid_e" value=" ">
<input type="hidden" name="empl_rcd_e" value=" ">
<input type="hidden" name="end_dt_e" value=" ">
<input type="hidden" name="pin_take_num_e" value=" ">
<input type="hidden" name="transactionid_e" value=" ">
<input type="hidden" name="transaction_nbr_e" value=" ">
<input type="hidden" name="eoawdefn_id_e" value=" "> 
<input type="hidden" name="punch_type_e" value=" ">
<input type="hidden" name="zz_job_opening_id_e" value=" ">
<input type="hidden" name="useraction_e" value=""> 

%bind(:3) 

</form> 



</body>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.js"></script>
</html>
 
And finally these to HTML objects get wrapped in then HTML.ZZ_MYAPPROVAL_XML

<?xml version="1.0"?>
<data psnonxml="Yes"><![CDATA[
%BIND(:1)
]]></data>
 
 
This last XML wrapper gets taken off via a transformation, but is essential in preventing tools pre 8.52 from creating an Integration Broker Error.


My final entry for this is my node.js web server code to facilitate the serving of the HTML content from Integration Broker. This code below will take any get and server up a simple login screen all posts that have the path /myApprovals then get posted to Integration Broker and the HTML response from above is then served to the device for the user to Approve or Deny.


var http = require('http');
var fs = require('fs');
var querystring = require('querystring');
var utils = require('util');
var request = require("request");

var options = {
  uri: "http://lsys113c.umb.com:13100/PSIGW//HttpListeningConnector?Operation=ZZ_MYAPPROVALS.v1&OperationType=Sync",
  method: "POST",
  form: {
  }
};


var templateEngine = function (template, data) {
  var vars = template.match(/\{\w+\}/g);

  if (!vars) {
    return template;
  }

  var nonVars = template.split(/\{\w+\}/g);
  var output = '';

  for (var i = 0; i < nonVars.length; i++) {
    output += nonVars[i];

    if (i < vars.length) {
      var key = vars[i].replace(/[\{\}]/g, '');
      output += data[key]
    }
  }

  return output;
};

http.createServer(function (req, res) {
  // set up some routes
  switch(req.url) {
    case '/':
  //  console.log("[200] " + req.method + " to " + req.url);
   res.statusCode = 200;
   res.setHeader("Content-Type", "text/html");

  fs.readFile('login.html', function (err, data) {
    if (!err) {
      res.write(templateEngine(data.toString(), {})); // use our template engine here
      res.end();
    }
  });
  break;


   case '/myApprovals':

  if (req.method == 'POST') {
//    console.log("[200] " + req.method + " to " + req.url);
    var fullBody = '';

    function PostData(toIB) {

     options.form = toIB;

     request(options, function(error, response, body) {
     res.writeHead(200, "OK", {'Content-Type': 'text/html'});
     res.write(body);
     res.end();
     });

    };
    
    req.on('data', function(chunk) {
      // append the current chunk of data to the fullBody variable
      fullBody += chunk.toString();
    });
    
    req.on('end', function() {
       // parse the received body data
      var decodedBody = querystring.parse(fullBody);
  //    console.log(decodedBody);
      var psoftRes = PostData(decodedBody);

    });
    
  } else {
    console.log("[405] " + req.method + " to " + req.url);
    res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
    res.end('<html><head><title>405 - Method not supported</title></head><body><h1>Method not supported.</h1></body></html>');
  }
  
  break;

    default:
      res.writeHead(404, "Not found", {'Content-Type': 'text/html'});
      res.end('<html><head><title>404 - Not found</title></head><body><h1>Not found.</h1></body></html>');
      console.log("[404] " + req.method + " to " + req.url);
  };
}).listen(8080);