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 [];
}