PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
4.4.9 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 All 140 releases
← All changes | inc/rest-api/v1/admin/class-lp-admin-rest-statistics-controller.php +777 -92 4.4.1 → 4.4.9 View file →
@@ -1,7 +1,15 @@
1 1 <?php
2 2
3 -use LearnPress\Helpers\Template;
3 +use LearnPress\Statistics\DashboardStatisticsDB;
4 +use LearnPress\Statistics\FilterOptionsProvider;
5 +use LearnPress\Statistics\HealthCheckProvider;
6 +use LearnPress\Statistics\InstructorStatisticsProvider;
7 +use LearnPress\Statistics\OrderExceptionsProvider;
8 +use LearnPress\Statistics\PeriodHelper;
9 +use LearnPress\Statistics\PeriodRange;
10 +use LearnPress\Statistics\PeriodResolver;
11 +use LearnPress\Statistics\StatisticsScope;
4 12
5 13 /**
6 14 * Class LP_REST_Admin_Statistics_Controller
7 15 *
@@ -17,9 +25,9 @@
17 25 }
18 26
19 27 public function register_routes() {
20 28 $this->routes = array(
21 - 'overviews-statistics' => array(
29 + 'overviews-statistics' => array(
22 30 array(
23 31 'methods' => WP_REST_Server::READABLE,
24 32 'callback' => array( $this, 'get_overviews_statistics' ),
25 33 'permission_callback' => array( $this, 'permission_check' ),
@@ -24,9 +32,9 @@
24 32 'callback' => array( $this, 'get_overviews_statistics' ),
25 33 'permission_callback' => array( $this, 'permission_check' ),
26 34 ),
27 35 ),
28 - 'order-statistics' => array(
36 + 'order-statistics' => array(
29 37 array(
30 38 'methods' => WP_REST_Server::READABLE,
31 39 'callback' => array( $this, 'get_order_statistics' ),
32 40 'permission_callback' => array( $this, 'permission_check' ),
@@ -31,9 +39,9 @@
31 39 'callback' => array( $this, 'get_order_statistics' ),
32 40 'permission_callback' => array( $this, 'permission_check' ),
33 41 ),
34 42 ),
35 - 'course-statistics' => array(
43 + 'course-statistics' => array(
36 44 array(
37 45 'methods' => WP_REST_Server::READABLE,
38 46 'callback' => array( $this, 'get_courses_statistics' ),
39 47 'permission_callback' => array( $this, 'permission_check' ),
@@ -38,9 +46,9 @@
38 46 'callback' => array( $this, 'get_courses_statistics' ),
39 47 'permission_callback' => array( $this, 'permission_check' ),
40 48 ),
41 49 ),
42 - 'user-statistics' => array(
50 + 'user-statistics' => array(
43 51 array(
44 52 'methods' => WP_REST_Server::READABLE,
45 53 'callback' => array( $this, 'get_users_statistics' ),
46 54 'permission_callback' => array( $this, 'permission_check' ),
@@ -45,8 +53,22 @@
45 53 'callback' => array( $this, 'get_users_statistics' ),
46 54 'permission_callback' => array( $this, 'permission_check' ),
47 55 ),
48 56 ),
57 + 'filter-options' => array(
58 + array(
59 + 'methods' => WP_REST_Server::READABLE,
60 + 'callback' => array( $this, 'get_filter_options' ),
61 + 'permission_callback' => array( $this, 'permission_check' ),
62 + ),
63 + ),
64 + 'instructor-statistics' => array(
65 + array(
66 + 'methods' => WP_REST_Server::READABLE,
67 + 'callback' => array( $this, 'get_instructor_statistics' ),
68 + 'permission_callback' => array( $this, 'permission_check' ),
69 + ),
70 + ),
49 71 );
50 72
51 73 parent::register_routes();
52 74 }
@@ -65,21 +87,21 @@
65 87 $params = $request->get_params();
66 88 $params = LP_Helper::sanitize_params_submitted( $params );
67 89 $filter = $this->get_statistics_filter( $params );
68 90
69 - $lp_statistic_db = LP_Statistics_DB::getInstance();
70 - $net_sales = $lp_statistic_db->get_net_sales_data( $filter['filter_type'], $filter['time'] );
71 - $total_courses = $lp_statistic_db->get_total_course_created( $filter['filter_type'], $filter['time'] );
72 - $total_orders = $lp_statistic_db->get_total_order_created( $filter['filter_type'], $filter['time'] );
73 - $total_instructors = $lp_statistic_db->get_total_instructor_created( $filter['filter_type'], $filter['time'] );
74 - $total_students = $lp_statistic_db->get_total_student_created( $filter['filter_type'], $filter['time'] );
75 - $chart_data = $this->process_chart_data( $filter, $net_sales );
76 - $top_courses = $lp_statistic_db->get_top_sold_courses( $filter['filter_type'], $filter['time'] );
77 - $top_categories = $lp_statistic_db->get_top_sold_categories( $filter['filter_type'], $filter['time'] );
78 - $chart_data['line_label'] = __( 'Net sales', 'learnpress' );
79 - $total_sales = html_entity_decode( learn_press_format_price( array_sum( $chart_data['data'] ) ) );
91 + $lp_statistic_db = LP_Statistics_DB::getInstance();
92 + $net_sales = $lp_statistic_db->get_net_sales_data( $filter['filter_type'], $filter['time'], null, $filter['granularity'] );
93 + $total_courses = $lp_statistic_db->get_total_course_created( $filter['filter_type'], $filter['time'] );
94 + $total_orders = $lp_statistic_db->get_total_order_created( $filter['filter_type'], $filter['time'] );
95 + $total_instructors = $lp_statistic_db->get_total_instructor_created( $filter['filter_type'], $filter['time'] );
96 + $total_students = $lp_statistic_db->get_total_student_created( $filter['filter_type'], $filter['time'] );
97 + $chart_data = $this->process_chart_data( $filter, $net_sales );
98 + $top_courses = $lp_statistic_db->get_top_sold_courses( $filter['filter_type'], $filter['time'] );
99 + $top_categories = $lp_statistic_db->get_top_sold_categories( $filter['filter_type'], $filter['time'] );
100 + $chart_data['line_label'] = __( 'Net sales', 'learnpress' );
101 + $total_sales = html_entity_decode( learn_press_format_price( array_sum( $chart_data['data'] ) ) );
80 102
81 - $data = array(
103 + $data = array(
82 104 'total_sales' => $total_sales,
83 105 'total_orders' => $total_orders,
84 106 'total_instructors' => $total_instructors,
85 107 'total_courses' => $total_courses,
@@ -87,10 +109,13 @@
87 109 'chart_data' => $chart_data,
88 110 'top_courses' => $top_courses,
89 111 'top_categories' => $top_categories,
90 112 );
91 - $response->data = $data;
92 - $response->status = 'success';
113 + // New dashboard payload (scoped); keys above stay byte-identical. @since 4.4.2
114 + $data['dashboard'] = $this->get_dashboard_data( $filter, $params );
115 + $data['range'] = $this->range_response( $filter );
116 + $response->data = $this->filter_rest_response( $data, 'overviews', $request );
117 + $response->status = 'success';
93 118 } catch ( Throwable $e ) {
94 119 $response->message = $e->getMessage();
95 120 $response->status = 'error';
96 121 }
@@ -105,21 +130,24 @@
105 130 public function get_order_statistics( WP_REST_Request $request ): LP_REST_Response {
106 131 $response = new LP_REST_Response();
107 132
108 133 try {
109 - $params = $request->get_params();
110 - $params = LP_Helper::sanitize_params_submitted( $params );
111 - $filter = $this->get_statistics_filter( $params );
134 + $params = $request->get_params();
135 + $params = LP_Helper::sanitize_params_submitted( $params );
136 + $filter = $this->get_statistics_filter( $params );
137 +
112 138 $lp_statistic_db = LP_Statistics_DB::getInstance();
113 139 $statistics = $lp_statistic_db->get_order_statics( $filter['filter_type'], $filter['time'] );
114 - $completed_orders = $lp_statistic_db->get_completed_order_data( $filter['filter_type'], $filter['time'] );
140 + $completed_orders = $lp_statistic_db->get_completed_order_data( $filter['filter_type'], $filter['time'], null, $filter['granularity'] );
115 141 $chart_data = $this->process_chart_data( $filter, $completed_orders );
116 142 $chart_data['line_label'] = __( 'Completed orders', 'learnpress' );
117 143 $data = array(
118 144 'statistics' => $statistics,
119 145 'chart_data' => $chart_data,
146 + 'dashboard' => $this->get_order_dashboard_data( $filter, $params ),
147 + 'range' => $this->range_response( $filter ),
120 148 );
121 - $response->data = $data;
149 + $response->data = $this->filter_rest_response( $data, 'orders', $request );
122 150 $response->status = 'success';
123 151 } catch ( Throwable $e ) {
124 152 $response->message = $e->getMessage();
125 153 $response->status = 'error';
@@ -129,13 +157,14 @@
129 157 }
130 158 public function get_courses_statistics( $request ) {
131 159 $response = new LP_REST_Response();
132 160 try {
133 - $params = $request->get_params();
134 - $params = LP_Helper::sanitize_params_submitted( $params );
135 - $filter = $this->get_statistics_filter( $params );
161 + $params = $request->get_params();
162 + $params = LP_Helper::sanitize_params_submitted( $params );
163 + $filter = $this->get_statistics_filter( $params );
164 +
136 165 $lp_statistic_db = LP_Statistics_DB::getInstance();
137 - $published_course = $lp_statistic_db->get_published_course_data( $filter['filter_type'], $filter['time'] );
166 + $published_course = $lp_statistic_db->get_published_course_data( $filter['filter_type'], $filter['time'], null, $filter['granularity'] );
138 167 $courses = $lp_statistic_db->get_course_count_by_statuses( $filter['filter_type'], $filter['time'] );
139 168 $items = $lp_statistic_db->get_course_items_count( $filter['filter_type'], $filter['time'] );
140 169 $chart_data = $this->process_chart_data( $filter, $published_course );
141 170 $chart_data['line_label'] = __( 'Published Courses', 'learnpress' );
@@ -142,10 +171,12 @@
142 171 $data = array(
143 172 'courses' => $courses,
144 173 'items' => $items,
145 174 'chart_data' => $chart_data,
175 + 'dashboard' => $this->get_courses_dashboard_data( $filter, $params ),
176 + 'range' => $this->range_response( $filter ),
146 177 );
147 - $response->data = $data;
178 + $response->data = $this->filter_rest_response( $data, 'courses', $request );
148 179 $response->status = 'success';
149 180 } catch ( Throwable $e ) {
150 181 $response->message = $e->getMessage();
151 182 $response->status = 'error';
@@ -160,13 +191,14 @@
160 191 */
161 192 public function get_users_statistics( $request ): LP_REST_Response {
162 193 $response = new LP_REST_Response();
163 194 try {
164 - $params = $request->get_params();
165 - $params = LP_Helper::sanitize_params_submitted( $params );
166 - $filter = $this->get_statistics_filter( $params );
195 + $params = $request->get_params();
196 + $params = LP_Helper::sanitize_params_submitted( $params );
197 + $filter = $this->get_statistics_filter( $params );
198 +
167 199 $lp_statistic_db = LP_Statistics_DB::getInstance();
168 - $user_registers = $lp_statistic_db->get_user_registered_data( $filter['filter_type'], $filter['time'] );
200 + $user_registers = $lp_statistic_db->get_user_registered_data( $filter['filter_type'], $filter['time'], $filter['granularity'] );
169 201 $user_course_statused = $lp_statistic_db->get_users_by_user_item_graduation_statuses( $filter['filter_type'], $filter['time'] );
170 202 $user_not_start_course = $lp_statistic_db->get_users_not_started_any_course( $filter['filter_type'], $filter['time'] );
171 203 $top_enrolled_courses = $lp_statistic_db->get_top_enrolled_courses( $filter['filter_type'], $filter['time'] );
172 204 $total_instructors = $lp_statistic_db->get_total_instructor_created( $filter['filter_type'], $filter['time'] );
@@ -184,9 +216,9 @@
184 216 $top_enrolled_instructor[ $course->instructor_id ]['students'] += (int) $course->enrolled_user;
185 217 }
186 218 }
187 219 }
188 - $chart_data['line_label'] = __( 'User registed', 'learnpress' );
220 + $chart_data['line_label'] = __( 'Registered users', 'learnpress' );
189 221 $data = array(
190 222 'chart_data' => $chart_data,
191 223 'user_course_statused' => $user_course_statused,
192 224 'user_not_start_course' => $user_not_start_course,
@@ -193,10 +225,12 @@
193 225 'top_enrolled_courses' => $top_enrolled_courses,
194 226 'top_enrolled_instructor' => $top_enrolled_instructor,
195 227 'total_instructors' => $total_instructors,
196 228 'total_students' => $total_students,
229 + 'dashboard' => $this->get_users_dashboard_data( $filter, $params, (int) $user_not_start_course ),
230 + 'range' => $this->range_response( $filter ),
197 231 );
198 - $response->data = $data;
232 + $response->data = $this->filter_rest_response( $data, 'users', $request );
199 233 $response->status = 'success';
200 234 } catch ( Throwable $e ) {
201 235 $response->message = $e->getMessage();
202 236 $response->status = 'error';
@@ -206,10 +240,10 @@
206 240 }
207 241 /**
208 242 * Process data use for chart js
209 243 *
210 - * @param array $filter The filter in get_statistics_filter
211 - * @param array $input_data The input data ( data from DB )
244 + * @param array $filter The filter in get_statistics_filter
245 + * @param array $input_data The input data ( data from DB )
212 246 *
213 247 * @return array $chart_data Data use for chart js
214 248 */
215 249 public function process_chart_data( array $filter, array $input_data ) {
@@ -233,8 +267,37 @@
233 267 } elseif ( $filter['filter_type'] == 'custom' ) {
234 268 $dates = $filter['time'];
235 269 $dates = explode( '+', $dates );
236 270 sort( $dates );
271 + $granularity = (string) ( $filter['granularity'] ?? '' );
272 + if ( '' !== $granularity ) {
273 + // Explicit resolution from PeriodResolver — mirrors
274 + // LP_Statistics_DB::chart_filter_granularity_group_by(), so the
275 + // zero-filled labels always match the SQL group-by. @since 4.4.2
276 + if ( PeriodResolver::GRAN_HOUR === $granularity ) {
277 + $data = $this->process_date_data( $input_data );
278 + $chart_data['x_label'] = __( 'Hour', 'learnpress' );
279 + } elseif ( PeriodResolver::GRAN_MONTH === $granularity ) {
280 + // Anchor on the 1st: 'Dec 31 -1 month' would overflow past November.
281 + $last_month = date( 'Y-m-01', strtotime( $dates[1] ) );
282 + $months = ( (int) date( 'Y', strtotime( $last_month ) ) * 12 + (int) date( 'n', strtotime( $last_month ) ) )
283 + - ( (int) date( 'Y', strtotime( $dates[0] ) ) * 12 + (int) date( 'n', strtotime( $dates[0] ) ) );
284 + $data = $this->process_previous_months_data( $months, $input_data, $last_month );
285 + $chart_data['x_label'] = __( 'Months', 'learnpress' );
286 + } else {
287 + $days = (int) date_diff( date_create( $dates[0] ), date_create( $dates[1] ), true )->days;
288 + $data = $this->process_previous_days_data( $days, $input_data, $dates[1] );
289 + $chart_data['x_label'] = __( 'Dates', 'learnpress' );
290 + }
291 +
292 + $chart_data['granularity'] = $granularity;
293 + foreach ( $data as $row ) {
294 + $chart_data['labels'][] = $row->x_data_label;
295 + $chart_data['data'][] = (float) $row->x_data;
296 + }
297 +
298 + return $chart_data;
299 + }
237 300 $diff = date_diff( date_create( $dates[0] ), date_create( $dates[1] ), true );
238 301 $y = $diff->y;
239 302 $m = $diff->m;
240 303 $d = $diff->d;
@@ -264,8 +327,10 @@
264 327 $data = $this->process_years_data( $y, $input_data, $dates[1] );
265 328 $chart_data['x_label'] = __( 'Years', 'learnpress' );
266 329 }
267 330 }
331 + // Label-format marker for the shared JS formatter; additive key. @since 4.4.2
332 + $chart_data['granularity'] = (string) ( $filter['granularity'] ?? '' );
268 333 foreach ( $data as $row ) {
269 334 $chart_data['labels'][] = $row->x_data_label;
270 335 $chart_data['data'][] = (float) $row->x_data;
271 336 }
@@ -275,57 +340,66 @@
275 340 }
276 341 /**
277 342 * Gets the statistics filter.
278 343 *
279 - * @param http request $params The parameters
344 + * Delegates to PeriodResolver — new WC-style presets ( week, last_month,
345 + * quarter, … ) resolve alongside the legacy ones, which keep producing
346 + * their historical { filter_type, time } pairs byte-for-byte. The legacy
347 + * keys stay first-class for BC; the @since 4.4.2 keys are additive.
280 348 *
281 - * @return array The statistics filter. use for process data
349 + * @param http request $params The parameters
350 + *
351 + * @return array The statistics filter. use for process data:
352 + * [ 'filter_type', 'time' ] as before, plus
353 + * 'granularity' ( hour|day|month — chart resolution + the
354 + * label-format marker for the JS formatter ),
355 + * 'range' ( the resolved PeriodRange ). @since 4.4.2
282 356 */
283 357 public function get_statistics_filter( $params ) {
284 - $filter = [];
285 - $filtertype = $params['filtertype'] ?? 'today';
286 - switch ( $filtertype ) {
287 - case 'today':
288 - $filter['filter_type'] = 'date';
289 - $filter['time'] = current_time( 'Y-m-d' );
290 - break;
291 - case 'yesterday':
292 - $filter['filter_type'] = 'date';
293 - $filter['time'] = date( 'Y-m-d', strtotime( current_time( 'Y-m-d' ) . '-1 days' ) );
294 - break;
295 - case 'last7days':
296 - $filter['filter_type'] = 'previous_days';
297 - $filter['time'] = 6;
298 - break;
299 - case 'last30days':
300 - $filter['filter_type'] = 'previous_days';
301 - $filter['time'] = 30;
302 - break;
303 - case 'thismonth':
304 - $filter['filter_type'] = 'month';
305 - $filter['time'] = current_time( 'Y-m-d' );
306 - break;
307 - case 'last12months':
308 - $filter['filter_type'] = 'previous_months';
309 - $filter['time'] = 11;
310 - break;
311 - case 'thisyear':
312 - $filter['filter_type'] = 'year';
313 - $filter['time'] = current_time( 'Y-m-d' );
314 - break;
315 - case 'custom':
316 - $filter['filter_type'] = 'custom';
317 - $filter['time'] = $params['date'];
318 - break;
319 - default:
320 - break;
358 + $range = PeriodResolver::resolve(
359 + (string) ( $params['filtertype'] ?? 'today' ),
360 + (string) ( $params['date'] ?? '' )
361 + );
362 +
363 + return array(
364 + 'filter_type' => $range->filter_type,
365 + 'time' => $range->time,
366 + 'granularity' => $range->granularity,
367 + 'range' => $range,
368 + );
369 + }
370 +
371 + /**
372 + * Authoritative resolved-range echo for the JS date-range toggle label.
373 + *
374 + * The client sets an optimistic label from state the instant a preset is
375 + * picked, then reconciles to this server-resolved label when the payload
376 + * lands — which is what corrects a "Month to date (Jul 1 – 15)" toggle left
377 + * open past midnight to "… Jul 1 – 16".
378 + *
379 + * @param array $filter From get_statistics_filter().
380 + * @return array{label:string,filtertype:string} Empty label for BC filters.
381 + * @since 4.4.2
382 + */
383 + private function range_response( array $filter ): array {
384 + $range = $filter['range'] ?? null;
385 + if ( ! $range instanceof PeriodRange ) {
386 + return array(
387 + 'label' => '',
388 + 'filtertype' => '',
389 + );
321 390 }
322 - return $filter;
391 +
392 + return array(
393 + 'label' => $range->label,
394 + 'filtertype' => $range->preset,
395 + );
323 396 }
397 +
324 398 /**
325 399 * process data of a date ( in 24h )
326 400 *
327 - * @param array $input_data The input data
401 + * @param array $input_data The input data
328 402 *
329 403 * @return array ( description_of_the_return_value )
330 404 */
331 405 public function process_date_data( array $input_data ) {
@@ -345,11 +419,11 @@
345 419 }
346 420 /**
347 421 * process data of days since the last date, if dont have last date, last date is current date
348 422 *
349 - * @param int $days The days
350 - * @param array $input_data The input data
351 - * @param bool $last_date The last date
423 + * @param int $days The days
424 + * @param array $input_data The input data
425 + * @param bool $last_date The last date
352 426 *
353 427 * @return array ( description_of_the_return_value )
354 428 */
355 429 public function process_previous_days_data( int $days, array $input_data, $last_date = false ) {
@@ -370,10 +444,10 @@
370 444 }
371 445 /**
372 446 * process data of a month
373 447 *
374 - * @param array $filter The filter
375 - * @param array $input_data The input data
448 + * @param array $filter The filter
449 + * @param array $input_data The input data
376 450 *
377 451 * @return array ( description_of_the_return_value )
378 452 */
379 453 public function process_month_data( array $filter, array $input_data ) {
@@ -394,11 +468,11 @@
394 468 }
395 469 /**
396 470 * process data of months since the last date, if dont have last date, last date is current date
397 471 *
398 - * @param int $months The months
399 - * @param array $input_data The input data
400 - * @param bool $last_date The last date
472 + * @param int $months The months
473 + * @param array $input_data The input data
474 + * @param bool $last_date The last date
401 475 *
402 476 * @return array ( description_of_the_return_value )
403 477 */
404 478 public function process_previous_months_data( int $months, array $input_data, $last_date = false ) {
@@ -418,10 +492,10 @@
418 492 return $data;
419 493 }
420 494 /**
421 495 *
422 - * @param array $dates The dates
423 - * @param array $input_data The input data
496 + * @param array $dates The dates
497 + * @param array $input_data The input data
424 498 *
425 499 * @return array process data for date range 2-5 years
426 500 */
427 501 public function process_quarters_data( array $dates, array $input_data ) {
@@ -463,9 +537,9 @@
463 537 }
464 538 /**
465 539 * process data of a year
466 540 *
467 - * @param array $input_data data from DB
541 + * @param array $input_data data from DB
468 542 *
469 543 * @return array chart data
470 544 */
471 545 public function process_year_data( array $input_data ) {
@@ -486,11 +560,11 @@
486 560
487 561 /**
488 562 * process data of years( when date range > 5 years )
489 563 *
490 - * @param int $years The years
491 - * @param array $input_data The input data
492 - * @param bool $last_date The last date
564 + * @param int $years The years
565 + * @param array $input_data The input data
566 + * @param bool $last_date The last date
493 567 *
494 568 * @return array ( description_of_the_return_value )
495 569 */
496 570 public function process_years_data( int $years, array $input_data, $last_date = false ) {
@@ -507,8 +581,619 @@
507 581 $data[ $row->x_data_label ] = $row;
508 582 }
509 583 }
510 584 return $data;
585 + }
586 +
587 + /**
588 + * Bucket order-count rows ( from get_order_statics ) by status.
589 + *
590 + * @param mixed $rows Rows of { count_order, order_status }.
591 + * @return array Known statuses => int counts.
592 + * @since 4.4.2
593 + */
594 + private function get_order_status_buckets( $rows ): array {
595 + $buckets = array(
596 + 'completed' => 0,
597 + 'processing' => 0,
598 + 'pending' => 0,
599 + 'cancelled' => 0,
600 + 'failed' => 0,
601 + );
602 +
603 + foreach ( (array) $rows as $row ) {
604 + $status = $row->order_status ?? '';
605 + if ( isset( $buckets[ $status ] ) ) {
606 + $buckets[ $status ] = (int) $row->count_order;
607 + }
608 + }
609 +
610 + return $buckets;
611 + }
612 +
613 + /**
614 + * Bucket course-count rows by status.
615 + *
616 + * @param mixed $rows Rows of { course_count, course_status }.
617 + * @return array Known statuses => int counts.
618 + * @since 4.4.2
619 + */
620 + private function get_course_status_buckets( $rows ): array {
621 + $buckets = array(
622 + 'publish' => 0,
623 + 'pending' => 0,
624 + 'future' => 0,
625 + 'draft' => 0,
626 + );
627 +
628 + foreach ( (array) $rows as $row ) {
629 + $status = $row->course_status ?? '';
630 + if ( isset( $buckets[ $status ] ) ) {
631 + $buckets[ $status ] = (int) $row->course_count;
632 + }
633 + }
634 +
635 + return $buckets;
636 + }
637 +
638 + /**
639 + * Sum x_data values from a chart-query result set.
640 + *
641 + * @param mixed $rows
642 + * @return float
643 + * @since 4.4.2
644 + */
645 + private function sum_chart_rows( $rows ): float {
646 + return round(
647 + array_sum(
648 + array_map(
649 + function ( $row ) {
650 + return (float) ( $row->x_data ?? 0 );
651 + },
652 + (array) $rows
653 + )
654 + ),
655 + 2
656 + );
657 + }
658 +
659 + /**
660 + * Assemble the scoped dashboard payload for the Orders tab.
661 + *
662 + * @param array $filter [ 'filter_type', 'time' ] from get_statistics_filter().
663 + * @param array $params Sanitized request params.
664 + * @return array
665 + * @since 4.4.2
666 + */
667 + private function get_order_dashboard_data( array $filter, array $params ): array {
668 + $scope = StatisticsScope::from_params( $params );
669 + $prev_filter = $this->get_previous_filter_for( $filter, $params );
670 + $db = DashboardStatisticsDB::getInstance();
671 + $lp_stats_db = LP_Statistics_DB::getInstance();
672 + $type = $filter['filter_type'];
673 + $time = (string) $filter['time'];
674 + $prev_type = $prev_filter['filter_type'] ?? '';
675 + $prev_time = isset( $prev_filter['time'] ) ? (string) $prev_filter['time'] : '';
676 +
677 + $order_buckets = $this->get_order_status_buckets( $lp_stats_db->get_order_statics( $type, $time, $scope ) );
678 + $prev_buckets = $prev_filter
679 + ? $this->get_order_status_buckets( $lp_stats_db->get_order_statics( $prev_type, $prev_time, $scope ) )
680 + : null;
681 + $total_orders = array_sum( $order_buckets );
682 + $prev_total = $prev_buckets ? array_sum( $prev_buckets ) : null;
683 + $cancelled_fail = $order_buckets['cancelled'] + $order_buckets['failed'];
684 + $prev_cf = $prev_buckets ? $prev_buckets['cancelled'] + $prev_buckets['failed'] : null;
685 +
686 + $net_sales = $this->sum_chart_rows( $lp_stats_db->get_net_sales_data( $type, $time, $scope ) );
687 + $prev_sales = $prev_filter ? $this->sum_chart_rows( $lp_stats_db->get_net_sales_data( $prev_type, $prev_time, $scope ) ) : null;
688 +
689 + $completed_orders = $order_buckets['completed'];
690 + $aov = $completed_orders > 0 ? round( $net_sales / $completed_orders, 2 ) : null;
691 + $cancel_fail_rate = $total_orders > 0 ? round( $cancelled_fail / $total_orders * 100, 1 ) : null;
692 +
693 + $paid_courses = $db->get_paid_courses_sold( $type, $time, $scope );
694 + $prev_paid = $prev_filter ? $db->get_paid_courses_sold( $prev_type, $prev_time, $scope ) : null;
695 +
696 + $top_sold_courses = array_map(
697 + function ( $row ) {
698 + $row['revenue_formatted'] = html_entity_decode( learn_press_format_price( $row['revenue'] ) );
699 + $row['aov_formatted'] = null !== $row['aov'] ? html_entity_decode( learn_press_format_price( $row['aov'] ) ) : null;
700 + return $row;
701 + },
702 + $db->get_top_sold_courses_detailed( $type, $time, $scope, 20 )
703 + );
704 +
705 + $payload = array(
706 + 'kpis' => array(
707 + 'net_sales' => PeriodHelper::kpi_payload( $net_sales, $prev_sales ) + array(
708 + 'formatted' => html_entity_decode( learn_press_format_price( $net_sales ) ),
709 + ),
710 + 'completed_orders' => PeriodHelper::kpi_payload( $completed_orders, $prev_buckets['completed'] ?? null ) + array(
711 + 'aov' => $aov,
712 + 'aov_formatted' => null !== $aov ? html_entity_decode( learn_press_format_price( $aov ) ) : null,
713 + ),
714 + 'processing' => PeriodHelper::kpi_payload( $order_buckets['processing'], $prev_buckets['processing'] ?? null ),
715 + 'pending' => PeriodHelper::kpi_payload( $order_buckets['pending'], $prev_buckets['pending'] ?? null ),
716 + 'cancelled_failed' => PeriodHelper::kpi_payload( $cancelled_fail, $prev_cf ) + array(
717 + 'rate_pct' => $cancel_fail_rate,
718 + 'prev_rate_pct' => $prev_total > 0 && null !== $prev_cf ? round( $prev_cf / $prev_total * 100, 1 ) : null,
719 + ),
720 + 'paid_courses_sold' => PeriodHelper::kpi_payload( $paid_courses, $prev_paid ),
721 + ),
722 + 'order_health' => $order_buckets,
723 + 'top_sold_courses' => $top_sold_courses,
724 + 'exceptions' => OrderExceptionsProvider::getInstance()->get_exceptions( $type, $time, $scope, 20 ),
725 + );
726 +
727 + return $this->filter_dashboard_payload( $payload, 'orders', $filter, $params, $scope );
728 + }
729 +
730 + /**
731 + * Assemble the scoped dashboard payload for the Courses tab.
732 + *
733 + * Legacy response keys are built unscoped in get_courses_statistics(); this
734 + * payload honors instructor/category scope for the upgraded dashboard UI.
735 + *
736 + * @param array $filter [ 'filter_type', 'time' ] from get_statistics_filter().
737 + * @param array $params Sanitized request params.
738 + * @return array
739 + * @since 4.4.2
740 + */
741 + private function get_courses_dashboard_data( array $filter, array $params ): array {
742 + $scope = StatisticsScope::from_params( $params );
743 + $db = DashboardStatisticsDB::getInstance();
744 + $lp_stats_db = LP_Statistics_DB::getInstance();
745 + $type = $filter['filter_type'];
746 + $time = (string) $filter['time'];
747 + $target = (int) apply_filters( 'learn-press/statistics/completion-target', 70 );
748 +
749 + $inventory = $db->get_content_inventory( $scope );
750 + $status_buckets = $this->get_course_status_buckets( $lp_stats_db->get_course_count_by_statuses( $type, $time, $scope ) );
751 + $completion_rows = $db->get_completion_rows( $type, $time, $scope );
752 + $completion = DashboardStatisticsDB::completion_from_rows( $completion_rows, $target );
753 + $health_raw = HealthCheckProvider::getInstance()->get_checks( $scope );
754 +
755 + // Scoped published-courses chart. The legacy top-level `chart_data` stays
756 + // unscoped for addon compatibility; this scoped copy is what the tab reads
757 + // so instructor/category changes actually redraw the chart.
758 + $chart = $this->process_chart_data( $filter, $lp_stats_db->get_published_course_data( $type, $time, $scope, $filter['granularity'] ) );
759 + $chart['line_label'] = __( 'Published Courses', 'learnpress' );
760 +
761 + $payload = array(
762 + 'kpis' => array(
763 + 'published' => array(
764 + 'value' => (int) ( $inventory['courses']['publish'] ?? 0 ),
765 + 'added_in_period' => $status_buckets['publish'],
766 + ),
767 + 'pending_review' => array(
768 + 'value' => (int) ( $inventory['courses']['pending'] ?? 0 ),
769 + 'added_in_period' => $status_buckets['pending'],
770 + ),
771 + 'future' => array(
772 + 'value' => (int) ( $inventory['courses']['future'] ?? 0 ),
773 + 'added_in_period' => $status_buckets['future'],
774 + ),
775 + 'enrollments' => array(
776 + 'value' => $db->get_enrollments_count( $type, $time, $scope ),
777 + ),
778 + 'avg_completion' => array(
779 + 'value' => DashboardStatisticsDB::average_completion_rate_from_rows( $completion_rows ),
780 + 'target' => $target,
781 + ),
782 + 'courses_without_enrollment' => array(
783 + 'value' => (int) ( $health_raw['no_enrollment'] ?? 0 ),
784 + ),
785 + ),
786 + 'performance' => self::format_course_performance_rows( $db->get_top_courses_performance( $type, $time, $scope, 10 ) ),
787 + 'chart' => $chart,
788 + 'health_checks' => array(
789 + 'no_curriculum' => (int) ( $health_raw['no_content'] ?? 0 ),
790 + 'no_students' => (int) ( $health_raw['no_enrollment'] ?? 0 ),
791 + 'low_completion' => (int) $completion['courses_below_target'],
792 + 'low_quiz_pass' => (int) ( $health_raw['quiz_low_pass'] ?? 0 ),
793 + 'pending_review' => (int) ( $health_raw['pending_review'] ?? 0 ),
794 + ),
795 + 'inventory' => $inventory,
796 + );
797 +
798 + return $this->filter_dashboard_payload( $payload, 'courses', $filter, $params, $scope );
799 + }
800 +
801 + /**
802 + * Format course performance rows for the Courses tab contract.
803 + *
804 + * @param array $rows Rows from DashboardStatisticsDB::get_top_courses_performance().
805 + * @return array
806 + * @since 4.4.2
807 + */
808 + public static function format_course_performance_rows( array $rows ): array {
809 + $course_ids = array_map(
810 + function ( $row ) {
811 + return absint( $row['course_id'] ?? 0 );
812 + },
813 + $rows
814 + );
815 + $instructors = self::get_course_instructor_map( $course_ids );
816 +
817 + return array_map(
818 + function ( $row ) use ( $instructors ) {
819 + $course_id = absint( $row['course_id'] ?? 0 );
820 + $revenue = (float) ( $row['revenue'] ?? 0 );
821 +
822 + return array(
823 + 'course_id' => $course_id,
824 + 'name' => (string) ( $row['course_name'] ?? '' ),
825 + 'instructor' => $instructors[ $course_id ] ?? '',
826 + 'revenue' => $revenue,
827 + 'revenue_formatted' => html_entity_decode( learn_press_format_price( $revenue ) ),
828 + 'orders' => (int) ( $row['order_count'] ?? 0 ),
829 + 'enrollments' => (int) ( $row['enrolled'] ?? 0 ),
830 + 'completed' => (int) ( $row['completed'] ?? 0 ),
831 + 'completion_rate' => $row['completion_rate'] ?? null,
832 + 'edit_link' => $course_id > 0 ? (string) get_edit_post_link( $course_id, 'raw' ) : '',
833 + );
834 + },
835 + $rows
836 + );
837 + }
838 +
839 + /**
840 + * Batch-map course IDs to instructor display names.
841 + *
842 + * @param array $course_ids
843 + * @return array course_id => display_name
844 + * @since 4.4.2
845 + */
846 + public static function get_course_instructor_map( array $course_ids ): array {
847 + global $wpdb;
848 +
849 + $course_ids = array_values( array_filter( array_unique( array_map( 'absint', $course_ids ) ) ) );
850 + if ( empty( $course_ids ) ) {
851 + return array();
852 + }
853 +
854 + $placeholders = implode( ', ', array_fill( 0, count( $course_ids ), '%d' ) );
855 + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic %d list is built from absint-normalized IDs.
856 + $sql = $wpdb->prepare(
857 + "SELECT p.ID AS course_id, u.display_name AS instructor
858 + FROM {$wpdb->posts} AS p
859 + LEFT JOIN {$wpdb->users} AS u ON u.ID = p.post_author
860 + WHERE p.ID IN ( {$placeholders} )",
861 + ...$course_ids
862 + );
863 + // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
864 + $rows = $wpdb->get_results( $sql );
865 + $map = array();
866 +
867 + foreach ( (array) $rows as $row ) {
868 + $map[ (int) $row->course_id ] = (string) $row->instructor;
869 + }
870 +
871 + return $map;
872 + }
873 +
874 + /**
875 + * Baseline { filter_type, time } pair for KPI deltas, honoring the
876 + * `compare` request param ( previous_period | previous_year, default
877 + * previous_period ). Falls back to the PeriodHelper mapping when the
878 + * filter was built without a PeriodRange ( BC callers ).
879 + *
880 + * @param array $filter From get_statistics_filter().
881 + * @param array $params Sanitized request params.
882 + * @return array|null Null when no baseline can be built ( KPI renders without a delta ).
883 + * @since 4.4.2
884 + */
885 + private function get_previous_filter_for( array $filter, array $params ): ?array {
886 + $compare = PeriodResolver::sanitize_compare( (string) ( $params['compare'] ?? '' ) );
887 + $range = $filter['range'] ?? null;
888 +
889 + if ( $range instanceof PeriodRange ) {
890 + $prev = PeriodResolver::previous( $range, $compare );
891 +
892 + return $prev ? $prev->legacy_pair() : null;
893 + }
894 +
895 + return PeriodHelper::get_previous_filter( $filter );
896 + }
897 +
898 + /**
899 + * Apply the shared dashboard payload transform filter.
900 + *
901 + * Single place every tab assembly routes its payload through, so the
902 + * `learn-press/statistics/dashboard/data` filter has one stable signature.
903 + *
904 + * @param array $payload Assembled tab payload ( includes the `kpis` array ).
905 + * @param string $tab Tab id: overview|orders|courses|users.
906 + * @param array $filter Resolved period filter [ filter_type, time, granularity, range ].
907 + * @param array $params Sanitized request params.
908 + * @param mixed $scope Resolved StatisticsScope.
909 + * @return array
910 + * @since 4.4.2
911 + */
912 + private function filter_dashboard_payload( array $payload, string $tab, array $filter, array $params, $scope ): array {
913 + /**
914 + * Filter a statistics tab's assembled dashboard payload before it is returned.
915 + *
916 + * Covers KPIs, chart series and tables in one place ( customize the `kpis`
917 + * key to reshape a metric, add a key for a companion add-on ).
918 + *
919 + * @param array $payload Tab payload.
920 + * @param string $tab Tab id: overview|orders|courses|users.
921 + * @param array $filter Resolved period filter.
922 + * @param array $params Sanitized request params.
923 + * @param mixed $scope Resolved StatisticsScope.
924 + * @since 4.4.2
925 + */
926 + $payload = apply_filters( 'learn-press/statistics/dashboard/data', $payload, $tab, $filter, $params, $scope );
927 +
928 + return is_array( $payload ) ? $payload : array();
929 + }
930 +
931 + /**
932 + * Apply the REST response transform filter for a statistics endpoint.
933 + *
934 + * @param mixed $data Assembled response data.
935 + * @param string $endpoint Endpoint id: overviews|orders|courses|users|instructor|filter-options.
936 + * @param WP_REST_Request $request The request.
937 + * @return mixed
938 + * @since 4.4.2
939 + */
940 + private function filter_rest_response( $data, string $endpoint, $request ) {
941 + /**
942 + * Filter the assembled statistics REST payload before it is sent.
943 + *
944 + * @param mixed $data Response data ( `data` of LP_REST_Response ).
945 + * @param string $endpoint Endpoint id.
946 + * @param WP_REST_Request $request The request.
947 + * @since 4.4.2
948 + */
949 + return apply_filters( 'learn-press/statistics/rest/response', $data, $endpoint, $request );
950 + }
951 +
952 + /**
953 + * Assemble the scoped dashboard payload for the Overview tab.
954 + *
955 + * Legacy response keys are built unscoped elsewhere and stay byte-identical;
956 + * everything here honors instructor_id/category_id and carries
957 + * previous-period deltas via PeriodHelper.
958 + *
959 + * @param array $filter [ 'filter_type', 'time' ] from get_statistics_filter().
960 + * @param array $params Sanitized request params.
961 + * @return array
962 + * @since 4.4.2
963 + */
964 + private function get_dashboard_data( array $filter, array $params ): array {
965 + $scope = StatisticsScope::from_params( $params );
966 + $prev_filter = $this->get_previous_filter_for( $filter, $params );
967 + $db = DashboardStatisticsDB::getInstance();
968 + $lp_stats_db = LP_Statistics_DB::getInstance();
969 + $type = $filter['filter_type'];
970 + $time = (string) $filter['time'];
971 + $prev_type = $prev_filter['filter_type'] ?? '';
972 + $prev_time = isset( $prev_filter['time'] ) ? (string) $prev_filter['time'] : '';
973 +
974 + // Orders: current + previous buckets (one query each).
975 + $order_buckets = $this->get_order_status_buckets( $lp_stats_db->get_order_statics( $type, $time, $scope ) );
976 + $prev_buckets = $prev_filter
977 + ? $this->get_order_status_buckets( $lp_stats_db->get_order_statics( $prev_type, $prev_time, $scope ) )
978 + : null;
979 + $total_orders = array_sum( $order_buckets );
980 +
981 + // Revenue: chart series + period sums.
982 + $revenue_chart = $this->process_chart_data( $filter, $lp_stats_db->get_net_sales_data( $type, $time, $scope, $filter['granularity'] ) );
983 + $net_sales = round( array_sum( $revenue_chart['data'] ), 2 );
984 + $prev_sales = null;
985 + if ( $prev_filter ) {
986 + $prev_rows = $lp_stats_db->get_net_sales_data( $prev_type, $prev_time, $scope );
987 + $prev_sales = round( array_sum( array_map( fn( $row ) => (float) $row->x_data, (array) $prev_rows ) ), 2 );
988 + }
989 +
990 + // Enrollments chart series (same label processing as revenue).
991 + $enroll_chart = $this->process_chart_data(
992 + $filter,
993 + $lp_stats_db->get_enrollment_chart_data( $type, $time, 0, $scope, $filter['granularity'] )
994 + );
995 +
996 + $enrollments = $db->get_enrollments_count( $type, $time, $scope );
997 + $prev_enrollments = $prev_filter ? $db->get_enrollments_count( $prev_type, $prev_time, $scope ) : null;
998 +
999 + $completion = $db->get_completion_stats( $type, $time, $scope );
1000 + $prev_completion = $prev_filter ? $db->get_completion_stats( $prev_type, $prev_time, $scope ) : null;
1001 +
1002 + $active_learners = $db->get_active_learners_count( $type, $time, $scope );
1003 + $prev_active = $prev_filter ? $db->get_active_learners_count( $prev_type, $prev_time, $scope ) : null;
1004 +
1005 + $completed_orders = $order_buckets['completed'];
1006 + $aov = $completed_orders > 0 ? round( $net_sales / $completed_orders, 2 ) : null;
1007 + $failed_orders = $order_buckets['failed'];
1008 + $fail_rate = $total_orders > 0 ? round( $failed_orders / $total_orders * 100, 1 ) : null;
1009 +
1010 + $kpis = array(
1011 + 'net_sales' => PeriodHelper::kpi_payload( $net_sales, $prev_sales ) + array(
1012 + 'formatted' => html_entity_decode( learn_press_format_price( $net_sales ) ),
1013 + ),
1014 + 'completed_orders' => PeriodHelper::kpi_payload( $completed_orders, $prev_buckets['completed'] ?? null ) + array(
1015 + 'aov' => $aov,
1016 + 'aov_formatted' => null !== $aov ? html_entity_decode( learn_press_format_price( $aov ) ) : null,
1017 + ),
1018 + 'enrollments' => PeriodHelper::kpi_payload( $enrollments, $prev_enrollments ),
1019 + 'completion_rate' => PeriodHelper::kpi_payload( $completion['rate'], $prev_completion['rate'] ?? null ) + array(
1020 + 'courses_below_target' => $completion['courses_below_target'],
1021 + ),
1022 + 'active_learners' => PeriodHelper::kpi_payload( $active_learners, $prev_active ),
1023 + 'failed_orders' => PeriodHelper::kpi_payload( $failed_orders, $prev_buckets['failed'] ?? null ) + array(
1024 + 'fail_rate_pct' => $fail_rate,
1025 + ),
1026 + );
1027 +
1028 + $top_courses = array_map(
1029 + function ( $row ) {
1030 + $row['revenue_formatted'] = html_entity_decode( learn_press_format_price( $row['revenue'] ) );
1031 + return $row;
1032 + },
1033 + $db->get_top_courses_performance( $type, $time, $scope )
1034 + );
1035 +
1036 + $instructor_summary = array_map(
1037 + function ( $row ) {
1038 + $row['revenue_formatted'] = html_entity_decode( learn_press_format_price( $row['revenue'] ) );
1039 + return $row;
1040 + },
1041 + $db->get_instructor_performance( $type, $time, $scope )
1042 + );
1043 +
1044 + $health_checks = HealthCheckProvider::getInstance()->get_checks( $scope );
1045 + $health_checks['low_completion'] = $completion['courses_below_target'];
1046 +
1047 + $payload = array(
1048 + 'kpis' => $kpis,
1049 + 'chart' => array(
1050 + 'labels' => $revenue_chart['labels'],
1051 + 'revenue' => $revenue_chart['data'],
1052 + 'enrollments' => $enroll_chart['data'],
1053 + 'x_label' => $revenue_chart['x_label'],
1054 + 'granularity' => $revenue_chart['granularity'] ?? '',
1055 + ),
1056 + 'funnel' => $db->get_learner_funnel( $type, $time, $scope ),
1057 + 'top_courses' => $top_courses,
1058 + 'instructor_summary' => $instructor_summary,
1059 + 'order_health' => $order_buckets + array(
1060 + 'total' => $total_orders,
1061 + 'cancelled_failed' => $order_buckets['cancelled'] + $failed_orders,
1062 + ),
1063 + 'health_checks' => $health_checks,
1064 + );
1065 +
1066 + return $this->filter_dashboard_payload( $payload, 'overview', $filter, $params, $scope );
1067 + }
1068 +
1069 + /**
1070 + * Assemble the scoped dashboard payload for the Users tab.
1071 + *
1072 + * users_activated/students/instructors totals are role-based user counts
1073 + * (no course dimension) and stay unscoped like their legacy siblings;
1074 + * everything course-linked honors instructor_id/category_id.
1075 + *
1076 + * @param array $filter [ 'filter_type', 'time' ] from get_statistics_filter().
1077 + * @param array $params Sanitized request params.
1078 + * @param int $not_started Reused from the legacy assembly — get_users_not_started_any_course() is expensive.
1079 + * @return array
1080 + * @since 4.4.2
1081 + */
1082 + private function get_users_dashboard_data( array $filter, array $params, int $not_started = 0 ): array {
1083 + $scope = StatisticsScope::from_params( $params );
1084 + $db = DashboardStatisticsDB::getInstance();
1085 + $lp_stats_db = LP_Statistics_DB::getInstance();
1086 + $type = $filter['filter_type'];
1087 + $time = (string) $filter['time'];
1088 +
1089 + $total_instructors = (int) $lp_stats_db->get_total_instructor_created( $type, $time );
1090 + $total_students = (int) $lp_stats_db->get_total_student_created( $type, $time );
1091 + $funnel = $db->get_learner_funnel( $type, $time, $scope, true );
1092 + $completion = $db->get_completion_stats( $type, $time, $scope );
1093 +
1094 + $payload = array(
1095 + 'kpis' => array(
1096 + 'users_activated' => array(
1097 + 'value' => $total_instructors + $total_students,
1098 + 'new_in_period' => $funnel['registered'],
1099 + ),
1100 + 'students' => array(
1101 + 'value' => $total_students,
1102 + // Follows the selected window ( was hard-coded last-7-days `active_7d` pre-release ).
1103 + 'active_in_period' => $db->get_active_learners_count( $type, $time, $scope ),
1104 + ),
1105 + 'instructors' => array(
1106 + 'value' => $total_instructors,
1107 + 'active_in_period' => $db->get_instructors_active_in_period( $type, $time, $scope ),
1108 + ),
1109 + 'not_started' => array(
1110 + 'value' => $not_started,
1111 + ),
1112 + 'in_progress' => array(
1113 + 'value' => $db->get_users_in_progress_count( $type, $time, $scope ),
1114 + ),
1115 + 'finished' => array(
1116 + 'value' => $funnel['completed'],
1117 + 'completion_rate' => $completion['rate'],
1118 + ),
1119 + ),
1120 + 'funnel' => $funnel,
1121 + 'top_students' => $db->get_top_students( $type, $time, $scope, 10 ),
1122 + 'top_courses_by_students' => $db->get_courses_by_students( $type, $time, $scope, 10 ),
1123 + );
1124 +
1125 + return $this->filter_dashboard_payload( $payload, 'users', $filter, $params, $scope );
1126 + }
1127 +
1128 + /**
1129 + * Instructors tab payload: KPIs, operations widget, performance + watchlist tables.
1130 + *
1131 + * @param WP_REST_Request $request
1132 + *
1133 + * @return LP_REST_Response
1134 + * @since 4.4.2
1135 + */
1136 + public function get_instructor_statistics( WP_REST_Request $request ): LP_REST_Response {
1137 + $response = new LP_REST_Response();
1138 +
1139 + try {
1140 + $params = $request->get_params();
1141 + $params = LP_Helper::sanitize_params_submitted( $params );
1142 + $filter = $this->get_statistics_filter( $params );
1143 + $scope = StatisticsScope::from_params( $params );
1144 +
1145 + $type = $filter['filter_type'];
1146 + $time = (string) $filter['time'];
1147 + $lp_stats_db = LP_Statistics_DB::getInstance();
1148 +
1149 + $revenue_chart = $this->process_chart_data( $filter, $lp_stats_db->get_net_sales_data( $type, $time, $scope, $filter['granularity'] ) );
1150 + $enroll_chart = $this->process_chart_data(
1151 + $filter,
1152 + $lp_stats_db->get_enrollment_chart_data( $type, $time, 0, $scope, $filter['granularity'] )
1153 + );
1154 +
1155 + $instructor_data = array(
1156 + 'dashboard' => InstructorStatisticsProvider::get_statistics( $type, $time, $scope ),
1157 + 'chart_data' => array(
1158 + 'labels' => $revenue_chart['labels'] ?? array(),
1159 + 'revenue' => $revenue_chart['data'] ?? array(),
1160 + 'enrollments' => $enroll_chart['data'] ?? array(),
1161 + 'x_label' => $revenue_chart['x_label'] ?? '',
1162 + 'granularity' => $revenue_chart['granularity'] ?? '',
1163 + ),
1164 + 'range' => $this->range_response( $filter ),
1165 + );
1166 + $response->data = $this->filter_rest_response( $instructor_data, 'instructor', $request );
1167 + $response->status = 'success';
1168 + } catch ( Throwable $e ) {
1169 + $response->message = $e->getMessage();
1170 + $response->status = 'error';
1171 + }
1172 +
1173 + return $response;
1174 + }
1175 +
1176 + /**
1177 + * Options for the global statistics filters (instructor/category dropdowns).
1178 + *
1179 + * @param WP_REST_Request $request
1180 + *
1181 + * @return LP_REST_Response
1182 + * @since 4.4.2
1183 + */
1184 + public function get_filter_options( WP_REST_Request $request ): LP_REST_Response {
1185 + $response = new LP_REST_Response();
1186 +
1187 + try {
1188 + $response->data = $this->filter_rest_response( FilterOptionsProvider::get_options(), 'filter-options', $request );
1189 + $response->status = 'success';
1190 + } catch ( Throwable $e ) {
1191 + $response->message = $e->getMessage();
1192 + $response->status = 'error';
1193 + }
1194 +
1195 + return $response;
511 1196 }
512 1197
513 1198 public function permission_check( $request ) {
514 1199 return apply_filters( 'learnpress/admin-statistics/permission', current_user_can( 'administrator' ) );