PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.0
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.0
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / inc / TemplateHooks / Admin / AdminListStudentsEnrolled.php

AdminListStudentsEnrolled.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.0, at inc/TemplateHooks/Admin/AdminListStudentsEnrolled.php

750 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace LearnPress\TemplateHooks\Admin;
4
5 use Exception;
6 use LearnPress\Databases\PostDB;
7 use LearnPress\Databases\UserItemsDB;
8 use LearnPress\Filters\PostFilter;
9 use LearnPress\Filters\UserItemsFilter;
10 use LearnPress\Helpers\Singleton;
11 use LearnPress\Helpers\Template;
12 use LearnPress\Models\CourseModel;
13 use LearnPress\Models\UserItems\UserCourseModel;
14 use LearnPress\Models\UserItems\UserItemModel;
15 use LearnPress\Models\UserModel;
16 use LearnPress\TemplateHooks\Instructor\SingleInstructorTemplate;
17 use LearnPress\TemplateHooks\Table\TableListTemplate;
18 use LearnPress\TemplateHooks\TemplateAJAX;
19 use LearnPress\TemplateHooks\UserItem\UserCourseTemplate;
20 use LP_Debug;
21 use LP_Helper;
22 use LP_Page_Controller;
23 use stdClass;
24 use Throwable;
25
26 /**
27 * Template Admin List Students Enrolled.
28 *
29 * Displays enrolled students for Admin (all courses) and Instructor (own courses only).
30 * Provides WP Admin submenu + Frontend profile tab.
31 *
32 * @since 4.3.3
33 * @version 1.0.0
34 */
35 class AdminListStudentsEnrolled {
36 use Singleton;
37
38 const PER_PAGE = 10;
39
40 public function init() {
41
42 // 1. Register render hook for both admin + frontend profile.
43 add_action( 'learn-press/admin/enrolled-students/layout', array( $this, 'enrolled_students_layout' ) );
44 // 2. Whitelist AJAX callback.
45 add_filter( 'lp/rest/ajax/allow_callback', array( $this, 'allow_callback' ) );
46 // 4. Render a modal toolbar template from PHP (used by JS modal).
47 add_action( 'admin_footer', array( $this, 'print_modal_toolbar_template' ) );
48 add_action( 'wp_footer', array( $this, 'print_modal_toolbar_template' ) );
49 }
50
51 /**
52 * Admin page output callback.
53 */
54 public function admin_page_output() {
55 $instructor_id = self::resolve_instructor_id_for_request( array() );
56
57 echo '<div class="wrap" id="lp-enrolled-students">';
58 echo '<h1 class="wp-heading-inline">' . esc_html__( 'Enrolled Students', 'learnpress' ) . '</h1>';
59 do_action( 'learn-press/admin/enrolled-students/layout', $instructor_id );
60 echo '</div>';
61 }
62
63 /**
64 * Allow callback for AJAX.
65 *
66 * @param array $callbacks
67 *
68 * @return array
69 */
70 public function allow_callback( array $callbacks ): array {
71 $callbacks[] = get_class( $this ) . ':render_enrolled_students';
72
73 return $callbacks;
74 }
75
76 /**
77 * Render initial layout with TemplateAJAX::load_content_via_ajax().
78 *
79 * @param int $instructor_id 0 for Admin (all), user ID for Instructor.
80 */
81 public function enrolled_students_layout( $instructor_id = 0 ) {
82 try {
83 $page_current = '';
84 if ( function_exists( 'get_current_screen' ) ) {
85 $wp_screen = get_current_screen();
86 if ( $wp_screen ) {
87 // Page on the Admin screen.
88 if ( $wp_screen->id === 'learnpress_page_learn-press-students-enrolled' ) {
89 $page_current = 'learnpress_page_learn-press-students-enrolled';
90 }
91 }
92 }
93
94 // Enqueue styles — lp-enrolled-students-table CSS is loaded via admin.css/frontend.css import.
95 // Build toolbar HTML (outside AJAX so it persists across reloads).
96 $instructor_id = self::resolve_instructor_id_for_request(
97 array(
98 'instructor_id' => $instructor_id,
99 )
100 );
101
102 $args = array(
103 'id_url' => 'lp-enrolled-students',
104 'instructor_id' => (int) $instructor_id,
105 'course_id' => abs( LP_Helper::sanitize_params_submitted( $_GET['course_id'] ?? 0, 'int' ) ),
106 'course_name' => LP_Helper::sanitize_params_submitted( $_GET['course_name'] ?? '' ),
107 'paged' => 1,
108 'search' => LP_Helper::sanitize_params_submitted( $_GET['search'] ?? '' ),
109 'start_date' => self::sanitize_date_filter( $_GET['start_date'] ?? '' ),
110 'end_date' => self::sanitize_date_filter( $_GET['end_date'] ?? '' ),
111 'enableUpdateParamsUrl' => false,
112 );
113
114 $call_back = array(
115 'class' => self::class,
116 'method' => 'render_enrolled_students',
117 );
118
119 $section = [
120 'wrap' => sprintf(
121 '<div class="lp-students-enrolled-layout %s">',
122 $page_current
123 ),
124 'toolbar' => $this->html_toolbar(),
125 'table' => TemplateAJAX::load_content_via_ajax( $args, $call_back ),
126 'wrap-end' => '</div>',
127 ];
128
129 echo Template::combine_components( $section );
130 } catch ( Throwable $e ) {
131 LP_Debug::error_log( $e );
132 Template::print_message( $e->getMessage(), 'error' );
133 }
134 }
135
136 /**
137 * Static render method called by AJAX — returns stdClass{ content }.
138 *
139 * Permission check: Admin=full, Instructor=own courses only.
140 *
141 * @param array $data
142 *
143 * @return stdClass
144 */
145 public static function render_enrolled_students( array $data ): stdClass {
146 $content = new stdClass();
147 $content->content = '';
148
149 try {
150 // Check permission
151 if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR )
152 && ! current_user_can( UserModel::ROLE_INSTRUCTOR ) ) {
153 throw new Exception( esc_html__( 'You do not have permission to view enrolled students.', 'learnpress' ) );
154 }
155
156 $instructor_id = self::resolve_instructor_id_for_request( $data );
157
158 $course_id = abs( LP_Helper::sanitize_params_submitted( $data['course_id'] ?? 0, 'int' ) );
159 $paged = max( 1, abs( LP_Helper::sanitize_params_submitted( $data['paged'] ?? 1, 'int' ) ) );
160 $course_name = LP_Helper::sanitize_params_submitted( $data['course_name'] ?? '' );
161 $search = LP_Helper::sanitize_params_submitted( $data['search'] ?? '' );
162 $start_date = self::sanitize_date_filter( $data['start_date'] ?? '' );
163 $end_date = self::sanitize_date_filter( $data['end_date'] ?? '' );
164 $per_page = self::PER_PAGE;
165
166 // Normalize date range if request is reversed.
167 if ( $start_date && $end_date && strtotime( $start_date ) > strtotime( $end_date ) ) {
168 $tmp = $start_date;
169 $start_date = $end_date;
170 $end_date = $tmp;
171 }
172
173 $lp_db_user_items = UserItemsDB::getInstance();
174 $filter = new UserItemsFilter();
175 $filter->item_type = LP_COURSE_CPT;
176 $filter->statues = array( 'enrolled', 'finished' );
177 $filter->limit = $per_page;
178 $filter->page = $paged;
179 $filter->order_by = 'ui.start_time';
180 $filter->order = 'DESC';
181 $filter->field_count = 'ui.user_item_id';
182 $filter->only_fields = array(
183 'ui.user_item_id',
184 'ui.user_id',
185 'ui.item_id',
186 'ui.start_time',
187 'ui.status',
188 'ui.graduation',
189 'u.display_name',
190 'u.user_email',
191 'p.post_title AS course_title',
192 );
193 $filter->join[] = "JOIN {$lp_db_user_items->wpdb->posts} p ON ui.item_id = p.ID";
194 $filter->join[] = "JOIN {$lp_db_user_items->wpdb->users} u ON ui.user_id = u.ID";
195
196 if ( $instructor_id > 0 ) {
197 $filter->where[] = $lp_db_user_items->wpdb->prepare( 'AND p.post_author = %d', $instructor_id );
198 }
199
200 if ( $course_id > 0 ) {
201 $filter->item_id = $course_id;
202 } elseif ( ! empty( $course_name ) ) {
203 $course_name_like = '%' . $lp_db_user_items->wpdb->esc_like( $course_name ) . '%';
204 $filter->where[] = $lp_db_user_items->wpdb->prepare(
205 'AND p.post_title LIKE %s',
206 $course_name_like
207 );
208 }
209
210 if ( ! empty( $search ) ) {
211 $search_like = '%' . $lp_db_user_items->wpdb->esc_like( $search ) . '%';
212 $filter->where[] = $lp_db_user_items->wpdb->prepare(
213 'AND ( u.display_name LIKE %s OR u.user_email LIKE %s )',
214 $search_like,
215 $search_like
216 );
217 }
218
219 if ( ! empty( $start_date ) ) {
220 $filter->where[] = $lp_db_user_items->wpdb->prepare(
221 'AND ui.start_time >= %s',
222 $start_date . ' 00:00:00'
223 );
224 }
225
226 if ( ! empty( $end_date ) ) {
227 $filter->where[] = $lp_db_user_items->wpdb->prepare(
228 'AND ui.start_time <= %s',
229 $end_date . ' 23:59:59'
230 );
231 }
232
233 $total_rows = 0;
234 $rows = $lp_db_user_items->get_user_items( $filter, $total_rows );
235 if ( ! is_array( $rows ) ) {
236 $rows = array();
237 }
238
239 // Build HTML.
240 $html = self::instance()->html_table(
241 $rows,
242 array(
243 'total' => $total_rows,
244 'paged' => $paged,
245 'per_page' => $per_page,
246 'students-of-course' => $course_id,
247 )
248 );
249
250 $content->content = $html;
251 } catch ( Throwable $e ) {
252 $content->content = Template::print_message( $e->getMessage(), 'error', false );
253 LP_Debug::error_log( $e );
254 }
255
256 return $content;
257 }
258
259 /**
260 * Check whether user can view enrolled students content.
261 *
262 * @param int $user_id
263 *
264 * @return bool
265 */
266 private static function can_view_enrolled_students( int $user_id = 0 ): bool {
267 if ( $user_id < 1 ) {
268 $user_id = get_current_user_id();
269 }
270
271 if ( $user_id < 1 ) {
272 return false;
273 }
274
275 $user_model = UserModel::find( $user_id, true );
276 if ( ! $user_model instanceof UserModel ) {
277 return false;
278 }
279
280 return $user_model->is_instructor();
281 }
282
283 /**
284 * Resolve instructor_id from request by permission.
285 * Admin can keep requested instructor_id; instructor is forced to self.
286 *
287 * @param array $data
288 *
289 * @return int
290 */
291 private static function resolve_instructor_id_for_request( array $data ): int {
292 $current_user_id = get_current_user_id();
293 if ( ! self::can_view_enrolled_students( $current_user_id ) ) {
294 return 0;
295 }
296
297 $is_admin = user_can( $current_user_id, UserModel::ROLE_ADMINISTRATOR );
298 if ( $is_admin ) {
299 return max( 0, abs( LP_Helper::sanitize_params_submitted( $data['instructor_id'] ?? 0, 'int' ) ) );
300 }
301
302 return $current_user_id;
303 }
304
305 /**
306 * HTML builder: toolbar (course filter, search).
307 *
308 * @return string
309 */
310 public function html_toolbar(): string {
311 $courses = array();
312
313 $data_get = LP_Helper::sanitize_params_submitted( $_GET );
314 $selected_course = abs( LP_Helper::sanitize_params_submitted( $data_get['course_id'] ?? 0, 'int' ) );
315 $search_course = LP_Helper::sanitize_params_submitted( $data_get['course_name'] ?? '' );
316 $search_student = LP_Helper::sanitize_params_submitted( $data_get['search'] ?? '' );
317 $search_start = self::sanitize_date_filter( $data_get['start_date'] ?? '' );
318 $search_end = self::sanitize_date_filter( $data_get['end_date'] ?? '' );
319
320 $section = array(
321 'wrap' => '<form class="lp-enrolled-students-table-toolbar lp-enrolled-students-form" onsubmit="return false;">',
322 'filter-row-open' => '<div class="lp-enrolled-students-table-toolbar__row lp-enrolled-students-table-toolbar__row--filters">',
323 'course-field-open' => '<div class="lp-enrolled-students-table-toolbar__field">',
324 'course-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-enrolled-filter-course-name">' . esc_html__( 'Course Filter', 'learnpress' ) . '</label>',
325 'course-input' => '<input id="lp-enrolled-filter-course-name" class="lp-enrolled-filter-course-name lp-enrolled-students-table-toolbar__input" type="text" name="course_name" list="lp-enrolled-course-list" value="' . esc_attr( $search_course ) . '" placeholder="' . esc_attr__( 'Search course...', 'learnpress' ) . '">',
326 'course-field-close' => '</div>',
327 'student-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--student">',
328 'student-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-enrolled-search-input">' . esc_html__( 'Student', 'learnpress' ) . '</label>',
329 'student-input' => '<input id="lp-enrolled-search-input" class="lp-enrolled-search-input lp-enrolled-students-table-toolbar__input" type="text" name="search" value="' . esc_attr( $search_student ) . '" placeholder="' . esc_attr__( 'Enter student name or email', 'learnpress' ) . '">',
330 'student-field-close' => '</div>',
331 'start-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--date">',
332 'start-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-enrolled-filter-start-date">' . esc_html__( 'Start Date', 'learnpress' ) . '</label>',
333 'start-input' => '<input id="lp-enrolled-filter-start-date" class="lp-enrolled-filter-start-date lp-enrolled-students-table-toolbar__input" type="date" name="start_date" value="' . esc_attr( $search_start ) . '" placeholder="mm/dd/yyyy">',
334 'start-field-close' => '</div>',
335 'end-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--date">',
336 'end-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-enrolled-filter-end-date">' . esc_html__( 'End date', 'learnpress' ) . '</label>',
337 'end-input' => '<input id="lp-enrolled-filter-end-date" class="lp-enrolled-filter-end-date lp-enrolled-students-table-toolbar__input" type="date" name="end_date" value="' . esc_attr( $search_end ) . '" placeholder="mm/dd/yyyy">',
338 'end-field-close' => '</div>',
339 'filter-row-close' => '</div>',
340 'actions-open' => '<div class="lp-enrolled-students-table-toolbar__actions">',
341 'search-btn' => sprintf(
342 '<button type="button" class="lp-button lp-enrolled-btn-search">%s</button>',
343 esc_html__( 'Search', 'learnpress' )
344 ),
345 'clear-btn' => sprintf(
346 '<button type="button" class="lp-button lp-enrolled-btn-clear">%s</button>',
347 esc_html__( 'Clear Filter', 'learnpress' )
348 ),
349 'actions-close' => '</div>',
350 'wrap-end' => '</form>',
351 );
352 $section = apply_filters(
353 'learn-press/admin/enrolled-students/toolbar/section',
354 $section,
355 $courses,
356 $selected_course,
357 );
358
359 return Template::combine_components( $section );
360 }
361
362 /**
363 * Render toolbar used inside View Students modal.
364 *
365 * @return string
366 */
367 public function html_toolbar_modal(): string {
368
369 $section = array(
370 'wrap' => '<form class="lp-enrolled-students-table-toolbar lp-enrolled-students-table-toolbar--modal lp-enrolled-students-form lp-enrolled-students-form--modal" id="lp-modal-enrolled-form" onsubmit="return false;">',
371 'filter-row-open' => '<div class="lp-enrolled-students-table-toolbar__row lp-enrolled-students-table-toolbar__row--filters">',
372 'student-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--student">',
373 'student-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-modal-enrolled-search-input">' . esc_html__( 'Student', 'learnpress' ) . '</label>',
374 'student-input' => '<input id="lp-modal-enrolled-search-input" class="lp-enrolled-search-input lp-enrolled-students-table-toolbar__input" type="text" name="search" placeholder="' . esc_attr__( 'Enter student name or email', 'learnpress' ) . '">',
375 'student-field-close' => '</div>',
376 'start-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--date">',
377 'start-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-modal-enrolled-filter-start-date">' . esc_html__( 'Start Date', 'learnpress' ) . '</label>',
378 'start-input' => '<input id="lp-modal-enrolled-filter-start-date" class="lp-enrolled-filter-start-date lp-enrolled-students-table-toolbar__input" type="date" name="start_date" placeholder="mm/dd/yyyy">',
379 'start-field-close' => '</div>',
380 'end-field-open' => '<div class="lp-enrolled-students-table-toolbar__field lp-enrolled-students-table-toolbar__field--date">',
381 'end-label' => '<label class="lp-enrolled-students-table-toolbar__label" for="lp-modal-enrolled-filter-end-date">' . esc_html__( 'End Date', 'learnpress' ) . '</label>',
382 'end-input' => '<input id="lp-modal-enrolled-filter-end-date" class="lp-enrolled-filter-end-date lp-enrolled-students-table-toolbar__input" type="date" name="end_date" placeholder="mm/dd/yyyy">',
383 'end-field-close' => '</div>',
384 'filter-row-close' => '</div>',
385 'actions-open' => '<div class="lp-enrolled-students-table-toolbar__actions">',
386 'search-btn' => sprintf(
387 '<button type="button" class="lp-button lp-enrolled-btn-search-modal">%s</button>',
388 esc_html__( 'Search', 'learnpress' )
389 ),
390 'clear-btn' => sprintf(
391 '<button type="button" class="lp-button lp-enrolled-btn-clear-modal">%s</button>',
392 esc_html__( 'Clear Filter', 'learnpress' )
393 ),
394 'actions-close' => '</div>',
395 'wrap-end' => '</form>',
396 );
397
398 return Template::combine_components( $section );
399 }
400
401 /**
402 * Render lp-target layout used in View Students modal.
403 *
404 * @return string
405 */
406 public function html_modal_students_target(): string {
407 $args = array(
408 'id_url' => 'lp-modal-enrolled-students',
409 'course_id' => 0,
410 'paged' => 1,
411 'enableScrollToView' => false,
412 'enableUpdateParamsUrl' => false,
413 'html_no_load_ajax_first' => sprintf(
414 '<div class="learn-press-message" style="%s">%s</div>',
415 'width: 95%;',
416 esc_html__( 'Loading', 'learnpress' )
417 ),
418 );
419
420 $call_back = array(
421 'class' => self::class,
422 'method' => 'render_enrolled_students',
423 );
424
425 $section = array(
426 'wrap' => '<div id="lp-modal-enrolled-wrap">',
427 'target' => TemplateAJAX::load_content_via_ajax( $args, $call_back ),
428 'wrap-end' => '</div>',
429 );
430
431 return Template::combine_components( $section );
432 }
433
434 /**
435 * Print HTML template for modal toolbar.
436 *
437 * @return void
438 */
439 public function print_modal_toolbar_template() {
440
441 $should_print = false;
442
443 if ( is_admin() ) {
444 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
445 if ( $screen && isset( $screen->id ) && $screen->id === 'edit-' . LP_COURSE_CPT ) {
446 $should_print = true;
447 }
448 } elseif ( class_exists( 'LP_Page_Controller' ) && defined( 'LP_PAGE_PROFILE' ) ) {
449 $should_print = LP_Page_Controller::page_current() === LP_PAGE_PROFILE;
450 }
451
452 if ( ! $should_print ) {
453 return;
454 }
455
456 echo '<script type="text/html" id="lp-tmpl-enrolled-students-toolbar-modal">';
457 echo $this->html_toolbar_modal();
458 echo '</script>';
459
460 echo '<script type="text/html" id="lp-tmpl-enrolled-students-target-modal">';
461 echo $this->html_modal_students_target();
462 echo '</script>';
463 }
464
465 /**
466 * HTML builder: table wrapper.
467 *
468 * @param array $rows DB result rows.
469 * @param array $meta [ total, paged, per_page ].
470 *
471 * @return string
472 */
473 private function html_table( array $rows, array $meta ): string {
474 if ( empty( $rows ) ) {
475 $section_empty = array(
476 'wrap' => '<div class="lp-enrolled-students-table-wrap">',
477 'empty' => '<div class="lp-enrolled-empty"><p>' . esc_html__( 'No students found.', 'learnpress' ) . '</p></div>',
478 'wrap-end' => '</div>',
479 );
480 $section_empty = apply_filters(
481 'learn-press/admin/enrolled-students/table/empty/section',
482 $section_empty,
483 $rows,
484 $meta
485 );
486
487 return Template::combine_components( $section_empty );
488 }
489
490 $rows_html = array();
491
492 $results_map = array();
493 foreach ( $rows as $item ) {
494 $userCourseModel = new UserCourseModel( $item );
495 $rows_html[] = $this->html_student_row( $userCourseModel, $meta );
496 }
497 $rows_html = apply_filters(
498 'learn-press/admin/enrolled-students/table/rows-html',
499 $rows_html,
500 $rows,
501 $results_map,
502 $meta
503 );
504
505 $table_args = array(
506 'class_table' => 'lp-enrolled-students-table',
507 'header' => array(
508 'student' => array(
509 'class' => 'lp-col-student',
510 'title' => esc_html__( 'Student', 'learnpress' ),
511 ),
512 'course' => array(
513 'class' => 'lp-col-course',
514 'title' => esc_html__( 'Course', 'learnpress' ),
515 ),
516 'date' => array(
517 'class' => 'lp-col-date',
518 'title' => esc_html__( 'Enrolled Date', 'learnpress' ),
519 ),
520 'progress' => array(
521 'class' => 'lp-col-progress',
522 'title' => esc_html__( 'Progress', 'learnpress' ),
523 ),
524 'status' => array(
525 'class' => 'lp-col-status',
526 'title' => esc_html__( 'Status', 'learnpress' ),
527 ),
528 ),
529 'body' => array(
530 'rows_html' => Template::combine_components( $rows_html ),
531 ),
532 );
533 $section_footer = array(
534 'wrap' => '<div class="lp-enrolled-students-table-footer">',
535 'page_result' => sprintf(
536 '<span class="lp-enrolled-students-table-footer__count">%s</span>',
537 TableListTemplate::instance()->html_page_result(
538 array(
539 'paged' => $meta['paged'],
540 'per_page' => $meta['per_page'],
541 'total_rows' => $meta['total'],
542 'item_name' => _n( 'student', 'students', $meta['total'], 'learnpress' ),
543 )
544 )
545 ),
546 'pagination' => $this->html_pagination( $meta['total'], $meta['paged'], $meta['per_page'] ),
547 'wrap-end' => '</div>',
548 );
549 $section_footer = apply_filters(
550 'learn-press/admin/enrolled-students/table/footer/section',
551 $section_footer,
552 $rows,
553 $results_map,
554 $meta
555 );
556 $table_args['footer'] = Template::combine_components( $section_footer );
557 $table_args = apply_filters(
558 'learn-press/admin/enrolled-students/table/args',
559 $table_args,
560 $rows,
561 $results_map,
562 $meta
563 );
564
565 if ( ! empty( $meta['students-of-course'] ) ) {
566 unset( $table_args['header']['course'] ); // Remove Course column if filtering by course, as it's redundant.
567 }
568
569 $section = array(
570 'wrap' => '<div class="lp-enrolled-students-table-wrap">',
571 'table' => TableListTemplate::instance()->html_table( $table_args ),
572 'wrap-end' => '</div>',
573 );
574 $section = apply_filters(
575 'learn-press/admin/enrolled-students/table/section',
576 $section,
577 $rows,
578 $results_map,
579 $meta
580 );
581
582 return Template::combine_components( $section );
583 }
584
585 /**
586 * HTML builder: single student row.
587 *
588 * @param UserCourseModel $userCourseModel
589 *
590 * @return string
591 */
592 private function html_student_row( UserCourseModel $userCourseModel, array $data = [] ): string {
593 // Avatar initials.
594 $userModel = $userCourseModel->get_user_model();
595 if ( ! $userModel instanceof UserModel ) {
596 return '';
597 }
598
599 $courseModel = $userCourseModel->get_course_model();
600 if ( ! $courseModel instanceof CourseModel ) {
601 return '';
602 }
603
604 // Progress.
605 $course_result = $userCourseModel->calculate_course_results();
606 $progress = (float) ( $course_result['result'] ?? 0 );
607
608 // Status badge.
609 $graduation = $userCourseModel->get_graduation();
610 $status_raw = $graduation !== UserItemModel::GRADUATION_IN_PROGRESS
611 ? $userCourseModel->get_graduation()
612 : $userCourseModel->get_status();
613 $status_label = ucfirst( str_replace( array( '-', '_' ), ' ', $status_raw ) );
614 $badge_class = 'lp-badge--' . sanitize_html_class( $status_raw );
615
616 // Date.
617 $date = UserCourseTemplate::instance()->html_start_date_time( $userCourseModel, false );
618
619 $user_display_name = $userModel->get_display_name();
620 if ( empty( $user_display_name ) ) {
621 $user_display_name = $userModel->get_username();
622 }
623
624 $section = array(
625 'row' => '<tr>',
626 'student-cell-open' => '<td class="lp-cell-student">',
627 'avatar' => SingleInstructorTemplate::instance()->html_avatar( $userModel ),
628 'meta-open' => '<div class="lp-meta">',
629 'name' => sprintf(
630 '<span class="lp-name">%s</span>',
631 esc_html( $user_display_name )
632 ),
633 'email' => sprintf(
634 '<span class="lp-email">%s</span>',
635 esc_html( $userModel->get_email() )
636 ),
637 'meta-close' => '</div>',
638 'student-cell-close' => '</td>',
639 'course-cell' => sprintf(
640 '<td class="lp-cell-course"><a href="%s">%s</a></td>',
641 esc_url_raw( $courseModel->get_permalink() ),
642 $courseModel->get_title()
643 ),
644 'date-cell' => sprintf(
645 '<td class="lp-cell-date">%s</td>',
646 wp_kses_post( $date )
647 ),
648 'progress-cell-open' => '<td class="lp-cell-progress">',
649 'progress-bar' => sprintf(
650 '<div class="lp-progress-bar"><span class="" style="width: %d%%;"></span></div>',
651 $progress
652 ),
653 'progress-text' => sprintf(
654 '<span class="lp-progress-text">%d%%</span>',
655 $progress
656 ),
657 'progress-cell-close' => '</td>',
658 'status-cell-open' => '<td class="lp-cell-status">',
659 'status-badge' => sprintf(
660 '<span class="lp-badge %s">%s</span>',
661 esc_attr( $badge_class ),
662 esc_html( $status_label )
663 ),
664 'status-cell-close' => '</td>',
665 'row-end' => '</tr>',
666 );
667
668 if ( ! empty( $data['students-of-course'] ) ) {
669 unset( $section['course-cell'] ); // Remove Course cell if filtering by course, as it's redundant.
670 }
671
672 $section = apply_filters(
673 'learn-press/admin/enrolled-students/row/section',
674 $section,
675 $userCourseModel
676 );
677
678 return Template::combine_components( $section );
679 }
680
681 /**
682 * HTML builder: pagination.
683 *
684 * Uses .page-numbers class for loadAJAX.js compatibility.
685 *
686 * @param int $total
687 * @param int $paged
688 * @param int $per_page
689 *
690 * @return string
691 */
692 private function html_pagination( int $total, int $paged, int $per_page ): string {
693 $total_pages = max( 1, ceil( $total / $per_page ) );
694
695 $pagination_items = Template::instance()->html_pagination(
696 array(
697 'total_pages' => $total_pages,
698 'paged' => $paged,
699 'wrapper' => array(
700 '<nav class="learn-press-pagination navigation pagination lp-pagination">' => '</nav>',
701 ),
702 )
703 );
704 $pagination_items = apply_filters(
705 'learn-press/admin/enrolled-students/pagination/items',
706 $pagination_items,
707 $total,
708 $paged,
709 $per_page,
710 $total_pages
711 );
712
713 $section = array(
714 'wrap' => '<div class="lp-enrolled-students-table-footer__pagination">',
715 'pagination' => $pagination_items,
716 'wrap-end' => '</div>',
717 );
718 $section = apply_filters(
719 'learn-press/admin/enrolled-students/pagination/section',
720 $section,
721 $total,
722 $paged,
723 $per_page,
724 $total_pages
725 );
726
727 return Template::combine_components( $section );
728 }
729
730 /**
731 * Sanitize date filter value in Y-m-d format.
732 *
733 * @param string $date
734 *
735 * @return string
736 */
737 private static function sanitize_date_filter( $date ): string {
738 if ( ! is_scalar( $date ) ) {
739 return '';
740 }
741
742 $date = LP_Helper::sanitize_params_submitted( (string) $date );
743 if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
744 return $date;
745 }
746
747 return '';
748 }
749 }
750