diff --git a/LEAF-Automated-Tests b/LEAF-Automated-Tests
index a7f6435a6..8d87883ef 160000
--- a/LEAF-Automated-Tests
+++ b/LEAF-Automated-Tests
@@ -1 +1 @@
-Subproject commit a7f6435a65ae82b646ebfd70cd3ea92015b96a38
+Subproject commit 8d87883efbef6bf4e028868c2db41ec42c2d0a35
diff --git a/LEAF_Nexus/css/editor.css b/LEAF_Nexus/css/editor.css
index 9b02159cc..369609f77 100644
--- a/LEAF_Nexus/css/editor.css
+++ b/LEAF_Nexus/css/editor.css
@@ -91,4 +91,32 @@
#editor_tools>span:hover {
background-color: #2372b0;
color: white;
+}
+
+#visual_alert_box_container {
+ max-width: 354px;
+ box-sizing: border-box;
+ margin-left: auto;
+}
+#visual_alert_box {
+ text-align: left;
+ position: relative;
+ padding: 0.25em;
+ background-color:#fff;
+}
+#visual_alert_box_container label {
+ display: flex;
+ justify-content:flex-end;
+ padding-top: 0.25rem;
+}
+#visual_alert_box.hide, #visual_alert_box_container label.hide {
+ display: none;
+}
+
+#visual_alert_box_container label input {
+ margin: 0 0 0 0.25rem;
+}
+
+#visual_alert_box_title {
+ font-weight: bolder;
}
\ No newline at end of file
diff --git a/LEAF_Nexus/js/ui/position.js b/LEAF_Nexus/js/ui/position.js
index a4a956f5b..3fb289027 100644
--- a/LEAF_Nexus/js/ui/position.js
+++ b/LEAF_Nexus/js/ui/position.js
@@ -47,7 +47,8 @@ position.prototype.initialize = function (parentContainerID) {
$("#" + prefixedPID + "_title").on("click keydown mouseenter", function(ev) {
//if they are newly focusing an open card just update the tab focus
const isNewFocus = document.activeElement !== ev.currentTarget;
- if (ev.type === "click" && isNewFocus) {
+ const cardDataAttr = ev.currentTarget.parentNode.getAttribute('data-moving');
+ if (ev.type === "click" && isNewFocus || cardDataAttr === "true") {
ev.currentTarget.focus();
return;
}
diff --git a/LEAF_Nexus/templates/editor.tpl b/LEAF_Nexus/templates/editor.tpl
index 3b7c4c3db..028c54b3c 100644
--- a/LEAF_Nexus/templates/editor.tpl
+++ b/LEAF_Nexus/templates/editor.tpl
@@ -19,9 +19,19 @@ placeholder
+
Loading...
@@ -120,6 +130,17 @@ function applyZoomLevel() {
}
}
+function toggleHideClass( elementID = '') {
+ let el = document.getElementById(elementID);
+ if(el !== null) {
+ if(el.classList.contains('hide')) {
+ el.classList.remove('hide');
+ } else {
+ el.classList.add('hide');
+ }
+ }
+}
+
function viewSupervisor() {
$.ajax({
url: './api/position//supervisor',
@@ -134,7 +155,7 @@ function viewSupervisor() {
});
}
-function saveLayout(positionID) {
+function saveLayout(positionID, repaint = false) {
const position = $('#' + positions[positionID].getDomID()).offset();
let newPosition = new Object();
newPosition.x = parseInt(position.left);
@@ -153,6 +174,9 @@ function saveLayout(positionID) {
if (+res === 1) {
positions[positionID].x = newPosition.x;
positions[positionID].y = newPosition.y;
+ if(repaint === true) {
+ jsPlumb.repaintEverything();
+ }
}
$('#busyIndicator').css('visibility', 'hidden');
},
@@ -259,72 +283,78 @@ function addSupervisor(positionID) {
}
function moveCoordinates(prefix, position) {
+ let card = document.getElementById(prefix + position);
+ const cardStyle = window.getComputedStyle(card);
+ const originalTopOrg = cardStyle.getPropertyValue('top');
+ const originalLeftOrg = cardStyle.getPropertyValue('left');
+
const moveCard = (e) => {
if (e.key === "Tab") {
- saveLayout(position);
+ saveLayout(position, true);
$('#' + prefix + position).css('box-shadow', 'none');
- $('#visual_alert_box').css('opacity', '0');
+ $('#visual_alert_box').addClass('hide');
document.removeEventListener('keydown', moveCard);
return;
} else if (controlKeys.includes(e.key)) {
e.preventDefault();
+ const cardStyle = window.getComputedStyle(card);
+ const topValue = Number(cardStyle.getPropertyValue('top').replace("px", ""));
+ const leftValue = Number(cardStyle.getPropertyValue('left').replace("px", ""));
+ //only show extra info if keyboard is being used to move the card
+ if(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
+ card.setAttribute("data-moving", "true");
+ if( $('#visual_alert_box_container label').hasClass('hide')) {
+ $('#visual_alert_box_container label').removeClass('hide');
+ $('#visual_alert_box').removeClass('hide');
+ } else {
+ if(document.getElementById('MovementInfoToggle').checked !== true) {
+ $('#visual_alert_box').removeClass('hide');
+ }
+ }
+ } else {
+ card.removeAttribute("data-moving");
+ }
switch (e.key) {
case "ArrowLeft":
- leftValue = (Number(leftValue) - 10);
- card.style.left = leftValue + "px";
+ card.style.left = leftValue - 10 + "px";
break;
case "ArrowRight":
- leftValue = (Number(leftValue) + 10);
- card.style.left = leftValue + "px";
+ card.style.left = leftValue + 10 + "px";
break;
case "ArrowUp":
- topValue = (Number(topValue) - 10);
- card.style.top = topValue + "px";
+ card.style.top = topValue - 10 + "px";
break;
case "ArrowDown":
- topValue = (Number(topValue) + 10);
- card.style.top = topValue + "px";
+ card.style.top = topValue + 10 + "px";
break;
case "Enter":
// save the coordinates as they are now
- saveLayout(position);
- abort = true;
+ saveLayout(position, true);
break;
case "Escape":
// revert coordinates back to original
- card.style.top = topOrg;
- card.style.left = leftOrg;
- abort = true;
+ card.style.top = originalTopOrg;
+ card.style.left = originalLeftOrg
+ $('#' + prefix + position).css('box-shadow', 'none');
+ $('#visual_alert_box').addClass('hide');
+ document.removeEventListener('keydown', moveCard);
break;
}
-
- if (abort) {
- $('#' + prefix + position).css('box-shadow', 'none');
- $('#visual_alert_box').css('opacity', '0');
- document.removeEventListener('keydown', moveCard);
- return;
- }
}
};
$('div.positionSmall').css('box-shadow', 'none');
$('#' + prefix + position).css('box-shadow', ' 0 0 6px #c00');
- let alert_box = document.getElementById('visual_alert_box');
+ let alert_box_card_title = document.getElementById('visual_alert_box_title');
let title = document.getElementById(prefix + position + '_title');
let titleText = title.innerHTML;
- alert_box.innerHTML = "You are moving the " + titleText + " card
Esc - return to original location
Enter - save current location
Tab - Save and move to next card";
- $('#visual_alert_box').css('opacity', '100');
+ alert_box_card_title.textContent = titleText;
- let card = document.getElementById(prefix + position);
- let cardStyle = window.getComputedStyle(card);
- let topOrg = cardStyle.getPropertyValue('top');
- let leftOrg = cardStyle.getPropertyValue('left');
- let topValue = cardStyle.getPropertyValue('top').replace("px", "");
- let leftValue = cardStyle.getPropertyValue('left').replace("px", "");
- let key;
- let abort = false;
const controlKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Escape'];
document.addEventListener('keydown', moveCard);
- title.addEventListener('blur', () => document.removeEventListener('keydown', moveCard));
+ title.addEventListener('blur', () => {
+ card.removeAttribute("data-moving");
+ document.removeEventListener('keydown', moveCard)
+ });
}
function addSubordinate(parentID) {
diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php
index d92116efd..0a3922f60 100644
--- a/LEAF_Request_Portal/sources/Form.php
+++ b/LEAF_Request_Portal/sources/Form.php
@@ -573,15 +573,16 @@ public function getIndicator($indicatorID, $series, $recordID = null, $parseTemp
$form[$idx]['value'] = $this->fileToArray($data[0]['data']);
$form[$idx]['raw'] = $data[0]['data'];
}
- // special handling for org chart data types
+ // special handling for org chart data types (request header questions, edited report builder cells)
else if ($data[0]['format'] == 'orgchart_employee'
&& !empty($data[0]['data']))
{
- $empRes = $this->employee->lookupEmpUID($data[0]['data']);
- if (!empty($empRes)) {
- $form[$idx]['displayedValue'] = "{$empRes[0]['firstName']} {$empRes[0]['lastName']}";
- } else {
- $form[$idx]['displayedValue'] = '';
+ $form[$idx]['displayedValue'] = '';
+ if (isset($data[0]['metadata'])) {
+ $orgchartInfo = json_decode($data[0]['metadata'], true);
+ if(!empty(trim($orgchartInfo['lastName']))) {
+ $form[$idx]['displayedValue'] = "{$orgchartInfo['firstName']} {$orgchartInfo['lastName']}";
+ }
}
}
else if ($data[0]['format'] == 'orgchart_position'
@@ -1008,9 +1009,9 @@ public function getRecordInfo($recordID)
);
}
- $dir = new VAMC_Directory;
- $user = $dir->lookupLogin($res[0]['userID']);
- $name = isset($user[0]) ? "{$user[0]['Fname']} {$user[0]['Lname']}" : $res[0]['userID'];
+ $userMetadata = json_decode($res[0]['userMetadata'], true);
+ $name = isset($userMetadata) && !empty(trim($userMetadata['lastName'])) ?
+ "{$userMetadata['firstName']} {$userMetadata['lastName']}" : $res[0]['userID'];
$data = array('name' => $name,
'service' => $res[0]['service'],
@@ -1095,6 +1096,7 @@ public function isCategory($categoryID)
*/
private function writeDataField($recordID, $key, $series)
{
+
if (is_array($_POST[$key])) //multiselect, checkbox, grid items
{
$_POST[$key] = XSSHelpers::scrubObjectOrArray($_POST[$key]);
@@ -2658,16 +2660,14 @@ public function getCustomData(array $recordID_list, string|null $indicatorID_lis
}
}
break;
- case 'orgchart_employee':
- $empRes = $this->employee->lookupEmpUID($item['data']);
- if (isset($empRes[0]))
- {
- $item['data'] = "{$empRes[0]['firstName']} {$empRes[0]['lastName']}";
- $item['dataOrgchart'] = $empRes[0];
- }
- else
- {
- $item['data'] = '';
+ case 'orgchart_employee': //report builder cells
+ $item['data'] = '';
+ if (isset($item['metadata'])) {
+ $orgchartInfo = json_decode($item['metadata'], true);
+ if(!empty(trim($orgchartInfo['lastName']))) {
+ $item['data'] = "{$orgchartInfo['firstName']} {$orgchartInfo['lastName']}";
+ $item['dataOrgchart'] = $orgchartInfo;
+ }
}
break;
case 'orgchart_position':
@@ -2785,14 +2785,14 @@ public function getActionComments(int $recordID): array
} else {
$vars = array(':recordID' => $recordID);
- $sql = 'SELECT actionTextPasttense, comment, time, userID
+ $sql = 'SELECT actionTextPasttense, comment, time, userID, userMetadata
FROM action_history
LEFT JOIN dependencies USING (dependencyID)
LEFT JOIN actions USING (actionType)
WHERE recordID = :recordID
AND comment != ""
UNION
- SELECT "Note Added", note, timestamp, userID
+ SELECT "Note Added", note, timestamp, userID, userMetadata
FROM notes
WHERE recordID = :recordID
AND deleted IS NULL
@@ -2800,13 +2800,12 @@ public function getActionComments(int $recordID): array
$res = $this->db->prepared_query($sql, $vars);
- $dir = new VAMC_Directory;
-
$total = count($res);
-
for ($i = 0; $i < $total; $i++) {
- $user = $dir->lookupLogin($res[$i]['userID']);
- $name = isset($user[0]) ? "{$user[0]['Fname']} {$user[0]['Lname']}" : $res[$i]['userID'];
+ $userMetadata = json_decode($res[$i]['userMetadata'], true);
+ $name = isset($userMetadata) && !empty(trim($userMetadata['lastName'])) ?
+ "{$userMetadata['firstName']} {$userMetadata['lastName']}" : $res[$i]['userID'];
+
$res[$i]['name'] = $name;
}
@@ -2958,11 +2957,9 @@ public function setInitiator($recordID, $userID)
userID=:userID, userMetadata=:userMetadata
WHERE recordID=:recordID', $vars);
- // write log entry
- $dir = new VAMC_Directory;
- $user = $dir->lookupLogin($userID);
- $name = isset($user[0]) ? "{$user[0]['Fname']} {$user[0]['Lname']}" : $userID;
+ $newInitiatorInfo = json_decode($newInitiatorMetadata, true);
+ $name = "{$newInitiatorInfo['firstName']} {$newInitiatorInfo['lastName']}";
$actionUserID = $this->login->getUserID();
$actionUserMetadata = $this->employee->getInfoForUserMetadata($actionUserID, false);
@@ -3765,14 +3762,27 @@ public function query(string $inQuery): mixed
WHERE format = 'orgchart_employee') rj_OCEmployeeData ON (lj_data.indicatorID = rj_OCEmployeeData.indicatorID) ";
}
- if ($joinInitiatorNames)
- {
- $joins .= "LEFT JOIN (SELECT userName, lastName, firstName FROM {$this->oc_dbName}.employee) lj_OCinitiatorNames ON records.userID = lj_OCinitiatorNames.userName ";
+
+ //joinInitiatorNames backwards compat - additional SQL for records.userMetadata replaces the previous join with orgchart.employee.
+ //userMetadata properties are empty for accounts that were inactive when prior metadata values were updated. Use lastName to check if empty
+ //because userName might be removed in the future. Display 'userID (inactive user)' instead of 'null, null' if metadata is empty.
+ $initiatorNamesSQL = '';
+ if ($joinInitiatorNames) {
+ $initiatorNamesSQL = ',
+ IF(
+ TRIM(JSON_VALUE(`userMetadata`, "$.lastName")) != "",
+ JSON_VALUE(`userMetadata`, "$.firstName"), "(inactive user)"
+ ) AS `firstName`,
+ IF(
+ TRIM(JSON_VALUE(`userMetadata`, "$.lastName")) != "",
+ JSON_VALUE(`userMetadata`, "$.lastName"), `userID`
+ ) AS `lastName`';
}
+ $resSQL = 'SELECT * ' . $initiatorNamesSQL . ' FROM `records` ' . $joins . ' WHERE ' . $conditions . $sort . $limit;
if(isset($_GET['debugQuery'])) {
if($this->login->checkGroup(1)) {
- $debugQuery = str_replace(["\r", "\n","\t", "%0d","%0a","%09","%20", ";"], ' ', 'SELECT * FROM records ' . $joins . 'WHERE ' . $conditions . $sort . $limit);
+ $debugQuery = str_replace(["\r", "\n","\t", "%0d","%0a","%09","%20", ";"], ' ', $resSQL);
$debugVars = [];
foreach($vars as $key => $value) {
if(strpos($key, ':data') !== false
@@ -3786,22 +3796,20 @@ public function query(string $inQuery): mixed
header('X-LEAF-Query: '. str_replace(array_keys($debugVars), $debugVars, $debugQuery));
- return $res = $this->db->prepared_query('EXPLAIN SELECT * FROM records
- ' . $joins . '
- WHERE ' . $conditions . $sort . $limit, $vars);
+ return $res = $this->db->prepared_query('EXPLAIN ' . $resSQL, $vars);
}
else {
return XSSHelpers::scrubObjectOrArray(json_decode(html_entity_decode(html_entity_decode($_GET['q'])), true));
}
}
- $res = $this->db->prepared_query('SELECT * FROM records
- ' . $joins . '
- WHERE ' . $conditions . $sort . $limit, $vars);
+
+ $res = $this->db->prepared_query($resSQL, $vars);
$data = array();
$recordIDs = '';
foreach ($res as $item)
{
+ $item['userMetadata'] = json_decode($item['userMetadata'], true);
if(!isset($data[$item['recordID']])) {
$recordIDs .= $item['recordID'] . ',';
}
@@ -3872,17 +3880,15 @@ public function query(string $inQuery): mixed
if ($joinActionHistory)
{
- $dir = new VAMC_Directory;
-
$actionHistorySQL =
- 'SELECT recordID, stepID, userID, time, description,
+ 'SELECT recordID, stepID, userID, userMetadata, time, description,
actionTextPasttense, actionType, comment
FROM action_history
LEFT JOIN dependencies USING (dependencyID)
LEFT JOIN actions USING (actionType)
WHERE recordID IN (' . $recordIDs . ')
UNION
- SELECT recordID, "-5", userID, timestamp, "Note Added",
+ SELECT recordID, "-5", userID, userMetadata, timestamp, "Note Added",
"Note Added", "LEAF_note", note
FROM notes
WHERE recordID IN (' . $recordIDs . ')
@@ -3892,8 +3898,11 @@ public function query(string $inQuery): mixed
$res2 = $this->db->prepared_query($actionHistorySQL, array());
foreach ($res2 as $item)
{
- $user = $dir->lookupLogin($item['userID'], true);
- $name = isset($user[0]) ? "{$user[0]['Fname']} {$user[0]['Lname']}" : $res[0]['userID'];
+ $item['userMetadata'] = json_decode($item['userMetadata'], true);
+ $userMetadata = $item['userMetadata'];
+ $name = isset($userMetadata) && trim("{$userMetadata['firstName']} {$userMetadata['lastName']}") !== "" ?
+ "{$userMetadata['firstName']} {$userMetadata['lastName']}" : $item['userID'];
+
$item['approverName'] = $name;
$data[$item['recordID']]['action_history'][] = $item;
@@ -3931,9 +3940,7 @@ public function query(string $inQuery): mixed
}
if ($joinRecordResolutionBy === true) {
- $dir = new VAMC_Directory;
-
- $recordResolutionBySQL = "SELECT recordID, action_history.userID as resolvedBy, action_history.stepID, action_history.actionType
+ $recordResolutionBySQL = "SELECT recordID, action_history.userID as resolvedBy, action_history.userMetadata, action_history.stepID, action_history.actionType
FROM action_history
LEFT JOIN records USING (recordID)
INNER JOIN workflow_routes USING (stepID)
@@ -3948,8 +3955,9 @@ public function query(string $inQuery): mixed
$res2 = $this->db->prepared_query($recordResolutionBySQL, array());
foreach ($res2 as $item) {
- $user = $dir->lookupLogin($item['resolvedBy'], true);
- $nameResolved = isset($user[0]) ? "{$user[0]['Lname']}, {$user[0]['Fname']} " : $item['resolvedBy'];
+ $userMetadata = json_decode($item['userMetadata'], true);
+ $nameResolved = isset($userMetadata) && trim("{$userMetadata['firstName']} {$userMetadata['lastName']}") !== "" ?
+ "{$userMetadata['firstName']} {$userMetadata['lastName']} " : $item['resolvedBy'];
$data[$item['recordID']]['recordResolutionBy']['resolvedBy'] = $nameResolved;
}
}
@@ -4361,7 +4369,7 @@ private function buildFormTree($id, $series = null, $recordID = null, $parseTemp
{
$var = array(':series' => (int)$series,
':recordID' => (int)$recordID, );
- $res2 = $this->db->prepared_query('SELECT data, timestamp, indicatorID, groupID, userID FROM data
+ $res2 = $this->db->prepared_query('SELECT data, metadata, timestamp, indicatorID, groupID, userID FROM data
LEFT JOIN indicator_mask USING (indicatorID)
WHERE indicatorID IN (' . $indicatorList . ') AND series=:series AND recordID=:recordID', $var);
@@ -4369,6 +4377,7 @@ private function buildFormTree($id, $series = null, $recordID = null, $parseTemp
{
$idx = $resIn['indicatorID'];
$data[$idx]['data'] = isset($resIn['data']) ? $resIn['data'] : '';
+ $data[$idx]['metadata'] = isset($resIn['metadata']) ? $resIn['metadata'] : null;
$data[$idx]['timestamp'] = isset($resIn['timestamp']) ? $resIn['timestamp'] : 0;
$data[$idx]['groupID'] = isset($resIn['groupID']) ? $resIn['groupID'] : null;
$data[$idx]['userID'] = isset($resIn['userID']) ? $resIn['userID'] : '';
@@ -4435,14 +4444,15 @@ private function buildFormTree($id, $series = null, $recordID = null, $parseTemp
$child[$idx]['value'] = $this->fileToArray($data[$idx]['data']);
}
- // special handling for org chart data types
+ // special handling for org chart data types (request subquestions / child)
if ($field['format'] == 'orgchart_employee')
{
- $empRes = $this->employee->lookupEmpUID($data[$idx]['data']);
$child[$idx]['displayedValue'] = '';
- if (isset($empRes[0]))
- {
- $child[$idx]['displayedValue'] = ($child[$idx]['isMasked']) ? '[protected data]' : "{$empRes[0]['firstName']} {$empRes[0]['lastName']}";
+ if (isset($data[$idx]['metadata'])) {
+ $orgchartInfo = json_decode($data[$idx]['metadata'], true);
+ if(!empty(trim($orgchartInfo['lastName']))) {
+ $child[$idx]['displayedValue'] = "{$orgchartInfo['firstName']} {$orgchartInfo['lastName']}";
+ }
}
}
if ($field['format'] == 'orgchart_position')
diff --git a/LEAF_Request_Portal/sources/FormWorkflow.php b/LEAF_Request_Portal/sources/FormWorkflow.php
index e5960b8e6..b06dcb250 100644
--- a/LEAF_Request_Portal/sources/FormWorkflow.php
+++ b/LEAF_Request_Portal/sources/FormWorkflow.php
@@ -447,7 +447,7 @@ public function getRecordsDependencyData(object $form, array $records, bool $sel
$strSQL = "";
if(!$selectUnfilled) {
- $strSQL = "SELECT dependencyID, recordID, stepID, stepTitle, blockingStepID, workflowID, serviceID, filled, stepBgColor, stepFontColor, stepBorder, `description`, indicatorID_for_assigned_empUID, indicatorID_for_assigned_groupID, jsSrc, userID, requiresDigitalSignature FROM records_workflow_state
+ $strSQL = "SELECT dependencyID, recordID, stepID, stepTitle, blockingStepID, workflowID, serviceID, filled, stepBgColor, stepFontColor, stepBorder, `description`, indicatorID_for_assigned_empUID, indicatorID_for_assigned_groupID, jsSrc, userID, userMetadata, requiresDigitalSignature FROM records_workflow_state
LEFT JOIN records USING (recordID)
LEFT JOIN workflow_steps USING (stepID)
LEFT JOIN step_dependencies USING (stepID)
@@ -456,7 +456,7 @@ public function getRecordsDependencyData(object $form, array $records, bool $sel
WHERE recordID IN ({$recordIDs})";
}
else {
- $strSQL = "SELECT dependencyID, recordID, stepTitle, serviceID, `description`, indicatorID_for_assigned_empUID, indicatorID_for_assigned_groupID, userID FROM records_workflow_state
+ $strSQL = "SELECT dependencyID, recordID, stepTitle, serviceID, `description`, indicatorID_for_assigned_empUID, indicatorID_for_assigned_groupID, userID, userMetadata FROM records_workflow_state
LEFT JOIN records USING (recordID)
LEFT JOIN workflow_steps USING (stepID)
LEFT JOIN step_dependencies USING (stepID)
@@ -476,6 +476,7 @@ public function getRecordsDependencyData(object $form, array $records, bool $sel
$groupDesignatedRecords = []; // map of records using "group designated"
$groupDesignatedIndicators = []; // map of indicators using "group designated"
foreach ($res as $i => $record) {
+ $res[$i]['userMetadata'] = json_decode($res[$i]['userMetadata'], true);
// override access if user is in the admin group
$res[$i]['isActionable'] = $this->login->checkGroup(1); // initialize isActionable
@@ -494,8 +495,8 @@ public function getRecordsDependencyData(object $form, array $records, bool $sel
$approver = $dir->lookupLogin($res[$i]['userID']);
if (empty($approver[0]['Fname']) && empty($approver[0]['Lname'])) {
- $res[$i]['description'] = $res[$i]['stepTitle'] . ' (Requestor followup)';
- $res[$i]['approverName'] = '(Requestor followup)';
+ $res[$i]['description'] = $res[$i]['stepTitle'] . ' (Inactive User)';
+ $res[$i]['approverName'] = '(Inactive User)';
$res[$i]['approverUID'] = $res[$i]['userID'];
}
else {
@@ -543,8 +544,8 @@ public function getRecordsDependencyData(object $form, array $records, bool $sel
$dir = $this->getDirectory();
$approver = $dir->lookupLogin($res[$i]['userID']);
if (empty($approver[0]['Fname']) && empty($approver[0]['Lname'])) {
- $res[$i]['description'] = $res[$i]['stepTitle'] . ' (Requestor followup)';
- $res[$i]['approverName'] = '(Requestor followup)';
+ $res[$i]['description'] = $res[$i]['stepTitle'] . ' (Inactive User)';
+ $res[$i]['approverName'] = '(Inactive User)';
$res[$i]['approverUID'] = $res[$i]['userID'];
}
else {
@@ -711,16 +712,19 @@ public function getLastAction(): array|null|int
LIMIT 1';
$res = $this->db->prepared_query($strSQL, $vars);
}
+ if (isset($res[0])) {
+ $res[0]['userMetadata'] = json_decode($res[0]['userMetadata'], true);
+ }
// dependencyID -1 is for a person designated by the requestor
if (isset($res[0])
&& $res[0]['dependencyID'] == -1)
{
- $dir = $this->getDirectory();
+ $approverMetadata = $res[0]['userMetadata'];
+ $display = isset($approverMetadata) && trim($approverMetadata['firstName'] . " " . $approverMetadata['lastName'] ) !== '' ?
+ $approverMetadata['firstName'] . " " . $approverMetadata['lastName'] : $res[0]['userID'];
- $approver = $dir->lookupLogin($res[0]['userID']);
-
- $res[0]['description'] = "{$approver[0]['firstName']} {$approver[0]['lastName']}";
+ $res[0]['description'] = $display;
}
// dependencyID -3 is for a group designated by the requestor
if (isset($res[0])
@@ -1672,7 +1676,7 @@ private function getFields(): array
default:
break;
}
-
+
$formattedFields["content"][$field['indicatorID']] = $data !== "" ? $data : $field["default"];
$formattedFields["to_cc_content"][$field['indicatorID']] = $emailValue;
}
diff --git a/LEAF_Request_Portal/sources/View.php b/LEAF_Request_Portal/sources/View.php
index a31dc25f2..2a4ec937e 100644
--- a/LEAF_Request_Portal/sources/View.php
+++ b/LEAF_Request_Portal/sources/View.php
@@ -61,7 +61,7 @@ public function buildViewStatus(int $recordID): array
$vars = array(':recordID' => $recordID);
$sql1 = 'SELECT time, description, actionText, stepTitle,
- dependencyID, comment, userID
+ dependencyID, comment, userID, userMetadata
FROM action_history
LEFT JOIN dependencies USING (dependencyID)
LEFT JOIN workflow_steps USING (stepID)
@@ -69,13 +69,13 @@ public function buildViewStatus(int $recordID): array
WHERE recordID=:recordID
UNION
SELECT timestamp, "Note Added", "N/A", "N/A",
- "N/A", note, userID
+ "N/A", note, userID, userMetadata
FROM notes
WHERE recordID = :recordID
AND deleted IS NULL
UNION
SELECT `timestamp`, "Email Sent", "N/A", "N/A",
- "N/A", concat(`recipients`, "
", `subject`), ""
+ "N/A", concat(`recipients`, "
", `subject`), "", ""
FROM `email_tracker`
WHERE recordID = :recordID
ORDER BY time ASC';
@@ -102,8 +102,12 @@ public function buildViewStatus(int $recordID): array
$packet['comment'] = $tmp['comment'];
if (!empty($tmp['userID'])) {
- $user = $dir->lookupLogin($tmp['userID']);
- $name = isset($user[0]) ? "{$user[0]['Fname']} {$user[0]['Lname']}" : $tmp['userID'];
+ $name = $tmp['userID'];
+ if(isset($tmp['userMetadata'])) {
+ $umd = json_decode($tmp['userMetadata'], true);
+ $display = trim($umd['firstName'] . " " . $umd['lastName']);
+ $name = !empty($display) ? $display : $name;
+ }
$packet['userName'] = $name;
}
diff --git a/LEAF_Request_Portal/templates/reports/LEAF_Sitemap_Search.tpl b/LEAF_Request_Portal/templates/reports/LEAF_Sitemap_Search.tpl
index f48ed746f..445aff1df 100644
--- a/LEAF_Request_Portal/templates/reports/LEAF_Sitemap_Search.tpl
+++ b/LEAF_Request_Portal/templates/reports/LEAF_Sitemap_Search.tpl
@@ -198,6 +198,7 @@ function addHeader(column) {
case 'dateCancelled':
filterData['deleted'] = 1;
filterData['action_history.approverName'] = 1;
+ filterData['action_history.actionType'] = 1;
leafSearch.getLeafFormQuery().join('action_history');
headers.push({
name: 'Date Cancelled', indicatorID: 'dateCancelled', editable: false, callback: function(data, blob) {
diff --git a/LEAF_Request_Portal/templates/view_reports.tpl b/LEAF_Request_Portal/templates/view_reports.tpl
index 61b91ef80..6ac37fac9 100644
--- a/LEAF_Request_Portal/templates/view_reports.tpl
+++ b/LEAF_Request_Portal/templates/view_reports.tpl
@@ -140,6 +140,7 @@ function addHeader(column) {
case 'dateCancelled':
filterData['deleted'] = 1;
filterData['action_history.approverName'] = 1;
+ filterData['action_history.actionType'] = 1;
leafSearch.getLeafFormQuery().join('action_history');
headers.push({
name: 'Date Cancelled', indicatorID: 'dateCancelled', editable: false, callback: function(data, blob) {
diff --git a/LEAF_Request_Portal/templates/view_status.tpl b/LEAF_Request_Portal/templates/view_status.tpl
index bf2455c3b..1ea210cd7 100644
--- a/LEAF_Request_Portal/templates/view_status.tpl
+++ b/LEAF_Request_Portal/templates/view_status.tpl
@@ -26,12 +26,13 @@ Title of request:
- by
+
+ by
- Comment:
+ Comment:
-
+
|
diff --git a/app/Leaf/Db.php b/app/Leaf/Db.php
index 0d6e4bbbf..a31c050b5 100644
--- a/app/Leaf/Db.php
+++ b/app/Leaf/Db.php
@@ -77,7 +77,7 @@ public function __construct($host, $user, $pass, $database, $abortOnError = fals
$this->isConnected = false;
}
-
+ $this->db->exec("SET NAMES 'utf8mb4'");
unset($pass);
}
diff --git a/app/Leaf/VAMCActiveDirectory.php b/app/Leaf/VAMCActiveDirectory.php
new file mode 100644
index 000000000..56a89ec8d
--- /dev/null
+++ b/app/Leaf/VAMCActiveDirectory.php
@@ -0,0 +1,397 @@
+ 'lname',
+ 'givenName' => 'fname',
+ 'initials' => 'midIni',
+ 'mail' => 'email',
+ 'telephoneNumber' => 'phone',
+ 94 => 'pager',
+ 'physicalDeliveryOfficeName' => 'roomNum',
+ 'title' => 'title',
+ 'description' => 'service',
+ 98 => 'mailcode',
+ 'sAMAccountName' => 'loginName',
+ 'mobile' => 'mobile',
+ 'DN' => 'domain',
+ 'objectGUID' => 'guid');
+
+ // Connect to the database
+ public function __construct($national_db)
+ {
+ $this->db = $national_db;
+ }
+
+ // Imports data from \t and \n delimited file of format:
+ // Name Business Phone Description Modified E-Mail Address User Logon Name
+ public function importADData(string $file): string
+ {
+ $data = $this->getData($file);
+ $rawdata = explode("\r\n", $data[0]['data']);
+ $rawheaders = trim(array_shift($rawdata));
+ $rawheaders = explode(',', $rawheaders);
+
+ foreach ($rawdata as $key => $line) {
+ $t = $this->splitWithEscape($line);
+ array_walk($t, array($this, 'trimField2'));
+
+ if (!is_array($t)) {
+ return 'invalid service';
+ }
+
+ foreach ($t as $t_key => $val) {
+ $head_check = $rawheaders[$t_key];
+
+ if (!isset($this->headers[$head_check])) {
+ return 'invalid header';
+ }
+
+ $write_data[$key][$this->headers[$head_check]] = $val;
+ }
+ }
+
+ $count = 0;
+
+ foreach ($write_data as $employee) {
+ if (
+ isset($employee['lname'])
+ && $employee['lname'] != ''
+ && isset($employee['loginName'])
+ && $employee['loginName'] != ''
+ && !is_numeric($employee['loginName'])
+ && !str_contains($employee['loginName'], '.')
+ ) {
+ $id = md5(strtoupper($employee['lname']) . strtoupper($employee['fname']) . strtoupper($employee['midIni']));
+
+ $this->users[$id]['lname'] = $employee['lname'];
+ $this->users[$id]['fname'] = $employee['fname'];
+ $this->users[$id]['midIni'] = $employee['midIni'];
+ $this->users[$id]['email'] = isset($employee['email']) ? $employee['email'] : null;
+ $this->users[$id]['phone'] = $employee['phone'];
+ $this->users[$id]['pager'] = isset($employee['pager']) ? $employee['pager'] : null;
+ $this->users[$id]['roomNum'] = $employee['roomNum'];
+ $this->users[$id]['title'] = $employee['title'];
+ $this->users[$id]['service'] = $employee['service'];
+ $this->users[$id]['mailcode'] = isset($employee['mailcode']) ? $employee['mailcode'] : null;
+ $this->users[$id]['loginName'] = $employee['loginName'];
+ $this->users[$id]['objectGUID'] = null;
+ $this->users[$id]['mobile'] = $employee['mobile'];
+ $this->users[$id]['domain'] = $employee['domain'];
+ $this->users[$id]['source'] = 'ad';
+ //echo "Grabbing data for $employee['lname'], $employee['fname']\n";
+ $count++;
+ } else {
+ $ln = isset($employee['loginName']) ? $employee['loginName'] : 'no login name';
+ $lan = isset($employee['lname']) ? $employee['lname'] : 'no last name';
+ $message = "{$ln} - {$lan} probably not a user, skipping.\n";
+ error_log($message, 3, '/var/www/php-logs/ad_processing.log');
+ }
+
+ if ($count > 100) {
+ $this->importData();
+ $count = 0;
+ }
+ }
+
+ // import any remaining entries
+ $this->importData();
+ }
+
+ public function disableNationalOrgchartEmployees(): void
+ {
+ // get all userNames that should be disabled
+ $disableUsersList = $this->getUserNamesToBeDisabled();
+
+ // Disable users not in this array
+ $this->preventRecycledUserName($disableUsersList);
+ }
+
+ // Imports data from \t and \n delimited file of format:
+ // Lname\t Fname Mid_Initial\t Email\t Phone\t Pager\t Room#\t Title\t Service\t MailCode\n
+ public function importData(): void
+ {
+ $time = time();
+ $sql1 = 'INSERT INTO employee (userName, lastName, firstName, middleName, phoneticFirstName, phoneticLastName, domain, lastUpdated, new_empUUID)
+ VALUES (:loginName, :lname, :fname, :midIni, :phoneticFname, :phoneticLname, :domain, :lastUpdated, uuid())';
+
+ $count = 0;
+
+ $userKeys = array_keys($this->users);
+
+ foreach ($userKeys as $key) {
+ $phoneticFname = metaphone($this->users[$key]['fname']);
+ $phoneticLname = metaphone($this->users[$key]['lname']);
+
+ $vars = array(':loginName' => $this->users[$key]['loginName']);
+ $sql = 'SELECT SQL_NO_CACHE *
+ FROM employee
+ WHERE username = :loginName';
+
+ $res = $this->db->prepared_query($sql, $vars);
+
+ if (count($res) > 0) {
+ //echo "Updating data for {$this->users[$key]['lname']}, {$this->users[$key]['fname']} \n";
+
+ $vars = array(':empUID' => $res[0]['empUID'],
+ ':indicatorID' => 6,
+ ':data' => $this->users[$key]['email']);
+ $sql = "INSERT INTO `employee_data` (`empUID`, `indicatorID`, `data`, `author`)
+ VALUES (:empUID, :indicatorID, :data, 'system')
+ ON DUPLICATE KEY UPDATE `data` = :data";
+
+ $this->db->prepared_query($sql, $vars);
+
+ $vars = array(':empUID' => $res[0]['empUID'],
+ ':indicatorID' => 5,
+ ':data' => $this->fixIfHex($this->users[$key]['phone']));
+
+ $this->db->prepared_query($sql, $vars);
+
+ $vars = array(':empUID' => $res[0]['empUID'],
+ ':indicatorID' => 8,
+ ':data' => $this->fixIfHex($this->users[$key]['roomNum']));
+
+ $this->db->prepared_query($sql, $vars);
+
+ $vars = array(':empUID' => $res[0]['empUID'],
+ ':indicatorID' => 23,
+ ':data' => $this->fixIfHex($this->users[$key]['title']));
+
+ $this->db->prepared_query($sql, $vars);
+
+ // don't store mobile # if it's the same as the primary phone #
+ if ($this->users[$key]['phone'] != $this->users[$key]['mobile']) {
+ $vars = array(':empUID' => $res[0]['empUID'],
+ ':indicatorID' => 16,
+ ':data' => $this->fixIfHex($this->users[$key]['mobile']));
+
+ $this->db->prepared_query($sql, $vars);
+ }
+
+ $vars = array(':lname' => $this->users[$key]['lname'],
+ ':fname' => $this->users[$key]['fname'],
+ ':midIni' => $this->users[$key]['midIni'],
+ ':phoneticFname' => $phoneticFname,
+ ':phoneticLname' => $phoneticLname,
+ ':domain' => $this->users[$key]['domain'],
+ ':lastUpdated' => $time,
+ ':userName' => $this->users[$key]['loginName']);
+ $sql = 'UPDATE employee
+ SET lastName = :lname,
+ firstName = :fname,
+ middleName = :midIni,
+ phoneticFirstName = :phoneticFname,
+ phoneticLastName = :phoneticLname,
+ domain = :domain,
+ lastUpdated = :lastUpdated,
+ deleted = 0
+ WHERE username = :userName';
+
+ $this->db->prepared_query($sql, $vars);
+ } else {
+ $vars = array(':loginName', $this->users[$key]['loginName'],
+ ':lname', $this->users[$key]['lname'],
+ ':fname', $this->users[$key]['fname'],
+ ':midIni', $this->users[$key]['midIni'],
+ ':phoneticFname', $phoneticFname,
+ ':phoneticLname', $phoneticLname,
+ ':domain', $this->users[$key]['domain'],
+ ':lastUpdated', $time);
+
+ $this->db->prepared_query($sql1, $vars);
+
+ //echo "Inserting data for {$this->users[$key]['lname']}, {$this->users[$key]['fname']} : " . $pq->errorCode() . "\n";
+
+ $lastEmpUID = $this->db->getLastInsertId();
+
+ // prioritize adding email to DB
+ $sql = "INSERT INTO employee_data (empUID, indicatorID, data, author)
+ VALUES (:empUID, :indicatorID, :data, 'system')
+ ON DUPLICATE KEY UPDATE data=:data";
+
+ $vars = array(':empUID', $lastEmpUID,
+ ':indicatorID', 6,
+ ':data', $this->users[$key]['email']);
+ $this->db->prepared_query($sql, $vars);
+ $count++;
+ }
+
+ unset($this->users[$key]);
+ }
+
+ echo 'Cleanup... ';
+ // TODO: do some clean up
+ echo "... Done.\n";
+
+ echo "Total: $count";
+ }
+
+ private function getUserNamesToBeDisabled(): array
+ {
+ $sql = 'SELECT `userName`
+ FROM `employee`
+ WHERE `deleted` = 0
+ AND `lastUpdated` < (UNIX_TIMESTAMP(NOW()) - 108000)';
+
+ $return_value = $this->db->prepared_query($sql, array());
+
+ return $return_value;
+ }
+
+ private function preventRecycledUserName(array $userNames): void
+ {
+ $deleteTime = time();
+
+ $vars = array(':deleteTime' => $deleteTime);
+ $sql = 'UPDATE `employee`
+ SET `deleted` = :deleteTime,
+ `userName` = concat("disabled_", `deleted`, "_", `userName`)
+ WHERE `userName` = :userName;
+
+ UPDATE `employee_data`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `employee_data_history`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `group_data`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `group_data_history`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `position_data`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `position_data_history`
+ SET `author` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `author` = :userName;
+
+ UPDATE `relation_employee_backup`
+ SET `approverUserName` = concat("disabled_", :deleteTime, "_", :userName)
+ WHERE `approverUserName` = :userName;';
+
+ foreach ($userNames as $user) {
+ $vars[':userName'] = $user['userName'];
+
+ $this->db->prepared_query($sql, $vars);
+ }
+ }
+
+ private function getData(string $file): array
+ {
+ $vars = array(':file' => $file);
+ $sql = 'SELECT `data`
+ FROM `cache`
+ WHERE `cacheID` = :file';
+
+ $data = $this->db->prepared_query($sql, $vars);
+
+ $this->removeData($file);
+
+ return $data;
+ }
+
+ private function removeData(string $file): void
+ {
+ $vars = array(':file' => $file);
+ $sql = 'DELETE
+ FROM `cache`
+ WHERE `cacheID` = :file';
+
+ $this->db->prepared_query($sql, $vars);
+ }
+
+ private function trimField2(string &$value, string $key): void
+ {
+ $value = trim($value);
+ $value = trim($value, '.');
+ }
+
+ // workaround for excel
+ // author: tajhlande at gmail dot com
+ private function splitWithEscape(string $str, string $delimiterChar = ',', string $escapeChar = '"'): array
+ {
+ $len = strlen($str);
+ $tokens = array();
+ $i = 0;
+ $inEscapeSeq = false;
+ $currToken = '';
+
+ while ($i < $len) {
+ $c = substr($str, $i, 1);
+
+ if ($inEscapeSeq) {
+ if ($c == $escapeChar) {
+ // lookahead to see if next character is also an escape char
+ if ($i == ($len - 1)) {
+ // c is last char, so must be end of escape sequence
+ $inEscapeSeq = false;
+ } elseif (substr($str, $i + 1, 1) == $escapeChar) {
+ // append literal escape char
+ $currToken .= $escapeChar;
+ $i++;
+ } else {
+ // end of escape sequence
+ $inEscapeSeq = false;
+ }
+ } else {
+ $currToken .= $c;
+ }
+ } else {
+ if ($c == $delimiterChar) {
+ // end of token, flush it
+ array_push($tokens, $currToken);
+ $currToken = '';
+ } elseif ($c == $escapeChar) {
+ // begin escape sequence
+ $inEscapeSeq = true;
+ } else {
+ $currToken .= $c;
+ }
+ }
+
+ $i++;
+ }
+
+ // flush the last token
+ array_push($tokens, $currToken);
+
+ return $tokens;
+ }
+
+ //tests stringToFix for format X'...', if it matches, it's a hex value, is decoded and returned
+ private function fixIfHex(string $stringToFix): string
+ {
+ if (substr( $stringToFix, 0, 2 ) === "X'") {
+ $stringToFix = ltrim($stringToFix, "X'");
+ $stringToFix = rtrim($stringToFix, "'");
+ $stringToFix = hex2bin($stringToFix);
+ }
+
+ return $stringToFix;
+ }
+}
diff --git a/docker/mysql/db/db_upgrade/portal/Update_RMC_DB_2024101500-2024112500.sql b/docker/mysql/db/db_upgrade/portal/Update_RMC_DB_2024101500-2024112500.sql
new file mode 100644
index 000000000..04ad32593
--- /dev/null
+++ b/docker/mysql/db/db_upgrade/portal/Update_RMC_DB_2024101500-2024112500.sql
@@ -0,0 +1,47 @@
+START TRANSACTION;
+
+ALTER TABLE `data`
+CHANGE `data` `data` text COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `series`,
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `timestamp`,
+COLLATE 'utf8mb4_general_ci';
+
+ALTER TABLE `data_history`
+CHANGE `data` `data` text COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `series`,
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `timestamp`,
+CHANGE `userDisplay` `userDisplay` varchar(90) COLLATE 'utf8mb4_general_ci' NULL AFTER `userID`,
+COLLATE 'utf8mb4_general_ci';
+
+ALTER TABLE `records`
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb4_general_ci' NOT NULL AFTER `serviceID`,
+CHANGE `title` `title` text COLLATE 'utf8mb4_general_ci' NULL AFTER `userID`,
+CHANGE `lastStatus` `lastStatus` text COLLATE 'utf8mb4_general_ci' NULL AFTER `priority`,
+COLLATE 'utf8mb4_general_ci';
+
+
+UPDATE `settings` SET `data` = '2024112500' WHERE `settings`.`setting` = 'dbversion';
+
+COMMIT;
+
+/**** Revert DB ***** NOTE: Data could have issues going back if it contains data that is in the mb4 set
+START TRANSACTION;
+
+ALTER TABLE `data`
+CHANGE `data` `data` text COLLATE 'utf8mb3_general_ci' NOT NULL AFTER `series`,
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb3_general_ci' NOT NULL AFTER `timestamp`,
+COLLATE 'utf8mb3_general_ci';
+
+ALTER TABLE `data_history`
+CHANGE `data` `data` text COLLATE 'utf8mb3_general_ci' NOT NULL AFTER `series`,
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb3_general_ci' NOT NULL AFTER `timestamp`,
+CHANGE `userDisplay` `userDisplay` varchar(90) COLLATE 'utf8mb3_general_ci' NULL AFTER `userID`,
+COLLATE 'utf8mb3_general_ci';
+
+ALTER TABLE `records`
+CHANGE `userID` `userID` varchar(50) COLLATE 'utf8mb3_general_ci' NOT NULL AFTER `serviceID`,
+CHANGE `title` `title` text COLLATE 'utf8mb3_general_ci' NULL AFTER `userID`,
+COLLATE 'utf8mb3_general_ci';
+
+UPDATE `settings` SET `data` = '2024101500' WHERE `settings`.`setting` = 'dbversion';
+
+COMMIT;
+*/
diff --git a/scripts/scheduled-task-commands/disableNationalOrgchartEmployees.php b/scripts/scheduled-task-commands/disableNationalOrgchartEmployees.php
new file mode 100644
index 000000000..7d4117175
--- /dev/null
+++ b/scripts/scheduled-task-commands/disableNationalOrgchartEmployees.php
@@ -0,0 +1,17 @@
+disableNationalOrgchartEmployees();
+
+$endTime = microtime(true);
+$totalTime = round(($endTime - $startTime)/60, 2);
+
+error_log(print_r($file . " took " . $totalTime . " minutes to complete.", true), 3 , '/var/www/php-logs/ad_processing.log');
\ No newline at end of file
diff --git a/scripts/scheduled-task-commands/updateNationalOrgchartEmployees.php b/scripts/scheduled-task-commands/updateNationalOrgchartEmployees.php
new file mode 100644
index 000000000..87bbb96ed
--- /dev/null
+++ b/scripts/scheduled-task-commands/updateNationalOrgchartEmployees.php
@@ -0,0 +1,22 @@
+query($sql);
+
+function updateEmps($VISNS) {
+ foreach ($VISNS as $visn) {
+ if (str_starts_with($visn['data'], 'DN,')) {
+ exec("php /var/www/scripts/updateNationalOrgchart.php {$visn['cacheID']} > /dev/null 2>/dev/null &");
+ echo "Deploying to: {$visn['cacheID']}\r\n";
+ }
+ }
+}
+
+updateEmps($VISNS);
\ No newline at end of file
diff --git a/scripts/updateNationalOrgchart.php b/scripts/updateNationalOrgchart.php
new file mode 100644
index 000000000..1431ff15d
--- /dev/null
+++ b/scripts/updateNationalOrgchart.php
@@ -0,0 +1,23 @@
+importADData($file);
+
+$endTime = microtime(true);
+$totalTime = round(($endTime - $startTime)/60, 2);
+
+error_log(print_r($file . " took " . $totalTime . " minutes to complete.", true), 3 , '/var/www/php-logs/ad_processing.log');
\ No newline at end of file