PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.5
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.5
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-instructors.js

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

402 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Instructors tab module.
3 *
4 * Fetches the `dashboard` payload and renders KPIs, the operations widget,
5 * instructor performance + course watchlist tables, per-instructor report
6 * popup, and CSV export.
7 *
8 * @since 4.4.2
9 * @version 1.0.0
10 */
11
12 import * as lpUtils from 'lpAssetsJsPath/utils.js';
13 import { lpStatsState, LP_STATS_FILTER_CHANGED, LP_STATS_EXPORT_CSV } from './state.js';
14 import { lpStatsFetch, getStatsConfig, getStatsI18n } from './api.js';
15 import { renderKpi } from './kpi.js';
16 import { renderLineChart } from './chart.js';
17 import { renderDataTable } from './data-table.js';
18 import { lpStatsReportModal } from './report-modal.js';
19 import { exportCsv, buildCsvFilename } from './csv.js';
20
21 const sprintfLite = ( template, value ) =>
22 String( template ).replace( /%[ds]/, String( value ) ).replace( /%%/g, '%' );
23
24 export class LpStatsTabInstructors {
25 static selectors = {
26 elContainer: '.lp-stats-tab-instructors',
27 elChartCanvas: '#instructor-chart-content',
28 elTablePerformance: '.lp-stats-table-instructor-performance',
29 elTableWatchlist: '.lp-stats-table-instructor-watchlist',
30 elBtnViewAllInstructors: '.lp-stats-view-all-instructors',
31 elPerformanceRow: '.lp-stats-instructor-performance-row',
32 elOperationsRow: '.lp-stats-operations__row',
33 elSkeleton: '.lp-skeleton-animation',
34 };
35
36 static kpiCards = {
37 active_instructors: '.lp-kpi-active-instructors',
38 instructor_revenue: '.lp-kpi-instructor-revenue',
39 courses_managed: '.lp-kpi-courses-managed',
40 students_reached: '.lp-kpi-students-reached',
41 avg_completion: '.lp-kpi-avg-completion',
42 needs_review: '.lp-kpi-needs-review',
43 };
44
45 constructor() {
46 this.elContainer = null;
47 this.isRequesting = false;
48 this.pendingReload = false;
49 this.tables = {};
50 }
51
52 init() {
53 this.elContainer = document.querySelector(
54 LpStatsTabInstructors.selectors.elContainer
55 );
56 if ( ! this.elContainer ) {
57 return;
58 }
59
60 this.events();
61 this.loadData();
62 }
63
64 events() {
65 if ( LpStatsTabInstructors._loadedEvents ) {
66 return;
67 }
68 LpStatsTabInstructors._loadedEvents = this;
69
70 lpUtils.eventHandlers( 'click', [
71 {
72 selector: LpStatsTabInstructors.selectors.elBtnViewAllInstructors,
73 class: this,
74 callBack: this.viewAllInstructors.name,
75 },
76 {
77 selector: LpStatsTabInstructors.selectors.elPerformanceRow,
78 class: this,
79 callBack: this.openInstructorReport.name,
80 },
81 ] );
82
83 document.addEventListener( LP_STATS_FILTER_CHANGED, () => this.loadData() );
84 document.addEventListener( LP_STATS_EXPORT_CSV, () => this.exportTables() );
85 }
86
87 toggleSkeletons( show ) {
88 this.elContainer
89 .querySelectorAll( LpStatsTabInstructors.selectors.elSkeleton )
90 .forEach( ( el ) => {
91 el.style.display = show ? 'block' : 'none';
92 } );
93 }
94
95 loadData() {
96 if ( this.isRequesting ) {
97 this.pendingReload = true;
98 return;
99 }
100 this.isRequesting = true;
101
102 lpStatsFetch(
103 'instructor-statistics',
104 {},
105 {
106 before: () => this.toggleSkeletons( true ),
107 success: ( response ) => this.render( response.data ),
108 error: ( err ) => {
109 console.error( 'LP Statistics instructors:', err );
110 this.render( null );
111 },
112 completed: () => {
113 this.toggleSkeletons( false );
114 this.isRequesting = false;
115 if ( this.pendingReload ) {
116 this.pendingReload = false;
117 this.loadData();
118 }
119 },
120 }
121 );
122 }
123
124 render( data ) {
125 if ( ! data?.dashboard ) {
126 console.error( 'LP Statistics instructors: dashboard payload missing.' );
127 data = { chart_data: {}, dashboard: {} };
128 }
129
130 const dashboard = data.dashboard || {};
131
132 this.renderKpis( dashboard.kpis || {} );
133 this.renderChart( data.chart_data || {} );
134 this.renderOperations( dashboard.operations || {} );
135 this.renderTables( dashboard );
136 }
137
138 renderKpis( kpis ) {
139 Object.entries( LpStatsTabInstructors.kpiCards ).forEach(
140 ( [ key, selector ] ) => {
141 const elCard = this.elContainer.querySelector( selector );
142 const payload = { ...( kpis[ key ] || {} ) };
143
144 if ( 'active_instructors' === key ) {
145 payload.subline = sprintfLite(
146 getStatsI18n( 'ofTotalInstructors', '%d total' ),
147 payload.total ?? 0
148 );
149 }
150 if ( 'instructor_revenue' === key && null != payload.contribution_pct ) {
151 payload.subline = sprintfLite(
152 getStatsI18n( 'ofNetSales', '%s%% of net sales' ),
153 payload.contribution_pct
154 );
155 }
156 if ( 'avg_completion' === key && 'number' === typeof payload.value ) {
157 payload.formatted = `${ payload.value }%`;
158 }
159
160 renderKpi( elCard, payload );
161 }
162 );
163 }
164
165 renderChart( chartData ) {
166 renderLineChart(
167 LpStatsTabInstructors.selectors.elChartCanvas,
168 {
169 labels: chartData.labels || [],
170 datasets: [
171 {
172 label: getStatsI18n( 'revenue', 'Revenue' ),
173 data: chartData.revenue || [],
174 yAxisID: 'y',
175 },
176 {
177 label: getStatsI18n( 'enrollments', 'Enrollments' ),
178 data: chartData.enrollments || [],
179 yAxisID: 'y1',
180 },
181 ],
182 xLabel: chartData.x_label || '',
183 granularity: chartData.granularity || '',
184 }
185 );
186 }
187
188 renderOperations( operations ) {
189 this.elContainer
190 .querySelectorAll( LpStatsTabInstructors.selectors.elOperationsRow )
191 .forEach( ( elRow ) => {
192 const op = elRow.dataset.op;
193 const elValue = elRow.querySelector( '.lp-stats-operations__value' );
194 const elName = elRow.querySelector( '.lp-stats-operations__name' );
195 const data = operations[ op ];
196
197 if ( elName ) {
198 elName.textContent = '';
199 }
200
201 if ( null == data ) {
202 if ( elValue ) {
203 elValue.textContent = '';
204 }
205 return;
206 }
207
208 // Scalar operations (counts) vs highlight objects ({ name, value }).
209 if ( 'object' === typeof data ) {
210 if ( elValue ) {
211 elValue.textContent =
212 data.value_formatted ??
213 ( 'number' === typeof data.value && op === 'top_completion'
214 ? `${ data.value }%`
215 : String( data.value ?? '' ) );
216 }
217 if ( elName ) {
218 elName.textContent = data.name || '';
219 }
220 } else if ( elValue ) {
221 elValue.textContent = String( data );
222 }
223 } );
224 }
225
226 completionBadge( rate ) {
227 if ( null == rate ) {
228 return '';
229 }
230 const { completionBadge = {} } = getStatsConfig();
231 const green = completionBadge.green ?? 60;
232 const yellow = completionBadge.yellow ?? 40;
233
234 if ( rate >= green ) {
235 return 'green';
236 }
237 return rate >= yellow ? 'yellow' : 'red';
238 }
239
240 riskLabel( slug ) {
241 const labels = {
242 high: getStatsI18n( 'riskHigh', 'High' ),
243 medium: getStatsI18n( 'riskMedium', 'Medium' ),
244 healthy: getStatsI18n( 'riskHealthy', 'Healthy' ),
245 };
246
247 return labels[ slug ] || String( slug || '' );
248 }
249
250 actionLabel( slug ) {
251 const { watchlistActions = {} } = getStatsConfig().i18n || {};
252 return watchlistActions[ slug ] || String( slug || '' );
253 }
254
255 performanceColumns() {
256 return [
257 { key: 'name', label: getStatsI18n( 'instructor', 'Instructor' ) },
258 { key: 'courses', label: getStatsI18n( 'courses', 'Courses' ) },
259 { key: 'students', label: getStatsI18n( 'students', 'Students' ) },
260 {
261 key: 'revenue_formatted',
262 label: getStatsI18n( 'revenue', 'Revenue' ),
263 csv: ( row ) => row.revenue,
264 },
265 {
266 key: 'avg_completion',
267 label: getStatsI18n( 'completion', 'Completion' ),
268 format: ( value ) => ( null == value ? '' : `${ value }%` ),
269 badge: ( row ) => this.completionBadge( row.avg_completion ),
270 },
271 ];
272 }
273
274 watchlistColumns() {
275 return [
276 { key: 'name', label: getStatsI18n( 'course', 'Course' ) },
277 { key: 'instructor', label: getStatsI18n( 'instructor', 'Instructor' ) },
278 {
279 key: 'completion_rate',
280 label: getStatsI18n( 'completion', 'Completion' ),
281 format: ( value ) => ( null == value ? '' : `${ value }%` ),
282 },
283 {
284 key: 'risk',
285 label: getStatsI18n( 'risk', 'Risk' ),
286 // Emoji lives in CSS pseudo-content on .lp-risk--{slug}; text stays clean.
287 format: ( value ) => {
288 const span = document.createElement( 'span' );
289 span.className = `lp-badge lp-risk lp-risk--${ value }`;
290 span.textContent = this.riskLabel( value );
291 return span;
292 },
293 csv: ( row ) => this.riskLabel( row.risk ),
294 },
295 {
296 key: 'action',
297 label: getStatsI18n( 'actionRequired', 'Action required' ),
298 format: ( value ) => this.actionLabel( value ),
299 csv: ( row ) => this.actionLabel( row.action ),
300 },
301 ];
302 }
303
304 renderTables( dashboard ) {
305 const performanceRows = dashboard.performance || [];
306 this.tables.performance = renderDataTable(
307 this.elContainer.querySelector(
308 LpStatsTabInstructors.selectors.elTablePerformance
309 ),
310 this.performanceColumns(),
311 performanceRows,
312 { emptyText: getStatsI18n( 'noInstructorData', 'No instructor data in this period.' ) }
313 );
314 this.decoratePerformanceRows( performanceRows );
315
316 this.tables.watchlist = renderDataTable(
317 this.elContainer.querySelector(
318 LpStatsTabInstructors.selectors.elTableWatchlist
319 ),
320 this.watchlistColumns(),
321 dashboard.watchlist || []
322 );
323 }
324
325 decoratePerformanceRows( rows = [] ) {
326 const scopedInstructor = lpStatsState.get().instructor_id;
327 const tableRows = this.elContainer.querySelectorAll(
328 `${ LpStatsTabInstructors.selectors.elTablePerformance } tbody tr`
329 );
330
331 rows.forEach( ( row, index ) => {
332 const tableRow = tableRows[ index ];
333 if ( ! tableRow || ! row.instructor_id ) {
334 return;
335 }
336
337 tableRow.classList.add( 'lp-stats-instructor-performance-row' );
338 tableRow.dataset.instructorId = String( row.instructor_id );
339 tableRow.dataset.instructorName = row.name || '';
340
341 if ( scopedInstructor && scopedInstructor === row.instructor_id ) {
342 tableRow.classList.add( 'is-highlighted' );
343 }
344 } );
345 }
346
347 openInstructorReport( args ) {
348 const row = args.target.closest(
349 LpStatsTabInstructors.selectors.elPerformanceRow
350 );
351 if ( ! row || ! this.elContainer.contains( row ) ) {
352 return;
353 }
354
355 const instructorId = parseInt( row.dataset.instructorId, 10 ) || 0;
356 if ( instructorId <= 0 ) {
357 return;
358 }
359 const instructorName =
360 row.dataset.instructorName || getStatsI18n( 'instructorReport', 'Instructor report' );
361
362 lpStatsReportModal.open( {
363 report: 'instructor_report',
364 title: instructorName,
365 tableId: `instructor-${ instructorId }`,
366 args: { instructor_id: instructorId },
367 } );
368 }
369
370 viewAllInstructors( args ) {
371 const btn = args.target.closest(
372 LpStatsTabInstructors.selectors.elBtnViewAllInstructors
373 );
374 if ( ! btn || ! this.elContainer.contains( btn ) ) {
375 return;
376 }
377
378 lpStatsReportModal.open( {
379 report: 'instructor_performance',
380 title: getStatsI18n(
381 'instructorPerformance',
382 'Instructor performance'
383 ),
384 tableId: 'instructor-performance',
385 } );
386 }
387
388 exportTables() {
389 Object.entries( this.tables ).forEach( ( [ tableId, handle ] ) => {
390 if ( handle && handle.rows.length ) {
391 exportCsv(
392 buildCsvFilename( 'instructors', tableId ),
393 handle.columns,
394 handle.rows
395 );
396 }
397 } );
398 }
399 }
400
401 export const lpStatsTabInstructors = new LpStatsTabInstructors();
402