PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.3
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.3
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 4.2.1 All 138 releases
learnpress / inc / Databases / class-lp-statistics-db.php

class-lp-statistics-db.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.3, at inc/Databases/class-lp-statistics-db.php

1,191 lines 48.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class LP_Statistics_DB
4 *
5 * @author thimpress
6 * @since 4.2.6
7 */
8
9 use LearnPress\Statistics\StatisticsScope;
10
11 defined( 'ABSPATH' ) || exit();
12
13 class LP_Statistics_DB extends LP_Database {
14 private static $_instance;
15
16 protected function __construct() {
17 parent::__construct();
18 }
19
20 public static function getInstance() {
21 if ( is_null( self::$_instance ) ) {
22 self::$_instance = new self();
23 }
24
25 return self::$_instance;
26 }
27
28 /**
29 * Apply a statistics-scoped filter to every LP_Filter query this class runs,
30 * then defer to the core query builder.
31 *
32 * Single choke point for all statistics queries: the DTO ( collection /
33 * only_fields / where / join / group_by / order_by ) is exposed for shaping,
34 * and no raw SQL string is passed, so a handler customizes a specific query
35 * by inspecting $filter ( e.g. $filter->collection, $filter->only_fields )
36 * and returning a mutated LP_Filter. Note the generic lp/query/* filters
37 * still fire downstream in the query builder as before.
38 *
39 * @param LP_Filter $filter Query DTO.
40 * @param int $total_rows Total rows, by reference.
41 * @return mixed
42 * @since 4.4.2
43 */
44 public function execute( $filter, int &$total_rows = 0 ) {
45 /**
46 * Filter a statistics query DTO before execution.
47 *
48 * @param LP_Filter $filter The query DTO.
49 * @param LP_Statistics_DB $db This DB instance.
50 * @since 4.4.2
51 */
52 $filter = apply_filters( 'learn-press/statistics/query/filter', $filter, $this );
53
54 return parent::execute( $filter, $total_rows );
55 }
56
57 /**
58 * filter to get data for chart of a day.
59 *
60 * @param LP_Filter $filter
61 * @param string $time_field the column use to filter time
62 * @return LP_Filter
63 */
64 public function chart_filter_date_group_by( LP_Filter $filter, string $time_field ) {
65 $filter->only_fields[] = "HOUR($time_field) as x_data_label";
66 $filter->group_by = 'x_data_label';
67 return $filter;
68 }
69 /**
70 * filter to get data for chart of last some days. ex: last 7 days, last 30 days,...
71 *
72 * @param LP_Filter $filter
73 * @param string $time_field the column use to filter time
74 * @return LP_Filter
75 */
76 public function chart_filter_previous_days_group_by( LP_Filter $filter, string $time_field ) {
77 $filter->only_fields[] = "CAST($time_field AS DATE) as x_data_label";
78 $filter->group_by = 'x_data_label';
79 return $filter;
80 }
81 /**
82 * filter to get data for chart of a month
83 *
84 * @param LP_Filter $filter
85 * @param string $time_field the column use to filter time
86 * @return LP_Filter
87 */
88 public function chart_filter_month_group_by( LP_Filter $filter, string $time_field ) {
89 $filter->only_fields[] = "DAY($time_field) as x_data_label";
90 $filter->group_by = 'x_data_label';
91 return $filter;
92 }
93 /**
94 * filter to get data for chart of months. ex: last 3 months, 6 months, 9 months,...
95 *
96 * @param LP_Filter $filter
97 * @param string $time_field the column use to filter time
98 * @return LP_Filter
99 */
100 public function chart_filter_previous_months_group_by( LP_Filter $filter, string $time_field ) {
101 $filter->only_fields[] = "DATE_FORMAT( $time_field , '%m-%Y') as x_data_label";
102 $filter->group_by = 'x_data_label';
103 return $filter;
104 }
105 /**
106 * filter to get data for chart of a year
107 *
108 * @param LP_Filter $filter
109 * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table
110 * @return LP_Filter
111 */
112 public function chart_filter_year_group_by( LP_Filter $filter, string $time_field ) {
113 $filter->only_fields[] = "MONTH($time_field) as x_data_label";
114 $filter->group_by = 'x_data_label';
115 return $filter;
116 }
117 /**
118 * filter to get data for chart of a custom date ranges
119 *
120 * @param LP_Filter $filter
121 * @param array $dates array of date range use to filer
122 * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table
123 * @return LP_Filter
124 */
125 public function chart_filter_custom_group_by( LP_Filter $filter, array $dates, string $time_field ) {
126 $diff1 = date_create( $dates[0] );
127 $diff2 = date_create( $dates[1] );
128 if ( ! $diff1 || ! $diff2 ) {
129 throw new Exception( 'Custom filter date is invalid.', 'learnpress' );
130 }
131 $diff = date_diff( $diff1, $diff2, true );
132 $y = $diff->y;
133 $m = $diff->m;
134 $d = $diff->d;
135 if ( $y < 1 ) {
136 if ( $m <= 1 ) {
137 if ( $d < 1 ) {
138 $filter = $this->chart_filter_date_group_by( $filter, $time_field );
139 } else {
140 // more thans 2 days return data of days
141 $filter = $this->chart_filter_previous_days_group_by( $filter, $time_field );
142 }
143 } else {
144 // more thans 2 months return data of months
145 $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );
146 }
147 } elseif ( $y < 2 ) {
148 // less thans 2 years return data of year months
149 $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );
150 } elseif ( $y < 5 ) {
151 // from 2-5years return data of year quarters
152 $filter->only_fields[] = $this->wpdb->prepare( "CONCAT( %s, QUARTER($time_field) ,%s, Year($time_field)) as x_data_label", array( 'q', '-' ) );
153 $filter->group_by = 'x_data_label';
154 } else {
155 // more than 5 years, return data of years
156 $filter->only_fields[] = "YEAR($time_field) as x_data_label";
157 $filter->group_by = 'x_data_label';
158 }
159 return $filter;
160 }
161 /**
162 * @param LP_Filter $filter
163 * @param string $date choose a date to query, format Y-m-d
164 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
165 * @return LP_Filter
166 */
167 public function date_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {
168 if ( $is_until ) {
169 $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date );
170 } else {
171 $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)= cast(%s as DATE)", $date );
172 }
173 return $filter;
174 }
175 /**
176 * @param LP_Filter $filter
177 * @param int $value ex: 7 - last 7 days, 10 - last 10 days, ...
178 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
179 * @return LP_Filter
180 */
181 public function previous_days_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) {
182 if ( $value < 2 ) {
183 throw new Exception( 'Day must be greater than 2 days.', 'learnpress' );
184 }
185 if ( $is_until ) {
186 $filter->where[] = "AND $time_field <= CURDATE()";
187 } else {
188 $filter->where[] = $this->wpdb->prepare( "AND $time_field >= DATE_ADD(CURDATE(), INTERVAL -%d DAY)", $value );
189 }
190
191 return $filter;
192 }
193 /**
194 * @param LP_Filter $filter
195 * @param string $date choose a date to query, format Y-m-d
196 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
197 * @return LP_Filter
198 */
199 public function month_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {
200 if ( $is_until ) {
201 $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date );
202 } else {
203 $filter->where[] = $this->wpdb->prepare( "AND EXTRACT(YEAR_MONTH FROM $time_field)= EXTRACT(YEAR_MONTH FROM %s)", $date );
204 }
205 return $filter;
206 }
207 /**
208 * @param LP_Filter $filter
209 * @param int $value ex: 3 - last 3 months, 10 - last 10 months, ...
210 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
211 * @return LP_Filter
212 */
213 public function previous_months_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) {
214 if ( $value < 2 ) {
215 throw new Exception( 'Values must be greater than 2 months.', 'learnpress' );
216 }
217 if ( $is_until ) {
218 $filter->where[] = "AND $time_field <= CURDATE()";
219 } else {
220 $filter->where[] = $this->wpdb->prepare( "AND EXTRACT(YEAR_MONTH FROM $time_field) >= EXTRACT(YEAR_MONTH FROM DATE_ADD(CURDATE(), INTERVAL -%d MONTH))", $value );
221 }
222 return $filter;
223 }
224 /**
225 * get data for each month in year
226 *
227 * @param LP_Filter $filter
228 * @param string $date choose a date to query, format Y-m-d
229 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
230 * @return LP_Filter
231 */
232 public function year_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {
233 if ( $is_until ) {
234 $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE) <= cast(%s as DATE)", $date );
235 } else {
236 $filter->where[] = $this->wpdb->prepare( "AND YEAR($time_field)= YEAR(%s)", $date );
237 }
238 return $filter;
239 }
240 /**
241 * custom query with data range
242 *
243 * @param LP_Filter $filter
244 * @param array $dates date ranges, array of 2 dates.
245 * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on
246 * @return LP_Filter
247 */
248 public function custom_time_filter( LP_Filter $filter, array $dates, string $time_field, $is_until = false ) {
249 if ( empty( $dates ) ) {
250 throw new Exception( 'Select date', 'learnpress' );
251 }
252 sort( $dates );
253 if ( $is_until ) {
254 $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE) <= cast(%s as DATE)", $dates[1] );
255 } else {
256 $filter->where[] = $this->wpdb->prepare(
257 "AND (DATE($time_field) BETWEEN %s AND %s)",
258 date( 'Y-m-d', strtotime( $dates[0] ) ),
259 date( 'Y-m-d', strtotime( $dates[1] ) )
260 );
261 }
262
263 return $filter;
264 }
265
266 /**
267 * choose filter type foreach filter time
268 *
269 * @param LP_Filter $filter
270 * @param string $type date|month|year|previous_days|custom
271 * @param string $time_field datetime colummn
272 * @param boolean $value value to query datetimes
273 * @param boolean $is_until filter time by the last date
274 * @return LP_Filter
275 */
276 public function filter_time( LP_Filter $filter, string $type, string $time_field, $value = false, $is_until = false ) {
277 if ( ! $value ) {
278 throw new Exception( 'Empty statistic time', 'learnpress' );
279 }
280 switch ( $type ) {
281 case 'date':
282 $filter = $this->date_filter( $filter, $value, $time_field, $is_until );
283 break;
284 case 'month':
285 $filter = $this->month_filter( $filter, $value, $time_field, $is_until );
286 break;
287 case 'year':
288 $filter = $this->year_filter( $filter, $value, $time_field, $is_until );
289 break;
290 case 'previous_days':
291 $filter = $this->previous_days_filter( $filter, (int) $value, $time_field, $is_until );
292 break;
293 case 'previous_months':
294 $filter = $this->previous_months_filter( $filter, (int) $value, $time_field, $is_until );
295 break;
296 case 'custom':
297 $value = explode( '+', $value );
298 if ( count( $value ) !== 2 ) {
299 throw new Exception( 'Invalid custom time', 'learnpress' );
300 }
301 $filter = $this->custom_time_filter( $filter, $value, $time_field, $is_until );
302 default:
303 // code...
304 break;
305 }
306 return $filter;
307 }
308 /**
309 * format return data foreach type of filter
310 *
311 * @param LP_Filter $filter
312 * @param string $type date|month|year|previous_days|custom
313 * @param string $time_field datetime colummn
314 * @param boolean $value value to query datetimes
315 * @param string|null $granularity explicit chart resolution ( hour|day|month ) from PeriodResolver;
316 * only honored for the custom type — named legacy types keep their
317 * historical grouping so existing output never changes. @since 4.4.2
318 * @return LP_Filter
319 */
320 public function chart_filter_group_by( LP_Filter $filter, string $type, string $time_field, $value = false, ?string $granularity = null ) {
321 switch ( $type ) {
322 case 'date':
323 $filter = $this->chart_filter_date_group_by( $filter, $time_field );
324 break;
325 case 'month':
326 $filter = $this->chart_filter_month_group_by( $filter, $time_field );
327 break;
328 case 'year':
329 $filter = $this->chart_filter_year_group_by( $filter, $time_field );
330 break;
331 case 'previous_days':
332 $filter = $this->chart_filter_previous_days_group_by( $filter, $time_field );
333 break;
334 case 'previous_months':
335 $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );
336 break;
337 case 'custom':
338 if ( $granularity ) {
339 $filter = $this->chart_filter_granularity_group_by( $filter, $granularity, $time_field );
340 break;
341 }
342 if ( empty( $value ) ) {
343 throw new Exception( 'Empty statistic time', 'learnpress' );
344 }
345 $value = explode( '+', $value );
346 if ( count( $value ) !== 2 ) {
347 throw new Exception( 'Invalid custom time', 'learnpress' );
348 }
349 $filter = $this->chart_filter_custom_group_by( $filter, $value, $time_field );
350 default:
351 // code...
352 break;
353 }
354 return $filter;
355 }
356
357 /**
358 * Group a custom-range chart by an explicit granularity instead of the
359 * span heuristics of chart_filter_custom_group_by().
360 *
361 * hour → HOUR( field ), day → CAST( field AS DATE ), month → 'mm-YYYY' —
362 * the same label shapes the date/previous_days/previous_months groupings
363 * produce, so the controller processors handle them unchanged.
364 *
365 * @param LP_Filter $filter
366 * @param string $granularity hour|day|month
367 * @param string $time_field datetime column
368 * @return LP_Filter
369 * @since 4.4.2
370 */
371 public function chart_filter_granularity_group_by( LP_Filter $filter, string $granularity, string $time_field ) {
372 switch ( $granularity ) {
373 case 'hour':
374 return $this->chart_filter_date_group_by( $filter, $time_field );
375 case 'month':
376 return $this->chart_filter_previous_months_group_by( $filter, $time_field );
377 case 'day':
378 default:
379 return $this->chart_filter_previous_days_group_by( $filter, $time_field );
380 }
381 }
382
383 /**
384 * get_completed_order_data use this for complete order report chart
385 *
386 * @param string $type time type filter: date|month|year|previous_days|custom
387 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
388 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
389 * @param string|null $granularity explicit chart resolution ( hour|day|month ), custom type only. @since 4.4.2
390 * @return array completed order data
391 */
392 public function get_completed_order_data( string $type, string $value, ?StatisticsScope $scope = null, ?string $granularity = null ) {
393 if ( ! $type || ! $value ) {
394 return array();
395 }
396 $filter = new LP_Order_Filter();
397 $filter->collection = $this->tb_posts;
398 $filter->collection_alias = 'p';
399 $time_field = 'p.post_date';
400
401 // count completed orders
402 $filter->only_fields[] = 'count( p.ID) as x_data';
403 $filter = $this->filter_time( $filter, $type, $time_field, $value );
404 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value, $granularity );
405 $filter->where[] = $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB );
406 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );
407 $filter->limit = -1;
408 $filter->order_by = $time_field;
409 $filter->order = 'asc';
410
411 if ( $scope && ! $scope->is_empty() ) {
412 $filter = $scope->apply_to_orders( $filter, 'p.ID' );
413 }
414
415 $filter->run_query_count = false;
416 $result = $this->execute( $filter );
417
418 return $result;
419 }
420
421 /**
422 * query to count LP Orders with all statuses
423 *
424 * @param LP_Order_Filter $filter
425 * @return LP_Order_Filter
426 */
427 public function filter_order_count_statics( LP_Order_Filter $filter ) {
428 // $filter->query_count = true;
429 $filter->only_fields[] = 'count( p.ID) as count_order';
430 $filter->only_fields[] = "REPLACE(p.post_status,'lp-','') as order_status";
431 $filter->group_by = 'p.post_status';
432 $filter->where[] = $this->wpdb->prepare( "AND p.post_status LIKE CONCAT(%s,'%')", 'lp-' );
433 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );
434 $filter->run_query_count = false;
435
436 return $filter;
437 }
438 /**
439 * get LP Order count of a filter time
440 *
441 * @param string $type date|month|year|previous_days|custom
442 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
443 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
444 * @return array result of LP Order count foreach status
445 */
446 public function get_order_statics( string $type, string $value, ?StatisticsScope $scope = null ) {
447 if ( ! $type || ! $value ) {
448 return;
449 }
450 $filter = new LP_Order_Filter();
451 $filter->collection = $this->tb_posts;
452 $filter->collection_alias = 'p';
453 $time_field = 'p.post_date';
454 $filter = $this->filter_time( $filter, $type, $time_field, $value );
455 $filter = $this->filter_order_count_statics( $filter );
456 if ( $scope && ! $scope->is_empty() ) {
457 $filter = $scope->apply_to_orders( $filter, 'p.ID' );
458 }
459 $filter->limit = -1;
460 $result = $this->execute( $filter );
461
462 return $result;
463 }
464 /*Overviews statistics*/
465 /**
466 * get sales amount of complete order
467 *
468 * @param string $type [time type filter: date|month|year|previous_days|custom]
469 * @param string $value [time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom ]
470 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
471 * @param string|null $granularity explicit chart resolution ( hour|day|month ), custom type only. @since 4.4.2
472 * @return array completed order data
473 */
474 public function get_net_sales_data( string $type, string $value, ?StatisticsScope $scope = null, ?string $granularity = null ) {
475 if ( ! $type || ! $value ) {
476 return array();
477 }
478 $filter = new LP_Order_Filter();
479 $filter->collection = $this->tb_posts;
480 $filter->collection_alias = 'p';
481 $oi_table = $this->tb_lp_order_items;
482 $oim_table = $this->tb_lp_order_itemmeta;
483 // net sales summary
484 $filter->only_fields[] = 'SUM(CAST(oim.meta_value AS DECIMAL(10,2))) as x_data';
485 $time_field = 'p.post_date';
486 $filter->join = array(
487 "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",
488 "INNER JOIN $oim_table AS oim ON oi.order_item_id = oim.learnpress_order_item_id",
489 );
490 $filter->limit = -1;
491 $filter->where = array(
492 $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),
493 $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),
494 $this->wpdb->prepare( 'AND oim.meta_key=%s', '_total' ),
495 );
496 $filter = $this->filter_time( $filter, $type, $time_field, $value );
497 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value, $granularity );
498 if ( $scope && ! $scope->is_empty() ) {
499 $filter = $scope->apply( $filter, 'oi.item_id' );
500 }
501 $filter->order_by = $time_field;
502 $filter->order = 'asc';
503 $filter->run_query_count = false;
504
505 $result = $this->execute( $filter );
506 // error_log( $this->check_execute_has_error() );
507 return $result;
508 }
509
510 /**
511 * get top categories of sold course
512 *
513 * @param string $type date|month|year|previous_days|custom
514 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
515 * @param integer $limit limit of query, default is 10
516 * @param boolean $exclude_free_course exclude free course
517 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
518 * @return array return term_id and term_count
519 */
520 public function get_top_sold_categories( string $type, string $value, $limit = 0, $exclude_free_course = false, ?StatisticsScope $scope = null ) {
521 if ( ! $type || ! $value ) {
522 return;
523 }
524 $filter = new LP_Order_Filter();
525 $filter->collection = $this->tb_posts;
526 $filter->collection_alias = 'p';
527 $filter->only_fields[] = 'r_term.term_taxonomy_id as term_id';
528 $filter->only_fields[] = 'SUM(CAST(oim_qty.meta_value AS UNSIGNED)) as term_count';
529 $filter->only_fields[] = 'terms.name as term_name';
530 $filter->limit = $limit > 0 ? $limit : 10;
531 $time_field = 'p.post_date';
532 $tb_term_relationships = $this->tb_term_relationships;
533 $tb_term_taxonomy = $this->tb_term_taxonomy;
534 $tb_terms = $this->tb_terms;
535 $oi_table = $this->tb_lp_order_items;
536 $oim_table = $this->tb_lp_order_itemmeta;
537
538 $filter->join = array(
539 "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",
540 "INNER JOIN $tb_term_relationships AS r_term ON oi.item_id = r_term.object_id",
541 "INNER JOIN $tb_term_taxonomy AS tax_term ON tax_term.term_taxonomy_id = r_term.term_taxonomy_id",
542 "INNER JOIN $tb_terms AS terms ON terms.term_id = r_term.term_taxonomy_id",
543 "INNER JOIN $oim_table AS oim_qty ON oi.order_item_id = oim_qty.learnpress_order_item_id AND oim_qty.meta_key = '_quantity'",
544 );
545 if ( $exclude_free_course ) {
546 $filter->join[] = "INNER JOIN $oim_table AS oim_total ON oi.order_item_id = oim_total.learnpress_order_item_id AND oim_total.meta_key = '_total' AND CAST(oim_total.meta_value AS DECIMAL(10,2)) > 0";
547 }
548
549 $filter->where = array(
550 $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),
551 $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),
552 $this->wpdb->prepare( 'AND oi.item_type=%s', LP_COURSE_CPT ),
553 $this->wpdb->prepare( 'AND tax_term.taxonomy=%s', LP_COURSE_CATEGORY_TAX ),
554 );
555 $filter = $this->filter_time( $filter, $type, $time_field, $value );
556 if ( $scope && ! $scope->is_empty() ) {
557 $filter = $scope->apply( $filter, 'oi.item_id' );
558 }
559 $filter->group_by = 'term_id';
560 $filter->order_by = 'term_count';
561 $filter->order = 'DESC';
562 $filter->run_query_count = false;
563 $result = $this->execute( $filter );
564
565 return $result;
566 }
567
568 /**
569 * get top courses was sold in the filter
570 *
571 * @param string $type date|month|year|previous_days|custom
572 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
573 * @param integer $limit limit of query, default 10
574 * @param boolean $exclude_free_course exclude free course, get result only purchase course
575 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
576 * @return array $result
577 */
578 public function get_top_sold_courses( string $type, string $value, $limit = 0, $exclude_free_course = false, ?StatisticsScope $scope = null ) {
579 if ( ! $type || ! $value ) {
580 return;
581 }
582 $filter = new LP_Order_Filter();
583 $tb_posts = $this->tb_posts;
584 $filter->collection = $tb_posts;
585 $filter->collection_alias = 'p';
586 $filter->only_fields[] = 'oi.item_id as course_id';
587 $filter->only_fields[] = 'SUM(CAST(oim_qty.meta_value AS UNSIGNED)) as course_count';
588 $filter->only_fields[] = 'p2.post_title as course_name';
589 $filter->limit = $limit > 0 ? $limit : 10;
590 $time_field = 'p.post_date';
591 $oi_table = $this->tb_lp_order_items;
592 $oim_table = $this->tb_lp_order_itemmeta;
593
594 $filter->join = array(
595 "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",
596 "INNER JOIN $tb_posts AS p2 ON p2.ID = oi.item_id",
597 "INNER JOIN $oim_table AS oim_qty ON oi.order_item_id = oim_qty.learnpress_order_item_id AND oim_qty.meta_key = '_quantity'",
598 );
599
600 if ( $exclude_free_course ) {
601 $filter->join[] = "INNER JOIN $oim_table AS oim_total ON oi.order_item_id = oim_total.learnpress_order_item_id AND oim_total.meta_key = '_total' AND CAST(oim_total.meta_value AS DECIMAL(10,2)) > 0";
602 }
603
604 $filter->where = array(
605 $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),
606 $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),
607 $this->wpdb->prepare( 'AND oi.item_type=%s', LP_COURSE_CPT ),
608 );
609
610 $filter = $this->filter_time( $filter, $type, $time_field, $value );
611 if ( $scope && ! $scope->is_empty() ) {
612 $filter = $scope->apply( $filter, 'oi.item_id' );
613 }
614 $filter->group_by = 'course_id';
615 $filter->order_by = 'course_count';
616 $filter->order = 'DESC';
617 $filter->run_query_count = false;
618 $result = $this->execute( $filter );
619
620 return $result;
621 }
622 /**
623 * Overviews get total courses was created ( all statuses )
624 *
625 * @param string $type date|month|year|previous_days|custom
626 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string
627 * @return int $result course count
628 */
629 public function get_total_course_created( string $type, string $value, ?StatisticsScope $scope = null ) {
630 if ( ! $type || ! $value ) {
631 return;
632 }
633 $filter = new LP_Course_Filter();
634 $filter->collection = $this->tb_posts;
635 $filter->collection_alias = 'p';
636 $filter->only_fields[] = 'p.ID';
637 $time_field = 'p.post_date';
638
639 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', LP_COURSE_CPT );
640 $filter = $this->filter_time( $filter, $type, $time_field, $value );
641 if ( $scope && ! $scope->is_empty() ) {
642 $filter = $scope->apply( $filter, 'p.ID' );
643 }
644 $filter->query_count = true;
645 $result = $this->execute( $filter );
646 return $result;
647 }
648 /**
649 * Overviews get total orders was created ( all statuses )
650 *
651 * @param string $type date|month|year|previous_days|custom
652 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string
653 * @return int $result order count
654 */
655 public function get_total_order_created( string $type, string $value, ?StatisticsScope $scope = null ) {
656 if ( ! $type || ! $value ) {
657 return;
658 }
659 $filter = new LP_Course_Filter();
660 $filter->collection = $this->tb_posts;
661 $filter->collection_alias = 'p';
662 $filter->only_fields[] = 'p.ID';
663 $time_field = 'p.post_date';
664
665 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type = %s', LP_ORDER_CPT );
666 $filter->where[] = $this->wpdb->prepare( 'AND p.post_status != %s', 'auto-draft' );
667 $filter = $this->filter_time( $filter, $type, $time_field, $value );
668 if ( $scope && ! $scope->is_empty() ) {
669 $filter = $scope->apply_to_orders( $filter, 'p.ID' );
670 }
671 $filter->query_count = true;
672 $result = $this->execute( $filter );
673 return $result;
674 }
675 /**
676 * Overviews get total instructors was created ( administrator and lp_teacher )
677 *
678 * @param string $type date|month|year|previous_days|custom
679 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string
680 * @return int $result user count
681 */
682 public function get_total_instructor_created( string $type, string $value ) {
683 if ( ! $type || ! $value ) {
684 return;
685 }
686 $filter = new LP_Filter();
687 $filter->collection = $this->wpdb->users;
688 $filter->collection_alias = 'u';
689 $filter->only_fields[] = 'u.ID';
690 $usermeta_table = $this->wpdb->usermeta;
691 $filter->join[] = "INNER JOIN $usermeta_table AS um ON um.user_id = u.ID";
692 $time_field = 'u.user_registered';
693 $filter->where[] = $this->wpdb->prepare( 'AND um.meta_key=%s', $this->wpdb->prefix . 'capabilities' );
694 $filter->where[] = $this->wpdb->prepare( 'AND um.meta_value LIKE %s', '%' . ADMIN_ROLE . '%' );
695 $filter->where[] = $this->wpdb->prepare( 'OR um.meta_value LIKE %s', '%' . LP_TEACHER_ROLE . '%' );
696 $filter = $this->filter_time( $filter, $type, $time_field, $value, true );
697 $filter->query_count = true;
698 $result = $this->execute( $filter );
699 return $result;
700 }
701 /**
702 * Overviews get total student was created ( subscriber )
703 *
704 * @param string $type date|month|year|previous_days|custom
705 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string
706 * @return int $result user count
707 */
708 public function get_total_student_created( string $type, string $value ) {
709 if ( ! $type || ! $value ) {
710 return;
711 }
712 $filter = new LP_Filter();
713 $filter->collection = $this->wpdb->users;
714 $filter->collection_alias = 'u';
715 $filter->only_fields[] = 'u.ID';
716 $usermeta_table = $this->wpdb->usermeta;
717 $filter->join[] = "INNER JOIN $usermeta_table AS um ON um.user_id = u.ID";
718 $time_field = 'u.user_registered';
719 $filter->where[] = $this->wpdb->prepare( 'AND um.meta_key=%s', $this->wpdb->prefix . 'capabilities' );
720 $filter->where[] = $this->wpdb->prepare( 'AND um.meta_value LIKE %s', '%subscriber%' );
721 $filter = $this->filter_time( $filter, $type, $time_field, $value, true );
722 $filter->query_count = true;
723 $result = $this->execute( $filter );
724 return $result;
725 }
726 /*Course statistics*/
727 /**
728 * Gets the published course data.
729 *
730 * @param string $type date|month|year|previous_days|custom
731 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
732 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
733 * @param string|null $granularity explicit chart resolution ( hour|day|month ), custom type only. @since 4.4.2
734 *
735 * @return array The published course data.
736 */
737 public function get_published_course_data( string $type, string $value, ?StatisticsScope $scope = null, ?string $granularity = null ) {
738 if ( ! $type || ! $value ) {
739 return array();
740 }
741 $filter = new LP_Course_Filter();
742 $filter->collection = $this->tb_posts;
743 $filter->collection_alias = 'p';
744 $time_field = 'p.post_date';
745 // count published course
746 $filter->only_fields[] = 'count( p.ID) as x_data';
747 $filter = $this->filter_time( $filter, $type, $time_field, $value );
748 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value, $granularity );
749 if ( $scope && ! $scope->is_empty() ) {
750 $filter = $scope->apply( $filter, 'p.ID' );
751 }
752 $filter->where[] = $this->wpdb->prepare( 'AND p.post_status=%s', 'publish' );
753 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );
754 $filter->limit = -1;
755 $filter->order_by = $time_field;
756 $filter->order = 'asc';
757
758 $filter->run_query_count = false;
759 $result = $this->execute( $filter );
760
761 return $result;
762 }
763 /**
764 * Gets the course count by statuses.
765 *
766 * @param string $type date|month|year|previous_days|custom
767 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
768 *
769 * @return array $result The course count by statuses.
770 */
771 public function get_course_count_by_statuses( string $type, string $value, ?StatisticsScope $scope = null ) {
772 if ( ! $type || ! $value ) {
773 return array();
774 }
775 $filter = new LP_Course_Filter();
776 $filter->collection = $this->tb_posts;
777 $filter->collection_alias = 'p';
778 $filter->only_fields[] = 'COUNT(p.ID) as course_count';
779 $filter->only_fields[] = 'p.post_status as course_status';
780 $time_field = 'p.post_date';
781 $filter = $this->filter_time( $filter, $type, $time_field, $value );
782 if ( $scope && ! $scope->is_empty() ) {
783 $filter = $scope->apply( $filter, 'p.ID' );
784 }
785 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );
786 $filter->where[] = $this->wpdb->prepare( 'AND p.post_status IN (%s, %s, %s)', 'publish', 'pending', 'future' );
787 $filter->limit = -1;
788 $filter->group_by = 'p.post_status';
789 $filter->run_query_count = false;
790 $result = $this->execute( $filter );
791 return $result;
792 }
793 /**
794 * Gets the course items count.
795 *
796 * @param string $type date|month|year|previous_days|custom
797 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
798 *
799 * @return int $result The course items count.
800 */
801 public function get_course_items_count( string $type, string $value ) {
802 if ( ! $type || ! $value ) {
803 return;
804 }
805 $filter = new LP_Filter();
806 $filter->collection = $this->tb_posts;
807 $filter->collection_alias = 'p';
808 $filter->only_fields[] = 'COUNT(p.ID) as item_count';
809 $filter->only_fields[] = 'p.post_type as item_type';
810 $time_field = 'p.post_date';
811 $filter = $this->filter_time( $filter, $type, $time_field, $value );
812 if ( class_exists( 'LP_Assignment' ) ) {
813 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type IN (%s, %s, %s)', LP_LESSON_CPT, LP_QUIZ_CPT, LP_ASSIGNMENT_CPT );
814 } else {
815 $filter->where[] = $this->wpdb->prepare( 'AND p.post_type IN (%s, %s)', LP_LESSON_CPT, LP_QUIZ_CPT );
816 }
817 $filter->where[] = $this->wpdb->prepare( 'AND p.post_status IN(%s,%s,%s)', 'publish', 'pending', 'future' );
818 $filter->group_by = 'p.post_type';
819 $filter->limit = -1;
820 $filter->run_query_count = false;
821 $result = $this->execute( $filter );
822 return $result;
823 }
824 /*User Statistics*/
825 /**
826 * Gets the user registered data.
827 *
828 * @param string $type date|month|year|previous_days|custom
829 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
830 * @param string|null $granularity explicit chart resolution ( hour|day|month ), custom type only. @since 4.4.2
831 *
832 * @return array $result The user registered data.
833 */
834 public function get_user_registered_data( string $type, string $value, ?string $granularity = null ) {
835 if ( ! $type || ! $value ) {
836 return array();
837 }
838 $filter = new LP_Filter();
839 $filter->collection = $this->tb_users;
840 $filter->collection_alias = 'u';
841 $time_field = 'u.user_registered';
842 // count user_registered
843 $filter->only_fields[] = 'count( u.ID) as x_data';
844 $filter = $this->filter_time( $filter, $type, $time_field, $value );
845 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value, $granularity );
846 $filter->limit = -1;
847 $filter->order_by = $time_field;
848 $filter->order = 'asc';
849
850 $filter->run_query_count = false;
851 $result = $this->execute( $filter );
852 return $result;
853 }
854
855 /**
856 * Gets the users by user item graduation statuses.
857 *
858 * @param string $type date|month|year|previous_days|custom
859 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
860 * @return int $result count users by graduation statuses.
861 */
862 public function get_users_by_user_item_graduation_statuses( string $type, string $value, ?StatisticsScope $scope = null ) {
863 if ( ! $type || ! $value ) {
864 return;
865 }
866 $filter = new LP_Filter();
867 $filter->collection = $this->tb_lp_user_items;
868 $filter->collection_alias = 'ui';
869 $filter->only_fields[] = 'ui.graduation as graduation_status';
870 $filter->only_fields[] = 'COUNT(distinct(ui.user_id)) as user_count';
871 $time_field = 'ui.start_time';
872 $filter->limit = -1;
873 $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );
874 $filter = $this->filter_time( $filter, $type, $time_field, $value );
875 if ( $scope && ! $scope->is_empty() ) {
876 $filter = $scope->apply( $filter, 'ui.item_id' );
877 }
878 $filter->group_by = 'graduation_status';
879 $filter->run_query_count = false;
880 $result = $this->execute( $filter );
881 return $result;
882 }
883 /**
884 * filter user dont study any course in the filter time
885 *
886 * @param string $type date|month|year|previous_days|custom
887 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
888 * @return int $result count users
889 */
890 public function get_users_not_started_any_course( string $type, string $value ) {
891 if ( ! $type || ! $value ) {
892 return;
893 }
894 $filter = new LP_Filter();
895 $table_useritems = $this->tb_lp_user_items;
896 $table_user = $this->tb_users;
897 $time_filter = $this->filter_time( $filter, $type, 'ui.start_time', $value );
898 // get time_filter condition SQL
899 $time_condition = $time_filter->where[0];
900 // reset where
901 $filter->where = array();
902 $filter->collection = $table_user;
903 $filter->collection_alias = 'u';
904 $filter->only_fields[] = 'u.ID';
905 $filter->where[] = "AND NOT EXISTS (SELECT * FROM $table_useritems as ui WHERE ui.user_id = u.ID $time_condition)";
906 $filter->limit = -1;
907 $filter->query_count = true;
908 // use this to see the sql query
909 // $filter->return_string_query= true;
910 $result = $this->execute( $filter );
911 return $result;
912 }
913 /**
914 * get top courses was enrolled by users
915 *
916 * @param string $type date|month|year|previous_days|custom
917 * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom
918 * @param integer $limit limit of query, default 10
919 * @param boolean $exclude_free_course exclude free course, get result only purchase course
920 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
921 * @return array $result
922 */
923 public function get_top_enrolled_courses( string $type, string $value, $limit = 0, $exclude_free_course = false, ?StatisticsScope $scope = null ) {
924 if ( ! $type || ! $value ) {
925 return;
926 }
927 $filter = new LP_Filter();
928 $filter->collection = $this->tb_lp_user_items;
929 $filter->collection_alias = 'ui';
930 $filter->only_fields[] = 'ui.item_id as course_id';
931 $filter->only_fields[] = 'COUNT(ui.user_item_id) as enrolled_user';
932 $filter->only_fields[] = 'p.post_author as instructor_id';
933 $filter->only_fields[] = 'p.post_title as course_name';
934 $filter->only_fields[] = 'u.display_name as instructor_name';
935 $filter->limit = ! $limit ? 10 : $limit;
936 $time_field = 'ui.start_time';
937 $filter->join[] = "INNER JOIN $this->tb_posts AS p ON p.ID = ui.item_id";
938 $filter->join[] = "INNER JOIN $this->tb_users AS u ON u.ID = p.post_author";
939 $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );
940 $filter = $this->filter_time( $filter, $type, $time_field, $value );
941 if ( $scope && ! $scope->is_empty() ) {
942 $filter = $scope->apply( $filter, 'ui.item_id' );
943 }
944 $filter->group_by = 'course_id';
945 $filter->order_by = 'enrolled_user';
946 $filter->order = 'DESC';
947 $filter->run_query_count = false;
948 $result = $this->execute( $filter );
949 return $result;
950 }
951
952 /**
953 * Get top sold courses by a specific instructor.
954 *
955 * @param int $instructor_id The instructor user ID.
956 * @param int $limit Number of courses to return, default 5.
957 *
958 * @return array Top sold courses for the instructor.
959 * @since 4.3.0
960 */
961 public function get_top_sold_courses_by_instructor( int $instructor_id, int $limit = 5, string $search = '' ): array {
962 $tb_posts = $this->tb_posts;
963 $oi_table = $this->tb_lp_order_items;
964 $oim_table = $this->tb_lp_order_itemmeta;
965
966 $filter = new \LP_Order_Filter();
967 $filter->collection = $tb_posts;
968 $filter->collection_alias = 'p';
969 $filter->only_fields[] = 'oi.item_id as course_id';
970 $filter->only_fields[] = 'SUM(CAST(oim_qty.meta_value AS UNSIGNED)) as course_count';
971 $filter->only_fields[] = 'p2.post_title as course_name';
972 $filter->only_fields[] = 'u.display_name as instructor_name';
973 $filter->only_fields[] = 'SUM(CAST(oim_total.meta_value AS DECIMAL(10,2))) as total_revenue';
974 $filter->limit = $limit > 0 ? $limit : 5;
975
976 $filter->join = array(
977 "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",
978 "INNER JOIN $tb_posts AS p2 ON p2.ID = oi.item_id",
979 "INNER JOIN {$this->tb_users} AS u ON u.ID = p2.post_author",
980 "INNER JOIN $oim_table AS oim_qty ON oi.order_item_id = oim_qty.learnpress_order_item_id AND oim_qty.meta_key = '_quantity'",
981 "INNER JOIN $oim_table AS oim_total ON oi.order_item_id = oim_total.learnpress_order_item_id AND oim_total.meta_key = '_total'",
982 );
983
984 $filter->where = array(
985 $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),
986 $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),
987 $this->wpdb->prepare( 'AND oi.item_type=%s', LP_COURSE_CPT ),
988 );
989
990 if ( $instructor_id > 0 ) {
991 $filter->where[] = $this->wpdb->prepare( 'AND p2.post_author=%d', $instructor_id );
992 }
993
994 $search = trim( $search );
995 if ( '' !== $search ) {
996 $filter->where[] = $this->wpdb->prepare( 'AND p2.post_title LIKE %s', '%' . $this->wpdb->esc_like( $search ) . '%' );
997 }
998
999 $filter->group_by = 'course_id';
1000 $filter->order_by = 'course_count';
1001 $filter->order = 'DESC';
1002 $filter->run_query_count = false;
1003 $result = $this->execute( $filter );
1004
1005 return is_array( $result ) ? $result : array();
1006 }
1007
1008 /**
1009 * Get top enrolled courses by a specific instructor.
1010 *
1011 * @param int $instructor_id The instructor user ID.
1012 * @param int $limit Number of courses to return, default 5.
1013 *
1014 * @return array Top enrolled courses for the instructor.
1015 * @since 4.3.0
1016 */
1017 public function get_top_enrolled_courses_by_instructor( int $instructor_id, int $limit = 5, string $search = '' ): array {
1018 $filter = new \LP_Filter();
1019 $filter->collection = $this->tb_lp_user_items;
1020 $filter->collection_alias = 'ui';
1021 $filter->only_fields[] = 'ui.item_id as course_id';
1022 $filter->only_fields[] = 'COUNT(ui.user_item_id) as enrollment_count';
1023 $filter->only_fields[] = 'p.post_title as course_name';
1024 $filter->only_fields[] = 'u.display_name as instructor_name';
1025 $filter->limit = $limit > 0 ? $limit : 5;
1026
1027 $filter->join[] = "INNER JOIN {$this->tb_posts} AS p ON p.ID = ui.item_id";
1028 $filter->join[] = "INNER JOIN {$this->tb_users} AS u ON u.ID = p.post_author";
1029
1030 $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );
1031
1032 if ( $instructor_id > 0 ) {
1033 $filter->where[] = $this->wpdb->prepare( 'AND p.post_author=%d', $instructor_id );
1034 }
1035
1036 $search = trim( $search );
1037 if ( '' !== $search ) {
1038 $filter->where[] = $this->wpdb->prepare( 'AND p.post_title LIKE %s', '%' . $this->wpdb->esc_like( $search ) . '%' );
1039 }
1040
1041 $filter->group_by = 'course_id';
1042 $filter->order_by = 'enrollment_count';
1043 $filter->order = 'DESC';
1044 $filter->run_query_count = false;
1045 $result = $this->execute( $filter );
1046
1047 return is_array( $result ) ? $result : array();
1048 }
1049
1050 /**
1051 * Get top instructors by course count and student count.
1052 *
1053 * @param int $limit Number of instructors to return.
1054 *
1055 * @return array Top instructors data.
1056 * @since 4.3.0
1057 */
1058 public function get_top_instructors( int $limit = 4 ): array {
1059 $tb_posts = $this->tb_posts;
1060 $tb_users = $this->tb_users;
1061 $tb_user_items = $this->tb_lp_user_items;
1062 $tb_usermeta = $this->wpdb->usermeta;
1063
1064 $sql = $this->wpdb->prepare(
1065 "SELECT u.ID as instructor_id,
1066 u.display_name as instructor_name,
1067 COUNT(DISTINCT p.ID) as course_count,
1068 (SELECT COUNT(DISTINCT ui.user_id)
1069 FROM {$tb_user_items} AS ui
1070 WHERE ui.item_id IN (SELECT p2.ID FROM {$tb_posts} AS p2 WHERE p2.post_author = u.ID AND p2.post_type = %s AND p2.post_status = 'publish')
1071 AND ui.item_type = %s
1072 ) as student_count
1073 FROM {$tb_users} AS u
1074 INNER JOIN {$tb_usermeta} AS um ON um.user_id = u.ID
1075 INNER JOIN {$tb_posts} AS p ON p.post_author = u.ID AND p.post_type = %s AND p.post_status = 'publish'
1076 WHERE um.meta_key = %s
1077 AND (um.meta_value LIKE %s OR um.meta_value LIKE %s)
1078 GROUP BY u.ID
1079 ORDER BY course_count DESC
1080 LIMIT %d",
1081 LP_COURSE_CPT,
1082 LP_COURSE_CPT,
1083 LP_COURSE_CPT,
1084 $this->wpdb->prefix . 'capabilities',
1085 '%administrator%',
1086 '%' . LP_TEACHER_ROLE . '%',
1087 $limit
1088 );
1089
1090 $results = $this->wpdb->get_results( $sql );
1091
1092 return is_array( $results ) ? $results : array();
1093 }
1094
1095 /**
1096 * Get net sales chart data scoped by instructor.
1097 *
1098 * @param string $type Time filter type.
1099 * @param string $value Time filter value.
1100 * @param int $instructor_id Instructor ID (0 for all).
1101 *
1102 * @return array Net sales data.
1103 * @since 4.3.0
1104 */
1105 public function get_net_sales_data_scoped( string $type, string $value, int $instructor_id = 0 ): array {
1106 if ( ! $type || ! $value ) {
1107 return array();
1108 }
1109
1110 $filter = new \LP_Order_Filter();
1111 $filter->collection = $this->tb_posts;
1112 $filter->collection_alias = 'p';
1113 $oi_table = $this->tb_lp_order_items;
1114 $oim_table = $this->tb_lp_order_itemmeta;
1115
1116 $filter->only_fields[] = 'SUM(CAST(oim.meta_value AS DECIMAL(10,2))) as x_data';
1117 $time_field = 'p.post_date';
1118
1119 $filter->join = array(
1120 "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",
1121 "INNER JOIN $oim_table AS oim ON oi.order_item_id = oim.learnpress_order_item_id",
1122 );
1123
1124 $filter->where = array(
1125 $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),
1126 $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),
1127 $this->wpdb->prepare( 'AND oim.meta_key=%s', '_total' ),
1128 );
1129
1130 if ( $instructor_id > 0 ) {
1131 $filter->join[] = "INNER JOIN {$this->tb_posts} AS p2 ON p2.ID = oi.item_id";
1132 $filter->where[] = $this->wpdb->prepare( 'AND p2.post_author=%d', $instructor_id );
1133 }
1134
1135 $filter->limit = -1;
1136 $filter = $this->filter_time( $filter, $type, $time_field, $value );
1137 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value );
1138 $filter->order_by = $time_field;
1139 $filter->order = 'asc';
1140 $filter->run_query_count = false;
1141
1142 $result = $this->execute( $filter );
1143 return is_array( $result ) ? $result : array();
1144 }
1145
1146 /**
1147 * Get enrollment chart data scoped by instructor.
1148 *
1149 * @param string $type Time filter type.
1150 * @param string $value Time filter value.
1151 * @param int $instructor_id Instructor ID (0 for all).
1152 * @param StatisticsScope $scope optional instructor/category scope. @since 4.4.2
1153 * @param string|null $granularity explicit chart resolution ( hour|day|month ), custom type only. @since 4.4.2
1154 *
1155 * @return array Enrollment chart data.
1156 * @since 4.3.0
1157 */
1158 public function get_enrollment_chart_data( string $type, string $value, int $instructor_id = 0, ?StatisticsScope $scope = null, ?string $granularity = null ): array {
1159 if ( ! $type || ! $value ) {
1160 return array();
1161 }
1162
1163 $filter = new \LP_Filter();
1164 $filter->collection = $this->tb_lp_user_items;
1165 $filter->collection_alias = 'ui';
1166 $time_field = 'ui.start_time';
1167
1168 $filter->only_fields[] = 'COUNT(ui.user_item_id) as x_data';
1169 $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );
1170
1171 if ( $instructor_id > 0 ) {
1172 $filter->join[] = "INNER JOIN {$this->tb_posts} AS p ON p.ID = ui.item_id";
1173 $filter->where[] = $this->wpdb->prepare( 'AND p.post_author=%d', $instructor_id );
1174 }
1175
1176 if ( $scope && ! $scope->is_empty() ) {
1177 $filter = $scope->apply( $filter, 'ui.item_id' );
1178 }
1179
1180 $filter->limit = -1;
1181 $filter = $this->filter_time( $filter, $type, $time_field, $value );
1182 $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value, $granularity );
1183 $filter->order_by = $time_field;
1184 $filter->order = 'asc';
1185 $filter->run_query_count = false;
1186
1187 $result = $this->execute( $filter );
1188 return is_array( $result ) ? $result : array();
1189 }
1190 }
1191