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