PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 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 All 142 releases
← All changes | inc/TemplateHooks/CourseBuilder/Dashboard/BuilderDashboardTemplate.php +811 -811 4.3.8 → 4.4.10 View file →
@@ -1,811 +1,811 @@
1 -<?php
2 -/**
3 - * Template hooks Dashboard in Course Builder.
4 - *
5 - * @since 4.3.0
6 - * @version 2.0.1
7 - */
8 -
9 -namespace LearnPress\TemplateHooks\CourseBuilder\Dashboard;
10 -
11 -use LearnPress\TemplateHooks\CourseBuilder\BuilderPopupTemplate;
12 -use LearnPress\CourseBuilder\CourseBuilder;
13 -use LearnPress\Helpers\Singleton;
14 -use LearnPress\Helpers\Template;
15 -use LearnPress\Models\UserModel;
16 -use LearnPress\TemplateHooks\CourseBuilder\Course\BuilderListCoursesTemplate;
17 -use LearnPress\TemplateHooks\TemplateAJAX;
18 -use LP_Course_Filter;
19 -use LP_Statistics_DB;
20 -use LP_WP_Filesystem;
21 -use Throwable;
22 -
23 -class BuilderDashboardTemplate {
24 - use Singleton;
25 -
26 - public function init() {
27 - add_action( 'learn-press/course-builder/dashboard/layout', [ $this, 'layout' ] );
28 - }
29 -
30 - /**
31 - * Render dashboard layout.
32 - *
33 - * @param array $data
34 - *
35 - * @return void
36 - */
37 - public function layout( array $data = [] ) {
38 - $html = '';
39 -
40 - try {
41 - $user = $data['userModel'] ?? false;
42 - if ( ! $user instanceof UserModel ) {
43 - $user_id = get_current_user_id();
44 - if ( ! $user_id ) {
45 - return;
46 - }
47 -
48 - $user = UserModel::find( $user_id );
49 - if ( ! $user ) {
50 - return;
51 - }
52 - }
53 -
54 - $html = $this->html_content( $user );
55 - } catch ( Throwable $e ) {
56 - error_log( __METHOD__ . ': ' . $e->getMessage() );
57 - }
58 -
59 - echo Template::combine_components(
60 - [
61 - 'wrapper' => '<div class="lp-course-builder-dashboard">',
62 - 'header' => $this->html_header(),
63 - 'content' => $html,
64 - 'wrapper_end' => '</div>',
65 - ]
66 - );
67 - }
68 -
69 - private function html_header(): string {
70 - return Template::combine_components(
71 - [
72 - 'wrapper' => '<div class="cb-tab-header">',
73 - 'title' => sprintf( '<h2 class="lp-cb-tab__title">%s</h2>', __( 'Dashboard', 'learnpress' ) ),
74 - 'wrapper_end' => '</div>',
75 - ]
76 - );
77 - }
78 -
79 - /**
80 - * Render dashboard content.
81 - *
82 - * @param UserModel $user
83 - * @return string
84 - */
85 - private function html_content( UserModel $user ): string {
86 - $is_admin = user_can( $user->get_id(), 'administrator' );
87 -
88 - if ( $is_admin ) {
89 - $statistic = $this->get_admin_statistic();
90 - } else {
91 - $statistic = $user->get_instructor_statistic();
92 - }
93 -
94 - $stats_html = $this->html_statistics_cards( $statistic, $is_admin );
95 - $charts_html = $this->html_charts_section( $is_admin, $user->get_id() );
96 - $top_instructors_html = $is_admin ? $this->html_top_instructors_section() : '';
97 - $charts_row_class = $is_admin
98 - ? 'lp-cb-dashboard__charts-row lp-cb-dashboard__charts-row--admin'
99 - : 'lp-cb-dashboard__charts-row lp-cb-dashboard__charts-row--instructor';
100 - $top_courses_html = $this->html_top_courses_section( $is_admin ? 0 : $user->get_id() );
101 - $recent_courses_html = $this->html_recent_courses_section( $user );
102 - $quick_actions_html = $this->html_quick_actions();
103 -
104 - return Template::combine_components(
105 - [
106 - 'stats' => $stats_html,
107 - 'charts_row' => sprintf(
108 - '<div class="%s">%s%s</div>',
109 - esc_attr( $charts_row_class ),
110 - $charts_html,
111 - $top_instructors_html
112 - ),
113 - 'top_courses' => $top_courses_html,
114 - 'quick_actions' => $quick_actions_html,
115 - 'recent_courses' => $recent_courses_html,
116 - ]
117 - );
118 - }
119 -
120 - /**
121 - * Get global statistics for admin.
122 - *
123 - * @return array
124 - */
125 - private function get_admin_statistic(): array {
126 - $statistic = array(
127 - 'total_course' => 0,
128 - 'published_course' => 0,
129 - 'pending_course' => 0,
130 - 'total_student' => 0,
131 - 'total_instructor' => 0,
132 - );
133 -
134 - try {
135 - global $wpdb;
136 -
137 - // Course counts via wp_count_posts
138 - $course_counts = wp_count_posts( LP_COURSE_CPT );
139 -
140 - $statistic['total_course'] = intval( $course_counts->publish ?? 0 )
141 - + intval( $course_counts->pending ?? 0 )
142 - + intval( $course_counts->draft ?? 0 )
143 - + intval( $course_counts->private ?? 0 );
144 - $statistic['published_course'] = intval( $course_counts->publish ?? 0 );
145 - $statistic['pending_course'] = intval( $course_counts->pending ?? 0 );
146 -
147 - // Total unique students enrolled in any course
148 - $tb_user_items = $wpdb->prefix . 'learnpress_user_items';
149 - $total_students = $wpdb->get_var(
150 - $wpdb->prepare(
151 - "SELECT COUNT(DISTINCT user_id) FROM {$tb_user_items} WHERE item_type = %s",
152 - LP_COURSE_CPT
153 - )
154 - );
155 - $statistic['total_student'] = intval( $total_students );
156 -
157 - // Total instructors
158 - $statistic['total_instructor'] = $this->count_total_instructors();
159 - } catch ( Throwable $e ) {
160 - error_log( __METHOD__ . ': ' . $e->getMessage() );
161 - }
162 -
163 - return $statistic;
164 - }
165 -
166 - /**
167 - * Count total instructors (admin + lp_teacher).
168 - *
169 - * @return int
170 - */
171 - private function count_total_instructors(): int {
172 - global $wpdb;
173 -
174 - $result = $wpdb->get_var(
175 - $wpdb->prepare(
176 - "SELECT COUNT(DISTINCT u.ID)
177 - FROM {$wpdb->users} AS u
178 - INNER JOIN {$wpdb->usermeta} AS um ON um.user_id = u.ID
179 - WHERE um.meta_key = %s
180 - AND (um.meta_value LIKE %s OR um.meta_value LIKE %s)",
181 - $wpdb->prefix . 'capabilities',
182 - '%administrator%',
183 - '%' . LP_TEACHER_ROLE . '%'
184 - )
185 - );
186 -
187 - return intval( $result );
188 - }
189 -
190 - /**
191 - * Render statistics cards.
192 - *
193 - * @param array $statistic
194 - * @param bool $is_admin
195 - * @return string
196 - */
197 - private function html_statistics_cards( array $statistic, bool $is_admin ): string {
198 - $cards = [
199 - [
200 - 'key' => 'total_course',
201 - 'label' => __( 'Total Courses', 'learnpress' ),
202 - 'color' => '#ef4444',
203 - 'icon' => 'ico-cb-total-courses.svg',
204 - ],
205 - [
206 - 'key' => 'published_course',
207 - 'label' => __( 'Published Courses', 'learnpress' ),
208 - 'color' => '#2E91FA',
209 - 'icon' => 'ico-cb-published-courses.svg',
210 - ],
211 - [
212 - 'key' => 'pending_course',
213 - 'label' => __( 'Pending Courses', 'learnpress' ),
214 - 'color' => '#F8A100',
215 - 'icon' => 'ico-cb-pending-courses.svg',
216 - ],
217 - [
218 - 'key' => 'total_student',
219 - 'label' => __( 'Total Students', 'learnpress' ),
220 - 'color' => '#28A746',
221 - 'icon' => 'ico-cb-total-students.svg',
222 - ],
223 - ];
224 -
225 - // Role-based summary card
226 - if ( $is_admin ) {
227 - $cards[] = [
228 - 'key' => 'total_instructor',
229 - 'label' => __( 'Total Instructors', 'learnpress' ),
230 - 'color' => '#06AED4',
231 - 'icon' => 'ico-cb-total-instructors.svg',
232 - ];
233 - } else {
234 - $cards[] = [
235 - 'key' => 'student_in_progress',
236 - 'label' => __( 'Total In-progress Students', 'learnpress' ),
237 - 'color' => '#06AED4',
238 - 'icon' => 'ico-cb-students-progress.svg',
239 - ];
240 - }
241 -
242 - $cards_html = '';
243 - foreach ( $cards as $card ) {
244 - $value = $statistic[ $card['key'] ] ?? 0;
245 - $cards_html .= sprintf(
246 - '<div class="lp-cb-dashboard__stat-card" style="--card-color: %s; --card-bg: %s15">
247 - <div class="stat-card__icon">%s</div>
248 - <span class="stat-card__label">%s</span>
249 - <span class="stat-card__value">%s</span>
250 - </div>',
251 - esc_attr( $card['color'] ),
252 - esc_attr( $card['color'] ),
253 - LP_WP_Filesystem::get_icon_svg( $card['icon'] ?? '' ),
254 - esc_html( $card['label'] ),
255 - esc_html( number_format_i18n( $value ) )
256 - );
257 - }
258 -
259 - return Template::combine_components(
260 - [
261 - 'wrapper' => '<div class="lp-cb-dashboard__stats">',
262 - 'content' => $cards_html,
263 - 'wrapper_end' => '</div>',
264 - ]
265 - );
266 - }
267 -
268 - /**
269 - * Render charts section with Net Sales and Students charts.
270 - *
271 - * @param bool $is_admin
272 - * @param int $user_id
273 - * @return string
274 - */
275 - private function html_charts_section( bool $is_admin, int $user_id ): string {
276 - $instructor_id = $is_admin ? 0 : $user_id;
277 - $nonce = wp_create_nonce( 'lp_cb_dashboard_nonce' );
278 -
279 - // Get initial data - use 'year' for sales and 'previous_days' for students so data appears immediately
280 - try {
281 - $lp_statistic_db = LP_Statistics_DB::getInstance();
282 - $current_date = current_time( 'Y-m-d' );
283 -
284 - $sales_data = $lp_statistic_db->get_net_sales_data_scoped( 'year', $current_date, $instructor_id );
285 - $students_data = $lp_statistic_db->get_enrollment_chart_data( 'previous_days', 6, $instructor_id );
286 -
287 - // Process chart data using the REST controller's logic
288 - require_once LP_PLUGIN_PATH . 'inc/rest-api/v1/admin/class-lp-admin-rest-statistics-controller.php';
289 - $stats_ctrl = new \LP_REST_Admin_Statistics_Controller();
290 -
291 - $sales_chart = $stats_ctrl->process_chart_data(
292 - [
293 - 'filter_type' => 'year',
294 - 'time' => $current_date,
295 - ],
296 - $sales_data
297 - );
298 - $students_chart = $stats_ctrl->process_chart_data(
299 - [
300 - 'filter_type' => 'previous_days',
301 - 'time' => 6,
302 - ],
303 - $students_data
304 - );
305 - } catch ( Throwable $e ) {
306 - $sales_chart = [
307 - 'labels' => [],
308 - 'data' => [],
309 - ];
310 - $students_chart = [
311 - 'labels' => [],
312 - 'data' => [],
313 - ];
314 - error_log( __METHOD__ . ': ' . $e->getMessage() );
315 - }
316 -
317 - $filter_options = '
318 - <option value="this_month">' . esc_html__( 'This month', 'learnpress' ) . '</option>
319 - <option value="this_week">' . esc_html__( 'This week', 'learnpress' ) . '</option>
320 - <option value="this_year" selected>' . esc_html__( 'This year', 'learnpress' ) . '</option>';
321 -
322 - $html = sprintf(
323 - '<div class="lp-cb-dashboard__chart-card">
324 - <div class="chart-card__header">
325 - <h3 class="chart-card__title">%s</h3>
326 - <select class="chart-card__filter" data-chart="sales" data-nonce="%s">%s</select>
327 - </div>
328 - <div class="chart-card__body">
329 - <canvas id="lp-cb-chart-sales"></canvas>
330 - </div>
331 - </div>
332 - <div class="lp-cb-dashboard__chart-card">
333 - <div class="chart-card__header">
334 - <h3 class="chart-card__title">%s</h3>
335 - <select class="chart-card__filter" data-chart="students" data-nonce="%s">
336 - <option value="this_week" selected>%s</option>
337 - <option value="this_month">%s</option>
338 - <option value="this_year">%s</option>
339 - </select>
340 - </div>
341 - <div class="chart-card__body">
342 - <canvas id="lp-cb-chart-students"></canvas>
343 - </div>
344 - </div>
345 - <script id="lp-cb-dashboard-chart-data" type="application/json">%s</script>',
346 - esc_html__( 'Net sales', 'learnpress' ),
347 - esc_attr( $nonce ),
348 - $filter_options,
349 - esc_html__( 'Students', 'learnpress' ),
350 - esc_attr( $nonce ),
351 - esc_html__( 'This week', 'learnpress' ),
352 - esc_html__( 'This month', 'learnpress' ),
353 - esc_html__( 'This year', 'learnpress' ),
354 - wp_json_encode(
355 - [
356 - 'sales' => [
357 - 'labels' => $sales_chart['labels'] ?? [],
358 - 'data' => $sales_chart['data'] ?? [],
359 - ],
360 - 'students' => [
361 - 'labels' => $students_chart['labels'] ?? [],
362 - 'data' => $students_chart['data'] ?? [],
363 - ],
364 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
365 - 'nonce' => $nonce,
366 - ]
367 - )
368 - );
369 -
370 - return $html;
371 - }
372 -
373 - /**
374 - * Render top instructors section (admin only).
375 - *
376 - * @return string
377 - */
378 - private function html_top_instructors_section(): string {
379 - try {
380 - $lp_statistic_db = LP_Statistics_DB::getInstance();
381 - $instructors = $lp_statistic_db->get_top_instructors( 4 );
382 - } catch ( Throwable $e ) {
383 - $instructors = [];
384 - error_log( __METHOD__ . ': ' . $e->getMessage() );
385 - }
386 -
387 - $items_html = '';
388 - if ( empty( $instructors ) ) {
389 - $items_html = '<div class="no-data">' . esc_html__( 'No instructors found', 'learnpress' ) . '</div>';
390 - } else {
391 - foreach ( $instructors as $instructor ) {
392 - $avatar = get_avatar( $instructor->instructor_id, 40 );
393 - $items_html .= sprintf(
394 - '<div class="instructor-item">
395 - <div class="instructor-item__avatar">%s</div>
396 - <div class="instructor-item__info">
397 - <span class="instructor-item__name">%s</span>
398 - <span class="instructor-item__meta">%s &middot; %s</span>
399 - </div>
400 - </div>',
401 - $avatar,
402 - esc_html( $instructor->instructor_name ),
403 - sprintf(
404 - /* translators: %s: number of courses */
405 - esc_html( _n( '%s course', '%s courses', $instructor->course_count, 'learnpress' ) ),
406 - number_format_i18n( $instructor->course_count )
407 - ),
408 - sprintf(
409 - /* translators: %s: number of students */
410 - esc_html( _n( '%s student', '%s students', $instructor->student_count, 'learnpress' ) ),
411 - number_format_i18n( $instructor->student_count )
412 - )
413 - );
414 - }
415 - }
416 -
417 - return sprintf(
418 - '<div class="lp-cb-dashboard__top-instructors">
419 - <div class="top-instructors__header">
420 - <h3 class="top-instructors__title">
421 - %s
422 - </h3>
423 - </div>
424 - <div class="top-instructors__list">%s</div>
425 - </div>',
426 - esc_html__( 'Top Instructors', 'learnpress' ),
427 - $items_html
428 - );
429 - }
430 -
431 - /**
432 - * Render top courses section.
433 - *
434 - * @param int $user_id
435 - * @return string
436 - */
437 - private function html_top_courses_section( int $user_id ): string {
438 - $top_enrolled_html = $this->html_top_enrolled_courses( $user_id );
439 - $top_selling_html = $this->html_top_selling_courses( $user_id );
440 -
441 - return Template::combine_components(
442 - [
443 - 'wrapper' => '<div class="lp-cb-dashboard__top-courses-wrapper">',
444 - 'top_enrolled' => $top_enrolled_html,
445 - 'top_selling' => $top_selling_html,
446 - 'wrapper_end' => '</div>',
447 - ]
448 - );
449 - }
450 -
451 - /**
452 - * Render top enrolled courses with rich card layout.
453 - *
454 - * @param int $user_id
455 - * @return string
456 - */
457 - private function html_top_enrolled_courses( int $user_id ): string {
458 - try {
459 - $lp_statistic_db = LP_Statistics_DB::getInstance();
460 - $top_courses = $lp_statistic_db->get_top_enrolled_courses_by_instructor( $user_id, 3 );
461 - } catch ( Throwable $e ) {
462 - $top_courses = [];
463 - error_log( __METHOD__ . ': ' . $e->getMessage() );
464 - }
465 -
466 - $total_enrolled = 0;
467 - $items_html = '';
468 -
469 - if ( empty( $top_courses ) ) {
470 - $items_html = '<div class="no-data">' . esc_html__( 'No enrollment data available', 'learnpress' ) . '</div>';
471 - } else {
472 - foreach ( $top_courses as $course ) {
473 - $total_enrolled += intval( $course->enrollment_count );
474 - $thumbnail = get_the_post_thumbnail( $course->course_id, 'thumbnail' );
475 - if ( empty( $thumbnail ) ) {
476 - $thumbnail = sprintf(
477 - '<div class="course-item__thumb-placeholder">%s</div>',
478 - LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
479 - );
480 - }
481 -
482 - $categories = wp_get_post_terms( $course->course_id, 'course_category', array( 'fields' => 'names' ) );
483 - $category_text = '';
484 - if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
485 - $category_text = sprintf( ' %s <span class="category">%s</span>', esc_html__( 'in', 'learnpress' ), esc_html( implode( ', ', $categories ) ) );
486 - }
487 -
488 - $items_html .= sprintf(
489 - '<div class="course-item">
490 - <div class="course-item__thumb">%s</div>
491 - <div class="course-item__info">
492 - <a href="%s" class="course-item__title">%s</a>
493 - <span class="course-item__meta">%s <span class="author">%s</span>%s</span>
494 - </div>
495 - <div class="course-item__badge-wrapper">
496 - <span class="course-item__badge">
497 - %s
498 - </span>
499 - </div>
500 - </div>',
501 - $thumbnail,
502 - esc_url( get_permalink( $course->course_id ) ),
503 - esc_html( $course->course_name ),
504 - esc_html__( 'by', 'learnpress' ),
505 - esc_html( $course->instructor_name ?? '' ),
506 - $category_text,
507 - sprintf(
508 - /* translators: %s: number of students */
509 - esc_html( _n( '%s student', '%s students', $course->enrollment_count, 'learnpress' ) ),
510 - number_format_i18n( $course->enrollment_count )
511 - )
512 - );
513 - }
514 - }
515 -
516 - return sprintf(
517 - '<div class="lp-cb-dashboard__top-courses lp-cb-dashboard__top-enrolled">
518 - <div class="top-courses__header">
519 - <h3 class="top-courses__title">%s</h3>
520 - <span class="top-courses__total">%s <strong class="enrolled-students-total">%s</strong> %s</span>
521 - </div>
522 - <div class="top-courses__list">%s</div>
523 - </div>',
524 - esc_html__( 'Top Enrolled Courses', 'learnpress' ),
525 - esc_html__( 'Total:', 'learnpress' ),
526 - esc_html( number_format_i18n( $total_enrolled ) ),
527 - esc_html__( 'enrolled students', 'learnpress' ),
528 - $items_html
529 - );
530 - }
531 -
532 - /**
533 - * Render top selling courses with rich card layout.
534 - *
535 - * @param int $user_id
536 - * @return string
537 - */
538 - private function html_top_selling_courses( int $user_id ): string {
539 - try {
540 - $lp_statistic_db = LP_Statistics_DB::getInstance();
541 - $top_courses = $lp_statistic_db->get_top_sold_courses_by_instructor( $user_id, 3 );
542 - } catch ( Throwable $e ) {
543 - $top_courses = [];
544 - error_log( __METHOD__ . ': ' . $e->getMessage() );
545 - }
546 -
547 - $total_revenue = 0;
548 - $items_html = '';
549 -
550 - if ( empty( $top_courses ) ) {
551 - $items_html = '<div class="no-data">' . esc_html__( 'No sales data available', 'learnpress' ) . '</div>';
552 - } else {
553 - foreach ( $top_courses as $course ) {
554 - $total_revenue += floatval( $course->total_revenue ?? 0 );
555 - $thumbnail = get_the_post_thumbnail( $course->course_id, 'thumbnail' );
556 - if ( empty( $thumbnail ) ) {
557 - $thumbnail = sprintf(
558 - '<div class="course-item__thumb-placeholder">%s</div>',
559 - LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
560 - );
561 - }
562 -
563 - $currency_symbol = function_exists( 'learn_press_get_currency_symbol' ) ? learn_press_get_currency_symbol() : '$';
564 -
565 - $lp_course = function_exists( 'learn_press_get_course' ) ? learn_press_get_course( $course->course_id ) : false;
566 - $price_html = '';
567 - if ( $lp_course ) {
568 - $price = $lp_course->get_price();
569 - if ( $price > 0 ) {
570 - $price_html = learn_press_format_price( $price, true );
571 - } else {
572 - $price_html = esc_html__( 'Free', 'learnpress' );
573 - }
574 - }
575 -
576 - $items_html .= sprintf(
577 - '<div class="course-item">
578 - <div class="course-item__thumb">%s</div>
579 - <div class="course-item__info">
580 - <a href="%s" class="course-item__title">%s</a>
581 - <span class="course-item__meta">%s <span class="author">%s</span> &bull; %s</span>
582 - <span class="course-item__price">%s</span>
583 - </div>
584 - <div class="course-item__stats">
585 - <div class="course-item__revenue">%s: <strong class="revenue-amount">%s%s</strong></div>
586 - <div class="course-item__sold">%s %s</div>
587 - </div>
588 - </div>',
589 - $thumbnail,
590 - esc_url( get_permalink( $course->course_id ) ),
591 - esc_html( $course->course_name ),
592 - esc_html__( 'Instructor:', 'learnpress' ),
593 - esc_html( $course->instructor_name ?? '' ),
594 - sprintf(
595 - /* translators: %s: number of students */
596 - esc_html( _n( '%s student', '%s students', $course->course_count, 'learnpress' ) ),
597 - number_format_i18n( $course->course_count )
598 - ),
599 - $price_html,
600 - esc_html__( 'Revenue', 'learnpress' ),
601 - esc_html( $currency_symbol ),
602 - esc_html( number_format_i18n( $course->total_revenue ?? 0, 2 ) ),
603 - esc_html( number_format_i18n( $course->course_count ) ),
604 - esc_html__( 'sold', 'learnpress' )
605 - );
606 - }
607 - }
608 -
609 - $currency_symbol = function_exists( 'learn_press_get_currency_symbol' ) ? learn_press_get_currency_symbol() : '$';
610 -
611 - return sprintf(
612 - '<div class="lp-cb-dashboard__top-courses lp-cb-dashboard__top-selling">
613 - <div class="top-courses__header">
614 - <h3 class="top-courses__title">%s</h3>
615 - <span class="top-courses__total">%s <strong class="revenue-total">%s%s</strong> %s</span>
616 - </div>
617 - <div class="top-courses__list">%s</div>
618 - </div>',
619 - esc_html__( 'Top Selling Courses', 'learnpress' ),
620 - esc_html__( 'Total:', 'learnpress' ),
621 - esc_html( $currency_symbol ),
622 - esc_html( number_format_i18n( $total_revenue, 2 ) ),
623 - esc_html__( 'revenue', 'learnpress' ),
624 - $items_html
625 - );
626 - }
627 -
628 - /**
629 - * Render quick action buttons.
630 - *
631 - * @return string
632 - */
633 - private function html_quick_actions(): string {
634 - $create_lesson_template_id = 'lp-tmpl-builder-popup-lesson-dashboard-new';
635 - $create_lesson_template = sprintf(
636 - '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
637 - esc_attr( $create_lesson_template_id ),
638 - TemplateAJAX::load_content_via_ajax(
639 - [
640 - 'id_url' => 'builder-popup-lesson-dashboard-new',
641 - 'lesson_id' => 0,
642 - 'html_no_load_ajax_first' => sprintf(
643 - '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
644 - esc_html__( 'Loading...', 'learnpress' )
645 - ),
646 - ],
647 - [
648 - 'class' => BuilderPopupTemplate::class,
649 - 'method' => 'render_lesson_popup',
650 - ]
651 - )
652 - );
653 -
654 - $actions = [
655 - [
656 - 'label' => __( 'Create Course', 'learnpress' ),
657 - 'url' => CourseBuilder::get_link_add_new( 'courses' ),
658 - 'color' => '#ef4444',
659 - 'svg' => LP_WP_Filesystem::get_icon_svg( 'ico-courses-2.svg' ),
660 - ],
661 - [
662 - 'label' => __( 'Create Lesson', 'learnpress' ),
663 - 'attr' => 'data-add-new-lesson',
664 - 'popup_type' => 'lesson',
665 - 'popup_id' => 0,
666 - 'template_id' => $create_lesson_template_id,
667 - 'template_html' => $create_lesson_template,
668 - 'color' => '#7067ED',
669 - 'icon' => 'lp-icon-file-text-o',
670 - ],
671 - [
672 - 'label' => __( 'Create Quiz', 'learnpress' ),
673 - 'url' => CourseBuilder::get_link_add_new( 'quizzes' ),
674 - 'color' => '#f59e0b',
675 - 'icon' => 'lp-icon-puzzle-piece',
676 - ],
677 - [
678 - 'label' => __( 'Create Question', 'learnpress' ),
679 - 'url' => CourseBuilder::get_link_add_new( 'questions', CourseBuilder::POST_NEW, 'overview' ),
680 - 'color' => '#6b7280',
681 - 'icon' => 'lp-icon-question-circle-o',
682 - ],
683 - ];
684 - $actions = apply_filters( 'learn-press/course-builder/dashboard/quick-actions', $actions );
685 -
686 - $buttons_html = '';
687 - foreach ( $actions as $action ) {
688 - if ( ! empty( $action['attr'] ) || ! empty( $action['template_id'] ) ) {
689 - $button_attrs = [];
690 - $template_html = $action['template_html'] ?? '';
691 - if ( ! empty( $action['attr'] ) ) {
692 - $button_attrs[] = $action['attr'];
693 - }
694 - if ( ! empty( $action['template_id'] ) ) {
695 - $button_attrs[] = sprintf( 'data-template="#%s"', esc_attr( $action['template_id'] ) );
696 - }
697 - if ( ! empty( $action['popup_type'] ) ) {
698 - $button_attrs[] = sprintf( 'data-popup-type="%s"', esc_attr( $action['popup_type'] ) );
699 - }
700 - if ( isset( $action['popup_id'] ) ) {
701 - $button_attrs[] = sprintf( 'data-popup-id="%d"', absint( $action['popup_id'] ) );
702 - }
703 - $button_attrs = ! empty( $button_attrs ) ? ' ' . implode( ' ', $button_attrs ) : '';
704 -
705 - // Render as button with data attribute (opens popup)
706 - $buttons_html .= sprintf(
707 - '<button type="button"%s class="quick-action__btn" style="--action-color: %s; --action-bg: %s10">
708 - <span class="quick-action__icon %s">%s</span>
709 - <span class="quick-action__label">%s</span>
710 - </button>%s',
711 - $button_attrs,
712 - esc_attr( $action['color'] ),
713 - esc_attr( $action['color'] ),
714 - esc_attr( $action['icon'] ?? '' ),
715 - $action['svg'] ?? '',
716 - esc_html( $action['label'] ),
717 - $template_html
718 - );
719 - } else {
720 - // Render as link
721 - $buttons_html .= sprintf(
722 - '<a href="%s" class="quick-action__btn" style="--action-color: %s; --action-bg: %s10">
723 - <span class="quick-action__icon %s">%s</span>
724 - <span class="quick-action__label">%s</span>
725 - </a>',
726 - esc_url( $action['url'] ),
727 - esc_attr( $action['color'] ),
728 - esc_attr( $action['color'] ),
729 - $action['icon'] ?? '',
730 - $action['svg'] ?? '',
731 - esc_html( $action['label'] )
732 - );
733 - }
734 - }
735 -
736 - return sprintf(
737 - '<div class="lp-cb-dashboard__quick-actions">
738 - <h3 class="quick-actions__title">%s</h3>
739 - <div class="quick-actions__grid">%s</div>
740 - </div>',
741 - esc_html__( 'Quick Action', 'learnpress' ),
742 - $buttons_html
743 - );
744 - }
745 -
746 - /**
747 - * Render recent courses section using course items from Courses tab.
748 - *
749 - * @param UserModel $user
750 - * @return string
751 - */
752 - private function html_recent_courses_section( UserModel $user ): string {
753 - $content = '';
754 -
755 - try {
756 - $filter = new LP_Course_Filter();
757 - $filter->limit = 3;
758 - $filter->order_by = 'post_date';
759 - $filter->order = 'DESC';
760 - $filter->post_status = [ 'publish', 'pending', 'draft', 'private' ];
761 -
762 - if ( ! user_can( $user->get_id(), 'administrator' ) ) {
763 - $filter->post_author = $user->get_id();
764 - }
765 -
766 - $total_courses = 0;
767 - $courses = \LearnPress\Models\Courses::get_courses( $filter, $total_courses );
768 -
769 - $list_html = '';
770 - if ( ! empty( $courses ) ) {
771 - $html_list_course = '';
772 - foreach ( $courses as $course_obj ) {
773 - $course = \LearnPress\Models\CourseModel::find( $course_obj->ID, true );
774 - if ( $course ) {
775 - // Reuse course item from Courses tab
776 - $html_list_course .= BuilderListCoursesTemplate::render_course( $course );
777 - }
778 - }
779 -
780 - if ( ! empty( $html_list_course ) ) {
781 - $list_html = Template::combine_components(
782 - [
783 - 'wrapper' => '<div class="courses-builder__course-tab learn-press-courses"><ul class="cb-list-course">',
784 - 'list_course' => $html_list_course,
785 - 'wrapper_end' => '</ul></div>',
786 - ]
787 - );
788 - }
789 - }
790 -
791 - if ( empty( $list_html ) ) {
792 - $list_html = '<div class="no-data">' . esc_html__( 'No recent courses found', 'learnpress' ) . '</div>';
793 - }
794 -
795 - $content = sprintf(
796 - '<div class="lp-cb-dashboard__recent-courses" style="margin-top: 30px;">
797 - <div class="recent-courses__header">
798 - <h3 class="recent-courses__title">%s</h3>
799 - </div>
800 - <div class="recent-courses__list">%s</div>
801 - </div>',
802 - esc_html__( 'Recent Courses', 'learnpress' ),
803 - $list_html
804 - );
805 - } catch ( Throwable $e ) {
806 - error_log( __METHOD__ . ': ' . $e->getMessage() );
807 - }
808 -
809 - return $content;
810 - }
811 -}
1 +<?php
2 +/**
3 + * Template hooks Dashboard in Course Builder.
4 + *
5 + * @since 4.3.0
6 + * @version 2.0.1
7 + */
8 +
9 +namespace LearnPress\TemplateHooks\CourseBuilder\Dashboard;
10 +
11 +use LearnPress\TemplateHooks\CourseBuilder\BuilderPopupTemplate;
12 +use LearnPress\CourseBuilder\CourseBuilder;
13 +use LearnPress\Helpers\Singleton;
14 +use LearnPress\Helpers\Template;
15 +use LearnPress\Models\UserModel;
16 +use LearnPress\TemplateHooks\CourseBuilder\Course\BuilderListCoursesTemplate;
17 +use LearnPress\TemplateHooks\TemplateAJAX;
18 +use LP_Course_Filter;
19 +use LP_Statistics_DB;
20 +use LP_WP_Filesystem;
21 +use Throwable;
22 +
23 +class BuilderDashboardTemplate {
24 + use Singleton;
25 +
26 + public function init() {
27 + add_action( 'learn-press/course-builder/dashboard/layout', [ $this, 'layout' ] );
28 + }
29 +
30 + /**
31 + * Render dashboard layout.
32 + *
33 + * @param array $data
34 + *
35 + * @return void
36 + */
37 + public function layout( array $data = [] ) {
38 + $html = '';
39 +
40 + try {
41 + $user = $data['userModel'] ?? false;
42 + if ( ! $user instanceof UserModel ) {
43 + $user_id = get_current_user_id();
44 + if ( ! $user_id ) {
45 + return;
46 + }
47 +
48 + $user = UserModel::find( $user_id );
49 + if ( ! $user ) {
50 + return;
51 + }
52 + }
53 +
54 + $html = $this->html_content( $user );
55 + } catch ( Throwable $e ) {
56 + error_log( __METHOD__ . ': ' . $e->getMessage() );
57 + }
58 +
59 + echo Template::combine_components(
60 + [
61 + 'wrapper' => '<div class="lp-course-builder-dashboard">',
62 + 'header' => $this->html_header(),
63 + 'content' => $html,
64 + 'wrapper_end' => '</div>',
65 + ]
66 + );
67 + }
68 +
69 + private function html_header(): string {
70 + return Template::combine_components(
71 + [
72 + 'wrapper' => '<div class="cb-tab-header">',
73 + 'title' => sprintf( '<h2 class="lp-cb-tab__title">%s</h2>', __( 'Dashboard', 'learnpress' ) ),
74 + 'wrapper_end' => '</div>',
75 + ]
76 + );
77 + }
78 +
79 + /**
80 + * Render dashboard content.
81 + *
82 + * @param UserModel $user
83 + * @return string
84 + */
85 + private function html_content( UserModel $user ): string {
86 + $is_admin = user_can( $user->get_id(), 'administrator' );
87 +
88 + if ( $is_admin ) {
89 + $statistic = $this->get_admin_statistic();
90 + } else {
91 + $statistic = $user->get_instructor_statistic();
92 + }
93 +
94 + $stats_html = $this->html_statistics_cards( $statistic, $is_admin );
95 + $charts_html = $this->html_charts_section( $is_admin, $user->get_id() );
96 + $top_instructors_html = $is_admin ? $this->html_top_instructors_section() : '';
97 + $charts_row_class = $is_admin
98 + ? 'lp-cb-dashboard__charts-row lp-cb-dashboard__charts-row--admin'
99 + : 'lp-cb-dashboard__charts-row lp-cb-dashboard__charts-row--instructor';
100 + $top_courses_html = $this->html_top_courses_section( $is_admin ? 0 : $user->get_id() );
101 + $recent_courses_html = $this->html_recent_courses_section( $user );
102 + $quick_actions_html = $this->html_quick_actions();
103 +
104 + return Template::combine_components(
105 + [
106 + 'stats' => $stats_html,
107 + 'charts_row' => sprintf(
108 + '<div class="%s">%s%s</div>',
109 + esc_attr( $charts_row_class ),
110 + $charts_html,
111 + $top_instructors_html
112 + ),
113 + 'top_courses' => $top_courses_html,
114 + 'quick_actions' => $quick_actions_html,
115 + 'recent_courses' => $recent_courses_html,
116 + ]
117 + );
118 + }
119 +
120 + /**
121 + * Get global statistics for admin.
122 + *
123 + * @return array
124 + */
125 + private function get_admin_statistic(): array {
126 + $statistic = array(
127 + 'total_course' => 0,
128 + 'published_course' => 0,
129 + 'pending_course' => 0,
130 + 'total_student' => 0,
131 + 'total_instructor' => 0,
132 + );
133 +
134 + try {
135 + global $wpdb;
136 +
137 + // Course counts via wp_count_posts
138 + $course_counts = wp_count_posts( LP_COURSE_CPT );
139 +
140 + $statistic['total_course'] = intval( $course_counts->publish ?? 0 )
141 + + intval( $course_counts->pending ?? 0 )
142 + + intval( $course_counts->draft ?? 0 )
143 + + intval( $course_counts->private ?? 0 );
144 + $statistic['published_course'] = intval( $course_counts->publish ?? 0 );
145 + $statistic['pending_course'] = intval( $course_counts->pending ?? 0 );
146 +
147 + // Total unique students enrolled in any course
148 + $tb_user_items = $wpdb->prefix . 'learnpress_user_items';
149 + $total_students = $wpdb->get_var(
150 + $wpdb->prepare(
151 + "SELECT COUNT(DISTINCT user_id) FROM {$tb_user_items} WHERE item_type = %s",
152 + LP_COURSE_CPT
153 + )
154 + );
155 + $statistic['total_student'] = intval( $total_students );
156 +
157 + // Total instructors
158 + $statistic['total_instructor'] = $this->count_total_instructors();
159 + } catch ( Throwable $e ) {
160 + error_log( __METHOD__ . ': ' . $e->getMessage() );
161 + }
162 +
163 + return $statistic;
164 + }
165 +
166 + /**
167 + * Count total instructors (admin + lp_teacher).
168 + *
169 + * @return int
170 + */
171 + private function count_total_instructors(): int {
172 + global $wpdb;
173 +
174 + $result = $wpdb->get_var(
175 + $wpdb->prepare(
176 + "SELECT COUNT(DISTINCT u.ID)
177 + FROM {$wpdb->users} AS u
178 + INNER JOIN {$wpdb->usermeta} AS um ON um.user_id = u.ID
179 + WHERE um.meta_key = %s
180 + AND (um.meta_value LIKE %s OR um.meta_value LIKE %s)",
181 + $wpdb->prefix . 'capabilities',
182 + '%administrator%',
183 + '%' . LP_TEACHER_ROLE . '%'
184 + )
185 + );
186 +
187 + return intval( $result );
188 + }
189 +
190 + /**
191 + * Render statistics cards.
192 + *
193 + * @param array $statistic
194 + * @param bool $is_admin
195 + * @return string
196 + */
197 + private function html_statistics_cards( array $statistic, bool $is_admin ): string {
198 + $cards = [
199 + [
200 + 'key' => 'total_course',
201 + 'label' => __( 'Total Courses', 'learnpress' ),
202 + 'color' => '#ef4444',
203 + 'icon' => 'ico-cb-total-courses.svg',
204 + ],
205 + [
206 + 'key' => 'published_course',
207 + 'label' => __( 'Published Courses', 'learnpress' ),
208 + 'color' => '#2E91FA',
209 + 'icon' => 'ico-cb-published-courses.svg',
210 + ],
211 + [
212 + 'key' => 'pending_course',
213 + 'label' => __( 'Pending Courses', 'learnpress' ),
214 + 'color' => '#F8A100',
215 + 'icon' => 'ico-cb-pending-courses.svg',
216 + ],
217 + [
218 + 'key' => 'total_student',
219 + 'label' => __( 'Total Students', 'learnpress' ),
220 + 'color' => '#28A746',
221 + 'icon' => 'ico-cb-total-students.svg',
222 + ],
223 + ];
224 +
225 + // Role-based summary card
226 + if ( $is_admin ) {
227 + $cards[] = [
228 + 'key' => 'total_instructor',
229 + 'label' => __( 'Total Instructors', 'learnpress' ),
230 + 'color' => '#06AED4',
231 + 'icon' => 'ico-cb-total-instructors.svg',
232 + ];
233 + } else {
234 + $cards[] = [
235 + 'key' => 'student_in_progress',
236 + 'label' => __( 'Total In-progress Students', 'learnpress' ),
237 + 'color' => '#06AED4',
238 + 'icon' => 'ico-cb-students-progress.svg',
239 + ];
240 + }
241 +
242 + $cards_html = '';
243 + foreach ( $cards as $card ) {
244 + $value = $statistic[ $card['key'] ] ?? 0;
245 + $cards_html .= sprintf(
246 + '<div class="lp-cb-dashboard__stat-card" style="--card-color: %s; --card-bg: %s15">
247 + <div class="stat-card__icon">%s</div>
248 + <span class="stat-card__label">%s</span>
249 + <span class="stat-card__value">%s</span>
250 + </div>',
251 + esc_attr( $card['color'] ),
252 + esc_attr( $card['color'] ),
253 + LP_WP_Filesystem::get_icon_svg( $card['icon'] ?? '' ),
254 + esc_html( $card['label'] ),
255 + esc_html( number_format_i18n( $value ) )
256 + );
257 + }
258 +
259 + return Template::combine_components(
260 + [
261 + 'wrapper' => '<div class="lp-cb-dashboard__stats">',
262 + 'content' => $cards_html,
263 + 'wrapper_end' => '</div>',
264 + ]
265 + );
266 + }
267 +
268 + /**
269 + * Render charts section with Net Sales and Students charts.
270 + *
271 + * @param bool $is_admin
272 + * @param int $user_id
273 + * @return string
274 + */
275 + private function html_charts_section( bool $is_admin, int $user_id ): string {
276 + $instructor_id = $is_admin ? 0 : $user_id;
277 + $nonce = wp_create_nonce( 'lp_cb_dashboard_nonce' );
278 +
279 + // Get initial data - use 'year' for sales and 'previous_days' for students so data appears immediately
280 + try {
281 + $lp_statistic_db = LP_Statistics_DB::getInstance();
282 + $current_date = current_time( 'Y-m-d' );
283 +
284 + $sales_data = $lp_statistic_db->get_net_sales_data_scoped( 'year', $current_date, $instructor_id );
285 + $students_data = $lp_statistic_db->get_enrollment_chart_data( 'previous_days', 6, $instructor_id );
286 +
287 + // Process chart data using the REST controller's logic
288 + require_once LP_PLUGIN_PATH . 'inc/rest-api/v1/admin/class-lp-admin-rest-statistics-controller.php';
289 + $stats_ctrl = new \LP_REST_Admin_Statistics_Controller();
290 +
291 + $sales_chart = $stats_ctrl->process_chart_data(
292 + [
293 + 'filter_type' => 'year',
294 + 'time' => $current_date,
295 + ],
296 + $sales_data
297 + );
298 + $students_chart = $stats_ctrl->process_chart_data(
299 + [
300 + 'filter_type' => 'previous_days',
301 + 'time' => 6,
302 + ],
303 + $students_data
304 + );
305 + } catch ( Throwable $e ) {
306 + $sales_chart = [
307 + 'labels' => [],
308 + 'data' => [],
309 + ];
310 + $students_chart = [
311 + 'labels' => [],
312 + 'data' => [],
313 + ];
314 + error_log( __METHOD__ . ': ' . $e->getMessage() );
315 + }
316 +
317 + $filter_options = '
318 + <option value="this_month">' . esc_html__( 'This month', 'learnpress' ) . '</option>
319 + <option value="this_week">' . esc_html__( 'This week', 'learnpress' ) . '</option>
320 + <option value="this_year" selected>' . esc_html__( 'This year', 'learnpress' ) . '</option>';
321 +
322 + $html = sprintf(
323 + '<div class="lp-cb-dashboard__chart-card">
324 + <div class="chart-card__header">
325 + <h3 class="chart-card__title">%s</h3>
326 + <select class="chart-card__filter" data-chart="sales" data-nonce="%s">%s</select>
327 + </div>
328 + <div class="chart-card__body">
329 + <canvas id="lp-cb-chart-sales"></canvas>
330 + </div>
331 + </div>
332 + <div class="lp-cb-dashboard__chart-card">
333 + <div class="chart-card__header">
334 + <h3 class="chart-card__title">%s</h3>
335 + <select class="chart-card__filter" data-chart="students" data-nonce="%s">
336 + <option value="this_week" selected>%s</option>
337 + <option value="this_month">%s</option>
338 + <option value="this_year">%s</option>
339 + </select>
340 + </div>
341 + <div class="chart-card__body">
342 + <canvas id="lp-cb-chart-students"></canvas>
343 + </div>
344 + </div>
345 + <script id="lp-cb-dashboard-chart-data" type="application/json">%s</script>',
346 + esc_html__( 'Net sales', 'learnpress' ),
347 + esc_attr( $nonce ),
348 + $filter_options,
349 + esc_html__( 'Students', 'learnpress' ),
350 + esc_attr( $nonce ),
351 + esc_html__( 'This week', 'learnpress' ),
352 + esc_html__( 'This month', 'learnpress' ),
353 + esc_html__( 'This year', 'learnpress' ),
354 + wp_json_encode(
355 + [
356 + 'sales' => [
357 + 'labels' => $sales_chart['labels'] ?? [],
358 + 'data' => $sales_chart['data'] ?? [],
359 + ],
360 + 'students' => [
361 + 'labels' => $students_chart['labels'] ?? [],
362 + 'data' => $students_chart['data'] ?? [],
363 + ],
364 + 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
365 + 'nonce' => $nonce,
366 + ]
367 + )
368 + );
369 +
370 + return $html;
371 + }
372 +
373 + /**
374 + * Render top instructors section (admin only).
375 + *
376 + * @return string
377 + */
378 + private function html_top_instructors_section(): string {
379 + try {
380 + $lp_statistic_db = LP_Statistics_DB::getInstance();
381 + $instructors = $lp_statistic_db->get_top_instructors( 4 );
382 + } catch ( Throwable $e ) {
383 + $instructors = [];
384 + error_log( __METHOD__ . ': ' . $e->getMessage() );
385 + }
386 +
387 + $items_html = '';
388 + if ( empty( $instructors ) ) {
389 + $items_html = '<div class="no-data">' . esc_html__( 'No instructors found', 'learnpress' ) . '</div>';
390 + } else {
391 + foreach ( $instructors as $instructor ) {
392 + $avatar = get_avatar( $instructor->instructor_id, 40 );
393 + $items_html .= sprintf(
394 + '<div class="instructor-item">
395 + <div class="instructor-item__avatar">%s</div>
396 + <div class="instructor-item__info">
397 + <span class="instructor-item__name">%s</span>
398 + <span class="instructor-item__meta">%s &middot; %s</span>
399 + </div>
400 + </div>',
401 + $avatar,
402 + esc_html( $instructor->instructor_name ),
403 + sprintf(
404 + /* translators: %s: number of courses */
405 + esc_html( _n( '%s course', '%s courses', $instructor->course_count, 'learnpress' ) ),
406 + number_format_i18n( $instructor->course_count )
407 + ),
408 + sprintf(
409 + /* translators: %s: number of students */
410 + esc_html( _n( '%s student', '%s students', $instructor->student_count, 'learnpress' ) ),
411 + number_format_i18n( $instructor->student_count )
412 + )
413 + );
414 + }
415 + }
416 +
417 + return sprintf(
418 + '<div class="lp-cb-dashboard__top-instructors">
419 + <div class="top-instructors__header">
420 + <h3 class="top-instructors__title">
421 + %s
422 + </h3>
423 + </div>
424 + <div class="top-instructors__list">%s</div>
425 + </div>',
426 + esc_html__( 'Top Instructors', 'learnpress' ),
427 + $items_html
428 + );
429 + }
430 +
431 + /**
432 + * Render top courses section.
433 + *
434 + * @param int $user_id
435 + * @return string
436 + */
437 + private function html_top_courses_section( int $user_id ): string {
438 + $top_enrolled_html = $this->html_top_enrolled_courses( $user_id );
439 + $top_selling_html = $this->html_top_selling_courses( $user_id );
440 +
441 + return Template::combine_components(
442 + [
443 + 'wrapper' => '<div class="lp-cb-dashboard__top-courses-wrapper">',
444 + 'top_enrolled' => $top_enrolled_html,
445 + 'top_selling' => $top_selling_html,
446 + 'wrapper_end' => '</div>',
447 + ]
448 + );
449 + }
450 +
451 + /**
452 + * Render top enrolled courses with rich card layout.
453 + *
454 + * @param int $user_id
455 + * @return string
456 + */
457 + private function html_top_enrolled_courses( int $user_id ): string {
458 + try {
459 + $lp_statistic_db = LP_Statistics_DB::getInstance();
460 + $top_courses = $lp_statistic_db->get_top_enrolled_courses_by_instructor( $user_id, 3 );
461 + } catch ( Throwable $e ) {
462 + $top_courses = [];
463 + error_log( __METHOD__ . ': ' . $e->getMessage() );
464 + }
465 +
466 + $total_enrolled = 0;
467 + $items_html = '';
468 +
469 + if ( empty( $top_courses ) ) {
470 + $items_html = '<div class="no-data">' . esc_html__( 'No enrollment data available', 'learnpress' ) . '</div>';
471 + } else {
472 + foreach ( $top_courses as $course ) {
473 + $total_enrolled += intval( $course->enrollment_count );
474 + $thumbnail = get_the_post_thumbnail( $course->course_id, 'thumbnail' );
475 + if ( empty( $thumbnail ) ) {
476 + $thumbnail = sprintf(
477 + '<div class="course-item__thumb-placeholder">%s</div>',
478 + LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
479 + );
480 + }
481 +
482 + $categories = wp_get_post_terms( $course->course_id, 'course_category', array( 'fields' => 'names' ) );
483 + $category_text = '';
484 + if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
485 + $category_text = sprintf( ' %s <span class="category">%s</span>', esc_html__( 'in', 'learnpress' ), esc_html( implode( ', ', $categories ) ) );
486 + }
487 +
488 + $items_html .= sprintf(
489 + '<div class="course-item">
490 + <div class="course-item__thumb">%s</div>
491 + <div class="course-item__info">
492 + <a href="%s" class="course-item__title">%s</a>
493 + <span class="course-item__meta">%s <span class="author">%s</span>%s</span>
494 + </div>
495 + <div class="course-item__badge-wrapper">
496 + <span class="course-item__badge">
497 + %s
498 + </span>
499 + </div>
500 + </div>',
501 + $thumbnail,
502 + esc_url( get_permalink( $course->course_id ) ),
503 + esc_html( $course->course_name ),
504 + esc_html__( 'by', 'learnpress' ),
505 + esc_html( $course->instructor_name ?? '' ),
506 + $category_text,
507 + sprintf(
508 + /* translators: %s: number of students */
509 + esc_html( _n( '%s student', '%s students', $course->enrollment_count, 'learnpress' ) ),
510 + number_format_i18n( $course->enrollment_count )
511 + )
512 + );
513 + }
514 + }
515 +
516 + return sprintf(
517 + '<div class="lp-cb-dashboard__top-courses lp-cb-dashboard__top-enrolled">
518 + <div class="top-courses__header">
519 + <h3 class="top-courses__title">%s</h3>
520 + <span class="top-courses__total">%s <strong class="enrolled-students-total">%s</strong> %s</span>
521 + </div>
522 + <div class="top-courses__list">%s</div>
523 + </div>',
524 + esc_html__( 'Top Enrolled Courses', 'learnpress' ),
525 + esc_html__( 'Total:', 'learnpress' ),
526 + esc_html( number_format_i18n( $total_enrolled ) ),
527 + esc_html__( 'students', 'learnpress' ),
528 + $items_html
529 + );
530 + }
531 +
532 + /**
533 + * Render top selling courses with rich card layout.
534 + *
535 + * @param int $user_id
536 + * @return string
537 + */
538 + private function html_top_selling_courses( int $user_id ): string {
539 + try {
540 + $lp_statistic_db = LP_Statistics_DB::getInstance();
541 + $top_courses = $lp_statistic_db->get_top_sold_courses_by_instructor( $user_id, 3 );
542 + } catch ( Throwable $e ) {
543 + $top_courses = [];
544 + error_log( __METHOD__ . ': ' . $e->getMessage() );
545 + }
546 +
547 + $total_revenue = 0;
548 + $items_html = '';
549 +
550 + if ( empty( $top_courses ) ) {
551 + $items_html = '<div class="no-data">' . esc_html__( 'No sales data available', 'learnpress' ) . '</div>';
552 + } else {
553 + foreach ( $top_courses as $course ) {
554 + $total_revenue += floatval( $course->total_revenue ?? 0 );
555 + $thumbnail = get_the_post_thumbnail( $course->course_id, 'thumbnail' );
556 + if ( empty( $thumbnail ) ) {
557 + $thumbnail = sprintf(
558 + '<div class="course-item__thumb-placeholder">%s</div>',
559 + LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
560 + );
561 + }
562 +
563 + $currency_symbol = function_exists( 'learn_press_get_currency_symbol' ) ? learn_press_get_currency_symbol() : '$';
564 +
565 + $lp_course = function_exists( 'learn_press_get_course' ) ? learn_press_get_course( $course->course_id ) : false;
566 + $price_html = '';
567 + if ( $lp_course ) {
568 + $price = $lp_course->get_price();
569 + if ( $price > 0 ) {
570 + $price_html = learn_press_format_price( $price, true );
571 + } else {
572 + $price_html = esc_html__( 'Free', 'learnpress' );
573 + }
574 + }
575 +
576 + $items_html .= sprintf(
577 + '<div class="course-item">
578 + <div class="course-item__thumb">%s</div>
579 + <div class="course-item__info">
580 + <a href="%s" class="course-item__title">%s</a>
581 + <span class="course-item__meta">%s <span class="author">%s</span> &bull; %s</span>
582 + <span class="course-item__price">%s</span>
583 + </div>
584 + <div class="course-item__stats">
585 + <div class="course-item__revenue">%s: <strong class="revenue-amount">%s%s</strong></div>
586 + <div class="course-item__sold">%s %s</div>
587 + </div>
588 + </div>',
589 + $thumbnail,
590 + esc_url( get_permalink( $course->course_id ) ),
591 + esc_html( $course->course_name ),
592 + esc_html__( 'Instructor:', 'learnpress' ),
593 + esc_html( $course->instructor_name ?? '' ),
594 + sprintf(
595 + /* translators: %s: number of students */
596 + esc_html( _n( '%s student', '%s students', $course->course_count, 'learnpress' ) ),
597 + number_format_i18n( $course->course_count )
598 + ),
599 + $price_html,
600 + esc_html__( 'Revenue', 'learnpress' ),
601 + esc_html( $currency_symbol ),
602 + esc_html( number_format_i18n( $course->total_revenue ?? 0, 2 ) ),
603 + esc_html( number_format_i18n( $course->course_count ) ),
604 + esc_html__( 'sold', 'learnpress' )
605 + );
606 + }
607 + }
608 +
609 + $currency_symbol = function_exists( 'learn_press_get_currency_symbol' ) ? learn_press_get_currency_symbol() : '$';
610 +
611 + return sprintf(
612 + '<div class="lp-cb-dashboard__top-courses lp-cb-dashboard__top-selling">
613 + <div class="top-courses__header">
614 + <h3 class="top-courses__title">%s</h3>
615 + <span class="top-courses__total">%s <strong class="revenue-total">%s%s</strong> %s</span>
616 + </div>
617 + <div class="top-courses__list">%s</div>
618 + </div>',
619 + esc_html__( 'Top Selling Courses', 'learnpress' ),
620 + esc_html__( 'Total:', 'learnpress' ),
621 + esc_html( $currency_symbol ),
622 + esc_html( number_format_i18n( $total_revenue, 2 ) ),
623 + esc_html__( 'revenue', 'learnpress' ),
624 + $items_html
625 + );
626 + }
627 +
628 + /**
629 + * Render quick action buttons.
630 + *
631 + * @return string
632 + */
633 + private function html_quick_actions(): string {
634 + $create_lesson_template_id = 'lp-tmpl-builder-popup-lesson-dashboard-new';
635 + $create_lesson_template = sprintf(
636 + '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
637 + esc_attr( $create_lesson_template_id ),
638 + TemplateAJAX::load_content_via_ajax(
639 + [
640 + 'id_url' => 'builder-popup-lesson-dashboard-new',
641 + 'lesson_id' => 0,
642 + 'html_no_load_ajax_first' => sprintf(
643 + '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
644 + esc_html__( 'Loading...', 'learnpress' )
645 + ),
646 + ],
647 + [
648 + 'class' => BuilderPopupTemplate::class,
649 + 'method' => 'render_lesson_popup',
650 + ]
651 + )
652 + );
653 +
654 + $actions = [
655 + [
656 + 'label' => __( 'Create Course', 'learnpress' ),
657 + 'url' => CourseBuilder::get_link_add_new( 'courses' ),
658 + 'color' => '#ef4444',
659 + 'svg' => LP_WP_Filesystem::get_icon_svg( 'ico-courses-2.svg' ),
660 + ],
661 + [
662 + 'label' => __( 'Create Lesson', 'learnpress' ),
663 + 'attr' => 'data-add-new-lesson',
664 + 'popup_type' => 'lesson',
665 + 'popup_id' => 0,
666 + 'template_id' => $create_lesson_template_id,
667 + 'template_html' => $create_lesson_template,
668 + 'color' => '#7067ED',
669 + 'icon' => 'lp-icon-file-text-o',
670 + ],
671 + [
672 + 'label' => __( 'Create Quiz', 'learnpress' ),
673 + 'url' => CourseBuilder::get_link_add_new( 'quizzes' ),
674 + 'color' => '#f59e0b',
675 + 'icon' => 'lp-icon-puzzle-piece',
676 + ],
677 + [
678 + 'label' => __( 'Create Question', 'learnpress' ),
679 + 'url' => CourseBuilder::get_link_add_new( 'questions', CourseBuilder::POST_NEW, 'overview' ),
680 + 'color' => '#6b7280',
681 + 'icon' => 'lp-icon-question-circle-o',
682 + ],
683 + ];
684 + $actions = apply_filters( 'learn-press/course-builder/dashboard/quick-actions', $actions );
685 +
686 + $buttons_html = '';
687 + foreach ( $actions as $action ) {
688 + if ( ! empty( $action['attr'] ) || ! empty( $action['template_id'] ) ) {
689 + $button_attrs = [];
690 + $template_html = $action['template_html'] ?? '';
691 + if ( ! empty( $action['attr'] ) ) {
692 + $button_attrs[] = $action['attr'];
693 + }
694 + if ( ! empty( $action['template_id'] ) ) {
695 + $button_attrs[] = sprintf( 'data-template="#%s"', esc_attr( $action['template_id'] ) );
696 + }
697 + if ( ! empty( $action['popup_type'] ) ) {
698 + $button_attrs[] = sprintf( 'data-popup-type="%s"', esc_attr( $action['popup_type'] ) );
699 + }
700 + if ( isset( $action['popup_id'] ) ) {
701 + $button_attrs[] = sprintf( 'data-popup-id="%d"', absint( $action['popup_id'] ) );
702 + }
703 + $button_attrs = ! empty( $button_attrs ) ? ' ' . implode( ' ', $button_attrs ) : '';
704 +
705 + // Render as button with data attribute (opens popup)
706 + $buttons_html .= sprintf(
707 + '<button type="button"%s class="quick-action__btn" style="--action-color: %s; --action-bg: %s10">
708 + <span class="quick-action__icon %s">%s</span>
709 + <span class="quick-action__label">%s</span>
710 + </button>%s',
711 + $button_attrs,
712 + esc_attr( $action['color'] ),
713 + esc_attr( $action['color'] ),
714 + esc_attr( $action['icon'] ?? '' ),
715 + $action['svg'] ?? '',
716 + esc_html( $action['label'] ),
717 + $template_html
718 + );
719 + } else {
720 + // Render as link
721 + $buttons_html .= sprintf(
722 + '<a href="%s" class="quick-action__btn" style="--action-color: %s; --action-bg: %s10">
723 + <span class="quick-action__icon %s">%s</span>
724 + <span class="quick-action__label">%s</span>
725 + </a>',
726 + esc_url( $action['url'] ),
727 + esc_attr( $action['color'] ),
728 + esc_attr( $action['color'] ),
729 + $action['icon'] ?? '',
730 + $action['svg'] ?? '',
731 + esc_html( $action['label'] )
732 + );
733 + }
734 + }
735 +
736 + return sprintf(
737 + '<div class="lp-cb-dashboard__quick-actions">
738 + <h3 class="quick-actions__title">%s</h3>
739 + <div class="quick-actions__grid">%s</div>
740 + </div>',
741 + esc_html__( 'Quick Action', 'learnpress' ),
742 + $buttons_html
743 + );
744 + }
745 +
746 + /**
747 + * Render recent courses section using course items from Courses tab.
748 + *
749 + * @param UserModel $user
750 + * @return string
751 + */
752 + private function html_recent_courses_section( UserModel $user ): string {
753 + $content = '';
754 +
755 + try {
756 + $filter = new LP_Course_Filter();
757 + $filter->limit = 3;
758 + $filter->order_by = 'post_date';
759 + $filter->order = 'DESC';
760 + $filter->post_status = [ 'publish', 'pending', 'draft', 'private' ];
761 +
762 + if ( ! user_can( $user->get_id(), 'administrator' ) ) {
763 + $filter->post_author = $user->get_id();
764 + }
765 +
766 + $total_courses = 0;
767 + $courses = \LearnPress\Models\Courses::get_courses( $filter, $total_courses );
768 +
769 + $list_html = '';
770 + if ( ! empty( $courses ) ) {
771 + $html_list_course = '';
772 + foreach ( $courses as $course_obj ) {
773 + $course = \LearnPress\Models\CourseModel::find( $course_obj->ID, true );
774 + if ( $course ) {
775 + // Reuse course item from Courses tab
776 + $html_list_course .= BuilderListCoursesTemplate::render_course( $course );
777 + }
778 + }
779 +
780 + if ( ! empty( $html_list_course ) ) {
781 + $list_html = Template::combine_components(
782 + [
783 + 'wrapper' => '<div class="courses-builder__course-tab learn-press-courses"><ul class="cb-list-course">',
784 + 'list_course' => $html_list_course,
785 + 'wrapper_end' => '</ul></div>',
786 + ]
787 + );
788 + }
789 + }
790 +
791 + if ( empty( $list_html ) ) {
792 + $list_html = '<div class="no-data">' . esc_html__( 'No recent courses found', 'learnpress' ) . '</div>';
793 + }
794 +
795 + $content = sprintf(
796 + '<div class="lp-cb-dashboard__recent-courses" style="margin-top: 30px;">
797 + <div class="recent-courses__header">
798 + <h3 class="recent-courses__title">%s</h3>
799 + </div>
800 + <div class="recent-courses__list">%s</div>
801 + </div>',
802 + esc_html__( 'Recent Courses', 'learnpress' ),
803 + $list_html
804 + );
805 + } catch ( Throwable $e ) {
806 + error_log( __METHOD__ . ': ' . $e->getMessage() );
807 + }
808 +
809 + return $content;
810 + }
811 +}