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
+
+
+ You are moving the card
+ Esc - return to original location
+ Enter - save current location +
+ +
-
+
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