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 +1201 -516 4.3.8 → 4.4.9 View file →
@@ -1,516 +1,1201 @@
1 -<?php
2 -
3 -use LearnPress\Helpers\Template;
4 -
5 -/**
6 - * Class LP_REST_Admin_Statistics_Controller
7 - *
8 - * @since 4.2.5.5
9 - */
10 -class LP_REST_Admin_Statistics_Controller extends LP_Abstract_REST_Controller {
11 - protected static $_instance = null;
12 - public function __construct() {
13 - $this->namespace = 'lp/v1';
14 - $this->rest_base = 'statistics';
15 -
16 - parent::__construct();
17 - }
18 -
19 - public function register_routes() {
20 - $this->routes = array(
21 - 'overviews-statistics' => array(
22 - array(
23 - 'methods' => WP_REST_Server::READABLE,
24 - 'callback' => array( $this, 'get_overviews_statistics' ),
25 - 'permission_callback' => array( $this, 'permission_check' ),
26 - ),
27 - ),
28 - 'order-statistics' => array(
29 - array(
30 - 'methods' => WP_REST_Server::READABLE,
31 - 'callback' => array( $this, 'get_order_statistics' ),
32 - 'permission_callback' => array( $this, 'permission_check' ),
33 - ),
34 - ),
35 - 'course-statistics' => array(
36 - array(
37 - 'methods' => WP_REST_Server::READABLE,
38 - 'callback' => array( $this, 'get_courses_statistics' ),
39 - 'permission_callback' => array( $this, 'permission_check' ),
40 - ),
41 - ),
42 - 'user-statistics' => array(
43 - array(
44 - 'methods' => WP_REST_Server::READABLE,
45 - 'callback' => array( $this, 'get_users_statistics' ),
46 - 'permission_callback' => array( $this, 'permission_check' ),
47 - ),
48 - ),
49 - );
50 -
51 - parent::register_routes();
52 - }
53 -
54 - /**
55 - * Gets the overviews statistics.
56 - *
57 - * @param WP_REST_Request $request
58 - *
59 - * @return LP_REST_Response.
60 - */
61 - public function get_overviews_statistics( WP_REST_Request $request ): LP_REST_Response {
62 - $response = new LP_REST_Response();
63 -
64 - try {
65 - $params = $request->get_params();
66 - $params = LP_Helper::sanitize_params_submitted( $params );
67 - $filter = $this->get_statistics_filter( $params );
68 -
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'] ) ) );
80 -
81 - $data = array(
82 - 'total_sales' => $total_sales,
83 - 'total_orders' => $total_orders,
84 - 'total_instructors' => $total_instructors,
85 - 'total_courses' => $total_courses,
86 - 'total_students' => $total_students,
87 - 'chart_data' => $chart_data,
88 - 'top_courses' => $top_courses,
89 - 'top_categories' => $top_categories,
90 - );
91 - $response->data = $data;
92 - $response->status = 'success';
93 - } catch ( Throwable $e ) {
94 - $response->message = $e->getMessage();
95 - $response->status = 'error';
96 - }
97 -
98 - return $response;
99 - }
100 -
101 - /**
102 - * @param WP_REST_Request $request
103 - * @return LP_REST_Response
104 - */
105 - public function get_order_statistics( WP_REST_Request $request ): LP_REST_Response {
106 - $response = new LP_REST_Response();
107 -
108 - try {
109 - $params = $request->get_params();
110 - $params = LP_Helper::sanitize_params_submitted( $params );
111 - $filter = $this->get_statistics_filter( $params );
112 - $lp_statistic_db = LP_Statistics_DB::getInstance();
113 - $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'] );
115 - $chart_data = $this->process_chart_data( $filter, $completed_orders );
116 - $chart_data['line_label'] = __( 'Completed orders', 'learnpress' );
117 - $data = array(
118 - 'statistics' => $statistics,
119 - 'chart_data' => $chart_data,
120 - );
121 - $response->data = $data;
122 - $response->status = 'success';
123 - } catch ( Throwable $e ) {
124 - $response->message = $e->getMessage();
125 - $response->status = 'error';
126 - }
127 -
128 - return $response;
129 - }
130 - public function get_courses_statistics( $request ) {
131 - $response = new LP_REST_Response();
132 - try {
133 - $params = $request->get_params();
134 - $params = LP_Helper::sanitize_params_submitted( $params );
135 - $filter = $this->get_statistics_filter( $params );
136 - $lp_statistic_db = LP_Statistics_DB::getInstance();
137 - $published_course = $lp_statistic_db->get_published_course_data( $filter['filter_type'], $filter['time'] );
138 - $courses = $lp_statistic_db->get_course_count_by_statuses( $filter['filter_type'], $filter['time'] );
139 - $items = $lp_statistic_db->get_course_items_count( $filter['filter_type'], $filter['time'] );
140 - $chart_data = $this->process_chart_data( $filter, $published_course );
141 - $chart_data['line_label'] = __( 'Published Courses', 'learnpress' );
142 - $data = array(
143 - 'courses' => $courses,
144 - 'items' => $items,
145 - 'chart_data' => $chart_data,
146 - );
147 - $response->data = $data;
148 - $response->status = 'success';
149 - } catch ( Throwable $e ) {
150 - $response->message = $e->getMessage();
151 - $response->status = 'error';
152 - }
153 -
154 - return $response;
155 - }
156 -
157 - /**
158 - * @param $request
159 - * @return LP_REST_Response
160 - */
161 - public function get_users_statistics( $request ): LP_REST_Response {
162 - $response = new LP_REST_Response();
163 - try {
164 - $params = $request->get_params();
165 - $params = LP_Helper::sanitize_params_submitted( $params );
166 - $filter = $this->get_statistics_filter( $params );
167 - $lp_statistic_db = LP_Statistics_DB::getInstance();
168 - $user_registers = $lp_statistic_db->get_user_registered_data( $filter['filter_type'], $filter['time'] );
169 - $user_course_statused = $lp_statistic_db->get_users_by_user_item_graduation_statuses( $filter['filter_type'], $filter['time'] );
170 - $user_not_start_course = $lp_statistic_db->get_users_not_started_any_course( $filter['filter_type'], $filter['time'] );
171 - $top_enrolled_courses = $lp_statistic_db->get_top_enrolled_courses( $filter['filter_type'], $filter['time'] );
172 - $total_instructors = $lp_statistic_db->get_total_instructor_created( $filter['filter_type'], $filter['time'] );
173 - $total_students = $lp_statistic_db->get_total_student_created( $filter['filter_type'], $filter['time'] );
174 - $chart_data = $this->process_chart_data( $filter, $user_registers );
175 - $top_enrolled_instructor = array();
176 - if ( ! empty( $top_enrolled_courses ) ) {
177 - foreach ( $top_enrolled_courses as $key => $course ) {
178 - if ( ! array_key_exists( $course->instructor_id, $top_enrolled_instructor ) ) {
179 - $top_enrolled_instructor[ $course->instructor_id ] = array(
180 - 'name' => $course->instructor_name,
181 - 'students' => (int) $course->enrolled_user,
182 - );
183 - } else {
184 - $top_enrolled_instructor[ $course->instructor_id ]['students'] += (int) $course->enrolled_user;
185 - }
186 - }
187 - }
188 - $chart_data['line_label'] = __( 'User registed', 'learnpress' );
189 - $data = array(
190 - 'chart_data' => $chart_data,
191 - 'user_course_statused' => $user_course_statused,
192 - 'user_not_start_course' => $user_not_start_course,
193 - 'top_enrolled_courses' => $top_enrolled_courses,
194 - 'top_enrolled_instructor' => $top_enrolled_instructor,
195 - 'total_instructors' => $total_instructors,
196 - 'total_students' => $total_students,
197 - );
198 - $response->data = $data;
199 - $response->status = 'success';
200 - } catch ( Throwable $e ) {
201 - $response->message = $e->getMessage();
202 - $response->status = 'error';
203 - }
204 -
205 - return $response;
206 - }
207 - /**
208 - * Process data use for chart js
209 - *
210 - * @param array $filter The filter in get_statistics_filter
211 - * @param array $input_data The input data ( data from DB )
212 - *
213 - * @return array $chart_data Data use for chart js
214 - */
215 - public function process_chart_data( array $filter, array $input_data ) {
216 - $chart_data = array();
217 - $data = array();
218 - if ( $filter['filter_type'] == 'date' ) {
219 - $data = $this->process_date_data( $input_data );
220 - $chart_data['x_label'] = __( 'Hour', 'learnpress' );
221 - } elseif ( $filter['filter_type'] == 'previous_days' ) {
222 - $data = $this->process_previous_days_data( $filter['time'], $input_data );
223 - $chart_data['x_label'] = __( 'Dates', 'learnpress' );
224 - } elseif ( $filter['filter_type'] == 'month' ) {
225 - $data = $this->process_month_data( $filter, $input_data );
226 - $chart_data['x_label'] = __( 'Dates', 'learnpress' );
227 - } elseif ( $filter['filter_type'] == 'previous_months' ) {
228 - $data = $this->process_previous_months_data( $filter['time'], $input_data );
229 - $chart_data['x_label'] = __( 'Months', 'learnpress' );
230 - } elseif ( $filter['filter_type'] == 'year' ) {
231 - $data = $this->process_year_data( $input_data );
232 - $chart_data['x_label'] = __( 'Months', 'learnpress' );
233 - } elseif ( $filter['filter_type'] == 'custom' ) {
234 - $dates = $filter['time'];
235 - $dates = explode( '+', $dates );
236 - sort( $dates );
237 - $diff = date_diff( date_create( $dates[0] ), date_create( $dates[1] ), true );
238 - $y = $diff->y;
239 - $m = $diff->m;
240 - $d = $diff->d;
241 - if ( $y < 1 ) {
242 - if ( $m <= 1 ) {
243 - if ( $d < 1 ) {
244 - $data = $this->process_date_data( $input_data );
245 - $chart_data['x_label'] = __( 'Hour', 'learnpress' );
246 - } else {
247 - $data = $this->process_previous_days_data( $d, $input_data, $dates[1] );
248 - $chart_data['x_label'] = __( 'Dates', 'learnpress' );
249 - }
250 - } else {
251 - $data = $this->process_previous_months_data( $m, $input_data, $dates[1] );
252 - $chart_data['x_label'] = __( 'Months', 'learnpress' );
253 - // $filter = $this->chart_filter_previous_months_group_by( $filter );
254 - }
255 - } elseif ( $y < 2 ) {
256 - $months = $y * 12 + $m;
257 - $data = $this->process_previous_months_data( $months, $input_data, $dates[1] );
258 - $chart_data['x_label'] = __( 'Months', 'learnpress' );
259 - } elseif ( $y < 5 ) {
260 - // TODO
261 - $data = $this->process_quarters_data( $dates, $input_data );
262 - $chart_data['x_label'] = __( 'Quarters', 'learnpress' );
263 - } else {
264 - $data = $this->process_years_data( $y, $input_data, $dates[1] );
265 - $chart_data['x_label'] = __( 'Years', 'learnpress' );
266 - }
267 - }
268 - foreach ( $data as $row ) {
269 - $chart_data['labels'][] = $row->x_data_label;
270 - $chart_data['data'][] = (float) $row->x_data;
271 - }
272 - // $chart_data['line_label'] = __( 'Completed orders', 'learnpress' );
273 -
274 - return $chart_data;
275 - }
276 - /**
277 - * Gets the statistics filter.
278 - *
279 - * @param http request $params The parameters
280 - *
281 - * @return array The statistics filter. use for process data
282 - */
283 - 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;
321 - }
322 - return $filter;
323 - }
324 - /**
325 - * process data of a date ( in 24h )
326 - *
327 - * @param array $input_data The input data
328 - *
329 - * @return array ( description_of_the_return_value )
330 - */
331 - public function process_date_data( array $input_data ) {
332 - $data = array();
333 - for ( $i = 0; $i < 24;$i++ ) {
334 - $row = new stdClass();
335 - $row->x_data_label = $i;
336 - $row->x_data = 0;
337 - $data[ $i ] = $row;
338 - }
339 - if ( ! empty( $input_data ) ) {
340 - foreach ( $input_data as $row ) {
341 - $data[ $row->x_data_label ] = $row;
342 - }
343 - }
344 - return $data;
345 - }
346 - /**
347 - * process data of days since the last date, if dont have last date, last date is current date
348 - *
349 - * @param int $days The days
350 - * @param array $input_data The input data
351 - * @param bool $last_date The last date
352 - *
353 - * @return array ( description_of_the_return_value )
354 - */
355 - public function process_previous_days_data( int $days, array $input_data, $last_date = false ) {
356 - $data = array();
357 - for ( $i = $days; $i >= 0; $i-- ) {
358 - $date = date( 'Y-m-d', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'days' ) );
359 - $row = new stdClass();
360 - $row->x_data_label = $date;
361 - $row->x_data = 0;
362 - $data[ $date ] = $row;
363 - }
364 - if ( ! empty( $input_data ) ) {
365 - foreach ( $input_data as $row ) {
366 - $data[ $row->x_data_label ] = $row;
367 - }
368 - }
369 - return $data;
370 - }
371 - /**
372 - * process data of a month
373 - *
374 - * @param array $filter The filter
375 - * @param array $input_data The input data
376 - *
377 - * @return array ( description_of_the_return_value )
378 - */
379 - public function process_month_data( array $filter, array $input_data ) {
380 - $data = array();
381 - $max_day = cal_days_in_month( 0, date( 'm', strtotime( $filter['time'] ) ), date( 'Y', strtotime( $filter['time'] ) ) );
382 - for ( $i = 1; $i <= $max_day; $i++ ) {
383 - $row = new stdClass();
384 - $row->x_data_label = $i;
385 - $row->x_data = 0;
386 - $data[ $i ] = $row;
387 - }
388 - if ( ! empty( $input_data ) ) {
389 - foreach ( $input_data as $row ) {
390 - $data[ $row->x_data_label ] = $row;
391 - }
392 - }
393 - return $data;
394 - }
395 - /**
396 - * process data of months since the last date, if dont have last date, last date is current date
397 - *
398 - * @param int $months The months
399 - * @param array $input_data The input data
400 - * @param bool $last_date The last date
401 - *
402 - * @return array ( description_of_the_return_value )
403 - */
404 - public function process_previous_months_data( int $months, array $input_data, $last_date = false ) {
405 - $data = array();
406 - for ( $i = $months; $i >= 0; $i-- ) {
407 - $date = date( 'm-Y', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'months' ) );
408 - $row = new stdClass();
409 - $row->x_data_label = $date;
410 - $row->x_data = 0;
411 - $data[ $date ] = $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 - *
422 - * @param array $dates The dates
423 - * @param array $input_data The input data
424 - *
425 - * @return array process data for date range 2-5 years
426 - */
427 - public function process_quarters_data( array $dates, array $input_data ) {
428 - $data = array();
429 - $start_time = strtotime( $dates[0] );
430 - $end_time = strtotime( $dates[1] );
431 - for ( $i = date( 'Y', $start_time ); $i <= date( 'Y', $end_time ); $i++ ) {
432 - if ( $i == date( 'Y', $start_time ) ) {
433 - $quarter = ceil( date( 'm', $start_time ) / 3 );
434 - for ( $j = $quarter;$j <= 4;$j++ ) {
435 - $row = new stdClass();
436 - $row->x_data_label = 'q' . $j . '-' . $i;
437 - $row->x_data = 0;
438 - $data[] = $row;
439 - }
440 - } elseif ( $i == date( 'Y', $start_time ) ) {
441 - $quarter = ceil( date( 'm', $end_time ) / 3 );
442 - for ( $j = 1;$j <= $quarter;$j++ ) {
443 - $row = new stdClass();
444 - $row->x_data_label = 'q' . $j . '-' . $i;
445 - $row->x_data = 0;
446 - $data[] = $row;
447 - }
448 - } else {
449 - for ( $j = 1; $j <= 4;$j++ ) {
450 - $row = new stdClass();
451 - $row->x_data_label = 'q' . $j . '-' . $i;
452 - $row->x_data = 0;
453 - $data[] = $row;
454 - }
455 - }
456 - }
457 - if ( ! empty( $input_data ) ) {
458 - foreach ( $input_data as $row ) {
459 - $data[ $row->x_data_label ] = $row;
460 - }
461 - }
462 - return $data;
463 - }
464 - /**
465 - * process data of a year
466 - *
467 - * @param array $input_data data from DB
468 - *
469 - * @return array chart data
470 - */
471 - public function process_year_data( array $input_data ) {
472 - $data = array();
473 - for ( $i = 1; $i <= 12; $i++ ) {
474 - $row = new stdClass();
475 - $row->x_data_label = $i;
476 - $row->x_data = 0;
477 - $data[ $i ] = $row;
478 - }
479 - if ( ! empty( $input_data ) ) {
480 - foreach ( $input_data as $row ) {
481 - $data[ $row->x_data_label ] = $row;
482 - }
483 - }
484 - return $data;
485 - }
486 -
487 - /**
488 - * process data of years( when date range > 5 years )
489 - *
490 - * @param int $years The years
491 - * @param array $input_data The input data
492 - * @param bool $last_date The last date
493 - *
494 - * @return array ( description_of_the_return_value )
495 - */
496 - public function process_years_data( int $years, array $input_data, $last_date = false ) {
497 - $data = array();
498 - for ( $i = $years; $i >= 0; $i-- ) {
499 - $year = date( 'Y', strtotime( ( $last_date ? $last_date : '' ) . -$i . 'years' ) );
500 - $row = new stdClass();
501 - $row->x_data_label = $year;
502 - $row->x_data = 0;
503 - $data[ $year ] = $row;
504 - }
505 - if ( ! empty( $input_data ) ) {
506 - foreach ( $input_data as $row ) {
507 - $data[ $row->x_data_label ] = $row;
508 - }
509 - }
510 - return $data;
511 - }
512 -
513 - public function permission_check( $request ) {
514 - return apply_filters( 'learnpress/admin-statistics/permission', current_user_can( 'administrator' ) );
515 - }
516 -}
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 +}