PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / assets / src / js / admin / statistics / tab-courses.js

tab-courses.js in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.8, at assets/src/js/admin/statistics/tab-courses.js

391 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Courses tab module.
3 *
4 * Fetches the `dashboard` payload and renders KPIs, course performance,
5 * published-courses chart, health checks, inventory, popups and CSV export.
6 *
7 * @since 4.4.2
8 * @version 1.0.0
9 */
10
11 import * as lpUtils from 'lpAssetsJsPath/utils.js';
12 import { LP_STATS_FILTER_CHANGED, LP_STATS_EXPORT_CSV } from './state.js';
13 import { lpStatsFetch, getStatsConfig, getStatsI18n } from './api.js';
14 import { renderKpi } from './kpi.js';
15 import { renderLineChart } from './chart.js';
16 import { renderDataTable } from './data-table.js';
17 import { lpStatsReportModal } from './report-modal.js';
18 import { exportCsv, buildCsvFilename } from './csv.js';
19
20 const sprintfLite = ( template, value ) =>
21 String( template ).replace( /%[ds]/, String( value ) ).replace( /%%/g, '%' );
22
23 export class LpStatsTabCourses {
24 static selectors = {
25 elContainer: '.lp-stats-tab-courses',
26 elChartCanvas: '#course-chart-content',
27 elTablePerformance: '.lp-stats-table-course-performance',
28 elTableInventory: '.lp-stats-table-content-inventory',
29 elBtnViewAllPerformance: '.lp-stats-view-all-performance',
30 elPerformanceRow: '.lp-stats-course-performance-row',
31 elHealthCheckCount: '.lp-stats-health-check__count',
32 elSkeleton: '.lp-skeleton-animation',
33 };
34
35 static kpiCards = {
36 published: '.lp-kpi-published',
37 pending_review: '.lp-kpi-pending-review',
38 future: '.lp-kpi-future',
39 enrollments: '.lp-kpi-enrollments',
40 avg_completion: '.lp-kpi-avg-completion',
41 courses_without_enrollment: '.lp-kpi-courses-without-enrollment',
42 };
43
44 constructor() {
45 this.elContainer = null;
46 this.isRequesting = false;
47 this.pendingReload = false;
48 this.tables = {};
49 }
50
51 init() {
52 this.elContainer = document.querySelector(
53 LpStatsTabCourses.selectors.elContainer
54 );
55 if ( ! this.elContainer ) {
56 return;
57 }
58
59 this.events();
60 this.loadData();
61 }
62
63 events() {
64 if ( LpStatsTabCourses._loadedEvents ) {
65 return;
66 }
67 LpStatsTabCourses._loadedEvents = this;
68
69 lpUtils.eventHandlers( 'click', [
70 {
71 selector: LpStatsTabCourses.selectors.elBtnViewAllPerformance,
72 class: this,
73 callBack: this.viewAllPerformance.name,
74 },
75 {
76 selector: LpStatsTabCourses.selectors.elPerformanceRow,
77 class: this,
78 callBack: this.openCourseEdit.name,
79 },
80 ] );
81
82 document.addEventListener( LP_STATS_FILTER_CHANGED, () => this.loadData() );
83 document.addEventListener( LP_STATS_EXPORT_CSV, () => this.exportTables() );
84 }
85
86 toggleSkeletons( show ) {
87 this.elContainer
88 .querySelectorAll( LpStatsTabCourses.selectors.elSkeleton )
89 .forEach( ( el ) => {
90 el.style.display = show ? 'block' : 'none';
91 } );
92 }
93
94 loadData() {
95 if ( this.isRequesting ) {
96 this.pendingReload = true;
97 return;
98 }
99 this.isRequesting = true;
100
101 lpStatsFetch(
102 'course-statistics',
103 {},
104 {
105 before: () => this.toggleSkeletons( true ),
106 success: ( response ) => this.render( response.data ),
107 error: ( err ) => {
108 console.error( 'LP Statistics courses:', err );
109 this.render( null );
110 },
111 completed: () => {
112 this.toggleSkeletons( false );
113 this.isRequesting = false;
114 if ( this.pendingReload ) {
115 this.pendingReload = false;
116 this.loadData();
117 }
118 },
119 }
120 );
121 }
122
123 render( data ) {
124 if ( ! data?.dashboard ) {
125 console.error( 'LP Statistics courses: dashboard payload missing.' );
126 data = { chart_data: {}, dashboard: {} };
127 }
128
129 const dashboard = data.dashboard || {};
130
131 this.renderKpis( dashboard.kpis || {} );
132 this.renderTables( dashboard );
133 // Prefer the scoped chart from the dashboard payload so instructor/category
134 // changes redraw the chart; fall back to the legacy unscoped series.
135 this.renderChart( dashboard.chart || data.chart_data || {} );
136 this.renderHealthChecks( dashboard.health_checks || {} );
137 }
138
139 renderKpis( kpis ) {
140 Object.entries( LpStatsTabCourses.kpiCards ).forEach( ( [ key, selector ] ) => {
141 const elCard = this.elContainer.querySelector( selector );
142 const payload = { ...( kpis[ key ] || {} ) };
143
144 if ( 'published' === key ) {
145 payload.subline = sprintfLite(
146 getStatsI18n( 'addedThisPeriod', '%d added this period' ),
147 payload.added_in_period ?? 0
148 );
149 }
150 if ( 'pending_review' === key ) {
151 payload.subline = getStatsI18n(
152 'needsInstructorAction',
153 'Needs instructor action'
154 );
155 }
156 if ( 'future' === key ) {
157 payload.subline = getStatsI18n(
158 'scheduledReleases',
159 'Scheduled releases'
160 );
161 }
162 if ( 'avg_completion' === key ) {
163 if ( 'number' === typeof payload.value ) {
164 payload.formatted = `${ payload.value }%`;
165 }
166 payload.subline = sprintfLite(
167 getStatsI18n( 'targetPercent', 'Target: %s%%' ),
168 payload.target ?? getStatsConfig().completionTarget ?? 0
169 );
170 }
171
172 renderKpi( elCard, payload );
173
174 if ( 'avg_completion' === key ) {
175 this.renderCompletionProgress( elCard, payload );
176 }
177 } );
178 }
179
180 renderCompletionProgress( elCard, payload ) {
181 const elBar = elCard?.querySelector( '.lp-kpi-progress__bar' );
182 if ( ! elBar ) {
183 return;
184 }
185
186 const value = Number( payload.value || 0 );
187 const target = Number( payload.target || getStatsConfig().completionTarget || 0 );
188 const width = target > 0 ? Math.min( 100, ( value / target ) * 100 ) : 0;
189 elBar.style.width = `${ width }%`;
190 }
191
192 renderChart( chartData ) {
193 renderLineChart(
194 LpStatsTabCourses.selectors.elChartCanvas,
195 {
196 labels: chartData.labels || [],
197 datasets: [
198 {
199 label:
200 chartData.line_label ||
201 getStatsI18n( 'publishedCourses', 'Published courses' ),
202 data: chartData.data || [],
203 yAxisID: 'y',
204 },
205 ],
206 xLabel: chartData.x_label || '',
207 granularity: chartData.granularity || '',
208 },
209 { yCurrency: false }
210 );
211 }
212
213 completionBadge( rate ) {
214 if ( null == rate ) {
215 return '';
216 }
217 const { completionBadge = {} } = getStatsConfig();
218 const green = completionBadge.green ?? 60;
219 const yellow = completionBadge.yellow ?? 40;
220
221 if ( rate >= green ) {
222 return 'green';
223 }
224 return rate >= yellow ? 'yellow' : 'red';
225 }
226
227 performanceColumns() {
228 return [
229 { key: 'name', label: getStatsI18n( 'course', 'Course' ) },
230 { key: 'instructor', label: getStatsI18n( 'instructor', 'Instructor' ) },
231 {
232 key: 'revenue_formatted',
233 label: getStatsI18n( 'revenue', 'Revenue' ),
234 csv: ( row ) => row.revenue,
235 },
236 {
237 key: 'enrollments',
238 label: getStatsI18n( 'enrollments', 'Enrollments' ),
239 },
240 {
241 key: 'completion_rate',
242 label: getStatsI18n( 'completion', 'Completion' ),
243 format: ( value ) => ( null == value ? '-' : `${ value }%` ),
244 badge: ( row ) => this.completionBadge( row.completion_rate ),
245 },
246 ];
247 }
248
249 inventoryLabel( key ) {
250 const labels = {
251 courses: getStatsI18n( 'courses', 'Courses' ),
252 lessons: getStatsI18n( 'lessons', 'Lessons' ),
253 quizzes: getStatsI18n( 'quizzes', 'Quizzes' ),
254 assignments: getStatsI18n( 'assignments', 'Assignments' ),
255 };
256
257 return labels[ key ] || String( key || '' );
258 }
259
260 inventoryStatusLabel( key ) {
261 const labels = {
262 publish: getStatsI18n( 'published', 'Published' ),
263 pending: getStatsI18n( 'pending', 'Pending' ),
264 future: getStatsI18n( 'future', 'Future' ),
265 draft: getStatsI18n( 'drafts', 'Drafts' ),
266 total: getStatsI18n( 'total', 'Total' ),
267 };
268
269 return labels[ key ] || String( key || '' );
270 }
271
272 inventoryRows( inventory = {} ) {
273 return Object.entries( inventory ).map( ( [ type, counts ] ) => ( {
274 type,
275 label: this.inventoryLabel( type ),
276 ...( counts || {} ),
277 } ) );
278 }
279
280 inventoryColumns( rows = [] ) {
281 const preferred = [ 'publish', 'pending', 'future', 'draft', 'total' ];
282 const statusKeys = preferred.filter( ( key ) =>
283 rows.some( ( row ) => Object.prototype.hasOwnProperty.call( row, key ) )
284 );
285
286 return [
287 { key: 'label', label: getStatsI18n( 'content', 'Content' ) },
288 ...statusKeys.map( ( key ) => ( {
289 key,
290 label: this.inventoryStatusLabel( key ),
291 csv: ( row ) => row[ key ],
292 } ) ),
293 ];
294 }
295
296 renderTables( dashboard ) {
297 const performanceRows = dashboard.performance || [];
298 const performanceHandle = renderDataTable(
299 this.elContainer.querySelector(
300 LpStatsTabCourses.selectors.elTablePerformance
301 ),
302 this.performanceColumns(),
303 performanceRows,
304 {
305 emptyText: getStatsI18n(
306 'noCoursePerformance',
307 'No course performance data in this period.'
308 ),
309 }
310 );
311 this.tables.performance = performanceHandle;
312 this.decoratePerformanceRows( performanceRows );
313
314 const inventoryRows = this.inventoryRows( dashboard.inventory || {} );
315 this.tables.inventory = renderDataTable(
316 this.elContainer.querySelector(
317 LpStatsTabCourses.selectors.elTableInventory
318 ),
319 this.inventoryColumns( inventoryRows ),
320 inventoryRows
321 );
322 }
323
324 decoratePerformanceRows( rows = [] ) {
325 const tableRows = this.elContainer.querySelectorAll(
326 `${ LpStatsTabCourses.selectors.elTablePerformance } tbody tr`
327 );
328
329 rows.forEach( ( row, index ) => {
330 const tableRow = tableRows[ index ];
331 if ( tableRow && row.edit_link ) {
332 tableRow.classList.add( 'lp-stats-course-performance-row' );
333 tableRow.dataset.editLink = row.edit_link;
334 }
335 } );
336 }
337
338 renderHealthChecks( healthChecks ) {
339 this.elContainer
340 .querySelectorAll( LpStatsTabCourses.selectors.elHealthCheckCount )
341 .forEach( ( elCount ) => {
342 const check = elCount.dataset.check;
343 elCount.textContent = String( healthChecks[ check ] ?? 0 );
344 } );
345 }
346
347 openCourseEdit( args ) {
348 const row = args.target.closest(
349 LpStatsTabCourses.selectors.elPerformanceRow
350 );
351 if ( ! row || ! this.elContainer.contains( row ) ) {
352 return;
353 }
354
355 const editLink = row.dataset.editLink || '';
356 const adminUrl = getStatsConfig().adminUrl || '';
357 if ( editLink && adminUrl && editLink.startsWith( adminUrl ) ) {
358 window.location.href = editLink;
359 }
360 }
361
362 viewAllPerformance( args ) {
363 const btn = args.target.closest(
364 LpStatsTabCourses.selectors.elBtnViewAllPerformance
365 );
366 if ( ! btn || ! this.elContainer.contains( btn ) ) {
367 return;
368 }
369
370 lpStatsReportModal.open( {
371 report: 'course_performance',
372 title: getStatsI18n( 'coursePerformance', 'Course performance' ),
373 tableId: 'course-performance',
374 } );
375 }
376
377 exportTables() {
378 Object.entries( this.tables ).forEach( ( [ tableId, handle ] ) => {
379 if ( handle && handle.rows.length ) {
380 exportCsv(
381 buildCsvFilename( 'courses', tableId ),
382 handle.columns,
383 handle.rows
384 );
385 }
386 } );
387 }
388 }
389
390 export const lpStatsTabCourses = new LpStatsTabCourses();
391