diff --git a/REDCapPRO.php b/REDCapPRO.php index 71086a2..ae08137 100644 --- a/REDCapPRO.php +++ b/REDCapPRO.php @@ -1028,29 +1028,57 @@ public function getUserRole(string $username) public function getAllUsers() { $projects = $this->framework->getProjectsWithModuleEnabled(); + $staff = $this->getStaff(); + $allUsers = $this->getAllUserInfo(); $users = array(); foreach ( $projects as $pid ) { - $project = new Project($this, $pid); - $staff_arr = $project->getStaff(); - $all_staff = $staff_arr["allStaff"]; + $all_staff = $staff[$pid]["allStaff"]; foreach ( $all_staff as $user ) { if ( isset($users[$user]) ) { array_push($users[$user]['projects'], $pid); } else { - $newUser = $this->framework->getUser($user); - $newUserArr = [ - "username" => $user, - "email" => $newUser->getEmail(), - "name" => $this->getUserFullname($user), - "projects" => [ $pid ] - ]; - $users[$user] = $newUserArr; + $newUser = $allUsers[$user]; + $newUser['projects'] = [ $pid ]; + $users[$user] = $newUser; } } } return $users; } + public function getAllUserInfo() { + $sql = 'SELECT username, user_email AS email, CONCAT(user_firstname, " ", user_lastname) AS name FROM redcap_user_information'; + $result = $this->framework->query($sql, []); + $userInfo = []; + while ($row = $result->fetch_assoc()) { + $userInfo[$row['username']] = $row; + } + return $userInfo; + } + + public function getStaff() + { + try { + $sql = "SELECT * FROM redcap_external_module_settings + WHERE external_module_id = (SELECT external_module_id FROM redcap_external_modules WHERE directory_prefix = ?) + AND `key` in ('managers', 'users', 'monitors')"; + $result = $this->framework->query($sql, [ $this->framework->getModuleInstance()->PREFIX ]); + $staff = []; + while ($row = $result->fetch_assoc()) { + $thisStaff = json_decode($row['value']); + $staff[$row['project_id']][$row['key']] = $thisStaff; + if (!isset($staff[$row['project_id']]['allStaff'])) { + $staff[$row['project_id']]['allStaff'] = []; + } + $staff[$row['project_id']]['allStaff'] = array_merge($staff[$row['project_id']]['allStaff'], $thisStaff); + } + + return $staff; + } catch (\Exception $e) { + $this->module->logError("Error getting staff", $e); + } + } + public function safeGetUsername() : string { try { diff --git a/src/cc_logs.php b/src/cc_logs.php index 19804e6..1401b06 100644 --- a/src/cc_logs.php +++ b/src/cc_logs.php @@ -16,11 +16,10 @@ ?> - - + + + + "> ') + .val(column.search()) .appendTo($(column.header())) .on('click', function (e) { e.stopPropagation(); @@ -191,7 +195,7 @@ className: "dt-center" }); console.log('End: ', performance.now()); - dataTable.columns.adjust().draw(); + dataTable.columns.adjust(); }, drawCallback: function (settings) { t2 = performance.now(); diff --git a/src/cc_participants.php b/src/cc_participants.php index fb33d49..4e63285 100644 --- a/src/cc_participants.php +++ b/src/cc_participants.php @@ -28,6 +28,11 @@ $ui = new UI($module); $ui->ShowControlCenterHeader("Participants"); +?> + + + + REDCapPRO Projects + + + + "> getJavascriptModuleObjectName() ?>; $(document).ready(function () { let dataTable = $('#RCPRO_TABLE').DataTable({ - dom: 'lftip', + // dom: 'lftip', deferRender: true, + processing: true, ajax: function (data, callback, settings) { RCPRO_module.ajax('getProjectsCC', {}) .then(response => { diff --git a/src/cc_staff.php b/src/cc_staff.php index e485318..7974938 100644 --- a/src/cc_staff.php +++ b/src/cc_staff.php @@ -16,6 +16,10 @@ echo ''; $module->initializeJavascriptModuleObject(); ?> + + + +
@@ -44,6 +48,7 @@ $(document).ready(function () { let dataTable = $('#RCPRO_TABLE').DataTable({ deferRender: true, + processing: true, ajax: function (data, callback, settings) { RCPRO_module.ajax('getStaffCC', {}) .then(response => { @@ -92,7 +97,7 @@ className: "dt-center", } }, ], - dom: 'lBfrtip', + // dom: 'lBfrtip', stateSave: true, stateSaveCallback: function (settings, data) { localStorage.setItem('DataTables_ccstaff_' + settings.sInstance, JSON.stringify(data)) diff --git a/src/classes/AjaxHandler.php b/src/classes/AjaxHandler.php index ce7983e..a1760b6 100644 --- a/src/classes/AjaxHandler.php +++ b/src/classes/AjaxHandler.php @@ -48,7 +48,7 @@ public function handleAjax() private function getLogs() { try { - if ( $this->params["cc"] && !$this->module->framework->isSuperUser() ) { + if ( $this->params["cc"] === TRUE && !$this->module->framework->isSuperUser() ) { throw new REDCapProException("You must be an admin to view Control Center logs"); } @@ -123,27 +123,42 @@ private function getLogs() $orderTerm .= ", "; } } - // if ( $orderTerm === "" ) { - // $orderTerm = " order by timestamp desc"; - // } - - //$queryParameters = [ ...$queryParameters, ...$generalSearchParameters ]; // BUILD A FAST QUERY OF THE LOGS - + if ($this->params['cc'] === TRUE) { + $columns = REDCapPRO::$logColumnsCC; + $projectClause = ""; + } else { + $columns = REDCapPRO::$logColumns; + $projectClause = " AND l.project_id = ?"; + } $sqlFull = "SELECT "; - foreach (REDCapPRO::$logColumnsCC as $key => $column) { + foreach ($columns as $key => $column) { if ( in_array( $column, $baseColumns, true) ) { $sqlFull .= " l." . $column; } else { $sqlFull .= " MAX(CASE WHEN p.name = '" . $column . "' THEN p.value END) AS $column "; } - if ($key !== array_key_last(REDCapPRO::$logColumnsCC)) { + if ($key !== array_key_last($columns)) { $sqlFull .= ", "; } } + // $newSql = "SELECT l.log_id, l.timestamp, l.ui_id, l.ip, l.project_id, l.record, l.message, group_concat(p.name SEPARATOR '||') name, group_concat(p.value SEPARATOR '||') value + // FROM redcap_external_modules_log l + // LEFT JOIN redcap_external_modules_log_parameters p + // ON p.log_id = l.log_id + // LEFT JOIN redcap_external_modules_log_parameters module_token + // ON module_token.log_id = l.log_id + // AND module_token.name = 'module_token' + // WHERE l.external_module_id = (SELECT external_module_id FROM redcap_external_modules WHERE directory_prefix = ?) + // AND module_token.value = ? + // AND p.name IN ('".implode("','", $columns)."') + // GROUP BY l.log_id"; + + + $sql = " from redcap_external_modules_log l left join redcap_external_modules_log_parameters p ON p.log_id = l.log_id @@ -151,8 +166,11 @@ private function getLogs() on module_token.log_id = l.log_id and module_token.name = 'module_token' WHERE l.external_module_id = (SELECT external_module_id FROM redcap_external_modules WHERE directory_prefix = ?) - and module_token.value = ? "; + and module_token.value = ? " . $projectClause; array_push($fastQueryParameters, $this->module->framework->getModuleInstance()->PREFIX, $this->module->getModuleToken()); + if (!$this->params['cc']) { + array_push($fastQueryParameters, $this->project_id); + } if ( $generalSearchTerm !== "" ) { $sql .= " and ("; @@ -176,7 +194,7 @@ private function getLogs() $queryResult = $this->module->framework->query($sqlFull . $sql . $orderTerm . $limitTerm, $fastQueryParameters); $countResult = $this->module->framework->query("SELECT * " . $sql, $fastQueryParameters)->num_rows; - // $queryText = "SELECT " . implode(', ', REDCapPRO::$logColumnsCC) . " WHERE " . $generalSearchText . $orderTerm . $limitTerm; + // $queryText = "SELECT " . implode(', ', $columns) . " WHERE " . $generalSearchText . $orderTerm . $limitTerm; // $queryResult = $this->module->queryLogs($queryText, $queryParameters); // $countResult = $this->module->countLogs($generalSearchText, $queryParameters); $totalCountResult = $this->module->countLogs("module_token = ?", [ $this->module->getModuleToken() ]); @@ -271,6 +289,7 @@ private function getParticipants() // TIMING $lastTime = microtime(true); $times = ["Start" => $lastTime]; + $startTime = $lastTime; $role = $this->module->getUserRole($this->module->safeGetUsername()); if ( $role < 1 ) { @@ -285,17 +304,39 @@ private function getParticipants() try { $participantHelper = new ParticipantHelper($this->module); $participantList = $participantHelper->getProjectParticipants($rcpro_project_id, $rcpro_user_dag); + // TIMING + $now = microtime(true); + $times["Get Participants"] = $now - $lastTime; + $lastTime = $now; + $links = $projectHelper->getAllLinks(); // TIMING $now = microtime(true); - $times["Before Loop"] = $now - $lastTime; + $times["Get Links"] = $now - $lastTime; $lastTime = $now; foreach ( $participantList as $participant ) { $rcpro_participant_id = (int) $participant["log_id"]; - $link_id = $projectHelper->getLinkId($rcpro_participant_id, $rcpro_project_id); - $dag_id = $dagHelper->getParticipantDag($link_id); + //$link_id = $projectHelper->getLinkId($rcpro_participant_id, $rcpro_project_id); + // $link_id = $links[$rcpro_participant_id][$rcpro_project_id]['link_id']; + // // TIMING + // $now = microtime(true); + // $times["Get Link ID"] = $now - $lastTime; + // $lastTime = $now; + + // $dag_id = $dagHelper->getParticipantDag($link_id); + $dag_id = $links[$rcpro_participant_id][$rcpro_project_id]['dag_id']; + // TIMING + $now = microtime(true); + $times["Get DAG ID"] = $now - $lastTime; + $lastTime = $now; + $dag_name = $dagHelper->getDagName($dag_id) ?? "Unassigned"; + // TIMING + $now = microtime(true); + $times["Get DAG Name"] = $now - $lastTime; + $lastTime = $now; + $thisParticipant = [ 'username' => $participant["rcpro_username"] ?? "", 'password_set' => $participant["pw_set"] === 'True', @@ -313,9 +354,8 @@ private function getParticipants() // TIMING $now = microtime(true); - $times["After Loop"] = $now - $lastTime; - $lastTime = $now; - $this->module->log('times', [ 'times' => json_encode($times) ]); + $times["End"] = $now - $startTime; + $this->module->log('times', [ 'times' => json_encode($times, JSON_PRETTY_PRINT) ]); } catch ( \Throwable $e ) { $this->module->logError('Error fetching participant list', $e); } finally { diff --git a/src/classes/ParticipantHelper.php b/src/classes/ParticipantHelper.php index 059961a..e023b0e 100644 --- a/src/classes/ParticipantHelper.php +++ b/src/classes/ParticipantHelper.php @@ -538,7 +538,7 @@ public function getAllParticipantProjects() * * @return array|NULL participants enrolled in given study */ - public function getProjectParticipants(string $rcpro_project_id, ?int $dag = NULL) + public function getProjectParticipantsOld(string $rcpro_project_id, ?int $dag = NULL) { $SQL = "SELECT rcpro_participant_id WHERE message = 'LINK' AND rcpro_project_id = ? AND active = 1 AND (project_id IS NULL OR project_id IS NOT NULL)"; $PARAMS = [ $rcpro_project_id ]; @@ -564,6 +564,48 @@ public function getProjectParticipants(string $rcpro_project_id, ?int $dag = NUL } } + /** + * get array of active enrolled participants given a rcpro project id + * + * @param string $rcpro_project_id Project ID (not REDCap PID!) + * @param int|NULL $dag Data Access Group to filter search by + * + * @return array|NULL participants enrolled in given study + */ + public function getProjectParticipants(string $rcpro_project_id, ?int $dag = NULL) + { + $SQL1 = "SELECT rcpro_participant_id WHERE message = 'LINK' AND rcpro_project_id = ? AND active = 1 AND (project_id IS NULL OR project_id IS NOT NULL)"; + $PARAMS1 = [ $rcpro_project_id ]; + if ( isset($dag) ) { + $SQL1 .= " AND project_dag IS NOT NULL AND project_dag = ?"; + array_push($PARAMS1, strval($dag)); + } + + $SQL2 = "SELECT log_id, rcpro_username, email, fname, lname, lockout_ts, pw WHERE message = 'PARTICIPANT' AND (project_id IS NULL OR project_id IS NOT NULL)"; + + try { + $result1 = $this->module->selectLogs($SQL1, $PARAMS1); + $result2 = $this->module->selectLogs($SQL2, []); + + $allParticipants = array(); + while ( $row = $result2->fetch_assoc() ) { + $allParticipants[$row['log_id']] = $row; + } + + $participants = array(); + while ( $row = $result1->fetch_assoc() ) { + $participant = $allParticipants[$row['rcpro_participant_id']]; + $participant["pw_set"] = (!isset($participant["pw"]) || $participant["pw"] === "") ? "False" : "True"; + unset($participant["pw"]); + $participants[$row['rcpro_participant_id']] = $participant; + } + + return $participants; + } catch ( \Exception $e ) { + $this->module->logError("Error fetching project participants", $e); + } + } + /** * Fetch username for given participant id * diff --git a/src/classes/ProjectHelper.php b/src/classes/ProjectHelper.php index b98b344..7b3b21a 100644 --- a/src/classes/ProjectHelper.php +++ b/src/classes/ProjectHelper.php @@ -176,6 +176,23 @@ public function getLinkId(int $rcpro_participant_id, int $rcpro_project_id) } } + public function getAllLinks() { + $SQL = "SELECT log_id, rcpro_participant_id, rcpro_project_id, project_dag WHERE message = 'LINK' AND (project_id IS NULL OR project_id IS NOT NULL)"; + try { + $result = $this->module->selectLogs($SQL, []); + $links = []; + while ($row = $result->fetch_assoc()) { + $links[$row['rcpro_participant_id']][$row['rcpro_project_id']] = [ + 'log_id' => $row['log_id'], + 'dag_id' => $row['project_dag'] + ]; + } + return $links; + } catch ( \Exception $e ) { + $this->module->logError("Error fetching links", $e); + } + } + /** * Get the REDCap PID corresponding with a project ID * diff --git a/src/logs.php b/src/logs.php index ec83616..4dd12d1 100644 --- a/src/logs.php +++ b/src/logs.php @@ -19,11 +19,9 @@ APPTITLE ?> - Enroll - - + + + " /> @@ -63,10 +61,23 @@ function logExport(type) { $(document).ready(function () { $('#RCPRO_TABLE').DataTable({ deferRender: true, + serverSide: true, + processing: true, + searchDelay: 1000, + order: [[0, 'desc']], ajax: function (data, callback, settings) { - RCPRO_module.ajax('getLogs', { cc: false }) + const payload = { + draw: data.draw, + search: data.search, + start: data.start, + length: data.length, + order: data.order, + columns: data.columns, + cc: false + } + RCPRO_module.ajax('getLogs', payload) .then(response => { - callback({ data: response }); + callback(response); }) .catch(error => { console.error(error); @@ -97,7 +108,11 @@ function logExport(type) { }); }); }, - dom: 'lBfrtip', + // dom: 'lBfrtip', + layout: { + topStart: ['pageLength', 'buttons'], + topEnd: 'search' + }, stateSave: true, stateSaveCallback: function (settings, data) { localStorage.setItem('DataTables_logs_' + settings.sInstance, JSON.stringify(data)) @@ -105,17 +120,18 @@ function logExport(type) { stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem('DataTables_logs_' + settings.sInstance)) }, - colReorder: true, - buttons: [{ - extend: 'searchPanes', - config: { - cascadePanes: true, - } + colReorder: false, + buttons: [ + // { + // extend: 'searchPanes', + // config: { + // cascadePanes: true, + // } - }, - { - extend: 'searchBuilder', - }, + // }, + // { + // extend: 'searchBuilder', + // }, 'colvis', { text: 'Restore Default', @@ -148,7 +164,10 @@ function logExport(type) { scrollX: true, scrollY: '50vh', scrollCollapse: true, - pageLength: 100 + // pageLength: 100, + initComplete: function() { + $('#RCPRO_TABLE').DataTable().columns.adjust(); + }, }); $('#logs').removeClass('dataTableParentHidden'); @@ -161,7 +180,7 @@ function logExport(type) { $('.dt-button-collection').draggable(); } }); - $('#RCPRO_TABLE').DataTable().columns.adjust().draw(); + //$('#RCPRO_TABLE').DataTable().columns.adjust().draw(); }); }(window.jQuery, window, document)); diff --git a/src/manage-users.php b/src/manage-users.php index 53f45d3..9989b45 100644 --- a/src/manage-users.php +++ b/src/manage-users.php @@ -164,6 +164,7 @@ function handleButtons() { const dt = $('#RCPRO_TABLE').DataTable({ deferRender: true, + processing: true, ajax: function (data, callback, settings) { RCPRO_module.ajax('getStaff', {}) .then(response => { diff --git a/src/manage.php b/src/manage.php index 3d2e77a..7f105f7 100644 --- a/src/manage.php +++ b/src/manage.php @@ -16,6 +16,7 @@ $ui->ShowHeader("Manage"); echo "" . $module->APPTITLE . " - Manage"; + // Check for errors if ( isset($_GET["error"]) ) { ?> @@ -565,6 +566,7 @@ className: 'dt-center' let datatable = $('#RCPRO_TABLE').DataTable({ deferRender: true, + processing: true, ajax: function (data, callback, settings) { RCPRO_module.ajax('getParticipants', {}) .then(response => {