# learnpress/4.4.4/assets/src/js/admin/statistics/tab-users.js

LearnPress – WordPress LMS Plugin for Create and Sell Online Courses, version 4.4.4. 413 lines.

- Page: https://pluginprobe.com/plugins/learnpress/4.4.4/code/assets/src/js/admin/statistics/tab-users.js
- Raw: https://pluginprobe.com/plugins/learnpress/4.4.4/raw/assets/src/js/admin/statistics/tab-users.js
- Modified: 2026-08-03T03:26:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/learnpress/4.4.4/code/assets/src/js/admin/statistics/tab-users.js#L10-L20`.

```javascript
/**
 * Users tab module.
 *
 * Fetches the `dashboard` payload and renders KPIs, registered-users chart,
 * 5-step funnel (incl. failed), Top Students and Top Courses by Students
 * tables, popups and CSV export.
 *
 * @since 4.4.2
 * @version 1.0.0
 */

import * as lpUtils from 'lpAssetsJsPath/utils.js';
import { LP_STATS_FILTER_CHANGED, LP_STATS_EXPORT_CSV } from './state.js';
import { lpStatsFetch, getStatsConfig, getStatsI18n } from './api.js';
import { renderKpi } from './kpi.js';
import { renderLineChart } from './chart.js';
import { renderDataTable } from './data-table.js';
import { lpStatsReportModal } from './report-modal.js';
import { exportCsv, buildCsvFilename } from './csv.js';

const sprintfLite = ( template, value ) =>
	String( template ).replace( /%[ds]/, String( value ) ).replace( /%%/g, '%' );

export class LpStatsTabUsers {
	static selectors = {
		elContainer: '.lp-stats-tab-users',
		elChartCanvas: '#user-chart-content',
		elFunnelStep: '.lp-stats-funnel__step',
		elTableTopStudents: '.lp-stats-table-top-students',
		elTableCoursesByStudents: '.lp-stats-table-courses-by-students',
		elBtnViewAllStudents: '.lp-stats-view-all-students',
		elBtnViewAllCourses: '.lp-stats-view-all-courses-by-students',
		elSkeleton: '.lp-skeleton-animation',
	};

	static kpiCards = {
		users_activated: '.lp-kpi-users-activated',
		students: '.lp-kpi-students',
		instructors: '.lp-kpi-instructors',
		not_started: '.lp-kpi-not-started',
		in_progress: '.lp-kpi-in-progress',
		finished: '.lp-kpi-finished',
	};

	constructor() {
		this.elContainer = null;
		this.isRequesting = false;
		this.pendingReload = false;
		this.tables = {};
	}

	init() {
		this.elContainer = document.querySelector(
			LpStatsTabUsers.selectors.elContainer
		);
		if ( ! this.elContainer ) {
			return;
		}

		this.events();
		this.loadData();
	}

	events() {
		if ( LpStatsTabUsers._loadedEvents ) {
			return;
		}
		LpStatsTabUsers._loadedEvents = this;

		lpUtils.eventHandlers( 'click', [
			{
				selector: LpStatsTabUsers.selectors.elBtnViewAllStudents,
				class: this,
				callBack: this.viewAllStudents.name,
			},
			{
				selector: LpStatsTabUsers.selectors.elBtnViewAllCourses,
				class: this,
				callBack: this.viewAllCoursesByStudents.name,
			},
		] );

		document.addEventListener( LP_STATS_FILTER_CHANGED, () => this.loadData() );
		document.addEventListener( LP_STATS_EXPORT_CSV, () => this.exportTables() );
	}

	toggleSkeletons( show ) {
		this.elContainer
			.querySelectorAll( LpStatsTabUsers.selectors.elSkeleton )
			.forEach( ( el ) => {
				el.style.display = show ? 'block' : 'none';
			} );
	}

	loadData() {
		if ( this.isRequesting ) {
			this.pendingReload = true;
			return;
		}
		this.isRequesting = true;

		lpStatsFetch(
			'user-statistics',
			{},
			{
				before: () => this.toggleSkeletons( true ),
				success: ( response ) => this.render( response.data ),
				error: ( err ) => {
					console.error( 'LP Statistics users:', err );
					this.render( null );
				},
				completed: () => {
					this.toggleSkeletons( false );
					this.isRequesting = false;
					if ( this.pendingReload ) {
						this.pendingReload = false;
						this.loadData();
					}
				},
			}
		);
	}

	render( data ) {
		if ( ! data?.dashboard ) {
			console.error( 'LP Statistics users: dashboard payload missing.' );
			data = { chart_data: {}, dashboard: {} };
		}

		const dashboard = data.dashboard || {};

		this.renderKpis( dashboard.kpis || {} );
		this.renderChart( data.chart_data || {} );
		this.renderFunnel( dashboard.funnel || {} );
		this.renderTables( dashboard );
	}

	renderKpis( kpis ) {
		Object.entries( LpStatsTabUsers.kpiCards ).forEach( ( [ key, selector ] ) => {
			const elCard = this.elContainer.querySelector( selector );
			const payload = { ...( kpis[ key ] || {} ) };

			if ( 'users_activated' === key ) {
				payload.subline = sprintfLite(
					getStatsI18n( 'newThisPeriod', '+%d this period' ),
					payload.new_in_period ?? 0
				);
			}
			if ( 'students' === key ) {
				payload.subline = sprintfLite(
					getStatsI18n( 'activeInPeriod', '%d active in this period' ),
					payload.active_in_period ?? 0
				);
			}
			if ( 'instructors' === key ) {
				payload.subline = `${ payload.active_in_period ?? 0 }/${
					payload.value ?? 0
				} ${ getStatsI18n( 'activeThisPeriod', 'active this period' ) }`;
			}
			if ( 'not_started' === key ) {
				payload.subline = getStatsI18n( 'afterEnrollment', 'After enrollment' );
			}
			if ( 'in_progress' === key ) {
				payload.subline = getStatsI18n( 'currentLearners', 'Current learners' );
			}
			if ( 'finished' === key && null != payload.completion_rate ) {
				payload.subline = sprintfLite(
					getStatsI18n( 'completionRateSub', '%s%% completion rate' ),
					payload.completion_rate
				);
			}

			renderKpi( elCard, payload );
		} );
	}

	renderChart( chartData ) {
		renderLineChart(
			LpStatsTabUsers.selectors.elChartCanvas,
			{
				labels: chartData.labels || [],
				datasets: [
					{
						label:
							chartData.line_label ||
							getStatsI18n( 'registeredUsers', 'Registered users' ),
						data: chartData.data || [],
						yAxisID: 'y',
					},
				],
				xLabel: chartData.x_label || '',
				granularity: chartData.granularity || '',
			},
			{ yCurrency: false }
		);
	}

	renderFunnel( funnel ) {
		const steps = [ 'registered', 'enrolled', 'started', 'completed', 'failed' ];
		let previous = null;

		steps.forEach( ( step ) => {
			const elStep = this.elContainer.querySelector(
				`${ LpStatsTabUsers.selectors.elFunnelStep }[data-step="${ step }"]`
			);
			if ( ! elStep ) {
				return;
			}

			const count = Number( funnel[ step ] ?? 0 );
			// 'failed' is an annotation on 'started', not the next narrowing step.
			let base = previous;
			if ( 'failed' === step ) {
				base = Number( funnel.started ?? 0 );
			} else if ( null === previous ) {
				base = count;
			}
			const width = base > 0 ? Math.min( 100, ( count / base ) * 100 ) : 0;

			elStep.querySelector( '.lp-stats-funnel__count' ).textContent =
				String( count );
			elStep.querySelector( '.lp-stats-funnel__bar' ).style.width = `${ width }%`;

			if ( 'failed' !== step ) {
				previous = count;
			}
		} );
	}

	completionBadge( rate ) {
		if ( null == rate ) {
			return '';
		}
		const { completionBadge = {} } = getStatsConfig();
		const green = completionBadge.green ?? 60;
		const yellow = completionBadge.yellow ?? 40;

		if ( rate >= green ) {
			return 'green';
		}
		return rate >= yellow ? 'yellow' : 'red';
	}

	studentStatusLabel( slug ) {
		const labels = {
			active: getStatsI18n( 'statusActive', 'Active' ),
			at_risk: getStatsI18n( 'statusAtRisk', 'At risk' ),
			idle: getStatsI18n( 'statusIdle', 'Idle' ),
		};

		return labels[ slug ] || String( slug || '' );
	}

	studentStatusBadge( slug ) {
		const badges = {
			active: 'green',
			at_risk: 'yellow',
			idle: 'grey',
		};

		return badges[ slug ] || '';
	}

	formatLastActive( value ) {
		if ( ! value ) {
			return '—';
		}

		const date = new Date( String( value ).replace( ' ', 'T' ) );
		if ( isNaN( date.getTime() ) ) {
			return '—';
		}

		try {
			const days = Math.round( ( date.getTime() - Date.now() ) / 86400000 );
			return new Intl.RelativeTimeFormat( undefined, { numeric: 'auto' } ).format(
				days,
				'day'
			);
		} catch {
			return date.toLocaleDateString();
		}
	}

	/**
	 * @param {boolean} withScore avg_score column only when the payload carries scores.
	 */
	topStudentsColumns( withScore = true ) {
		const columns = [
			{ key: 'name', label: getStatsI18n( 'student', 'Student' ) },
			{ key: 'enrolled', label: getStatsI18n( 'enrolled', 'Enrolled' ) },
			{ key: 'completed', label: getStatsI18n( 'completedLabel', 'Completed' ) },
		];

		if ( withScore ) {
			columns.push( {
				key: 'avg_score',
				label: getStatsI18n( 'avgScore', 'Quiz pass rate' ),
				format: ( value ) => ( null == value ? '—' : `${ value }%` ),
			} );
		}

		columns.push(
			{
				key: 'last_active',
				label: getStatsI18n( 'lastActive', 'Last active' ),
				format: ( value ) => this.formatLastActive( value ),
				csv: ( row ) => row.last_active || '',
			},
			{
				key: 'status',
				label: getStatsI18n( 'status', 'Status' ),
				format: ( value ) => this.studentStatusLabel( value ),
				badge: ( row ) => this.studentStatusBadge( row.status ),
				csv: ( row ) => row.status,
			}
		);

		return columns;
	}

	coursesByStudentsColumns() {
		return [
			{ key: 'name', label: getStatsI18n( 'course', 'Course' ) },
			{ key: 'enrolled', label: getStatsI18n( 'enrolled', 'Enrolled' ) },
			{ key: 'started', label: getStatsI18n( 'startedLabel', 'Started' ) },
			{ key: 'completed', label: getStatsI18n( 'completedLabel', 'Completed' ) },
			{
				key: 'completion_rate',
				label: getStatsI18n( 'completion', 'Completion' ),
				format: ( value ) => ( null == value ? '—' : `${ value }%` ),
				badge: ( row ) => this.completionBadge( row.completion_rate ),
			},
			{ key: 'active_7d', label: getStatsI18n( 'activeLast7dShort', 'Active 7d' ) },
		];
	}

	/**
	 * avg_score is null when quiz data is unavailable — hide the whole column.
	 *
	 * @param {Array} rows
	 */
	hasScores( rows = [] ) {
		return rows.some( ( row ) => null != row.avg_score );
	}

	renderTables( dashboard ) {
		const students = dashboard.top_students || [];
		this.tables[ 'top-students' ] = renderDataTable(
			this.elContainer.querySelector(
				LpStatsTabUsers.selectors.elTableTopStudents
			),
			this.topStudentsColumns( this.hasScores( students ) ),
			students
		);

		this.tables[ 'courses-by-students' ] = renderDataTable(
			this.elContainer.querySelector(
				LpStatsTabUsers.selectors.elTableCoursesByStudents
			),
			this.coursesByStudentsColumns(),
			dashboard.top_courses_by_students || []
		);
	}

	viewAllStudents( args ) {
		const btn = args.target.closest(
			LpStatsTabUsers.selectors.elBtnViewAllStudents
		);
		if ( ! btn || ! this.elContainer.contains( btn ) ) {
			return;
		}

		lpStatsReportModal.open( {
			report: 'top_students',
			title: getStatsI18n( 'topStudents', 'Top students' ),
			tableId: 'top-students',
		} );
	}

	viewAllCoursesByStudents( args ) {
		const btn = args.target.closest(
			LpStatsTabUsers.selectors.elBtnViewAllCourses
		);
		if ( ! btn || ! this.elContainer.contains( btn ) ) {
			return;
		}

		lpStatsReportModal.open( {
			report: 'courses_by_students',
			title: getStatsI18n(
				'topCoursesByStudents',
				'Top courses by students'
			),
			tableId: 'courses-by-students',
		} );
	}

	exportTables() {
		Object.entries( this.tables ).forEach( ( [ tableId, handle ] ) => {
			if ( handle && handle.rows.length ) {
				exportCsv(
					buildCsvFilename( 'users', tableId ),
					handle.columns,
					handle.rows
				);
			}
		} );
	}
}

export const lpStatsTabUsers = new LpStatsTabUsers();

```
