diff --git a/modules/behavioural_qc/php/module.class.inc b/modules/behavioural_qc/php/module.class.inc index 64043f3a351..e282abe7ab0 100644 --- a/modules/behavioural_qc/php/module.class.inc +++ b/modules/behavioural_qc/php/module.class.inc @@ -60,4 +60,50 @@ class Module extends \Module { return dgettext("behavioural_qc", "Behavioural Quality Control"); } + + /** + * {@inheritDoc} + * + * @param string $type The type of widgets to get. + * @param \User $user The user widgets are being retrieved for. + * @param array $options A type dependent list of options to provide + * to the widget. + * + * @return \LORIS\GUI\Widget[] + */ + public function getWidgets(string $type, \User $user, array $options) : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + switch ($type) { + case 'study-progression': + $DB = $factory->database(); + $data = $DB->pselectWithIndexKey( + "SELECT + p.ProjectID, + COUNT(*) AS count, + 'rgb(252, 241, 255)' as colour, + CONCAT('$baseURL/behavioural_qc/?Project=', p.ProjectID) + AS url + FROM flag f + JOIN session s ON f.SessionID = s.ID + JOIN Project p ON p.ProjectID = s.ProjectID + WHERE DataID IS NOT NULL + AND s.Active <> 'N' + AND s.CenterID <> 1 + AND f.CommentID NOT LIKE 'DDE_%' + GROUP BY p.Name", + [], + 'ProjectID' + ); + return [ + new \LORIS\dashboard\DataWidget( + "Behavioural Session", + $data, + "", + ) + ]; + } + return []; + } } diff --git a/modules/candidate_list/php/module.class.inc b/modules/candidate_list/php/module.class.inc index 7e3256eb6a3..c2ef39eb4c7 100644 --- a/modules/candidate_list/php/module.class.inc +++ b/modules/candidate_list/php/module.class.inc @@ -96,4 +96,57 @@ class Module extends \Module return true; } + /** + * {@inheritDoc} + * + * @param string $type The type of widgets to get. + * @param \User $user The user widgets are being retrieved for. + * @param array $options A type dependent list of options to provide + * to the widget. + * + * @return \LORIS\GUI\Widget[] + */ + public function getWidgets(string $type, \User $user, array $options) : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + switch ($type) { + case 'study-progression': + $DB = $factory->database(); + $data = $DB->pselectWithIndexKey( + "SELECT + ProjectID, + COUNT(DISTINCT PSCID) AS count, + 'rgb(255, 252, 199)' AS colour, + CONCAT('$baseURL/candidate_list/?project=', ProjectName) + AS url, + ProjectName + FROM ( + SELECT + c.PSCID, + COALESCE(p.ProjectID, p2.ProjectID) AS ProjectID, + COALESCE(p.Name, p2.Name) AS ProjectName + FROM candidate c + LEFT JOIN session s ON s.CandidateID = c.ID + LEFT JOIN Project p ON p.ProjectID = s.ProjectID + JOIN Project p2 ON c.RegistrationProjectID = p2.ProjectID + WHERE c.Active <> 'N' + AND s.Active <> 'N' + AND s.CenterID <> 1 + ) AS sub + GROUP BY ProjectID, ProjectName;", + [], + 'ProjectID' + ); + return [ + new \LORIS\dashboard\DataWidget( + "Participant", + $data, + "", + ) + ]; + } + return []; + } + } diff --git a/modules/dashboard/php/datawidget.class.inc b/modules/dashboard/php/datawidget.class.inc new file mode 100644 index 00000000000..c6b4e23ec68 --- /dev/null +++ b/modules/dashboard/php/datawidget.class.inc @@ -0,0 +1,90 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris + */ +namespace LORIS\dashboard; + +/** + * A DataWidget is a type of dashboard widget which contains data from + * a database query, a label, and a link to go to when the user clicks on it. + * + * @category Main + * @package Loris + * @author Saagar Arya + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris + */ +class DataWidget implements \LORIS\GUI\Widget +{ + protected $label; + protected $data; + protected $cssclass; + + /** + * Construct a DataWidget with the given properties. + * + * @param string $label The label to describe the data. + * @param array $data The array data. + * @param string $cssclass A CSS class to add to the element. + */ + public function __construct( + string $label, + array $data, + string $cssclass, + ) { + $this->label = $label; + $this->data = $data; + $this->cssclass = $cssclass; + } + + /** + * Returns the label for this task. + * + * @return string + */ + public function label() : string + { + return $this->label; + } + + /** + * Returns the number associated with this task. + * + * @return array + */ + public function data() : array + { + return $this->data; + } + + /** + * If non-empty, add this class name to the task item. + * + * @return string + */ + public function CSSClass() : string + { + return $this->cssclass; + } + + /** + * TaskWidgets get serialized to a string by the MyTasks panel. + * + * @return string + */ + public function __toString() + { + // The dashboard module just uses the methods on this + // to get metadata, it handles the rendering itself. + return ""; + } +} diff --git a/modules/dashboard/test/DashboardTest.php b/modules/dashboard/test/DashboardTest.php index 480f7ab76ff..abb37b49504 100644 --- a/modules/dashboard/test/DashboardTest.php +++ b/modules/dashboard/test/DashboardTest.php @@ -704,10 +704,8 @@ private function _testPlan5And6() ); } /** - * 7. Check that scans per site (study progression panel) view is correct - * (scan dates and scan numbers). - * 8. Check that recruitment per site view is correct - * (study progression panel). + * 7. Check that study progression panel is correct. + * 8. Check that there is no error message in the panel. * * @return void */ @@ -721,7 +719,7 @@ private function _testPlan7And8() )->getText(); $this->assertStringContainsString( - "Scan sessions per site", + "Participants", $testText ); diff --git a/modules/imaging_browser/php/module.class.inc b/modules/imaging_browser/php/module.class.inc index 20f5adf29b0..6914ca8d96c 100644 --- a/modules/imaging_browser/php/module.class.inc +++ b/modules/imaging_browser/php/module.class.inc @@ -122,6 +122,31 @@ class Module extends \Module 1, ) ]; + case 'study-progression': + $DB = $factory->database(); + $data = $DB->pselectWithIndexKey( + "SELECT + p.ProjectID, + p.Name AS ProjectName, + COUNT(s.ID) AS count, + 'rgb(235, 255, 254)' as colour, + '".$baseURL."/imaging_browser' as url + FROM session s + JOIN Project p ON p.ProjectID = s.ProjectID + JOIN mri_upload mu ON mu.SessionID = s.ID + WHERE s.Active <> 'N' + AND s.CenterID <> 1 + GROUP BY p.Name", + [], + 'ProjectID' + ); + return [ + new \LORIS\dashboard\DataWidget( + "Imaging Session", + $data, + "", + ) + ]; } return []; } diff --git a/modules/statistics/css/recruitment.css b/modules/statistics/css/recruitment.css index dace82f893e..d711341bf6e 100644 --- a/modules/statistics/css/recruitment.css +++ b/modules/statistics/css/recruitment.css @@ -15,6 +15,31 @@ display: none !important; } +.study-progression-container { + flex-direction: row; + display: flex; + border-bottom: 1px solid #ccc; + margin-bottom: 10px; +} + +.study-progression-button { + background-color: #ffffff; + border-radius: 15px; + padding: 10px; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.25); + transition: all 0.3s ease; + text-align: center; + margin: 0 10px 10px 0; + padding: 10px; +} + +.study-progression-button:hover { + transform: translateY(-10px); + background-color: #f9fdff; + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25); + border: 1px solid #acacac; +} + /* ===== Chart Header (Title + Dropdown) ===== */ .chart-header { display: flex; @@ -104,7 +129,7 @@ background-color: #f9fdff; box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25); border: 1px solid #acacac; -} +} /* Ensure the form spans full width */ .site-breakdown-filters { diff --git a/modules/statistics/jsx/widgets/recruitment.js b/modules/statistics/jsx/widgets/recruitment.js index aecf67fcce8..9f552fdb01c 100644 --- a/modules/statistics/jsx/widgets/recruitment.js +++ b/modules/statistics/jsx/widgets/recruitment.js @@ -9,7 +9,6 @@ import {setupCharts} from './helpers/chartBuilder'; /** * Recruitment - a widget containing statistics for recruitment data. - * * @param {object} props * @return {JSX.Element} */ @@ -163,7 +162,7 @@ const Recruitment = (props) => { > {Object.entries(json['recruitment']).map( ([key, value]) => { - if (key !== 'overall') { + if (key !== 'overall' && value['total_recruitment'] > 0) { return
{progressBarBuilder(value)}
; diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 911f3cfb878..598da9705e0 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -7,7 +7,6 @@ import {setupCharts} from './helpers/chartBuilder'; /** * StudyProgression - a widget containing statistics for study data. - * * @param {object} props * @return {JSX.Element} */ @@ -86,6 +85,64 @@ const StudyProgression = (props) => { } }} views={[ + { + content: + // ############ CBIGR OVERRIDE START ############ +
+ {Object.entries(json['studyprogression'] + ['progressionData']).map( + ([projectName, projectData]) => { + if (projectData.length > 0) { + return
+

{projectName}

+
+ {projectData.map((data) => { + const commonProps = { + className: 'study-progression-button', + style: { + backgroundColor: data['colour'], + color: 'black', + textDecoration: 'none', + }, + key: `progress_${projectName}_${data['title']}`, + }; + + const content = ( + <> +

{data['count']}

+
+ {data['title'].replace('_', ' ')} + {data['count'] !== 1 && 's'} +
+ + ); + + return data['url'] ? ( + + {content} + + ) : ( +
+ {content} +
+ ); + })} +
+
; + } + } + )} +
, + title: 'Study Progression - summary', + }, { content: json['studyprogression']['total_scans'] > 0 ? (
{ StudyProgression.propTypes = { data: PropTypes.object, baseURL: PropTypes.string, - updateFilters: PropTypes.function, - showChart: PropTypes.function, + updateFilters: PropTypes.func, + showChart: PropTypes.func, }; StudyProgression.defaultProps = { data: {}, diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index b947ae58476..f2ed8aae578 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -99,27 +99,55 @@ class Widgets extends \NDB_Page implements ETagCalculator $centerIDs = $user->getCenterIDs(); $sites = \Utility::getSiteList(); - foreach ($projects as $projectID) { + $modules = $this->loris->getActiveModules(); + + $studyWidgets = []; + foreach ($modules as $module) { + $mwidget = $module->getWidgets( + 'study-progression', + $user, + [] + ); + foreach ($mwidget as $mwidget) { + if ($mwidget instanceof \LORIS\dashboard\DataWidget) { + $studyWidgets[$mwidget->label()] = $mwidget->data(); + if (!$module->hasAccess($user)) { + foreach ( + $studyWidgets[$mwidget->label()] as $key => $value + ) { + if (isset($value['url'])) { + $studyWidgets[$mwidget->label()][$key]['url'] = null; + } + } + } + } + } + } + $studyProgressionProjects = []; + foreach ($projects as $pid) { // Set project recruitment data - $projectInfo = $config->getProjectSettings(intval(strval($projectID))); + $projectInfo = $config->getProjectSettings(intval(strval($pid))); if (is_null($projectInfo)) { throw new \LorisException( 'No project settings exist in the Database for ' . - 'project ID ' . intval(strval($projectID)) + 'project ID ' . intval(strval($pid)) ); } - $recruitment[intval(strval($projectID))] - = $this->_createProjectProgressBar( - strval($projectID), - $projectInfo['Name'], - $projectInfo['recruitmentTarget'], - $this->getTotalRecruitmentByProject( - $recruitmentRaw, - $projectID - ), - $recruitmentRaw + $recruitment[intval(strval($pid))] = $this->_createProjectProgressBar( + strval($pid), + $projectInfo['Name'], + $projectInfo['recruitmentTarget'], + $this->getTotalRecruitmentByProject( + $recruitmentRaw, + $pid + ), + $recruitmentRaw + ); + $studyProgressionProjects[$projectInfo['Name']] + = $this->_createProjectSummary( + strval($pid), + $studyWidgets ); - // Set cohort recruitment data $centerList = "'" . implode("','", $centerIDs) . "'"; $projectList = "'" . implode("','", $projects) . "'"; @@ -164,15 +192,16 @@ class Widgets extends \NDB_Page implements ETagCalculator 'cohorts' => $cohortOptions, 'sites' => $siteOptions, 'visits' => $visitOptions, - 'participantStatus' => $participantStatusOptions + 'participantStatus' => $participantStatusOptions, ]; $values = []; // Used for the react widget recruitment.js $values['recruitment'] = $recruitment; // Used for the react widget studyprogression.js $values['studyprogression'] = [ - 'total_scans' => $totalScans, - 'recruitment' => $recruitment + 'total_scans' => $totalScans, + 'recruitment' => $recruitment, + 'progressionData' => $studyProgressionProjects, ]; $values['options'] = $options; $values['recruitmentcohorts'] = $recruitmentCohorts; @@ -363,6 +392,35 @@ class Widgets extends \NDB_Page implements ETagCalculator return $rv; } + /** + * Generates the template data for a project summary. + * + * @param string $ID The name of the progress bar being created. + * @param array $widgets The raw data from the database + * + * @return array data for the project summary + */ + private function _createProjectSummary( + $ID, + $widgets + ): array { + $projectData = []; + foreach ($widgets as $name => $widget) { + foreach ($widget as $projectID => $data) { + if ($projectID == $ID) { + $projectData[] = [ + 'title' => $name, + 'url' => $data['url'], + 'colour' => $data['colour'], + 'count' => $data['count'] + ]; + } + } + } + + return $projectData; + } + /** * Gets the total count of candidates of a specific sex *