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-overview.js

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

336 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Overview tab module — fetches the `dashboard` payload and renders
3 * KPIs, dual-line chart, funnel, tables, order health and health checks.
4 *
5 * Listens to lp-stats:filter-changed; never mutates state itself.
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 LpStatsTabOverview {
24 static selectors = {
25 elContainer: '.lp-stats-tab-overview',
26 elChartCanvas: '#net-sales-chart-content',
27 elFunnelStep: '.lp-stats-funnel__step',
28 elTableTopCourses: '.lp-stats-table-top-courses',
29 elTableInstructors: '.lp-stats-table-instructors',
30 elBtnViewAllCourses: '.lp-stats-view-all-courses',
31 elOrderHealthBox: '.lp-stats-order-health .lp-stats-health-box',
32 elHealthCheckCount: '.lp-stats-health-check__count',
33 elSkeleton: '.lp-skeleton-animation',
34 };
35
36 static kpiCards = {
37 net_sales: '.lp-kpi-net-sales',
38 completed_orders: '.lp-kpi-completed-orders',
39 enrollments: '.lp-kpi-enrollments',
40 completion_rate: '.lp-kpi-completion-rate',
41 active_learners: '.lp-kpi-active-learners',
42 failed_orders: '.lp-kpi-failed-orders',
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 LpStatsTabOverview.selectors.elContainer
55 );
56 if ( ! this.elContainer ) {
57 return;
58 }
59
60 this.events();
61 this.loadData();
62 }
63
64 events() {
65 if ( LpStatsTabOverview._loadedEvents ) {
66 return;
67 }
68 LpStatsTabOverview._loadedEvents = this;
69
70 lpUtils.eventHandlers( 'click', [
71 {
72 selector: LpStatsTabOverview.selectors.elBtnViewAllCourses,
73 class: this,
74 callBack: this.viewAllCourses.name,
75 },
76 ] );
77
78 document.addEventListener( LP_STATS_FILTER_CHANGED, () => this.loadData() );
79 document.addEventListener( LP_STATS_EXPORT_CSV, () => this.exportTables() );
80 }
81
82 toggleSkeletons( show ) {
83 this.elContainer
84 .querySelectorAll( LpStatsTabOverview.selectors.elSkeleton )
85 .forEach( ( el ) => {
86 el.style.display = show ? 'block' : 'none';
87 } );
88 }
89
90 loadData() {
91 if ( this.isRequesting ) {
92 // Latest filter wins: re-run once the in-flight request finishes.
93 this.pendingReload = true;
94 return;
95 }
96 this.isRequesting = true;
97
98 lpStatsFetch(
99 'overviews-statistics',
100 {},
101 {
102 before: () => this.toggleSkeletons( true ),
103 success: ( response ) => this.render( response.data?.dashboard ),
104 error: ( err ) => {
105 console.error( 'LP Statistics overview:', err );
106 this.render( null );
107 },
108 completed: () => {
109 this.toggleSkeletons( false );
110 this.isRequesting = false;
111 if ( this.pendingReload ) {
112 this.pendingReload = false;
113 this.loadData();
114 }
115 },
116 }
117 );
118 }
119
120 /**
121 * @param {Object|null} dashboard `dashboard` key of the response; null/missing
122 * renders empty states, never throws.
123 */
124 render( dashboard ) {
125 if ( ! dashboard ) {
126 console.error( 'LP Statistics overview: dashboard payload missing.' );
127 dashboard = {};
128 }
129
130 this.renderKpis( dashboard.kpis || {} );
131 this.renderChart( dashboard.chart || {} );
132 this.renderFunnel( dashboard.funnel || {} );
133 this.renderTables( dashboard );
134 this.renderOrderHealth( dashboard.order_health || {} );
135 this.renderHealthChecks( dashboard.health_checks || {} );
136 }
137
138 renderKpis( kpis ) {
139 const i18n = ( key, fallback ) => getStatsI18n( key, fallback );
140
141 Object.entries( LpStatsTabOverview.kpiCards ).forEach(
142 ( [ key, selector ] ) => {
143 const elCard = this.elContainer.querySelector( selector );
144 const payload = { ...( kpis[ key ] || {} ) };
145
146 if ( 'completed_orders' === key && payload.aov_formatted ) {
147 payload.subline = sprintfLite(
148 i18n( 'aov', 'Avg. order value: %s' ),
149 payload.aov_formatted
150 );
151 }
152 if ( 'completion_rate' === key ) {
153 if ( 'number' === typeof payload.value ) {
154 payload.formatted = `${ payload.value }%`;
155 }
156 payload.subline = sprintfLite(
157 i18n( 'belowTarget', '%d below completion target' ),
158 payload.courses_below_target ?? 0
159 );
160 }
161 if ( 'failed_orders' === key && null != payload.fail_rate_pct ) {
162 payload.subline = sprintfLite(
163 i18n( 'failRate', '%s%% of all orders' ),
164 payload.fail_rate_pct
165 );
166 }
167
168 renderKpi( elCard, payload );
169 }
170 );
171 }
172
173 renderChart( chart ) {
174 renderLineChart( LpStatsTabOverview.selectors.elChartCanvas, {
175 labels: chart.labels || [],
176 datasets: [
177 {
178 label: getStatsI18n( 'revenue', 'Revenue' ),
179 data: chart.revenue || [],
180 yAxisID: 'y',
181 },
182 {
183 label: getStatsI18n( 'enrollments', 'Enrollments' ),
184 data: chart.enrollments || [],
185 yAxisID: 'y1',
186 },
187 ],
188 xLabel: chart.x_label || '',
189 granularity: chart.granularity || '',
190 } );
191 }
192
193 renderFunnel( funnel ) {
194 const steps = [ 'registered', 'enrolled', 'started', 'completed' ];
195 let previous = null;
196
197 steps.forEach( ( step ) => {
198 const elStep = this.elContainer.querySelector(
199 `${ LpStatsTabOverview.selectors.elFunnelStep }[data-step="${ step }"]`
200 );
201 if ( ! elStep ) {
202 return;
203 }
204
205 const count = Number( funnel[ step ] ?? 0 );
206 const base = null === previous ? count : previous;
207 const width = base > 0 ? Math.min( 100, ( count / base ) * 100 ) : 0;
208
209 elStep.querySelector( '.lp-stats-funnel__count' ).textContent =
210 String( count );
211 elStep.querySelector( '.lp-stats-funnel__bar' ).style.width = `${ width }%`;
212 previous = count;
213 } );
214 }
215
216 completionBadge( rate ) {
217 if ( null == rate ) {
218 return '';
219 }
220 const { completionBadge = {} } = getStatsConfig();
221 const green = completionBadge.green ?? 60;
222 const yellow = completionBadge.yellow ?? 40;
223
224 if ( rate >= green ) {
225 return 'green';
226 }
227 return rate >= yellow ? 'yellow' : 'red';
228 }
229
230 topCoursesColumns() {
231 return [
232 { key: 'course_name', label: getStatsI18n( 'course', 'Course' ) },
233 {
234 key: 'revenue_formatted',
235 label: getStatsI18n( 'revenue', 'Revenue' ),
236 csv: ( row ) => row.revenue,
237 },
238 { key: 'order_count', label: getStatsI18n( 'orders', 'Orders' ) },
239 { key: 'enrolled', label: getStatsI18n( 'enrolled', 'Enrolled' ) },
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 instructorColumns() {
250 return [
251 { key: 'instructor_name', label: getStatsI18n( 'instructor', 'Instructor' ) },
252 { key: 'course_count', label: getStatsI18n( 'courses', 'Courses' ) },
253 {
254 key: 'revenue_formatted',
255 label: getStatsI18n( 'revenue', 'Revenue' ),
256 csv: ( row ) => row.revenue,
257 },
258 { key: 'enrolled', label: getStatsI18n( 'enrolled', 'Enrolled' ) },
259 {
260 key: 'completion_rate',
261 label: getStatsI18n( 'completion', 'Completion' ),
262 format: ( value ) => ( null == value ? '' : `${ value }%` ),
263 badge: ( row ) => this.completionBadge( row.completion_rate ),
264 },
265 ];
266 }
267
268 renderTables( dashboard ) {
269 this.tables[ 'top-courses' ] = renderDataTable(
270 this.elContainer.querySelector(
271 LpStatsTabOverview.selectors.elTableTopCourses
272 ),
273 this.topCoursesColumns(),
274 dashboard.top_courses || []
275 );
276
277 this.tables.instructors = renderDataTable(
278 this.elContainer.querySelector(
279 LpStatsTabOverview.selectors.elTableInstructors
280 ),
281 this.instructorColumns(),
282 dashboard.instructor_summary || []
283 );
284 }
285
286 renderOrderHealth( orderHealth ) {
287 this.elContainer
288 .querySelectorAll( LpStatsTabOverview.selectors.elOrderHealthBox )
289 .forEach( ( elBox ) => {
290 const status = elBox.dataset.status;
291 const elCount = elBox.querySelector( '.lp-stats-health-box__count' );
292 if ( elCount ) {
293 elCount.textContent = String( orderHealth[ status ] ?? 0 );
294 }
295 } );
296 }
297
298 renderHealthChecks( healthChecks ) {
299 this.elContainer
300 .querySelectorAll( LpStatsTabOverview.selectors.elHealthCheckCount )
301 .forEach( ( elCount ) => {
302 const check = elCount.dataset.check;
303 elCount.textContent = String( healthChecks[ check ] ?? 0 );
304 } );
305 }
306
307 viewAllCourses( args ) {
308 const btn = args.target.closest(
309 LpStatsTabOverview.selectors.elBtnViewAllCourses
310 );
311 if ( ! btn || ! this.elContainer.contains( btn ) ) {
312 return;
313 }
314
315 lpStatsReportModal.open( {
316 report: 'top_courses',
317 title: getStatsI18n( 'topCourses', 'Top courses' ),
318 tableId: 'top-courses',
319 } );
320 }
321
322 exportTables() {
323 Object.entries( this.tables ).forEach( ( [ tableId, handle ] ) => {
324 if ( handle && handle.rows.length ) {
325 exportCsv(
326 buildCsvFilename( 'overview', tableId ),
327 handle.columns,
328 handle.rows
329 );
330 }
331 } );
332 }
333 }
334
335 export const lpStatsTabOverview = new LpStatsTabOverview();
336