diff --git a/modules/statistics/jsx/WidgetIndex.js b/modules/statistics/jsx/WidgetIndex.js index 00fad3e8ab..e71bd40ef0 100644 --- a/modules/statistics/jsx/WidgetIndex.js +++ b/modules/statistics/jsx/WidgetIndex.js @@ -3,6 +3,7 @@ import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import Recruitment from './widgets/recruitment'; import StudyProgression from './widgets/studyprogression'; +import AdminStats from './widgets/adminstats'; import {fetchData} from './Fetch'; import Modal from 'Modal'; import Loader from 'Loader'; @@ -13,6 +14,7 @@ import '../css/WidgetIndex.css'; import {setupCharts, unloadCharts} from './widgets/helpers/chartBuilder'; import jaStrings from '../locale/ja/LC_MESSAGES/statistics.json'; +import hiStrings from '../locale/hi/LC_MESSAGES/statistics.json'; import frStrings from '../locale/fr/LC_MESSAGES/statistics.json'; import zhStrings from '../locale/zh/LC_MESSAGES/statistics.json'; @@ -25,10 +27,12 @@ import zhStrings from '../locale/zh/LC_MESSAGES/statistics.json'; const WidgetIndex = (props) => { const [recruitmentData, setRecruitmentData] = useState({}); const [studyProgressionData, setStudyProgressionData] = useState({}); + const [adminStatsData, setAdminStatsData] = useState({}); const [modalChart, setModalChart] = useState(null); const {t, i18n} = useTranslation(); useEffect( () => { i18n.addResourceBundle('ja', 'statistics', jaStrings); + i18n.addResourceBundle('hi', 'statistics', hiStrings); i18n.addResourceBundle('fr', 'statistics', frStrings); i18n.addResourceBundle('zh', 'statistics', zhStrings); }, []); @@ -273,6 +277,7 @@ const WidgetIndex = (props) => { ); setRecruitmentData(data); setStudyProgressionData(data); + setAdminStatsData(data); }; setup().catch( (error) => { @@ -368,6 +373,11 @@ const WidgetIndex = (props) => { showChart ={showChart} updateFilters ={updateFilters} /> + {loris.userHasPermission('user_account_multisite') && } ); }; @@ -452,3 +462,4 @@ const exportChartAsImage = (chartId) => { 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); }; + diff --git a/modules/statistics/jsx/widgets/adminstats.js b/modules/statistics/jsx/widgets/adminstats.js new file mode 100644 index 0000000000..79f37d3d27 --- /dev/null +++ b/modules/statistics/jsx/widgets/adminstats.js @@ -0,0 +1,150 @@ +import React, {useEffect, useState} from 'react'; +import PropTypes from 'prop-types'; +import i18n from 'I18nSetup'; +import Loader from 'Loader'; +import Panel from 'Panel'; +import {setupCharts} from './helpers/chartBuilder'; +import {useTranslation} from 'react-i18next'; +import jaStrings from '../../locale/ja/LC_MESSAGES/statistics.json'; +import hiStrings from '../../locale/hi/LC_MESSAGES/statistics.json'; +import zhStrings from '../../locale/zh/LC_MESSAGES/statistics.json'; +import frStrings from '../../locale/fr/LC_MESSAGES/statistics.json'; + +/** + * AdminStats - a widget containing admin account statistics. + * + * @param {object} props + * @return {JSX.Element} + */ +const AdminStats = (props) => { + const {t} = useTranslation(); + const [loading, setLoading] = useState(true); + let json = props.data; + + const [chartDetails, setChartDetails] = useState({ + 'adminStats': { + 'userregistrations_bydate': { + sizing: 11, + title: t('User Registrations', {ns: 'statistics'}), + filters: '', + chartType: 'line', + dataType: 'line', + label: t('Registrations', {ns: 'statistics'}), + legend: '', + options: {line: 'line'}, + chartObject: null, + includeTotal: false, + titlePrefix: t('Month', {ns: 'loris'}), + dateFormat: '%m-%Y', + }, + 'uniquelogins_bymonth': { + sizing: 11, + title: t('Unique Monthly Logins', {ns: 'statistics'}), + filters: '', + chartType: 'line', + dataType: 'line', + label: t('Logins', {ns: 'statistics'}), + legend: '', + options: {line: 'line'}, + chartObject: null, + includeTotal: false, + titlePrefix: t('Month', {ns: 'loris'}), + dateFormat: '%m-%Y', + }, + }, + }); + + useEffect(() => { + i18n.addResourceBundle('ja', 'statistics', jaStrings); + i18n.addResourceBundle('zh', 'statistics', zhStrings); + i18n.addResourceBundle('hi', 'statistics', hiStrings); + i18n.addResourceBundle('fr', 'statistics', frStrings); + + let newdetails = {...chartDetails}; + newdetails['adminStats']['userregistrations_bydate']['label'] + = t('Registrations', {ns: 'statistics'}); + newdetails['adminStats']['userregistrations_bydate']['title'] + = t('User Registrations', {ns: 'statistics'}); + newdetails['adminStats']['userregistrations_bydate']['titlePrefix'] + = t('Month', {ns: 'loris'}); + newdetails['adminStats']['uniquelogins_bymonth']['label'] + = t('Logins', {ns: 'statistics'}); + newdetails['adminStats']['uniquelogins_bymonth']['title'] + = t('Unique Monthly Logins', {ns: 'statistics'}); + newdetails['adminStats']['uniquelogins_bymonth']['titlePrefix'] + = t('Month', {ns: 'loris'}); + setChartDetails(newdetails); + }, []); + + const showChart = (section, chartID) => { + return props.showChart(section, chartID, chartDetails, setChartDetails); + }; + + useEffect(() => { + if (json && Object.keys(json).length !== 0) { + const newChartDetails = { + 'adminStats': { + ...chartDetails.adminStats, + 'userregistrations_bydate': { + ...chartDetails.adminStats.userregistrations_bydate, + data: json['adminstats']?.['User Registrations'], + }, + 'uniquelogins_bymonth': { + ...chartDetails.adminStats.uniquelogins_bymonth, + data: json['adminstats']?.['Unique Monthly Logins'], + }, + }, + }; + + setLoading(false); + setTimeout(() => { + setupCharts( + t, + false, + newChartDetails, + t('Total', {ns: 'loris'}) + ).then((data) => { + setChartDetails(data); + }); + }, 0); + json = props.data; + } + }, [props.data]); + + const title = (subtitle) => t('Admin stats', {ns: 'statistics'}) + + ' — ' + t(subtitle, {ns: 'statistics'}); + + return loading ? : ( + { + setupCharts(t, false, chartDetails, t('Total', {ns: 'loris'})); + }} + views={[ + { + content: + showChart('adminStats', 'userregistrations_bydate'), + title: title('User Registrations'), + }, + { + content: + showChart('adminStats', 'uniquelogins_bymonth'), + title: title('Unique Monthly Logins'), + }, + ]} + /> + ); +}; + +AdminStats.propTypes = { + data: PropTypes.object, + baseURL: PropTypes.string, + showChart: PropTypes.func, +}; + +AdminStats.defaultProps = { + data: {}, +}; + +export default AdminStats; diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index 3b979a5576..ce6777f796 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -166,7 +166,15 @@ const createBarChart = (labels, columns, id, targetModal, colours, dataType, yLa return newChart; } -const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => { +const createLineChart = ( + data, + columns, + id, + label, + targetModal, + titlePrefix, + dateFormat, +) => { // Calculate grand total across all data points for percentage calculation let grandTotal = 0; if (data && data.datasets) { @@ -176,6 +184,21 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => } } } + const xFormat = dateFormat || (id.includes('bymonth') ? '%m-%Y' : null); + const axis = xFormat ? { + x: { + type: 'timeseries', + tick: { + format: xFormat, + rotate: -65, + multiline: true, + }, + }, + y: { + max: maxY(data), + label: label, + }, + } : undefined; let newChart = c3.generate({ size: { @@ -185,25 +208,12 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => bindto: targetModal ? targetModal : id, data: { x: 'x', - xFormat: id.includes('bymonth') && '%m-%Y', + xFormat: xFormat || undefined, columns: columns, type: 'area-spline', }, spline: {interpolation: {type: 'monotone'}}, - axis: id.includes('bymonth') && { - x: { - type: 'timeseries', - tick: { - format: '%m-%Y', - rotate: -65, - multiline: true, - }, - }, - y: { - max: maxY(data), - label: label, - }, - }, + axis: axis, zoom: { enabled: true, }, @@ -317,7 +327,11 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { labels = chartData.labels; colours = sexColours; } else if (chart.dataType === 'line') { - columns = formatLineData(chartData, totalLabel); + columns = formatLineData( + chartData, + totalLabel, + chart.includeTotal !== false, + ); } let chartObject = null; if (chart.chartType === 'pie') { @@ -325,7 +339,15 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { } else if (chart.chartType === 'bar') { chartObject = createBarChart(labels, columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.dataType, chart.yLabel); } else if (chart.chartType === 'line') { - chartObject = createLineChart(chartData, columns, `#${chartID}`, chart.label, targetIsModal && '#dashboardModal', chart.titlePrefix); + chartObject = createLineChart( + chartData, + columns, + `#${chartID}`, + chart.label, + targetIsModal && '#dashboardModal', + t(chart.titlePrefix, {ns: 'loris'}), + chart.dateFormat, + ); } newChartDetails[section][chartID].data = chartData; newChartDetails[section][chartID].chartObject = chartObject; @@ -345,7 +367,7 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { * @param {object} data * @return {*[]} */ -const formatLineData = (data, totalLabel) => { +const formatLineData = (data, totalLabel, includeTotal = true) => { const processedData = []; const labels = []; labels.push('x'); @@ -358,16 +380,18 @@ const formatLineData = (data, totalLabel) => { dataset.push(data['datasets'][i].name); processedData.push(dataset.concat(data['datasets'][i].data)); } - const totals = []; - totals.push(totalLabel); - for (let j = 0; j < data['datasets'][0].data.length; j++) { - let total = 0; - for (let i = 0; i < data['datasets'].length; i++) { - total += parseInt(data['datasets'][i].data[j]); + if (includeTotal) { + const totals = []; + totals.push(totalLabel); + for (let j = 0; j < data['datasets'][0].data.length; j++) { + let total = 0; + for (let i = 0; i < data['datasets'].length; i++) { + total += parseInt(data['datasets'][i].data[j]); + } + totals.push(total); } - totals.push(total); + processedData.push(totals); } - processedData.push(totals); return processedData; }; diff --git a/modules/statistics/jsx/widgets/recruitment.js b/modules/statistics/jsx/widgets/recruitment.js index 811af278a1..c7e079f3e7 100644 --- a/modules/statistics/jsx/widgets/recruitment.js +++ b/modules/statistics/jsx/widgets/recruitment.js @@ -82,6 +82,7 @@ const Recruitment = (props) => { options: {line: 'line'}, yLabel: t('Candidates registered', {ns: 'statistics'}), chartObject: null, + titlePrefix: t('Age', {ns: 'loris'}), }, }, } diff --git a/modules/statistics/locale/fr/LC_MESSAGES/statistics.po b/modules/statistics/locale/fr/LC_MESSAGES/statistics.po index bda6141270..1a316d51d0 100644 --- a/modules/statistics/locale/fr/LC_MESSAGES/statistics.po +++ b/modules/statistics/locale/fr/LC_MESSAGES/statistics.po @@ -25,6 +25,9 @@ msgstr "Statistiques" msgid "Study Progression" msgstr "Progression des études" +msgid "Admin stats" +msgstr "Statistiques d'administration" + msgid "Summary" msgstr "Résumé" @@ -74,6 +77,18 @@ msgstr "Répartition selon le sexe biologique par site" msgid "Candidate Age at Registration" msgstr "Âge du candidat au moment de l'inscription" +msgid "User Registrations" +msgstr "Inscriptions d'utilisateurs" + +msgid "Registrations" +msgstr "Inscriptions" + +msgid "Unique Monthly Logins" +msgstr "Connexions mensuelles uniques" + +msgid "Logins" +msgstr "Connexions" + msgid "Candidates registered" msgstr "Candidats inscrits" diff --git a/modules/statistics/locale/hi/LC_MESSAGES/statistics.po b/modules/statistics/locale/hi/LC_MESSAGES/statistics.po new file mode 100644 index 0000000000..b68f1c56ef --- /dev/null +++ b/modules/statistics/locale/hi/LC_MESSAGES/statistics.po @@ -0,0 +1,182 @@ +# Default LORIS strings to be translated (English). +# Copy this to a language specific file and add translations to the +# new file. +# Copyright (C) 2025 +# This file is distributed under the same license as the LORIS package. +# Saagar Arya , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: LORIS 27\n" +"Report-Msgid-Bugs-To: https://github.com/aces/Loris/issues\n" +"POT-Creation-Date: 2025-04-08 14:37-0400\n" +"PO-Revision-Date: 2026-06-10 21:23-0500\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.8\n" + +msgid "Statistics" +msgstr "सांख्यिकी" + +msgid "Study Progression" +msgstr "अध्ययन प्रगति" + +msgid "Admin stats" +msgstr "प्रशासनिक आँकड़े" + +msgid "Summary" +msgstr "सारांश" + +msgid "Site Scans" +msgstr "साइट स्कैन" + +msgid "Site Recruitment" +msgstr "साइट भर्ती" + +msgid "-- Clear Selection --" +msgstr "-- चयन साफ़ करें --" + +msgid "Scan sessions per site" +msgstr "प्रति साइट स्कैन सत्र" + +msgid "Recruitment per site" +msgstr "प्रति साइट भर्ती" + +msgid "Download as PNG" +msgstr "PNG के रूप में डाउनलोड करें" + +msgid "Labels" +msgstr "लेबल" + +msgid "Date Registered" +msgstr "पंजीकरण तिथि" + +msgid "Range Start" +msgstr "सीमा प्रारंभ" + +msgid "Range End" +msgstr "सीमा समाप्त" + +msgid "Total recruitment by Age" +msgstr "आयु के अनुसार कुल भर्ती" + +msgid "Ethnicity at Screening" +msgstr "स्क्रीनिंग के समय जातीयता" + +msgid "Total Recruitment per Site" +msgstr "प्रति साइट कुल भर्ती" + +msgid "Biological sex breakdown by site" +msgstr "साइट के अनुसार जैविक लिंग विभाजन" + +msgid "Candidate Age at Registration" +msgstr "पंजीकरण के समय उम्मीदवार की आयु" + +msgid "User Registrations" +msgstr "उपयोगकर्ता पंजीकरण" + +msgid "Registrations" +msgstr "पंजीकरण" + +msgid "Unique Monthly Logins" +msgstr "अद्वितीय मासिक लॉगिन" + +msgid "Logins" +msgstr "लॉगिन" + +msgid "Candidates registered" +msgstr "पंजीकृत उम्मीदवार" + +msgid "Target: {{target}}" +msgstr "लक्ष्य: {{target}}" + +msgid "Recruitment target of {{target}} was reached." +msgstr "{{target}} का भर्ती लक्ष्य प्राप्त हो गया।" + +msgid "Recruitment target of {{target}} was not reached." +msgstr "{{target}} का भर्ती लक्ष्य प्राप्त नहीं हुआ।" + +msgid "No target set" +msgstr "कोई लक्ष्य निर्धारित नहीं" + +msgid "{{total}} total participants." +msgstr "कुल {{total}} प्रतिभागी।" + +msgid "Overall Recruitment" +msgstr "कुल भर्ती" + +msgid "Recruitment" +msgstr "भर्ती" + +msgid "Overall" +msgstr "समग्र" + +msgid "Site Breakdown" +msgstr "साइट विभाजन" + +msgid "Project Breakdown" +msgstr "परियोजना विभाजन" + +msgid "Cohort Breakdown" +msgstr "कोहोर्ट विभाजन" + +msgid "Total Participants: {{count}}" +msgstr "कुल प्रतिभागी: {{count}}" + +msgid "Projects: {{count}}" +msgstr "परियोजनाएँ: {{count}}" + +msgid "Cohorts: {{count}}" +msgstr "कोहोर्ट: {{count}}" + +msgid "Age (Years)" +msgstr "आयु (वर्ष)" + +msgid "Participants" +msgstr "प्रतिभागी" + +msgid "Unknown" +msgstr "अज्ञात" + +msgid "Total Scans: {{count}}" +msgstr "कुल स्कैन: {{count}}" + +msgid "There have been no scans yet." +msgstr "अभी तक कोई स्कैन नहीं हुए हैं।" + +msgid "There have been no candidates registered yet." +msgstr "अभी तक कोई उम्मीदवार पंजीकृत नहीं हुए हैं।" + +msgid "There is no data yet." +msgstr "अभी तक कोई डेटा नहीं है।." + +# Project sizes +msgid "Project Dataset Sizes" +msgstr "परियोजना डेटासेट आकार" + +msgid "Dataset size breakdown by project" +msgstr "परियोजना के अनुसार डेटासेट आकार का विवरण" + +msgid "Dataset Size" +msgid_plural "Dataset Size" +msgstr[0] "डेटासेट का आकार" +msgstr[1] "डेटासेट का आकार" + +msgid "Size (GB)" +msgstr "आकार (जीबी)" + +msgid "%s GB" +msgstr "%s जीबी" + +msgid "Total Size: {{count}} GB" +msgstr "कुल आकार: {{count}} जीबी" + +msgid "Pie" +msgstr "पाई" + +msgid "Bar" +msgstr "बार" diff --git a/modules/statistics/locale/ja/LC_MESSAGES/statistics.po b/modules/statistics/locale/ja/LC_MESSAGES/statistics.po index 27f16d3005..2270013605 100644 --- a/modules/statistics/locale/ja/LC_MESSAGES/statistics.po +++ b/modules/statistics/locale/ja/LC_MESSAGES/statistics.po @@ -24,6 +24,9 @@ msgstr "統計" msgid "Study Progression" msgstr "学習の進捗" +msgid "Admin stats" +msgstr "管理統計" + msgid "Summary" msgstr "まとめ" @@ -73,6 +76,18 @@ msgstr "部位別の生物学的性別の内訳" msgid "Candidate Age at Registration" msgstr "登録時の候補者の年齢" +msgid "User Registrations" +msgstr "ユーザー登録数" + +msgid "Registrations" +msgstr "登録数" + +msgid "Unique Monthly Logins" +msgstr "月別ユニークログイン数" + +msgid "Logins" +msgstr "ログイン" + msgid "Candidates registered" msgstr "登録候補者" @@ -126,3 +141,42 @@ msgstr "参加者" msgid "Unknown" msgstr "未知" + +msgid "Total Scans: {{count}}" +msgstr "スキャン総数:{{count}}" + +msgid "There have been no scans yet." +msgstr "まだスキャンは行われていません。" + +msgid "There have been no candidates registered yet." +msgstr "まだ登録された候補者はいません。" + +msgid "There is no data yet." +msgstr "まだデータはありません。" + +# Project sizes +msgid "Project Dataset Sizes" +msgstr "プロジェクトのデータセットのサイズ" + +msgid "Dataset size breakdown by project" +msgstr "プロジェクト別のデータセット規模の内訳" + +msgid "Dataset Size" +msgid_plural "Dataset Size" +msgstr[0] "データセットのサイズ" +msgstr[1] "データセットのサイズ" + +msgid "Size (GB)" +msgstr "容量(GB)" + +msgid "%s GB" +msgstr "%s GB" + +msgid "Total Size: {{count}} GB" +msgstr "合計サイズ:{{count}} GB" + +msgid "Pie" +msgstr "円グラフ" + +msgid "Bar" +msgstr "棒グラフ" diff --git a/modules/statistics/locale/statistics.pot b/modules/statistics/locale/statistics.pot index 0d68ca86ad..f7f2774e14 100644 --- a/modules/statistics/locale/statistics.pot +++ b/modules/statistics/locale/statistics.pot @@ -24,6 +24,9 @@ msgstr "" msgid "Study Progression" msgstr "" +msgid "Admin stats" +msgstr "" + msgid "Summary" msgstr "" @@ -72,6 +75,18 @@ msgstr "" msgid "Candidate Age at Registration" msgstr "" +msgid "User Registrations" +msgstr "" + +msgid "Registrations" +msgstr "" + +msgid "Unique Monthly Logins" +msgstr "" + +msgid "Logins" +msgstr "" + msgid "Candidates registered" msgstr "" @@ -148,6 +163,7 @@ msgstr "" msgid "Dataset Size" msgid_plural "Dataset Size" msgstr[0] "" +msgstr[1] "" msgid "Size (GB)" msgstr "" diff --git a/modules/statistics/locale/zh/LC_MESSAGES/statistics.po b/modules/statistics/locale/zh/LC_MESSAGES/statistics.po index b5793d5774..18061dd23e 100644 --- a/modules/statistics/locale/zh/LC_MESSAGES/statistics.po +++ b/modules/statistics/locale/zh/LC_MESSAGES/statistics.po @@ -25,6 +25,9 @@ msgstr "统计数据" msgid "Study Progression" msgstr "学习进展" +msgid "Admin stats" +msgstr "管理统计" + msgid "Summary" msgstr "概括" @@ -73,6 +76,18 @@ msgstr "按地点划分的生物性别细分" msgid "Candidate Age at Registration" msgstr "候选人注册时的年龄" +msgid "User Registrations" +msgstr "用户注册" + +msgid "Registrations" +msgstr "注册" + +msgid "Unique Monthly Logins" +msgstr "每月唯一登录次数" + +msgid "Logins" +msgstr "登录" + msgid "Candidates registered" msgstr "候选人已登记" @@ -126,3 +141,41 @@ msgstr "参加者" msgid "Unknown" msgstr "未知" + +msgid "Total Scans: {{count}}" +msgstr "扫描总数:{{count}}" + +msgid "There have been no scans yet." +msgstr "目前还没有扫描。" + +msgid "There have been no candidates registered yet." +msgstr "目前还没有已注册的候选人。" + +msgid "There is no data yet." +msgstr "目前还没有数据。" + +# Project sizes +msgid "Project Dataset Sizes" +msgstr "项目数据集大小" + +msgid "Dataset size breakdown by project" +msgstr "按项目划分的数据集大小明细" + +msgid "Dataset Size" +msgid_plural "Dataset Size" +msgstr[0] "数据集大小" + +msgid "Size (GB)" +msgstr "大小(GB)" + +msgid "%s GB" +msgstr "%s GB" + +msgid "Total Size: {{count}} GB" +msgstr "总大小:{{count}} GB" + +msgid "Pie" +msgstr "饼图" + +msgid "Bar" +msgstr "柱状图" diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 76a6fba686..09f7384d11 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -113,6 +113,7 @@ class Widgets extends \NDB_Page implements ETagCalculator $sites = \Utility::getSiteList(); $studyWidgets = $this->_collectStudyProgressionWidgets($user); + $adminStats = $this->_collectAdminStatsWidgets($user); $studyProgressionProjects = $this->_createStudyProgressionProjects( $projects, $config, @@ -228,6 +229,7 @@ class Widgets extends \NDB_Page implements ETagCalculator 'progressionData' => $studyProgressionProjects, 'total_size' => $totalSizeOfProjectsGB, ]; + $values['adminstats'] = $adminStats; $values['options'] = $options; $values['recruitmentcohorts'] = $recruitmentCohorts; @@ -533,6 +535,40 @@ class Widgets extends \NDB_Page implements ETagCalculator return $widgetData; } + /** + * Collects admin stats widgets from all active modules. + * + * @param \User $user The current user + * + * @return array Collected admin stats widget data + */ + private function _collectAdminStatsWidgets(\User $user): array + { + $module = $this->loris->getModule('user_accounts'); + if (!$module->hasAccess($user)) { + return []; + } + $adminStats = []; + + $moduleWidgets = $module->getWidgets('admin-stats', $user, []); + + foreach ($moduleWidgets as $widget) { + if (!($widget instanceof \LORIS\dashboard\DataWidget)) { + continue; + } + + $widgetData = [ + ...$widget->data(), + 'colour' => $widget->colour(), + 'ref' => $widget, + ]; + + $adminStats[$widget->label()->getN(2)] = $widgetData; + } + + return $adminStats; + } + /** * Creates study progression data for all projects. * diff --git a/modules/user_accounts/php/module.class.inc b/modules/user_accounts/php/module.class.inc index d93f7dc995..071752520b 100644 --- a/modules/user_accounts/php/module.class.inc +++ b/modules/user_accounts/php/module.class.inc @@ -75,11 +75,12 @@ class Module extends \Module */ public function getWidgets(string $type, \User $user, array $options) : array { + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + $baseURL = $factory->settings()->getBaseURL(); + switch ($type) { case "usertasks": - $factory = \NDB_Factory::singleton(); - $DB = $factory->database(); - $baseURL = $factory->settings()->getBaseURL(); return [ new \LORIS\dashboard\TaskQueryWidget( $user, @@ -100,6 +101,99 @@ class Module extends \Module "pending-accounts" ) ]; + case "admin-stats": + $registrations = iterator_to_array( + $DB->pselect( + "SELECT + EXTRACT(YEAR FROM account_request_date) AS RequestYear, + EXTRACT(MONTH FROM account_request_date) AS RequestMonth, + COUNT(ID) AS RequestCount + FROM users + WHERE Pending_approval = 'N' + AND account_request_date IS NOT NULL + GROUP BY RequestYear, RequestMonth + ORDER BY RequestYear, RequestMonth", + [] + ) + ); + + $registrationsData = [ + 'labels' => array_map( + fn(array $row) : string => sprintf( + '%02d-%04d', + intval($row['RequestMonth']), + intval($row['RequestYear']) + ), + $registrations, + ), + 'datasets' => [ + [ + 'name' => dgettext('statistics', 'User Registrations'), + 'data' => array_map( + fn(array $row) : int => intval($row['RequestCount']), + $registrations, + ), + ], + ], + ]; + + $uniqueLogins = iterator_to_array( + $DB->pselect( + "SELECT + EXTRACT(YEAR FROM Login_timestamp) AS LoginYear, + EXTRACT(MONTH FROM Login_timestamp) AS LoginMonth, + COUNT(DISTINCT ulh.userID) AS UniqueLogins + FROM user_login_history AS ulh + JOIN users AS u USING(userID) + WHERE Fax IS NULL + GROUP BY LoginYear, LoginMonth + ORDER BY LoginYear, LoginMonth", + [] + ) + ); + + $uniqueLoginsData = [ + 'labels' => array_map( + fn(array $row) : string => sprintf( + '%02d-%04d', + intval($row['LoginMonth']), + intval($row['LoginYear']) + ), + $uniqueLogins, + ), + 'datasets' => [ + [ + 'name' => dgettext('statistics', 'Unique Monthly Logins'), + 'data' => array_map( + fn(array $row) : int => intval($row['UniqueLogins']), + $uniqueLogins, + ), + ], + ], + ]; + + return [ + new \LORIS\dashboard\DataWidget( + new \LORIS\GUI\LocalizableString( + 'statistics', + 'User Registration', + 'User Registrations' + ), + $registrationsData, + '', + 'rgb(214, 235, 255)', + ), + new \LORIS\dashboard\DataWidget( + new \LORIS\GUI\LocalizableString( + 'statistics', + 'Unique Monthly Login', + 'Unique Monthly Logins' + ), + $uniqueLoginsData, + '', + 'rgb(223, 245, 221)', + ), + ]; } return []; }