PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.6
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.6
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 / inc / rest-api / v1 / admin / class-lp-admin-rest-statistics-controller.php

class-lp-admin-rest-statistics-controller.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.6, at inc/rest-api/v1/admin/class-lp-admin-rest-statistics-controller.php

1,202 lines 46.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
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;
12
13 /**
14 * Class LP_REST_Admin_Statistics_Controller
15 *
16 * @since 4.2.5.5
17 */
18 class LP_REST_Admin_Statistics_Controller extends LP_Abstract_REST_Controller {
19 protected static $_instance = null;
20 public function __construct() {
21 $this->namespace = 'lp/v1';
22 $this->rest_base = 'statistics';
23
24 parent::__construct();
25 }
26
27 public function register_routes() {
28 $this->routes = array(
29 'overviews-statistics' => array(
30 array(
31 'methods' => WP_REST_Server::READABLE,
32 'callback' => array( $this, 'get_overviews_statistics' ),
33 'permission_callback' => array( $this, 'permission_check' ),
34 ),
35 ),
36 'order-statistics' => array(
37 array(
38 'methods' => WP_REST_Server::READABLE,
39 'callback' => array( $this, 'get_order_statistics' ),
40 'permission_callback' => array( $this, 'permission_check' ),
41 ),
42 ),
43 'course-statistics' => array(
44 array(
45 'methods' => WP_REST_Server::READABLE,
46 'callback' => array( $this, 'get_courses_statistics' ),
47 'permission_callback' => array( $this, 'permission_check' ),
48 ),
49 ),
50 'user-statistics' => array(
51 array(
52 'methods' => WP_REST_Server::READABLE,
53 'callback' => array( $this, 'get_users_statistics' ),
54 'permission_callback' => array( $this, 'permission_check' ),
55 ),
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 ),
71 );
72
73 parent::register_routes();
74 }
75
76 /**
77 * Gets the overviews statistics.
78 *
79 * @param WP_REST_Request $request
80 *
81 * @return LP_REST_Response.
82 */
83 public function get_overviews_statistics( WP_REST_Request $request ): LP_REST_Response {
84 $response = new LP_REST_Response();
85
86 try {
87 $params = $request->get_params();
88 $params = LP_Helper::sanitize_params_submitted( $params );
89 $filter = $this->get_statistics_filter( $params );
90
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'] ) ) );
102
103 $data = array(
104 'total_sales' => $total_sales,
105 'total_orders' => $total_orders,
106 'total_instructors' => $total_instructors,
107 'total_courses' => $total_courses,
108 'total_students' => $total_students,
109 'chart_data' => $chart_data,
110 'top_courses' => $top_courses,
111 'top_categories' => $top_categories,
112 );
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';
118 } catch ( Throwable $e ) {
119 $response->message = $e->getMessage();
120 $response->status = 'error';
121 }
122
123 return $response;
124 }
125
126 /**
127 * @param WP_REST_Request $request
128 * @return LP_REST_Response
129 */
130 public function get_order_statistics( WP_REST_Request $request ): LP_REST_Response {
131 $response = new LP_REST_Response();
132
133 try {
134 $params = $request->get_params();
135 $params = LP_Helper::sanitize_params_submitted( $params );
136 $filter = $this->get_statistics_filter( $params );
137
138 $lp_statistic_db = LP_Statistics_DB::getInstance();
139 $statistics = $lp_statistic_db->get_order_statics( $filter['filter_type'], $filter['time'] );
140 $completed_orders = $lp_statistic_db->get_completed_order_data( $filter['filter_type'], $filter['time'], null, $filter['granularity'] );
141 $chart_data = $this->process_chart_data( $filter, $completed_orders );
142 $chart_data['line_label'] = __( 'Completed orders', 'learnpress' );
143 $data = array(
144 'statistics' => $statistics,
145 'chart_data' => $chart_data,
146 'dashboard' => $this->get_order_dashboard_data( $filter, $params ),
147 'range' => $this->range_response( $filter ),
148 );
149 $response->data = $this->filter_rest_response( $data, 'orders', $request );
150 $response->status = 'success';
151 } catch ( Throwable $e ) {
152 $response->message = $e->getMessage();
153 $response->status = 'error';
154 }
155
156 return $response;
157 }
158 public function get_courses_statistics( $request ) {
159 $response = new LP_REST_Response();
160 try {
161 $params = $request->get_params();
162 $params = LP_Helper::sanitize_params_submitted( $params );
163 $filter = $this->get_statistics_filter( $params );
164
165 $lp_statistic_db = LP_Statistics_DB::getInstance();
166 $published_course = $lp_statistic_db->get_published_course_data( $filter['filter_type'], $filter['time'], null, $filter['granularity'] );
167 $courses = $lp_statistic_db->get_course_count_by_statuses( $filter['filter_type'], $filter['time'] );
168 $items = $lp_statistic_db->get_course_items_count( $filter['filter_type'], $filter['time'] );
169 $chart_data = $this->process_chart_data( $filter, $published_course );
170 $chart_data['line_label'] = __( 'Published Courses', 'learnpress' );
171 $data = array(
172 'courses' => $courses,
173 'items' => $items,
174 'chart_data' => $chart_data,
175 'dashboard' => $this->get_courses_dashboard_data( $filter, $params ),
176 'range' => $this->range_response( $filter ),
177 );
178 $response->data = $this->filter_rest_response( $data, 'courses', $request );
179 $response->status = 'success';
180 } catch ( Throwable $e ) {
181 $response->message = $e->getMessage();
182 $response->status = 'error';
183 }
184
185 return $response;
186 }
187
188 /**
189 * @param $request
190 * @return LP_REST_Response
191 */
192 public function get_users_statistics( $request ): LP_REST_Response {
193 $response = new LP_REST_Response();
194 try {
195 $params = $request->get_params();
196 $params = LP_Helper::sanitize_params_submitted( $params );
197 $filter = $this->get_statistics_filter( $params );
198
199 $lp_statistic_db = LP_Statistics_DB::getInstance();
200 $user_registers = $lp_statistic_db->get_user_registered_data( $filter['filter_type'], $filter['time'], $filter['granularity'] );
201 $user_course_statused = $lp_statistic_db->get_users_by_user_item_graduation_statuses( $filter['filter_type'], $filter['time'] );
202 $user_not_start_course = $lp_statistic_db->get_users_not_started_any_course( $filter['filter_type'], $filter['time'] );
203 $top_enrolled_courses = $lp_statistic_db->get_top_enrolled_courses( $filter['filter_type'], $filter['time'] );
204 $total_instructors = $lp_statistic_db->get_total_instructor_created( $filter['filter_type'], $filter['time'] );
205 $total_students = $lp_statistic_db->get_total_student_created( $filter['filter_type'], $filter['time'] );
206 $chart_data = $this->process_chart_data( $filter, $user_registers );
207 $top_enrolled_instructor = array();
208 if ( ! empty( $top_enrolled_courses ) ) {
209 foreach ( $top_enrolled_courses as $key => $course ) {
210 if ( ! array_key_exists( $course->instructor_id, $top_enrolled_instructor ) ) {
211 $top_enrolled_instructor[ $course->instructor_id ] = array(
212 'name' => $course->instructor_name,
213 'students' => (int) $course->enrolled_user,
214 );
215 } else {
216 $top_enrolled_instructor[ $course->instructor_id ]['students'] += (int) $course->enrolled_user;
217 }
218 }
219 }
220 $chart_data['line_label'] = __( 'Registered users', 'learnpress' );
221 $data = array(
222 'chart_data' => $chart_data,
223 'user_course_statused' => $user_course_statused,
224 'user_not_start_course' => $user_not_start_course,
225 'top_enrolled_courses' => $top_enrolled_courses,
226 'top_enrolled_instructor' => $top_enrolled_instructor,
227 'total_instructors' => $total_instructors,
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 ),
231 );
232 $response->data = $this->filter_rest_response( $data, 'users', $request );
233 $response->status = 'success';
234 } catch ( Throwable $e ) {
235 $response->message = $e->getMessage();
236 $response->status = 'error';
237 }
238
239 return $response;
240 }
241 /**
242 * Process data use for chart js
243 *
244 * @param array $filter The filter in get_statistics_filter
245 * @param array $input_data The input data ( data from DB )
246 *
247 * @return array $chart_data Data use for chart js
248 */
249 public function process_chart_data( array $filter, array $input_data ) {
250 $chart_data = array();
251 $data = array();
252 if ( $filter['filter_type'] == 'date' ) {
253 $data = $this->process_date_data( $input_data );
254 $chart_data['x_label'] = __( 'Hour', 'learnpress' );
255 } elseif ( $filter['filter_type'] == 'previous_days' ) {
256 $data = $this->process_previous_days_data( $filter['time'], $input_data );
257 $chart_data['x_label'] = __( 'Dates', 'learnpress' );
258 } elseif ( $filter['filter_type'] == 'month' ) {
259 $data = $this->process_month_data( $filter, $input_data );
260 $chart_data['x_label'] = __( 'Dates', 'learnpress' );
261 } elseif ( $filter['filter_type'] == 'previous_months' ) {
262 $data = $this->process_previous_months_data( $filter['time'], $input_data );
263 $chart_data['x_label'] = __( 'Months', 'learnpress' );
264 } elseif ( $filter['filter_type'] == 'year' ) {
265 $data = $this->process_year_data( $input_data );
266 $chart_data['x_label'] = __( 'Months', 'learnpress' );
267 } elseif ( $filter['filter_type'] == 'custom' ) {
268 $dates = $filter['time'];
269 $dates = explode( '+', $dates );
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 }
300 $diff = date_diff( date_create( $dates[0] ), date_create( $dates[1] ), true );
301 $y = $diff->y;
302 $m = $diff->m;
303 $d = $diff->d;
304 if ( $y < 1 ) {
305 if ( $m <= 1 ) {
306 if ( $d < 1 ) {
307 $data = $this->process_date_data( $input_data );
308 $chart_data['x_label'] = __( 'Hour', 'learnpress' );
309 } else {
310 $data = $this->process_previous_days_data( $d, $input_data, $dates[1] );
311 $chart_data['x_label'] = __( 'Dates', 'learnpress' );
312 }
313 } else {
314 $data = $this->process_previous_months_data( $m, $input_data, $dates[1] );
315 $chart_data['x_label'] = __( 'Months', 'learnpress' );
316 // $filter = $this->chart_filter_previous_months_group_by( $filter );
317 }
318 } elseif ( $y < 2 ) {
319 $months = $y * 12 + $m;
320 $data = $this->process_previous_months_data( $months, $input_data, $dates[1] );
321 $chart_data['x_label'] = __( 'Months', 'learnpress' );
322 } elseif ( $y < 5 ) {
323 // TODO
324 $data = $this->process_quarters_data( $dates, $input_data );
325 $chart_data['x_label'] = __( 'Quarters', 'learnpress' );
326 } else {
327 $data = $this->process_years_data( $y, $input_data, $dates[1] );
328 $chart_data['x_label'] = __( 'Years', 'learnpress' );
329 }
330 }
331 // Label-format marker for the shared JS formatter; additive key. @since 4.4.2
332 $chart_data['granularity'] = (string) ( $filter['granularity'] ?? '' );
333 foreach ( $data as $row ) {
334 $chart_data['labels'][] = $row->x_data_label;
335 $chart_data['data'][] = (float) $row->x_data;
336 }
337 // $chart_data['line_label'] = __( 'Completed orders', 'learnpress' );
338
339 return $chart_data;
340 }
341 /**
342 * Gets the statistics filter.
343 *
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.
348 *
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
356 */
357 public function get_statistics_filter( $params ) {
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 );
390 }
391
392 return array(
393 'label' => $range->label,
394 'filtertype' => $range->preset,
395 );
396 }
397
398 /**
399 * process data of a date ( in 24h )
400 *
401 * @param array $input_data The input data
402 *
403 * @return array ( description_of_the_return_value )
404 */
405 public function process_date_data( array $input_data ) {
406 $data = array();
407 for ( $i = 0; $i < 24;$i++ ) {
408 $row = new stdClass();
409 $row->x_data_label = $i;
410 $row->x_data = 0;
411 $data[ $i ] = $row;
412 }
413 if ( ! empty( $input_data ) ) {
414 foreach ( $input_data as $row ) {
415 $data[ $row->x_data_label ] = $row;
416 }
417 }
418 return $data;
419 }
420 /**
421 * process data of days since the last date, if dont have last date, last date is current date
422 *
423 * @param int $days The days
424 * @param array $input_data The input data
425 * @param bool $last_date The last date
426 *
427 * @return array ( description_of_the_return_value )
428 */
429 public function process_previous_days_data( int $days, array $input_data, $last_date = false ) {
430 $data = array();
431 for ( $i = $days; $i >= 0; $i-- ) {
432 $date = date( 'Y-m-d', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'days' ) );
433 $row = new stdClass();
434 $row->x_data_label = $date;
435 $row->x_data = 0;
436 $data[ $date ] = $row;
437 }
438 if ( ! empty( $input_data ) ) {
439 foreach ( $input_data as $row ) {
440 $data[ $row->x_data_label ] = $row;
441 }
442 }
443 return $data;
444 }
445 /**
446 * process data of a month
447 *
448 * @param array $filter The filter
449 * @param array $input_data The input data
450 *
451 * @return array ( description_of_the_return_value )
452 */
453 public function process_month_data( array $filter, array $input_data ) {
454 $data = array();
455 $max_day = cal_days_in_month( 0, date( 'm', strtotime( $filter['time'] ) ), date( 'Y', strtotime( $filter['time'] ) ) );
456 for ( $i = 1; $i <= $max_day; $i++ ) {
457 $row = new stdClass();
458 $row->x_data_label = $i;
459 $row->x_data = 0;
460 $data[ $i ] = $row;
461 }
462 if ( ! empty( $input_data ) ) {
463 foreach ( $input_data as $row ) {
464 $data[ $row->x_data_label ] = $row;
465 }
466 }
467 return $data;
468 }
469 /**
470 * process data of months since the last date, if dont have last date, last date is current date
471 *
472 * @param int $months The months
473 * @param array $input_data The input data
474 * @param bool $last_date The last date
475 *
476 * @return array ( description_of_the_return_value )
477 */
478 public function process_previous_months_data( int $months, array $input_data, $last_date = false ) {
479 $data = array();
480 for ( $i = $months; $i >= 0; $i-- ) {
481 $date = date( 'm-Y', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'months' ) );
482 $row = new stdClass();
483 $row->x_data_label = $date;
484 $row->x_data = 0;
485 $data[ $date ] = $row;
486 }
487 if ( ! empty( $input_data ) ) {
488 foreach ( $input_data as $row ) {
489 $data[ $row->x_data_label ] = $row;
490 }
491 }
492 return $data;
493 }
494 /**
495 *
496 * @param array $dates The dates
497 * @param array $input_data The input data
498 *
499 * @return array process data for date range 2-5 years
500 */
501 public function process_quarters_data( array $dates, array $input_data ) {
502 $data = array();
503 $start_time = strtotime( $dates[0] );
504 $end_time = strtotime( $dates[1] );
505 for ( $i = date( 'Y', $start_time ); $i <= date( 'Y', $end_time ); $i++ ) {
506 if ( $i == date( 'Y', $start_time ) ) {
507 $quarter = ceil( date( 'm', $start_time ) / 3 );
508 for ( $j = $quarter;$j <= 4;$j++ ) {
509 $row = new stdClass();
510 $row->x_data_label = 'q' . $j . '-' . $i;
511 $row->x_data = 0;
512 $data[] = $row;
513 }
514 } elseif ( $i == date( 'Y', $start_time ) ) {
515 $quarter = ceil( date( 'm', $end_time ) / 3 );
516 for ( $j = 1;$j <= $quarter;$j++ ) {
517 $row = new stdClass();
518 $row->x_data_label = 'q' . $j . '-' . $i;
519 $row->x_data = 0;
520 $data[] = $row;
521 }
522 } else {
523 for ( $j = 1; $j <= 4;$j++ ) {
524 $row = new stdClass();
525 $row->x_data_label = 'q' . $j . '-' . $i;
526 $row->x_data = 0;
527 $data[] = $row;
528 }
529 }
530 }
531 if ( ! empty( $input_data ) ) {
532 foreach ( $input_data as $row ) {
533 $data[ $row->x_data_label ] = $row;
534 }
535 }
536 return $data;
537 }
538 /**
539 * process data of a year
540 *
541 * @param array $input_data data from DB
542 *
543 * @return array chart data
544 */
545 public function process_year_data( array $input_data ) {
546 $data = array();
547 for ( $i = 1; $i <= 12; $i++ ) {
548 $row = new stdClass();
549 $row->x_data_label = $i;
550 $row->x_data = 0;
551 $data[ $i ] = $row;
552 }
553 if ( ! empty( $input_data ) ) {
554 foreach ( $input_data as $row ) {
555 $data[ $row->x_data_label ] = $row;
556 }
557 }
558 return $data;
559 }
560
561 /**
562 * process data of years( when date range > 5 years )
563 *
564 * @param int $years The years
565 * @param array $input_data The input data
566 * @param bool $last_date The last date
567 *
568 * @return array ( description_of_the_return_value )
569 */
570 public function process_years_data( int $years, array $input_data, $last_date = false ) {
571 $data = array();
572 for ( $i = $years; $i >= 0; $i-- ) {
573 $year = date( 'Y', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'years' ) );
574 $row = new stdClass();
575 $row->x_data_label = $year;
576 $row->x_data = 0;
577 $data[ $year ] = $row;
578 }
579 if ( ! empty( $input_data ) ) {
580 foreach ( $input_data as $row ) {
581 $data[ $row->x_data_label ] = $row;
582 }
583 }
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;
1196 }
1197
1198 public function permission_check( $request ) {
1199 return apply_filters( 'learnpress/admin-statistics/permission', current_user_can( 'administrator' ) );
1200 }
1201 }
1202