# learnpress/4.4.6/assets/src/js/admin/statistics/tab-courses.js

LearnPress – WordPress LMS Plugin for Create and Sell Online Courses, version 4.4.6. 391 lines.

- Page: https://pluginprobe.com/plugins/learnpress/4.4.6/code/assets/src/js/admin/statistics/tab-courses.js
- Raw: https://pluginprobe.com/plugins/learnpress/4.4.6/raw/assets/src/js/admin/statistics/tab-courses.js
- Modified: 2026-08-31T10:33:54+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.6/code/assets/src/js/admin/statistics/tab-courses.js#L10-L20`.

```javascript
/**
 * Courses tab module.
 *
 * Fetches the `dashboard` payload and renders KPIs, course performance,
 * published-courses chart, health checks, inventory, 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 LpStatsTabCourses {
	static selectors = {
		elContainer: '.lp-stats-tab-courses',
		elChartCanvas: '#course-chart-content',
		elTablePerformance: '.lp-stats-table-course-performance',
		elTableInventory: '.lp-stats-table-content-inventory',
		elBtnViewAllPerformance: '.lp-stats-view-all-performance',
		elPerformanceRow: '.lp-stats-course-performance-row',
		elHealthCheckCount: '.lp-stats-health-check__count',
		elSkeleton: '.lp-skeleton-animation',
	};

	static kpiCards = {
		published: '.lp-kpi-published',
		pending_review: '.lp-kpi-pending-review',
		future: '.lp-kpi-future',
		enrollments: '.lp-kpi-enrollments',
		avg_completion: '.lp-kpi-avg-completion',
		courses_without_enrollment: '.lp-kpi-courses-without-enrollment',
	};

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

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

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

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

		lpUtils.eventHandlers( 'click', [
			{
				selector: LpStatsTabCourses.selectors.elBtnViewAllPerformance,
				class: this,
				callBack: this.viewAllPerformance.name,
			},
			{
				selector: LpStatsTabCourses.selectors.elPerformanceRow,
				class: this,
				callBack: this.openCourseEdit.name,
			},
		] );

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

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

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

		lpStatsFetch(
			'course-statistics',
			{},
			{
				before: () => this.toggleSkeletons( true ),
				success: ( response ) => this.render( response.data ),
				error: ( err ) => {
					console.error( 'LP Statistics courses:', 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 courses: dashboard payload missing.' );
			data = { chart_data: {}, dashboard: {} };
		}

		const dashboard = data.dashboard || {};

		this.renderKpis( dashboard.kpis || {} );
		this.renderTables( dashboard );
		// Prefer the scoped chart from the dashboard payload so instructor/category
		// changes redraw the chart; fall back to the legacy unscoped series.
		this.renderChart( dashboard.chart || data.chart_data || {} );
		this.renderHealthChecks( dashboard.health_checks || {} );
	}

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

			if ( 'published' === key ) {
				payload.subline = sprintfLite(
					getStatsI18n( 'addedThisPeriod', '%d added this period' ),
					payload.added_in_period ?? 0
				);
			}
			if ( 'pending_review' === key ) {
				payload.subline = getStatsI18n(
					'needsInstructorAction',
					'Needs instructor action'
				);
			}
			if ( 'future' === key ) {
				payload.subline = getStatsI18n(
					'scheduledReleases',
					'Scheduled releases'
				);
			}
			if ( 'avg_completion' === key ) {
				if ( 'number' === typeof payload.value ) {
					payload.formatted = `${ payload.value }%`;
				}
				payload.subline = sprintfLite(
					getStatsI18n( 'targetPercent', 'Target: %s%%' ),
					payload.target ?? getStatsConfig().completionTarget ?? 0
				);
			}

			renderKpi( elCard, payload );

			if ( 'avg_completion' === key ) {
				this.renderCompletionProgress( elCard, payload );
			}
		} );
	}

	renderCompletionProgress( elCard, payload ) {
		const elBar = elCard?.querySelector( '.lp-kpi-progress__bar' );
		if ( ! elBar ) {
			return;
		}

		const value = Number( payload.value || 0 );
		const target = Number( payload.target || getStatsConfig().completionTarget || 0 );
		const width = target > 0 ? Math.min( 100, ( value / target ) * 100 ) : 0;
		elBar.style.width = `${ width }%`;
	}

	renderChart( chartData ) {
		renderLineChart(
			LpStatsTabCourses.selectors.elChartCanvas,
			{
				labels: chartData.labels || [],
				datasets: [
					{
						label:
							chartData.line_label ||
							getStatsI18n( 'publishedCourses', 'Published courses' ),
						data: chartData.data || [],
						yAxisID: 'y',
					},
				],
				xLabel: chartData.x_label || '',
				granularity: chartData.granularity || '',
			},
			{ yCurrency: false }
		);
	}

	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';
	}

	performanceColumns() {
		return [
			{ key: 'name', label: getStatsI18n( 'course', 'Course' ) },
			{ key: 'instructor', label: getStatsI18n( 'instructor', 'Instructor' ) },
			{
				key: 'revenue_formatted',
				label: getStatsI18n( 'revenue', 'Revenue' ),
				csv: ( row ) => row.revenue,
			},
			{
				key: 'enrollments',
				label: getStatsI18n( 'enrollments', 'Enrollments' ),
			},
			{
				key: 'completion_rate',
				label: getStatsI18n( 'completion', 'Completion' ),
				format: ( value ) => ( null == value ? '-' : `${ value }%` ),
				badge: ( row ) => this.completionBadge( row.completion_rate ),
			},
		];
	}

	inventoryLabel( key ) {
		const labels = {
			courses: getStatsI18n( 'courses', 'Courses' ),
			lessons: getStatsI18n( 'lessons', 'Lessons' ),
			quizzes: getStatsI18n( 'quizzes', 'Quizzes' ),
			assignments: getStatsI18n( 'assignments', 'Assignments' ),
		};

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

	inventoryStatusLabel( key ) {
		const labels = {
			publish: getStatsI18n( 'published', 'Published' ),
			pending: getStatsI18n( 'pending', 'Pending' ),
			future: getStatsI18n( 'future', 'Future' ),
			draft: getStatsI18n( 'drafts', 'Drafts' ),
			total: getStatsI18n( 'total', 'Total' ),
		};

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

	inventoryRows( inventory = {} ) {
		return Object.entries( inventory ).map( ( [ type, counts ] ) => ( {
			type,
			label: this.inventoryLabel( type ),
			...( counts || {} ),
		} ) );
	}

	inventoryColumns( rows = [] ) {
		const preferred = [ 'publish', 'pending', 'future', 'draft', 'total' ];
		const statusKeys = preferred.filter( ( key ) =>
			rows.some( ( row ) => Object.prototype.hasOwnProperty.call( row, key ) )
		);

		return [
			{ key: 'label', label: getStatsI18n( 'content', 'Content' ) },
			...statusKeys.map( ( key ) => ( {
				key,
				label: this.inventoryStatusLabel( key ),
				csv: ( row ) => row[ key ],
			} ) ),
		];
	}

	renderTables( dashboard ) {
		const performanceRows = dashboard.performance || [];
		const performanceHandle = renderDataTable(
			this.elContainer.querySelector(
				LpStatsTabCourses.selectors.elTablePerformance
			),
			this.performanceColumns(),
			performanceRows,
			{
				emptyText: getStatsI18n(
					'noCoursePerformance',
					'No course performance data in this period.'
				),
			}
		);
		this.tables.performance = performanceHandle;
		this.decoratePerformanceRows( performanceRows );

		const inventoryRows = this.inventoryRows( dashboard.inventory || {} );
		this.tables.inventory = renderDataTable(
			this.elContainer.querySelector(
				LpStatsTabCourses.selectors.elTableInventory
			),
			this.inventoryColumns( inventoryRows ),
			inventoryRows
		);
	}

	decoratePerformanceRows( rows = [] ) {
		const tableRows = this.elContainer.querySelectorAll(
			`${ LpStatsTabCourses.selectors.elTablePerformance } tbody tr`
		);

		rows.forEach( ( row, index ) => {
			const tableRow = tableRows[ index ];
			if ( tableRow && row.edit_link ) {
				tableRow.classList.add( 'lp-stats-course-performance-row' );
				tableRow.dataset.editLink = row.edit_link;
			}
		} );
	}

	renderHealthChecks( healthChecks ) {
		this.elContainer
			.querySelectorAll( LpStatsTabCourses.selectors.elHealthCheckCount )
			.forEach( ( elCount ) => {
				const check = elCount.dataset.check;
				elCount.textContent = String( healthChecks[ check ] ?? 0 );
			} );
	}

	openCourseEdit( args ) {
		const row = args.target.closest(
			LpStatsTabCourses.selectors.elPerformanceRow
		);
		if ( ! row || ! this.elContainer.contains( row ) ) {
			return;
		}

		const editLink = row.dataset.editLink || '';
		const adminUrl = getStatsConfig().adminUrl || '';
		if ( editLink && adminUrl && editLink.startsWith( adminUrl ) ) {
			window.location.href = editLink;
		}
	}

	viewAllPerformance( args ) {
		const btn = args.target.closest(
			LpStatsTabCourses.selectors.elBtnViewAllPerformance
		);
		if ( ! btn || ! this.elContainer.contains( btn ) ) {
			return;
		}

		lpStatsReportModal.open( {
			report: 'course_performance',
			title: getStatsI18n( 'coursePerformance', 'Course performance' ),
			tableId: 'course-performance',
		} );
	}

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

export const lpStatsTabCourses = new LpStatsTabCourses();

```
