PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / Instructor.php
tutor / classes Last commit date
Addons.php 17 hours ago Admin.php 17 hours ago Ajax.php 17 hours ago Announcements.php 17 hours ago Assets.php 17 hours ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 17 hours ago Container.php 11 months ago Course.php 17 hours ago Course_Embed.php 3 years ago Course_Filter.php 17 hours ago Course_List.php 17 hours ago Course_Settings_Tabs.php 17 hours ago Course_Widget.php 1 year ago Custom_Validation.php 17 hours ago Dashboard.php 17 hours ago Earnings.php 9 months ago FormHandler.php 17 hours ago Frontend.php 17 hours ago Gutenberg.php 1 year ago Icon.php 17 hours ago Input.php 17 hours ago Instructor.php 17 hours ago Instructors_List.php 17 hours ago Lesson.php 17 hours ago Options_V2.php 17 hours ago Permalink.php 17 hours ago Post_types.php 1 day ago Private_Course_Access.php 17 hours ago Q_And_A.php 17 hours ago Question_Answers_List.php 11 months ago Quiz.php 17 hours ago QuizBuilder.php 17 hours ago Quiz_Attempts_List.php 17 hours ago RestAPI.php 2 years ago Reviews.php 17 hours ago Rewrite_Rules.php 2 years ago SampleCourse.php 17 hours ago Shortcode.php 17 hours ago Singleton.php 1 year ago Student.php 17 hours ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 17 hours ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 4 weeks ago Tutor.php 17 hours ago TutorEDD.php 17 hours ago Tutor_Base.php 2 years ago Tutor_Setup.php 17 hours ago Upgrader.php 17 hours ago User.php 17 hours ago UserPreference.php 17 hours ago Utils.php 17 hours ago Video_Stream.php 3 years ago WhatsNew.php 10 months ago Withdraw.php 17 hours ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 17 hours ago
Instructor.php
916 lines
1 <?php
2 /**
3 * Manage Instructor
4 *
5 * @package Tutor
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 1.0.0
9 */
10
11 namespace TUTOR;
12
13 defined( 'ABSPATH' ) || exit;
14
15 use DateInterval;
16 use DateTime;
17 use Tutor\GDPR\Controllers\LegalConsent;
18 use Tutor\Helpers\DateTimeHelper;
19 use Tutor\Helpers\QueryHelper;
20 use Tutor\Traits\JsonResponse;
21
22 /**
23 * Instructor class
24 *
25 * @since 1.0.0
26 */
27 class Instructor {
28 use JsonResponse;
29
30 /**
31 * Error message
32 *
33 * @var string
34 */
35 protected $error_msgs = '';
36
37 /**
38 * Constructor
39 *
40 * @since 1.0.0
41 *
42 * @param bool $register_hook register hook or not.
43 *
44 * @return void
45 */
46 public function __construct( $register_hook = true ) {
47 if ( ! $register_hook ) {
48 return;
49 }
50 add_action( 'template_redirect', array( $this, 'register_instructor' ) );
51 add_action( 'template_redirect', array( $this, 'apply_instructor' ) );
52
53 // Add instructor from admin panel.
54 add_action( 'wp_ajax_tutor_add_instructor', array( $this, 'add_new_instructor' ) );
55
56 /**
57 * Instructor Approval
58 * Block Unblock
59 *
60 * @since 1.5.3
61 */
62 add_action( 'wp_ajax_instructor_approval_action', array( $this, 'instructor_approval_action' ) );
63
64 /**
65 * Check if instructor can publish courses
66 *
67 * @since 1.5.9
68 */
69 add_action( 'tutor_option_save_after', array( $this, 'can_publish_tutor_courses' ) );
70
71 /**
72 * Hide instructor rejection message
73 *
74 * @since 1.9.2
75 */
76 add_action( 'wp_loaded', array( $this, 'hide_instructor_notice' ) );
77
78 add_action( 'wp_ajax_tutor_save_instructor_home_sections_order', array( $this, 'ajax_save_home_sections_order' ) );
79 add_action( 'wp_ajax_tutor_save_instructor_home_sections_visibility', array( $this, 'ajax_save_home_section_visibility' ) );
80 }
81
82 /**
83 * Template Redirect Callback
84 * For Register new user and mark him as instructor
85 *
86 * @since 1.0.0
87 * @return void|null
88 */
89 public function register_instructor() {
90 // Here tutor_action checking required before nonce checking.
91 if ( 'tutor_register_instructor' !== Input::post( 'tutor_action' ) || ! get_option( 'users_can_register', false ) ) {
92 return;
93 }
94
95 // Checking nonce.
96 tutor_utils()->checking_nonce();
97 $required_fields = apply_filters(
98 'tutor_instructor_registration_required_fields',
99 array(
100 'first_name' => __( 'First name field is required', 'tutor' ),
101 'last_name' => __( 'Last name field is required', 'tutor' ),
102 'email' => __( 'E-Mail field is required', 'tutor' ),
103 'user_login' => __( 'User Name field is required', 'tutor' ),
104 'password' => __( 'Password field is required', 'tutor' ),
105 'password_confirmation' => __( 'Password Confirmation field is required', 'tutor' ),
106 )
107 );
108
109 $validation_errors = array();
110
111 /*
112 * Push into validation_errors
113 * Error registration_errors
114 */
115 $errors = apply_filters( 'registration_errors', new \WP_Error(), '', '' );
116 foreach ( $errors->errors as $key => $value ) {
117 $validation_errors[ $key ] = $value[0];
118 }
119
120 foreach ( $required_fields as $required_key => $required_value ) {
121 if ( empty( $_POST[ $required_key ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
122 $validation_errors[ $required_key ] = $required_value;
123 }
124 }
125
126 $validate_consent = LegalConsent::validate_consent( LegalConsent::DISPLAY_ON_INS_REG, $_POST );
127 if ( is_wp_error( $validate_consent ) ) {
128 $validation_errors[ $validate_consent->get_error_code() ] = $validate_consent->get_error_message();
129 }
130
131 if ( ! filter_var( tutor_utils()->input_old( 'email' ), FILTER_VALIDATE_EMAIL ) ) {
132 $validation_errors['email'] = __( 'Valid E-Mail is required', 'tutor' );
133 }
134
135 if ( tutor_utils()->input_old( 'password' ) !== tutor_utils()->input_old( 'password_confirmation' ) ) {
136 $validation_errors['password_confirmation'] = __( 'Your passwords should match each other. Please recheck.', 'tutor' );
137 }
138
139 if ( count( $validation_errors ) ) {
140 $this->error_msgs = $validation_errors;
141 add_filter( 'tutor_instructor_register_validation_errors', array( $this, 'tutor_instructor_form_validation_errors' ) );
142 return;
143 }
144
145 $first_name = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) );
146 $last_name = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) );
147 $email = sanitize_text_field( tutor_utils()->input_old( 'email' ) );
148 $user_login = sanitize_text_field( tutor_utils()->input_old( 'user_login' ) );
149 $password = sanitize_text_field( tutor_utils()->input_old( 'password' ) );
150
151 $userdata = array(
152 'user_login' => $user_login,
153 'user_email' => $email,
154 'first_name' => $first_name,
155 'last_name' => $last_name,
156 'user_pass' => $password,
157 );
158
159 global $wpdb;
160 $wpdb->query( 'START TRANSACTION' );
161 $user_id = wp_insert_user( $userdata );
162 if ( is_wp_error( $user_id ) ) {
163 $this->error_msgs = $user_id->get_error_messages();
164 add_filter( 'tutor_instructor_register_validation_errors', array( $this, 'tutor_instructor_form_validation_errors' ) );
165 return;
166 }
167
168 $user = get_user_by( 'id', $user_id );
169
170 $is_req_email_verification = apply_filters( 'tutor_require_email_verification', false );
171 if ( $is_req_email_verification ) {
172 do_action( 'tutor_send_verification_mail', get_userdata( $user_id ), 'instructor-registration' );
173 $reg_done = apply_filters( 'tutor_registration_done', true );
174 if ( ! $reg_done ) {
175 $wpdb->query( 'ROLLBACK' );
176 return;
177 } else {
178 $wpdb->query( 'COMMIT' );
179 }
180 } else {
181 /**
182 * Tutor Free - regular instructor reg process.
183 */
184 $this->update_instructor_meta( $user_id );
185 $wpdb->query( 'COMMIT' );
186
187 if ( $user ) {
188 wp_set_current_user( $user_id, $user->user_login );
189 wp_set_auth_cookie( $user_id );
190 do_action( 'tutor_after_instructor_signup', $user_id );
191 }
192 }
193
194 if ( $user ) {
195 do_action( 'tutor_new_instructor_registered', $user_id, $validate_consent );
196 }
197
198 wp_safe_redirect( tutor_utils()->get_nocache_url( tutor_utils()->input_old( '_wp_http_referer' ) ) );
199 die();
200 }
201
202 /**
203 * Get instructor reg validation errors.
204 *
205 * @since 1.0.0
206 * @return string
207 */
208 public function tutor_instructor_form_validation_errors() {
209 return $this->error_msgs;
210 }
211
212 /**
213 * Template Redirect Callback
214 * for instructor applying when a user already logged in
215 *
216 * @since 1.0.0
217 * @return void|null
218 */
219 public function apply_instructor() {
220 // Here tutor_action checking required before nonce checking.
221 if ( 'tutor_apply_instructor' !== Input::post( 'tutor_action' ) ) {
222 return;
223 }
224
225 // Checking nonce.
226 tutor_utils()->checking_nonce();
227
228 $user_id = get_current_user_id();
229 if ( $user_id ) {
230 if ( tutor_utils()->is_instructor() ) {
231 die( esc_html__( 'Already applied for instructor', 'tutor' ) );
232 } else {
233 update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
234 update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) );
235 update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_STUDENT_DASHBOARD );
236
237 do_action( 'tutor_new_instructor_after', $user_id );
238 }
239 } else {
240 die( esc_html__( 'Permission denied', 'tutor' ) );
241 }
242
243 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
244 die();
245 }
246
247
248 /**
249 * Add new instructor
250 *
251 * @since 1.0.0
252 * @return void
253 */
254 public function add_new_instructor() {
255 tutor_utils()->checking_nonce();
256
257 // Only admin should be able to add instructor.
258 if ( ! current_user_can( 'manage_options' ) || ! get_option( 'users_can_register', false ) ) {
259 wp_send_json_error();
260 }
261
262 $required_fields = apply_filters(
263 'tutor_instructor_registration_required_fields',
264 array(
265 'first_name' => __( 'First name field is required', 'tutor' ),
266 'last_name' => __( 'Last name field is required', 'tutor' ),
267 'email' => __( 'E-Mail field is required', 'tutor' ),
268 'user_login' => __( 'User Name field is required', 'tutor' ),
269 'password' => __( 'Password field is required', 'tutor' ),
270 'password_confirmation' => __( 'Your passwords should match each other. Please recheck.', 'tutor' ),
271 )
272 );
273
274 $validation_errors = array();
275 foreach ( $required_fields as $required_key => $required_value ) {
276 if ( empty( $_POST[ $required_key ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
277 $validation_errors[ $required_key ] = $required_value;
278 }
279 }
280
281 if ( ! filter_var( tutor_utils()->input_old( 'email' ), FILTER_VALIDATE_EMAIL ) ) {
282 $validation_errors['email'] = __( 'Valid E-Mail is required', 'tutor' );
283 }
284 if ( tutor_utils()->input_old( 'password' ) !== tutor_utils()->input_old( 'password_confirmation' ) ) {
285 $validation_errors['password_confirmation'] = __( 'Your passwords should match each other. Please recheck.', 'tutor' );
286 }
287
288 if ( count( $validation_errors ) ) {
289 wp_send_json_error( array( 'errors' => $validation_errors ) );
290 }
291
292 $first_name = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) );
293 $last_name = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) );
294 $email = sanitize_text_field( tutor_utils()->input_old( 'email' ) );
295 $user_login = sanitize_text_field( tutor_utils()->input_old( 'user_login' ) );
296 $phone_number = sanitize_text_field( tutor_utils()->input_old( 'phone_number' ) );
297 $password = sanitize_text_field( tutor_utils()->input_old( 'password' ) );
298 $tutor_profile_bio = Input::post( 'tutor_profile_bio', '', Input::TYPE_KSES_POST );
299 $tutor_profile_job_title = sanitize_text_field( tutor_utils()->input_old( 'tutor_profile_job_title' ) );
300
301 $userdata = apply_filters(
302 'add_new_instructor_data',
303 array(
304 'user_login' => $user_login,
305 'user_email' => $email,
306 'first_name' => $first_name,
307 'last_name' => $last_name,
308 'role' => tutor()->instructor_role,
309 'user_pass' => $password,
310 )
311 );
312
313 do_action( 'tutor_add_new_instructor_before' );
314
315 $user_id = wp_insert_user( $userdata );
316 if ( ! is_wp_error( $user_id ) ) {
317 update_user_meta( $user_id, 'phone_number', $phone_number );
318 update_user_meta( $user_id, 'description', $tutor_profile_bio );
319 update_user_meta( $user_id, '_tutor_profile_bio', $tutor_profile_bio );
320 update_user_meta( $user_id, '_tutor_profile_job_title', $tutor_profile_job_title );
321 update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
322 update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'approved' ) );
323
324 do_action( 'tutor_add_new_instructor_after', $user_id );
325
326 wp_send_json_success( array( 'msg' => __( 'Instructor has been added successfully', 'tutor' ) ) );
327 }
328
329 wp_send_json_error( array( 'errors' => $user_id ) );
330 }
331
332 /**
333 * Handle instructor approval action
334 * This function not used maybe, will be removed
335 *
336 * @since 1.0.0
337 * @return void
338 */
339 public function instructor_approval_action() {
340 tutor_utils()->checking_nonce();
341
342 if ( ! current_user_can( 'manage_options' ) ) {
343 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
344 }
345
346 $instructor_id = Input::post( 'instructor_id', 0, Input::TYPE_INT );
347 $action = Input::post( 'action_name' );
348
349 if ( 'approve' === $action ) {
350 do_action( 'tutor_before_approved_instructor', $instructor_id );
351
352 update_user_meta( $instructor_id, '_tutor_instructor_status', 'approved' );
353 update_user_meta( $instructor_id, '_tutor_instructor_approved', tutor_time() );
354 update_user_meta( $instructor_id, User::INSTRUCTOR_APPROVAL_NOTICE_META, true );
355
356 $instructor = new \WP_User( $instructor_id );
357 $instructor->add_role( tutor()->instructor_role );
358
359 // Send E-Mail to this user about instructor approval via hook.
360 do_action( 'tutor_after_approved_instructor', $instructor_id );
361 }
362
363 if ( 'blocked' === $action ) {
364 do_action( 'tutor_before_blocked_instructor', $instructor_id );
365 update_user_meta( $instructor_id, '_tutor_instructor_status', 'blocked' );
366
367 $instructor = new \WP_User( $instructor_id );
368 $instructor->remove_role( tutor()->instructor_role );
369 do_action( 'tutor_after_blocked_instructor', $instructor_id );
370
371 // TODO: send E-Mail to this user about instructor blocked, should via hook.
372 }
373
374 if ( 'remove-instructor' === $action ) {
375 do_action( 'tutor_before_rejected_instructor', $instructor_id );
376
377 $user = new \WP_User( $instructor_id );
378 $user->remove_role( tutor()->instructor_role );
379
380 tutor_utils()->remove_instructor_role( $instructor_id );
381 update_user_meta( $instructor_id, '_is_tutor_instructor_rejected', tutor_time() );
382 update_user_meta( $instructor_id, 'tutor_instructor_show_rejection_message', true );
383
384 // Send E-Mail to this user about instructor rejection via hook.
385 do_action( 'tutor_after_rejected_instructor', $instructor_id );
386 }
387
388 wp_send_json_success();
389 }
390
391 /**
392 * Hide instructor notice
393 *
394 * @since 1.0.0
395 * @return void
396 */
397 public function hide_instructor_notice() {
398 if ( 'hide_instructor_notice' === Input::get( 'tutor_action' ) ) {
399 delete_user_meta( get_current_user_id(), 'tutor_instructor_show_rejection_message' );
400 } elseif ( 'hide_instructor_approval_notice' === Input::get( 'tutor_action' ) ) {
401 delete_user_meta( get_current_user_id(), User::INSTRUCTOR_APPROVAL_NOTICE_META );
402 }
403 }
404
405 /**
406 * Can instructor publish courses directly
407 * Fixed in Gutenberg
408 *
409 * @since 1.5.9
410 * @return void
411 */
412 public function can_publish_tutor_courses() {
413 $can_publish_course = (bool) tutor_utils()->get_option( 'instructor_can_publish_course' );
414
415 $instructor_role = tutor()->instructor_role;
416 $instructor = get_role( $instructor_role );
417
418 if ( $can_publish_course ) {
419 $instructor->add_cap( 'publish_tutor_courses' );
420 } else {
421 $instructor->remove_cap( 'publish_tutor_courses' );
422 }
423 }
424
425 /**
426 * Update instructor meta just after register
427 *
428 * @since 2.1.9
429 *
430 * @param integer $user_id user id.
431 *
432 * @return void
433 */
434 public function update_instructor_meta( int $user_id ) {
435 update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
436 update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) );
437 update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_INSTRUCTOR_REGISTRATION );
438
439 do_action( 'tutor_new_instructor_after', $user_id );
440 }
441
442 /**
443 * Calculate the previous comparison date range based on a selected date range.
444 *
445 * @since 4.0.0
446 *
447 * @param string|null $selected_start_date Selected start date (Y-m-d).
448 * @param string|null $selected_end_date Selected end date (Y-m-d).
449 *
450 * @return array {
451 * @type string $previous_start_date Previous period start date (Y-m-d).
452 * @type string $previous_end_date Previous period end date (Y-m-d).
453 * }
454 */
455 public static function get_comparison_date_range( $selected_start_date, $selected_end_date ) {
456
457 $format = DateTimeHelper::FORMAT_DATE;
458
459 if ( empty( $selected_start_date ) && empty( $selected_end_date ) ) {
460
461 $now = DateTimeHelper::now();
462 return array(
463 'previous_start_date' => $now->create( 'first day of this month' )->format( $format ),
464 'previous_end_date' => $now->create( 'last day of this month' )->format( $format ),
465 );
466 }
467
468 $start = new DateTime( $selected_start_date );
469 $end = new DateTime( $selected_end_date );
470 $days = $start->diff( $end )->days + 1;
471
472 $previous_start_date = $start->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format );
473 $previous_end_date = $end->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format );
474
475 return array(
476 'previous_start_date' => $previous_start_date,
477 'previous_end_date' => $previous_end_date,
478 );
479 }
480
481 /**
482 * Get course completion distribution data for a specific instructor.
483 *
484 * @since 4.0.0
485 *
486 * @param array $instructor_course_ids Optional list of course IDs.
487 *
488 * @return array {
489 * Enrollment distribution counts.
490 *
491 * @type int $enrolled Total number of enrollments.
492 * @type int $completed Number of fully completed enrollments (100% progress).
493 * @type int $inprogress Number of enrollments with partial progress (>0 and <100).
494 * @type int $inactive Number of enrollments with no progress (0%).
495 * @type int $cancelled Number of cancelled enrollments.
496 * }
497 */
498 public static function get_course_completion_distribution_data_by_instructor( $instructor_course_ids = array() ) {
499
500 global $wpdb;
501
502 $counts = array(
503 'enrolled' => 0,
504 'completed' => 0,
505 'inprogress' => 0,
506 'inactive' => 0,
507 'cancelled' => 0,
508 );
509
510 $cancel_statuses = array( 'cancel', 'canceled', 'cancelled' );
511 $post_statuses = array_merge( $cancel_statuses, array( 'completed' ) );
512
513 if ( empty( $instructor_course_ids ) ) {
514 return $counts;
515 }
516
517 $where = array(
518 'post_type' => 'tutor_enrolled',
519 'post_status' => array( 'IN', $post_statuses ),
520 'post_parent' => array( 'IN', $instructor_course_ids ),
521 );
522
523 $args = array(
524 'select' => array( 'id', 'post_status', 'post_author', 'post_parent' ),
525 'where' => $where,
526 );
527
528 $enrollments = QueryHelper::query( $wpdb->posts, $args );
529
530 if ( empty( $enrollments ) ) {
531 return $counts;
532 }
533
534 $counts['enrolled'] = count( $enrollments );
535
536 foreach ( $enrollments as $enrollment ) {
537
538 // Cancelled enrollment.
539 if ( in_array( $enrollment->post_status, $cancel_statuses, true ) ) {
540 ++$counts['cancelled'];
541 continue;
542 }
543
544 // Completed course.
545 if ( tutor_utils()->is_completed_course( $enrollment->post_parent, $enrollment->post_author ) ) {
546 ++$counts['completed'];
547 continue;
548 }
549
550 $course_progress = (int) tutor_utils()->get_course_completed_percent( $enrollment->post_parent, $enrollment->post_author );
551
552 if ( 100 === $course_progress ) { // If progress is 100% but the `Complete Course` button hasn't been clicked.
553 ++$counts['completed'];
554 continue;
555 }
556
557 if ( $course_progress > 0 ) {
558 ++$counts['inprogress'];
559 continue;
560 }
561
562 ++$counts['inactive'];
563 }
564
565 return $counts;
566 }
567
568 /**
569 * Retrieve the top-performing courses for a given instructor.
570 *
571 * @since 4.0.0
572 *
573 * @param int $instructor_id Instructor user ID.
574 * @param array $args {
575 * Optional query arguments.
576 *
577 * @type string $start_date Optional start date (Y-m-d).
578 * @type string $end_date Optional end date (Y-m-d).
579 * @type string $order_by Sorting criteria. Accepts 'revenue' or 'student'.
580 * }
581
582 * @param int $limit Maximum number of courses to return. Default 4.
583 *
584 * @return array List of course objects containing:
585 * - course_id (int)
586 * - course_title (string)
587 * - total_revenue (float)
588 * - total_student (int)
589 *
590 * @throws \Exception When a database error occurs.
591 */
592 public static function get_top_performing_courses_by_instructor( $instructor_id, $args, $limit = 4 ) {
593
594 global $wpdb;
595
596 $start_date = $args['start_date'] ?? null;
597 $end_date = $args['end_date'] ?? null;
598 $order_by = 'revenue' === $args['order_by'] ? 'total_revenue' : 'total_student';
599
600 $complete_status = tutor_utils()->get_earnings_completed_statuses();
601
602 $amount_type = User::is_admin() && is_admin() ? 'earnings.admin_amount' : 'earnings.instructor_amount';
603 $amount_rate = User::is_admin() && is_admin() ? 'earnings.admin_rate' : 'earnings.instructor_rate';
604
605 $amount_condition = "CASE
606 WHEN orders.tax_type = 'inclusive' AND earnings.course_price_grand_total > 0
607 THEN ( earnings.course_price_grand_total - orders.tax_amount ) * ( $amount_rate/100 )
608 ELSE $amount_type
609 END";
610
611 $earning_where_clause = array(
612 'earnings.user_id' => $instructor_id,
613 'earnings.order_status' => array( 'IN', $complete_status ),
614 );
615
616 $enrollment_where_clause = array(
617 'post_type' => 'tutor_enrolled',
618 'post_status' => 'completed',
619 );
620
621 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
622 $earning_where_clause['earnings.created_at'] = array( 'BETWEEN', array( $start_date, $end_date ) );
623 $enrollment_where_clause['post_date'] = array( 'BETWEEN', array( $start_date, $end_date ) );
624 }
625
626 $earning_where_clause = QueryHelper::prepare_where_clause( $earning_where_clause );
627 $enrollment_where_clause = QueryHelper::prepare_where_clause( $enrollment_where_clause );
628
629 $earnings_sql = "SELECT
630 earnings.course_id,
631 SUM($amount_condition) AS total_revenue
632 FROM {$wpdb->tutor_earnings} earnings
633 LEFT JOIN {$wpdb->tutor_orders} orders ON orders.id = earnings.order_id
634 WHERE {$earning_where_clause}
635 GROUP BY earnings.course_id";
636
637 $enrollment_sql = QueryHelper::prepare_raw_query(
638 "SELECT
639 post_parent AS course_id,
640 COUNT(DISTINCT post_author) AS total_student
641 FROM {$wpdb->posts}
642 WHERE {$enrollment_where_clause}
643 GROUP BY post_parent",
644 array()
645 );
646
647 //phpcs:disabled
648 $result = $wpdb->get_results(
649 $wpdb->prepare(
650 "SELECT
651 post.ID AS course_id,
652 post.post_title AS course_title,
653 COALESCE(earnings.total_revenue, 0) AS total_revenue,
654 COALESCE(enrollments.total_student, 0) AS total_student
655 FROM {$wpdb->posts} post
656 INNER JOIN ({$earnings_sql}) earnings
657 ON earnings.course_id = post.ID
658 LEFT JOIN ({$enrollment_sql}) enrollments
659 ON enrollments.course_id = post.ID
660 WHERE post.post_type = %s
661 ORDER BY {$order_by} DESC
662 LIMIT %d",
663 tutor()->course_post_type,
664 $limit
665 )
666 );
667 //phpcs:enable
668
669 // If error occurred then throw new exception.
670 if ( $wpdb->last_error ) {
671 throw new \Exception( $wpdb->last_error ); //phpcs:ignore.
672 }
673
674 return $result;
675 }
676
677 /**
678 * Format top performing instructor courses for presentation.
679 *
680 * @since 4.0.0
681 *
682 * @param array $top_courses List of course objects returned from analytics.
683 *
684 * @return array Formatted top performing courses data.
685 */
686 public static function format_instructor_top_performing_courses( $top_courses ) {
687
688 if ( empty( $top_courses ) ) {
689 return array();
690 }
691
692 return array_map(
693 function ( $course ) {
694 return array(
695 'name' => $course->course_title,
696 'url' => get_permalink( $course->course_id ),
697 'revenue' => tutor_utils()->tutor_price( $course->total_revenue ?? 0 ),
698 'students' => $course->total_student ?? 0,
699 );
700 },
701 $top_courses
702 );
703 }
704
705 /**
706 * Retrieve upcoming live session tasks (Zoom / Google Meet) for an instructor.
707 *
708 * @since 4.0.0
709 *
710 * @param int $instructor_id Instructor (author) user ID.
711 *
712 * @return array List of upcoming live task posts.
713 */
714 public static function get_instructor_upcoming_live_tasks( $instructor_id ) {
715
716 $is_google_meet_enable = tutor_utils()->is_addon_enabled( 'google-meet' );
717 $is_zoom_enable = tutor_utils()->is_addon_enabled( 'tutor-zoom' );
718
719 $meta_keys = array_filter(
720 array(
721 $is_google_meet_enable ? 'tutor-google-meet-start-datetime' : null,
722 $is_zoom_enable ? '_tutor_zm_start_datetime' : null,
723 )
724 );
725
726 $post_types = array_filter(
727 array(
728 $is_google_meet_enable ? tutor()->meet_post_type : null,
729 $is_zoom_enable ? tutor()->zoom_post_type : null,
730 )
731 );
732
733 if ( empty( $meta_keys ) ) {
734 return array();
735 }
736
737 return get_posts(
738 array(
739 'post_type' => $post_types,
740 'post_status' => 'publish',
741 'author' => $instructor_id,
742 'numberposts' => 5,
743 'meta_query' => array(
744 array(
745 'key' => $meta_keys,
746 'value' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ),
747 'compare' => '>=',
748 'type' => 'DATETIME',
749 ),
750 ),
751 )
752 );
753 }
754
755 /**
756 * Format upcoming instructor live tasks for presentation.
757 *
758 * @since 4.0.0
759 *
760 * @param array $upcoming_live_tasks List of live task post objects.
761 *
762 * @return array Formatted upcoming live tasks data.
763 */
764 public static function format_instructor_upcoming_live_tasks( $upcoming_live_tasks ) {
765
766 if ( empty( $upcoming_live_tasks ) ) {
767 return array();
768 }
769
770 return array_map(
771 function ( $task ) {
772
773 $is_zoom = tutor()->zoom_post_type === $task->post_type;
774 $is_meet = tutor()->meet_post_type === $task->post_type;
775
776 $live_meta_key = $is_zoom ? '_tutor_zm_start_datetime'
777 : ( $is_meet ? 'tutor-google-meet-start-datetime' : '' );
778
779 $url = $is_zoom ? ( json_decode( get_post_meta( $task->ID, '_tutor_zm_data' )[0] )->join_url ?? '' )
780 : ( $is_meet ? get_post_meta( $task->ID, 'tutor-google-meet-link', true ) : '' );
781
782 $start_date = get_post_meta( $task->ID, $live_meta_key, true );
783
784 return array(
785 'name' => $task->post_title,
786 'date' => wp_date( 'Y-m-d h:i A', strtotime( $start_date ) ),
787 'url' => $url,
788 'post_type' => $task->post_type,
789 );
790 },
791 $upcoming_live_tasks
792 );
793 }
794
795 /**
796 * Format recent instructor reviews for display.
797 *
798 * @since 4.0.0
799 *
800 * @param array $reviews List of review objects.
801 *
802 * @return array Formatted recent reviews data.
803 */
804 public static function format_instructor_recent_reviews( $reviews ) {
805
806 if ( empty( $reviews ) ) {
807 return array();
808 }
809
810 return array_map(
811 function ( $review ) {
812 return array(
813 'user' => array(
814 'id' => $review->user_id,
815 'name' => $review->display_name,
816 'avatar' => tutor_utils()->get_user_avatar_url( $review->user_id ),
817 ),
818 'course_name' => get_the_title( $review->comment_post_ID ),
819 'date' => $review->comment_date,
820 'rating' => $review->rating,
821 'review_text' => $review->comment_content,
822 );
823 },
824 $reviews
825 );
826 }
827
828 /**
829 *
830 * Calculates percentage change and UI metadata for a statistics card.
831 *
832 * @since 4.0.0
833 *
834 * @param float $current_data Current period value.
835 * @param float $previous_data Previous period value.
836 *
837 * @return array{
838 * percentage: string,
839 * icon: string,
840 * class: string,
841 * icon_class: string
842 * }
843 */
844 public static function get_stat_card_details( float $current_data, float $previous_data ) {
845
846 if ( empty( $previous_data ) && empty( $current_data ) ) {
847 return array(
848 'percentage' => '',
849 'icon' => Icon::MINUS,
850 'class' => 'tutor-text-primary',
851 );
852 }
853
854 if ( empty( $previous_data ) ) {
855 $percentage = 100;
856 } else {
857 $percentage = ( ( $current_data - $previous_data ) / $previous_data ) * 100;
858 }
859
860 $is_negative = $percentage < 0;
861 $icon = $is_negative ? Icon::ARROW_DOWN : Icon::ARROW_UP;
862 $class = $is_negative ? 'tutor-p2 tutor-actions-critical-primary' : 'tutor-p2 tutor-actions-success-primary';
863
864 return array(
865 'percentage' => number_format( abs( $percentage ), 2 ) . '%',
866 'icon' => $icon,
867 'class' => $class,
868 'icon_class' => '-tutor-mb-1',
869 );
870 }
871
872 /**
873 * Save the instructor home sections order for the current user.
874 *
875 * @since 4.0.0
876 *
877 * @return void Sends JSON success or error response and exits.
878 */
879 public function ajax_save_home_sections_order() {
880 tutor_utils()->check_nonce();
881
882 if ( ! User::is_instructor() ) {
883 $this->response_bad_request( tutor_utils()->error_message() );
884 }
885
886 $order = Input::post( 'order', array(), Input::TYPE_ARRAY );
887 $order = array_values( array_map( 'sanitize_key', $order ) );
888
889 update_user_meta( get_current_user_id(), '_tutor_instructor_home_sections_order', array_flip( $order ) );
890
891 $this->response_success( __( 'Settings saved successfully', 'tutor' ) );
892 }
893
894 /**
895 * Save the visibility state of instructor home sections.
896 *
897 * @since 4.0.0
898 *
899 * @return void Sends JSON success or error response and exits.
900 */
901 public function ajax_save_home_section_visibility() {
902 tutor_utils()->check_nonce();
903
904 if ( ! User::is_instructor() ) {
905 $this->response_bad_request( tutor_utils()->error_message() );
906 }
907
908 $items = Input::post( 'items', '', Input::TYPE_STRING );
909 $items = array_map( 'rest_sanitize_boolean', (array) json_decode( $items ) );
910
911 update_user_meta( get_current_user_id(), '_tutor_instructor_home_sections_visibility', $items );
912
913 $this->response_success( __( 'Settings saved successfully', 'tutor' ) );
914 }
915 }
916