diff --git a/assets/icons/arrow-down.svg b/assets/icons/arrow-down.svg new file mode 100644 index 0000000000..6cd9acf402 --- /dev/null +++ b/assets/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-up.svg b/assets/icons/arrow-up.svg new file mode 100644 index 0000000000..5816ce1ff2 --- /dev/null +++ b/assets/icons/arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/commission.svg b/assets/icons/commission.svg new file mode 100644 index 0000000000..1b3fb72e59 --- /dev/null +++ b/assets/icons/commission.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/earning-fill.svg b/assets/icons/earning-fill.svg new file mode 100644 index 0000000000..2e6f69614b --- /dev/null +++ b/assets/icons/earning-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/fees.svg b/assets/icons/fees.svg new file mode 100644 index 0000000000..2a329397dd --- /dev/null +++ b/assets/icons/fees.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/passed-fill.svg b/assets/icons/passed-fill.svg new file mode 100644 index 0000000000..981f4ff733 --- /dev/null +++ b/assets/icons/passed-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/progress.svg b/assets/icons/progress.svg new file mode 100644 index 0000000000..eae3c8831e --- /dev/null +++ b/assets/icons/progress.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/reload-4.svg b/assets/icons/reload-4.svg new file mode 100644 index 0000000000..26120d4715 --- /dev/null +++ b/assets/icons/reload-4.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/sale.svg b/assets/icons/sale.svg new file mode 100644 index 0000000000..0d5c91cfec --- /dev/null +++ b/assets/icons/sale.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/wallet.svg b/assets/icons/wallet.svg new file mode 100644 index 0000000000..9d559af3f3 --- /dev/null +++ b/assets/icons/wallet.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/withdraw.svg b/assets/icons/withdraw.svg index db6d375049..64ee448f06 100644 --- a/assets/icons/withdraw.svg +++ b/assets/icons/withdraw.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/images/instructor-export.svg b/assets/images/instructor-export.svg new file mode 100644 index 0000000000..41e3c4ded8 --- /dev/null +++ b/assets/images/instructor-export.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/src/js/frontend/dashboard/pages/instructor/home-charts.ts b/assets/src/js/frontend/dashboard/pages/instructor/home-charts.ts index 83c919eab8..4c5d1abb95 100644 --- a/assets/src/js/frontend/dashboard/pages/instructor/home-charts.ts +++ b/assets/src/js/frontend/dashboard/pages/instructor/home-charts.ts @@ -544,7 +544,7 @@ export const overviewChart = (data: OverviewChartProps) => ({ }, createChartConfig(data: OverviewChartProps, colors: OverviewChartColors): ChartConfiguration<'line'> { - const dataLength = data.earnings.length; + const dataLength = data?.earnings?.length || data?.enrolled?.length; return { type: 'line', diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index a81f07f584..284f3c57a5 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -15,10 +15,12 @@ export const icons = [ 'announcement', 'archive', 'archive2', + 'arrowDown', 'arrowLeft', 'arrowLeft2', 'arrowLeftAlt', 'arrowRight2', + 'arrowUp', 'arrowsIn', 'arrowsOut', 'assignment', @@ -81,6 +83,7 @@ export const icons = [ 'colorOption', 'command', 'comments', + 'commission', 'completed', 'completedCircle', 'completedColorize', @@ -129,6 +132,7 @@ export const icons = [ 'duplicate', 'dwg', 'earning', + 'earningFill', 'edit', 'edit2', 'elementorColorized', @@ -149,6 +153,7 @@ export const icons = [ 'eyeOff', 'facebook', 'feather', + 'fees', 'file', 'fileAttachement', 'filter', @@ -244,6 +249,7 @@ export const icons = [ 'open', 'outlineNone', 'passed', + 'passedFill', 'passing', 'pauseCircle', 'pdf', @@ -269,6 +275,7 @@ export const icons = [ 'profile', 'profileCircle', 'profileCircleFill', + 'progress', 'psd', 'publish', 'qa', @@ -295,11 +302,13 @@ export const icons = [ 'reload', 'reload2', 'reload3', + 'reload4', 'removeImage', 'report', 'resources', 'rotate', 'rtf', + 'sale', 'saleType', 'save', 'search', @@ -371,6 +380,7 @@ export const icons = [ 'videoQuality', 'vimeo', 'visited', + 'wallet', 'warning', 'warningLine', 'weightBox', diff --git a/assets/src/scss/frontend/dashboard/_profile.scss b/assets/src/scss/frontend/dashboard/_profile.scss index 0d49ff00ab..eb62fbf329 100644 --- a/assets/src/scss/frontend/dashboard/_profile.scss +++ b/assets/src/scss/frontend/dashboard/_profile.scss @@ -21,7 +21,7 @@ border: 1px solid $tutor-border-idle; border-radius: $tutor-radius-2xl; padding: $tutor-spacing-6; - margin-top: $tutor-spacing-9; + margin-top: $tutor-spacing-6; @include tutor-breakpoint-down(sm) { margin-top: $tutor-spacing-5; diff --git a/assets/src/scss/frontend/dashboard/_stat-card.scss b/assets/src/scss/frontend/dashboard/_stat-card.scss index e89317f32e..8ce0896be1 100644 --- a/assets/src/scss/frontend/dashboard/_stat-card.scss +++ b/assets/src/scss/frontend/dashboard/_stat-card.scss @@ -92,6 +92,99 @@ } } + &-time-spent { + .tutor-stat-card-icon { + color: $tutor-text-exception4; + } + + .tutor-stat-card-value { + color: $tutor-text-exception4; + } + } + + &-courses { + .tutor-stat-card-icon { + color: $tutor-text-success; + } + + .tutor-stat-card-value { + color: $tutor-text-success; + } + } + + &-passed, + &-students { + .tutor-stat-card-icon { + color: $tutor-text-brand; + } + + .tutor-stat-card-value { + color: $tutor-text-brand; + } + } + + &-star-line, + &-star-fill, + &-reviews { + .tutor-stat-card-icon { + color: $tutor-warning-400; + } + + .tutor-stat-card-value { + color: $tutor-warning-400; + } + } + + &-passed { + .tutor-stat-card-icon { + color: $tutor-text-brand; + } + + .tutor-stat-card-value { + color: $tutor-text-brand; + } + } + + &-progress { + .tutor-stat-card-icon { + color: $tutor-exception-5; + } + + .tutor-stat-card-value { + color: $tutor-exception-5; + } + } + + &-completed { + .tutor-stat-card-icon { + color: $tutor-text-success; + } + + .tutor-stat-card-value { + color: $tutor-text-success; + } + } + + &-qa { + .tutor-stat-card-icon { + color: $tutor-exception-2; + } + + .tutor-stat-card-value { + color: $tutor-exception-2; + } + } + + &-book-2 { + .tutor-stat-card-icon { + color: $tutor-exception-1; + } + + .tutor-stat-card-value { + color: $tutor-exception-1; + } + } + &-exception1 { .tutor-stat-card-icon { color: $tutor-exception-1; @@ -132,4 +225,91 @@ color: $tutor-text-exception5; } } + + &-earning { + .tutor-stat-card-icon { + color: $tutor-text-success; + } + + .tutor-stat-card-value { + color: $tutor-text-success; + } + } + + &-wallet { + .tutor-stat-card-icon { + color: $tutor-text-brand; + } + + .tutor-stat-card-value { + color: $tutor-text-brand; + } + } + + &-withdraw { + .tutor-stat-card-icon { + color: $tutor-text-exception5; + } + + .tutor-stat-card-value { + color: $tutor-text-exception5; + } + } + + &-sale { + .tutor-stat-card-icon { + color: $tutor-text-exception1; + } + + .tutor-stat-card-value { + color: $tutor-text-exception1; + } + } + + &-commission { + .tutor-stat-card-icon { + color: $tutor-text-exception2; + } + + .tutor-stat-card-value { + color: $tutor-text-exception2; + } + } + + &-fees { + .tutor-stat-card-icon { + color: $tutor-text-exception4; + } + + .tutor-stat-card-value { + color: $tutor-text-exception4; + } + } +} + +.tutor-stat-card-hover { + + &-wrap{ + position: relative; + display: inline-block; + margin-top: $tutor-spacing-1; + + &:hover .tutor-stat-card-hover-card { + display: block; + } + } + + &-card { + display: none; + position: absolute; + top: 50%; + left: 100%; + min-width: 213px; + background: $tutor-surface-l1; + border: 1px solid $tutor-border-idle;; + border-radius: $tutor-radius-lg; + box-shadow: $tutor-shadow-lg; + padding: $tutor-spacing-5 $tutor-spacing-6; + white-space: nowrap; + } } diff --git a/assets/src/scss/frontend/dashboard/instructor/dashboard.scss b/assets/src/scss/frontend/dashboard/instructor/dashboard.scss index 9e5003e89c..7ea026be42 100644 --- a/assets/src/scss/frontend/dashboard/instructor/dashboard.scss +++ b/assets/src/scss/frontend/dashboard/instructor/dashboard.scss @@ -226,6 +226,19 @@ color: $tutor-text-brand; width: fit-content; } + + &-live-tag { + position: relative; + margin-top: $tutor-spacing-5; + + &-badge { + @include tutor-flex(row, center, flex-start); + opacity: 1; + visibility: visible; + z-index: $tutor-z-positive; + @include tutor-transition((opacity, visibility)); + } + } } &-activity { diff --git a/classes/Dashboard.php b/classes/Dashboard.php index a1040339c5..96112a7146 100644 --- a/classes/Dashboard.php +++ b/classes/Dashboard.php @@ -10,8 +10,6 @@ namespace TUTOR; -use Tutor\Helpers\UrlHelper; - if ( ! defined( 'ABSPATH' ) ) { exit; } diff --git a/classes/Icon.php b/classes/Icon.php index 7d5378d3ae..775d6d8cea 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -33,10 +33,12 @@ final class Icon { const ARCHIVE_2 = 'archive-2'; const ARROWS_IN = 'arrows-in'; const ARROWS_OUT = 'arrows-out'; + const ARROW_DOWN = 'arrow-down'; const ARROW_LEFT = 'arrow-left'; const ARROW_LEFT_2 = 'arrow-left-2'; const ARROW_LEFT_ALT = 'arrow-left-alt'; const ARROW_RIGHT_2 = 'arrow-right-2'; + const ARROW_UP = 'arrow-up'; const ASSIGNMENT = 'assignment'; const ATTACH = 'attach'; const ATTACHMENT_LINE = 'attachment-line'; @@ -97,6 +99,7 @@ final class Icon { const COLOR_OPTION = 'color-option'; const COMMAND = 'command'; const COMMENTS = 'comments'; + const COMMISSION = 'commission'; const COMPLETED = 'completed'; const COMPLETED_CIRCLE = 'completed-circle'; const COMPLETED_COLORIZE = 'completed-colorize'; @@ -145,6 +148,7 @@ final class Icon { const DUPLICATE = 'duplicate'; const DWG = 'dwg'; const EARNING = 'earning'; + const EARNING_FILL = 'earning-fill'; const EDIT = 'edit'; const EDIT_2 = 'edit-2'; const ELEMENTOR_COLORIZED = 'elementor-colorized'; @@ -165,6 +169,7 @@ final class Icon { const EYE_OFF = 'eye-off'; const FACEBOOK = 'facebook'; const FEATHER = 'feather'; + const FEES = 'fees'; const FILE = 'file'; const FILE_ATTACHEMENT = 'file-attachement'; const FILTER = 'filter'; @@ -260,6 +265,7 @@ final class Icon { const OPEN = 'open'; const OUTLINE_NONE = 'outline-none'; const PASSED = 'passed'; + const PASSED_FILL = 'passed-fill'; const PASSING = 'passing'; const PAUSE_CIRCLE = 'pause-circle'; const PDF = 'pdf'; @@ -285,6 +291,7 @@ final class Icon { const PROFILE = 'profile'; const PROFILE_CIRCLE = 'profile-circle'; const PROFILE_CIRCLE_FILL = 'profile-circle-fill'; + const PROGRESS = 'progress'; const PSD = 'psd'; const PUBLISH = 'publish'; const QA = 'qa'; @@ -311,11 +318,13 @@ final class Icon { const RELOAD = 'reload'; const RELOAD_2 = 'reload-2'; const RELOAD_3 = 'reload-3'; + const RELOAD_4 = 'reload-4'; const REMOVE_IMAGE = 'remove-image'; const REPORT = 'report'; const RESOURCES = 'resources'; const ROTATE = 'rotate'; const RTF = 'rtf'; + const SALE = 'sale'; const SALE_TYPE = 'sale-type'; const SAVE = 'save'; const SEARCH = 'search'; @@ -387,6 +396,7 @@ final class Icon { const VIDEO_QUALITY = 'video-quality'; const VIMEO = 'vimeo'; const VISITED = 'visited'; + const WALLET = 'wallet'; const WARNING = 'warning'; const WARNING_LINE = 'warning-line'; const WEIGHT_BOX = 'weight-box'; diff --git a/classes/Instructor.php b/classes/Instructor.php index 7acf3f1dc2..68b43e3663 100644 --- a/classes/Instructor.php +++ b/classes/Instructor.php @@ -14,6 +14,12 @@ exit; } +use DateTime; +use DateInterval; +use Tutor\Models\CourseModel; +use Tutor\Helpers\QueryHelper; +use Tutor\Helpers\DateTimeHelper; + /** * Instructor class * @@ -416,4 +422,473 @@ public function update_instructor_meta( int $user_id ) { do_action( 'tutor_new_instructor_after', $user_id ); } + + /** + * Calculate the previous comparison date range based on a selected date range. + * + * @since 4.0.0 + * + * @param string|null $selected_start_date Selected start date (Y-m-d). + * @param string|null $selected_end_date Selected end date (Y-m-d). + * + * @return array { + * @type string $previous_start_date Previous period start date (Y-m-d). + * @type string $previous_end_date Previous period end date (Y-m-d). + * } + */ + public static function get_comparison_date_range( $selected_start_date, $selected_end_date ) { + + $format = DateTimeHelper::FORMAT_DATE; + + if ( empty( $selected_start_date ) && empty( $selected_end_date ) ) { + + $now = DateTimeHelper::now(); + return array( + 'previous_start_date' => $now->create( 'first day of this month' )->format( $format ), + 'previous_end_date' => $now->create( 'last day of this month' )->format( $format ), + ); + } + + $start = new DateTime( $selected_start_date ); + $end = new DateTime( $selected_end_date ); + $days = $start->diff( $end )->days + 1; + + $previous_start_date = $start->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format ); + $previous_end_date = $end->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format ); + + return array( + 'previous_start_date' => $previous_start_date, + 'previous_end_date' => $previous_end_date, + ); + } + + /** + * Get the total number of students enrolled in an instructor's courses + * within a given date range. + * + * @since 4.0.0 + * + * @param string|null $start_date Start date (Y-m-d). + * @param string|null $end_date End date (Y-m-d). + * @param int $user_id Instructor user ID. + * + * @return int Total number of enrolled students. + */ + public static function get_instructor_total_students_by_date_range( $start_date, $end_date, $user_id ) { + + global $wpdb; + + $primary_table = "{$wpdb->posts} AS enrollment"; + $joining_table = array( + array( + 'type' => 'INNER', + 'table' => "{$wpdb->posts} AS course", + 'on' => 'enrollment.post_parent=course.ID', + ), + ); + $select_columns = array( 'COUNT(enrollment.ID) AS students' ); + + $where = array( + 'course.post_author' => $user_id, + 'course.post_type' => tutor()->course_post_type, + 'course.post_status' => CourseModel::STATUS_PUBLISH, + 'enrollment.post_type' => 'tutor_enrolled', + 'enrollment.post_status' => 'completed', + ); + + if ( ! empty( $start_date ) && ! empty( $end_date ) ) { + $where['enrollment.post_date'] = array( 'BETWEEN', array( $start_date, $end_date ) ); + } + + $result = QueryHelper::get_joined_data( + $primary_table, + $joining_table, + $select_columns, + $where, + array(), + '', + -1, + 0, + 'DESC', + OBJECT, + true + ); + + return $result->students ?? 0; + } + + /** + * Get course completion distribution data for a specific instructor. + * + * @since 4.0.0 + * + * @param array $instructor_course_ids Optional list of course IDs. + * + * @return array { + * Enrollment distribution counts. + * + * @type int $enrolled Total number of enrollments. + * @type int $completed Number of fully completed enrollments (100% progress). + * @type int $inprogress Number of enrollments with partial progress (>0 and <100). + * @type int $inactive Number of enrollments with no progress (0%). + * @type int $cancelled Number of cancelled enrollments. + * } + */ + public static function get_course_completion_distribution_data_by_instructor( $instructor_course_ids = array() ) { + + global $wpdb; + + $counts = array( + 'enrolled' => 0, + 'completed' => 0, + 'inprogress' => 0, + 'inactive' => 0, + 'cancelled' => 0, + ); + + $cancel_statuses = array( 'cancel', 'canceled', 'cancelled' ); + $post_statuses = array_merge( $cancel_statuses, array( 'completed' ) ); + + if ( empty( $instructor_course_ids ) ) { + return $counts; + } + + $where = array( + 'post_type' => 'tutor_enrolled', + 'post_status' => array( 'IN', $post_statuses ), + 'post_parent' => array( 'IN', $instructor_course_ids ), + ); + + $args = array( + 'select' => array( 'id', 'post_status', 'post_author', 'post_parent' ), + 'where' => $where, + ); + + $enrollments = QueryHelper::query( $wpdb->posts, $args ); + + if ( empty( $enrollments ) ) { + return $counts; + } + + $counts['enrolled'] = count( $enrollments ); + + foreach ( $enrollments as $enrollment ) { + + if ( in_array( $enrollment->post_status, $cancel_statuses, true ) ) { + ++$counts['cancelled']; + continue; + } + + $course_progress = tutor_utils()->get_course_completed_percent( $enrollment->post_parent, $enrollment->post_author ); + + if ( 100 === $course_progress ) { + ++$counts['completed']; + } elseif ( $course_progress > 0 && $course_progress < 100 ) { + ++$counts['inprogress']; + } else { + ++$counts['inactive']; + } + } + + return $counts; + } + + /** + * Retrieve the top-performing courses for a given instructor. + * + * @since 4.0.0 + * + * @param int $instructor_id Instructor user ID. + * @param array $args { + * Optional query arguments. + * + * @type string $start_date Optional start date (Y-m-d). + * @type string $end_date Optional end date (Y-m-d). + * @type string $order_by Sorting criteria. Accepts 'revenue' or 'student'. + * } + + * @param int $limit Maximum number of courses to return. Default 4. + * + * @return array List of course objects containing: + * - course_id (int) + * - course_title (string) + * - total_revenue (float) + * - total_student (int) + * + * @throws \Exception When a database error occurs. + */ + public static function get_top_performing_courses_by_instructor( $instructor_id, $args, $limit = 4 ) { + + global $wpdb; + + $start_date = $args['start_date'] ?? null; + $end_date = $args['end_date'] ?? null; + $order_by = 'revenue' === $args['order_by'] ? 'total_revenue' : 'total_student'; + + $complete_status = tutor_utils()->get_earnings_completed_statuses(); + + $amount_type = is_admin() ? 'earnings.admin_amount' : 'earnings.instructor_amount'; + $amount_rate = is_admin() ? 'earnings.admin_rate' : 'earnings.instructor_rate'; + + $amount_condition = "CASE + WHEN orders.tax_type = 'inclusive' AND earnings.course_price_grand_total > 0 + THEN ( earnings.course_price_grand_total - orders.tax_amount ) * ( $amount_rate/100 ) + ELSE $amount_type + END"; + + $earning_where_clause = array( + 'earnings.user_id' => $instructor_id, + 'earnings.order_status' => array( 'IN', $complete_status ), + ); + + $enrollment_where_clause = array( 'post_type' => 'tutor_enrolled' ); + + if ( ! empty( $start_date ) && ! empty( $end_date ) ) { + $earning_where_clause['earnings.created_at'] = array( 'BETWEEN', array( $start_date, $end_date ) ); + $enrollment_where_clause['post_date'] = array( 'BETWEEN', array( $start_date, $end_date ) ); + } + + $earning_where_clause = QueryHelper::prepare_where_clause( $earning_where_clause ); + $enrollment_where_clause = QueryHelper::prepare_where_clause( $enrollment_where_clause ); + + $earnings_sql = "SELECT + earnings.course_id, + SUM($amount_condition) AS total_revenue + FROM {$wpdb->tutor_earnings} earnings + LEFT JOIN {$wpdb->tutor_orders} orders ON orders.id = earnings.order_id + WHERE {$earning_where_clause} + GROUP BY earnings.course_id"; + + $enrollment_sql = QueryHelper::prepare_raw_query( + "SELECT + post_parent AS course_id, + COUNT(ID) AS total_student + FROM {$wpdb->posts} + WHERE {$enrollment_where_clause} + GROUP BY post_parent", + array() + ); + + $result = $wpdb->get_results( + $wpdb->prepare( + "SELECT + post.ID AS course_id, + post.post_title AS course_title, + COALESCE(earnings.total_revenue, 0) AS total_revenue, + COALESCE(enrollments.total_student, 0) AS total_student + FROM wp_posts post + INNER JOIN ({$earnings_sql}) earnings + ON earnings.course_id = post.ID + LEFT JOIN ({$enrollment_sql}) enrollments + ON enrollments.course_id = post.ID + WHERE post.post_type = %s + ORDER BY {$order_by} DESC + LIMIT %d", + tutor()->course_post_type, + $limit + ) + ); + + // If error occurred then throw new exception. + if ( $wpdb->last_error ) { + throw new \Exception( $wpdb->last_error ); //phpcs:ignore. + } + + return $result; + } + + /** + * Format top performing instructor courses for presentation. + * + * @since 4.0.0 + * + * @param array $top_courses List of course objects returned from analytics. + * @return array Formatted top performing courses data. + */ + public static function format_instructor_top_performing_courses( $top_courses ) { + + if ( empty( $top_courses ) ) { + return array(); + } + + return array_map( + function ( $course ) { + return array( + 'name' => $course->course_title, + 'url' => get_permalink( $course->course_id ), + 'revenue' => wp_kses_post( tutor_utils()->tutor_price( $course->total_revenue ?? 0 ) ), + 'students' => $course->total_student ?? 0, + ); + }, + $top_courses + ); + } + + /** + * Retrieve upcoming live session tasks (Zoom / Google Meet) for an instructor. + * + * @since 4.0.0 + * + * @param int $instructor_id Instructor (author) user ID. + * @return array List of upcoming live task posts. + */ + public static function get_instructor_upcoming_live_tasks( $instructor_id ) { + + $is_google_meet_enable = tutor_utils()->is_addon_enabled( 'google-meet' ); + $is_zoom_enable = tutor_utils()->is_addon_enabled( 'tutor-zoom' ); + + $meta_keys = array_filter( + array( + $is_google_meet_enable ? 'tutor-google-meet-start-datetime' : null, + $is_zoom_enable ? '_tutor_zm_start_datetime' : null, + ) + ); + + $post_types = array_filter( + array( + $is_google_meet_enable ? tutor()->meet_post_type : null, + $is_zoom_enable ? tutor()->zoom_post_type : null, + ) + ); + + if ( empty( $meta_keys ) ) { + return array(); + } + + return get_posts( + array( + 'post_type' => $post_types, + 'post_status' => 'publish', + 'post_author' => $instructor_id, + 'numberposts' => 5, + 'meta_query' => array( + array( + 'key' => $meta_keys, + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ), + 'compare' => '>=', + 'type' => 'DATETIME', + ), + ), + ) + ); + } + + /** + * Format upcoming instructor live tasks for presentation. + * + * @since 4.0.0 + * + * @param array $upcoming_live_tasks List of live task post objects. + * @return array Formatted upcoming live tasks data. + */ + public static function format_instructor_upcoming_live_tasks( $upcoming_live_tasks ) { + + if ( empty( $upcoming_live_tasks ) ) { + return array(); + } + + return array_map( + function ( $task ) { + + $is_zoom = tutor()->zoom_post_type === $task->post_type; + $is_meet = tutor()->meet_post_type === $task->post_type; + + $live_meta_key = $is_zoom ? '_tutor_zm_start_datetime' + : ( $is_meet ? 'tutor-google-meet-start-datetime' : '' ); + + $url = $is_zoom ? ( json_decode( get_post_meta( $task->ID, '_tutor_zm_data' )[0] )->join_url ?? '' ) + : ( $is_meet ? get_post_meta( $task->ID, 'tutor-google-meet-link', true ) : '' ); + + $start_date = get_post_meta( $task->ID, $live_meta_key, true ); + + return array( + 'name' => $task->post_title, + 'date' => wp_date( 'Y-m-d h:i A', strtotime( $start_date ) ), + 'url' => $url, + 'post_type' => $task->post_type, + ); + }, + $upcoming_live_tasks + ); + } + + /** + * Format recent instructor reviews for display. + * + * @since 4.0.0 + * + * @param array $reviews List of review objects. + * @return array Formatted recent reviews data. + */ + public static function format_instructor_recent_reviews( $reviews ) { + + if ( empty( $reviews ) ) { + return array(); + } + + return array_map( + function ( $review ) { + return array( + 'user' => array( + 'name' => $review->display_name, + 'avatar' => get_avatar_url( $review->user_id ), + ), + 'course_name' => get_the_title( $review->comment_post_ID ), + 'date' => $review->comment_date, + 'rating' => $review->rating, + 'review_text' => $review->comment_content, + ); + }, + $reviews + ); + } + + public static function get_stat_card_details( float $current_data, float $previous_data ) { + + if ( empty( $previous_data ) && empty( $current_data ) ) { + return array( + 'percentage' => '', + 'icon' => Icon::MINUS, + 'class' => 'tutor-text-primary', + ); + } + + if ( empty( $previous_data ) ) { + $percentage = 100; + } else { + $percentage = ( ( $current_data - $previous_data ) / $previous_data ) * 100; + } + + $is_negative = $percentage < 0; + $icon = $is_negative ? Icon::ARROW_DOWN : Icon::ARROW_UP; + $class = $is_negative ? 'tutor-p2 tutor-actions-critical-primary' : 'tutor-p2 tutor-actions-success-primary'; + + return array( + 'percentage' => number_format( abs( $percentage ), 2 ) . '%', + 'icon' => $icon, + 'class' => $class, + 'icon_class' => '-tutor-mb-1', + ); + } + + /** + * Render a template and return its output as a string. + * + * @since 4.0.0 + * + * @param string $template Template file path or slug. + * @param array $data Data to be passed to the template. + * @param bool $once Whether the template should be loaded only once. + * Defaults to true. + * + * @return string Rendered template output. + */ + public static function get_template_output( $template, $data, $once = true ) { + + ob_start(); + + tutor_load_template_from_custom_path( $template, $data, $once ); + + return (string) ob_get_clean(); + } } diff --git a/classes/Utils.php b/classes/Utils.php index 0899ee466a..2a5db7f52d 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -14,13 +14,13 @@ use Tutor\Ecommerce\Tax; use Tutor\Cache\TutorCache; use Tutor\Models\QuizModel; +use Tutor\Helpers\UrlHelper; use Tutor\Helpers\HttpHelper; use Tutor\Models\CourseModel; use Tutor\Ecommerce\Ecommerce; use Tutor\Helpers\QueryHelper; use Tutor\Traits\JsonResponse; use Tutor\Helpers\DateTimeHelper; -use Tutor\Helpers\UrlHelper; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -3407,15 +3407,15 @@ public function get_total_students_by_instructor( $instructor_id ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(enrollment.ID) - FROM {$wpdb->posts} enrollment - INNER JOIN {$wpdb->posts} course - ON enrollment.post_parent=course.ID - WHERE course.post_author = %d + FROM {$wpdb->posts} enrollment + INNER JOIN {$wpdb->posts} course + ON enrollment.post_parent=course.ID + WHERE course.post_author = %d AND course.post_type = %s AND course.post_status = %s AND enrollment.post_type = %s AND enrollment.post_status = %s; - ", + ", $instructor_id, $course_post_type, 'publish', @@ -3499,11 +3499,11 @@ public function get_students_by_instructor( int $instructor_id, int $offset, int $students = $wpdb->get_results( $wpdb->prepare( "SELECT COUNT(enrollment.post_author) AS course_taken, user.*, (SELECT post_date FROM {$wpdb->posts} WHERE post_author = user.ID LIMIT 1) AS enroll_date - FROM {$wpdb->posts} enrollment - INNER JOIN {$wpdb->posts} AS course - ON enrollment.post_parent=course.ID - INNER JOIN {$wpdb->users} AS user - ON user.ID = enrollment.post_author + FROM {$wpdb->posts} enrollment + INNER JOIN {$wpdb->posts} AS course + ON enrollment.post_parent=course.ID + INNER JOIN {$wpdb->users} AS user + ON user.ID = enrollment.post_author WHERE course.post_type = %s AND course.post_status IN ({$post_status}) AND enrollment.post_type = %s @@ -3512,11 +3512,9 @@ public function get_students_by_instructor( int $instructor_id, int $offset, int {$course_query} {$date_query} AND ( user.display_name LIKE %s OR user.user_nicename LIKE %s OR user.user_email = %s OR user.user_login LIKE %s ) - GROUP BY enrollment.post_author ORDER BY {$order_by} {$order} - LIMIT %d, %d - ", + LIMIT %d, %d", $course_post_type, 'tutor_enrolled', 'completed', @@ -3545,9 +3543,7 @@ public function get_students_by_instructor( int $instructor_id, int $offset, int {$course_query} {$date_query} GROUP BY enrollment.post_author - ORDER BY {$order_by} {$order} - - ", + ORDER BY {$order_by} {$order}", $course_post_type, 'tutor_enrolled', 'completed', @@ -4251,16 +4247,18 @@ public function get_reviews_by_user( $user_id = 0, $offset = 0, $limit = null, $ * @since 1.0.0 * @since 1.4.0 $course_id $date_filter param added. * @since 1.9.9 Course id & date filter is sorting with specific course and date. + * @since 4.0.0 $args parameter added. * * @param int $instructor_id user id. * @param int $offset offset. * @param int $limit limit. * @param string $course_id course id. * @param string $date_filter date filter. + * @param array $args Optional query arguments. * * @return array|null|object */ - public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $limit = 150, $course_id = '', $date_filter = '' ) { + public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $limit = 150, $course_id = '', $date_filter = '', $args = array() ) { global $wpdb; $instructor_id = sanitize_text_field( $instructor_id ); $offset = sanitize_text_field( $offset ); @@ -4268,10 +4266,17 @@ public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $lim $course_id = sanitize_text_field( $course_id ); $date_filter = sanitize_text_field( $date_filter ); $instructor_id = $this->get_user_id( $instructor_id ); + $args = $this->sanitize_array( $args ); $course_query = ''; $date_query = ''; + $where_clause = ''; + + if ( ! empty( $args['where'] ) ) { + $where_clause = ' AND ' . $args['where']; + } + if ( '' !== $course_id ) { $course_query = " AND {$wpdb->comments}.comment_post_ID = {$course_id} "; } @@ -4302,6 +4307,7 @@ public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $lim WHERE {$wpdb->comments}.comment_post_ID IN({$implode_ids}) AND comment_type = %s AND meta_key = %s + {$where_clause} {$course_query} {$date_query} ", @@ -4310,6 +4316,7 @@ public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $lim ) ); + $order_by = $args['order_by'] ?? 'comment_ID'; // Results. $results['results'] = $wpdb->get_results( $wpdb->prepare( @@ -4324,21 +4331,21 @@ public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $lim {$wpdb->users}.display_name, {$wpdb->posts}.post_title as course_title - FROM {$wpdb->comments} - INNER JOIN {$wpdb->commentmeta} - ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id - INNER JOIN {$wpdb->users} - ON {$wpdb->comments}.user_id = {$wpdb->users}.ID - INNER JOIN {$wpdb->posts} - ON {$wpdb->posts}.ID = {$wpdb->comments}.comment_post_ID - WHERE {$wpdb->comments}.comment_post_ID IN({$implode_ids}) + FROM {$wpdb->comments} + INNER JOIN {$wpdb->commentmeta} + ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id + INNER JOIN {$wpdb->users} + ON {$wpdb->comments}.user_id = {$wpdb->users}.ID + INNER JOIN {$wpdb->posts} + ON {$wpdb->posts}.ID = {$wpdb->comments}.comment_post_ID + WHERE {$wpdb->comments}.comment_post_ID IN({$implode_ids}) AND comment_type = %s AND meta_key = %s + {$where_clause} {$course_query} {$date_query} - ORDER BY comment_ID DESC - LIMIT %d, %d; - ", + ORDER BY {$order_by} DESC + LIMIT %d, %d", 'tutor_course_rating', 'tutor_rating', $offset, @@ -4354,12 +4361,14 @@ public function get_reviews_by_instructor( $instructor_id = 0, $offset = 0, $lim * Get instructors rating * * @since 1.0.0 + * @since 4.0.0 Added $where Parameter. * - * @param int $instructor_id instructor id. + * @param int $instructor_id instructor id. + * @param array $where Optional additional WHERE conditions. * * @return object */ - public function get_instructor_ratings( $instructor_id ) { + public function get_instructor_ratings( $instructor_id, $where = array() ) { global $wpdb; $ratings = array( @@ -4368,25 +4377,29 @@ public function get_instructor_ratings( $instructor_id ) { 'rating_avg' => 0.00, ); - $rating = $wpdb->get_row( - $wpdb->prepare( - "SELECT COUNT(rating.meta_value) as rating_count, SUM(rating.meta_value) as rating_sum - FROM {$wpdb->usermeta} courses - INNER JOIN {$wpdb->comments} reviews - ON courses.meta_value = reviews.comment_post_ID - AND reviews.comment_type = 'tutor_course_rating' - AND reviews.comment_approved = 'approved' - INNER JOIN {$wpdb->commentmeta} rating - ON reviews.comment_ID = rating.comment_id - AND rating.meta_key = 'tutor_rating' - WHERE courses.user_id = %d - AND courses.meta_key = %s - ", - $instructor_id, - '_tutor_instructor_course_id' + // Prepare where clause. + $where_clause = QueryHelper::prepare_where_clause( + $this->sanitize_array( + $where + array( + 'courses.user_id' => $instructor_id, + 'courses.meta_key' => '_tutor_instructor_course_id', + ) ) ); + $rating = $wpdb->get_row( + "SELECT COUNT(rating.meta_value) as rating_count, SUM(rating.meta_value) as rating_sum + FROM {$wpdb->usermeta} courses + INNER JOIN {$wpdb->comments} reviews + ON courses.meta_value = reviews.comment_post_ID + AND reviews.comment_type = 'tutor_course_rating' + AND reviews.comment_approved = 'approved' + INNER JOIN {$wpdb->commentmeta} rating + ON reviews.comment_ID = rating.comment_id + AND rating.meta_key = 'tutor_rating' + WHERE {$where_clause}" + ); + if ( $rating->rating_count ) { $avg_rating = number_format( ( $rating->rating_sum / $rating->rating_count ), 2 ); @@ -8545,12 +8558,12 @@ public function is_tutor_dashboard( $subpage = null ) { * * @return boolean */ - public function is_tutor_frontend_dashboard( $subpage = null ) { + public function is_tutor_frontend_dashboard( $subpage = '' ) { global $wp_query; if ( $wp_query->is_page ) { $dashboard_page = $this->array_get( 'tutor_dashboard_page', $wp_query->query_vars ); - if ($subpage) { + if ( $subpage ) { $subpage_parts = explode( '/', $subpage, 2 ); if ( isset( $subpage_parts[1] ) ) { $dashboard_subpage = $this->array_get( 'tutor_dashboard_sub_page', $wp_query->query_vars ); @@ -10871,4 +10884,16 @@ public function get_script_locale_data( string $filename, string $locale = 'en_U return null; } + + /** + * Normalizes WooCommerce monetization output to plain text. + * + * @since 4.0.0 + * + * @param string $value WooCommerce price HTML or encoded string. + * @return string Plain text monetary value. + */ + public function fix_wc_monetization_format( $value ): string { + return wp_strip_all_tags( wp_specialchars_decode( $value ) ); + } } diff --git a/components/BaseComponent.php b/components/BaseComponent.php index d682f60e14..cbae0dfbae 100644 --- a/components/BaseComponent.php +++ b/components/BaseComponent.php @@ -164,5 +164,4 @@ public function render(): void { // phpcs:ignore -- Sanitization is performed within each child class’s `get` method implementation. echo $this->get(); } - } diff --git a/components/DateFilter.php b/components/DateFilter.php index f708c00ce7..a611327229 100644 --- a/components/DateFilter.php +++ b/components/DateFilter.php @@ -10,9 +10,9 @@ namespace Tutor\Components; -use Tutor\Components\Constants\Size; use TUTOR\Icon; use TUTOR\Input; +use Tutor\Components\Constants\Size; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -82,6 +82,15 @@ class DateFilter extends BaseComponent { */ protected $placement = self::PLACEMENT_BOTTOM_START; + /** + * CSS class name used for the icon element. + * + * @since 4.0.0 + * + * @var string + */ + protected $icon_class; + /** * Button size. * @@ -96,6 +105,15 @@ class DateFilter extends BaseComponent { */ protected $icon_size = 20; + /** + * Whether to display the label text. + * + * @since 4.0.0 + * + * @var bool + */ + protected $show_label = true; + /** * Set filter type. * @@ -163,6 +181,33 @@ public function placement( string $placement ): self { return $this; } + /** + * Set Icon Class. + * + * @since 4.0.0 + * + * @param string $icon_class CSS class name used for the icon element. + * + * @return self + */ + public function icon_class( string $icon_class ): self { + $this->icon_class = $icon_class; + return $this; + } + + /** + * Enable or disable the display of the label text. + * + * @since 4.0.0 + * + * @param bool $show_label True to show the label, false to hide it. + * @return $this + */ + public function show_label( bool $show_label ) { + $this->show_label = $show_label; + return $this; + } + /** * Render the component. * @@ -189,6 +234,10 @@ public function get(): string { $popover_classes = 'tutor-popover'; $icon = Icon::CALENDAR_2; + if ( ! empty( $this->icon_class ) ) { + $button_classes .= " {$this->icon_class}"; + } + if ( $is_range ) { $calendar_options = array( 'type' => 'multiple', @@ -266,6 +315,10 @@ protected function calculate_label(): string { return Input::get( 'date', '' ); } + if ( ! $this->show_label ) { + return ''; + } + $start_date = Input::get( 'start_date' ); $end_date = Input::get( ( 'end_date' ) ); diff --git a/components/InputField.php b/components/InputField.php index c2567c2d98..ea245bce39 100644 --- a/components/InputField.php +++ b/components/InputField.php @@ -15,12 +15,10 @@ defined( 'ABSPATH' ) || exit; +use TUTOR\Icon; use ReflectionClass; -use Tutor\Components\Constants\InputType; use Tutor\Components\Constants\Size; -use Tutor\Components\Constants\Variant; -use Tutor\Components\Button; -use TUTOR\Icon; +use Tutor\Components\Constants\InputType; /** * InputField Component Class. diff --git a/components/Table.php b/components/Table.php index 5c26847715..66c69453f2 100644 --- a/components/Table.php +++ b/components/Table.php @@ -177,7 +177,8 @@ protected function render_table_headings(): string { foreach ( $this->cell_headers as $heading ) { $headings .= sprintf( - '%1$s', + '%s', + $heading['class'], apply_filters( 'tutor_table_heading', $heading['content'] ) ); } diff --git a/components/Tabs.php b/components/Tabs.php index ff03f77596..5324bd6cf3 100644 --- a/components/Tabs.php +++ b/components/Tabs.php @@ -108,6 +108,54 @@ class Tabs extends BaseComponent { 'paramName' => 'tab', ); + /** + * CSS class for the main tabs wrapper element. + * + * @since 4.0.0 + * @var string + */ + protected string $wrapper_class = ''; + + /** + * CSS class for the tab list container. + * + * @since 4.0.0 + * @var string + */ + protected string $tab_list_class = ''; + + /** + * CSS class applied to each tab button element. + * + * @since 4.0.0 + * @var string + */ + protected string $tab_button_class = ''; + + /** + * CSS class for the inner content wrapper inside a tab button + * + * @since 4.0.0 + * @var string + */ + protected string $tab_button_content_class = ''; + + /** + * CSS class for the tab label text. + * + * @since 4.0.0 + * @var string + */ + protected string $tab_label_class = ''; + + /** + * CSS class for the sub-label text. + * + * @since 4.0.0 + * @var string + */ + protected string $tab_sub_label_class = ''; + /** * Set the tab data. * @@ -172,6 +220,101 @@ public function url_params( array $params ) { return $this; } + /** + * Set the CSS class for the main tabs wrapper element. + * + * @since 4.0.0 + * + * @param string $wrapper_class CSS class name(s) for the wrapper. + * @return $this + */ + public function wrapper_class( string $wrapper_class ) { + + $this->wrapper_class = ! empty( $wrapper_class ) + ? sprintf( ' class="%s"', esc_attr( $wrapper_class ) ) + : ''; + + return $this; + } + + + /** + * Set the CSS class for the tab list container. + * + * @since 4.0.0 + * + * @param string $tab_list_class CSS class name(s) for the tab list. + * @return $this + */ + public function tab_list_class( string $tab_list_class ) { + + $this->tab_list_class = esc_attr( $tab_list_class ); + + return $this; + } + + + /** + * Set the CSS class for each tab button element. + * + * @since 4.0.0 + * + * @param string $tab_button_class CSS class name(s) for the tab button. + * @return $this + */ + public function tab_button_class( string $tab_button_class ) { + $this->tab_button_class = ! empty( $tab_button_class ) + ? sprintf( ' class="%s"', esc_attr( $tab_button_class ) ) + : ''; + return $this; + } + + /** + * Set the CSS class for the inner content wrapper inside a tab button. + * + * @since 4.0.0 + * + * @param string $tab_button_content_class CSS class name(s) for the tab button content. + * @return $this + */ + public function tab_button_content_class( string $tab_button_content_class ) { + $this->tab_button_content_class = ! empty( $tab_button_content_class ) + ? sprintf( ' class="%s"', esc_attr( $tab_button_content_class ) ) + : ''; + return $this; + } + + /** + * Set the CSS class for the tab label text. + * + * @since 4.0.0 + * + * @param string $tab_label_class CSS class name(s) for the tab label. + * @return $this + */ + public function tab_label_class( string $tab_label_class ) { + $this->tab_label_class = ! empty( $tab_label_class ) + ? sprintf( ' class="%s"', esc_attr( $tab_label_class ) ) + : ''; + return $this; + } + + + /** + * Set the CSS class for the tab sub-label text. + * + * @since 4.0.0 + * + * @param string $tab_sub_label_class CSS class name(s) for the tab sub-label. + * @return $this + */ + public function tab_sub_label_class( string $tab_sub_label_class ) { + $this->tab_sub_label_class = ! empty( $tab_sub_label_class ) + ? sprintf( ' class="%s"', esc_attr( $tab_sub_label_class ) ) + : ''; + return $this; + } + /** * Get the tabs component. * @@ -194,8 +337,9 @@ public function get(): string { defaultTab: "", urlParams: })' + wrapper_class; // phpcs:ignore ?> > -
+
diff --git a/components/Tooltip.php b/components/Tooltip.php index 7ae350bea9..eca9934563 100644 --- a/components/Tooltip.php +++ b/components/Tooltip.php @@ -115,6 +115,31 @@ class Tooltip extends BaseComponent { 'hide' => 0, ); + /** + * Allowed HTML tags and Alpine.js attributes for wp_kses sanitization. + * + * @since 4.0.0 + * + * @var array + */ + protected $allowed_attributes = array(); + + /** + * Allowed Alpine.js HTML attributes. + * + * @since 4.0.0 + * + * @var array + */ + private const ALLOWED_ALPINE_ATTRS = array( + 'x-data', + 'x-text', + 'x-show', + 'x-cloak', + 'x-ref', + 'x-transition', + ); + /** * Set the tooltip content. * @@ -240,6 +265,46 @@ public function delay( int $show, int $hide = 0 ): self { return $this; } + /** + * Adds Alpine.js attributes to the wp_kses allow-list. + * + * @since 4.0.0 + * + * @param array $attributes attribute map. + * + * @return $this + */ + public function add_alpine_attributes( array $attributes ) { + + $this->allowed_attributes = wp_kses_allowed_html( 'post' ); + + foreach ( $attributes as $tag => $attr ) { + $tag = sanitize_key( $tag ); + + if ( ! isset( $this->allowed_attributes[ $tag ][ $attr ] ) && in_array( $attr, self::ALLOWED_ALPINE_ATTRS, true ) ) { + $this->allowed_attributes[ $tag ][ $attr ] = true; + } + } + + return $this; + } + + /** + * Returns the allowed HTML configuration for wp_kses(). + * + * @since 4.0.0 + * + * @return array + */ + private function allowed_attributes() { + + if ( empty( $this->allowed_attributes ) ) { + return wp_kses_allowed_html( 'post' ); + } + + return $this->allowed_attributes; + } + /** * Get the final tooltip HTML. * @@ -278,7 +343,7 @@ public function get(): string { esc_attr( $this->attributes['class'] ?? '' ), $this->get_attributes_string(), $trigger_html, - wp_kses_post( $this->content ) + wp_kses( $this->content, $this->allowed_attributes() ) ); return $this->component_string; diff --git a/models/CourseModel.php b/models/CourseModel.php index 062b10920b..33558e6313 100644 --- a/models/CourseModel.php +++ b/models/CourseModel.php @@ -10,6 +10,7 @@ namespace Tutor\Models; +use TUTOR\Icon; use TUTOR\Course; use Tutor\Ecommerce\Tax; use Tutor\Helpers\QueryHelper; @@ -327,12 +328,12 @@ public static function get_courses_by_instructor( //phpcs:disable $query = $wpdb->prepare( "SELECT $select_col - FROM $wpdb->posts + FROM $wpdb->posts LEFT JOIN {$wpdb->usermeta} - ON $wpdb->usermeta.user_id = %d - AND $wpdb->usermeta.meta_key = %s - AND $wpdb->usermeta.meta_value = $wpdb->posts.ID - WHERE 1 = 1 {$where_post_status} + ON $wpdb->usermeta.user_id = %d + AND $wpdb->usermeta.meta_key = %s + AND $wpdb->usermeta.meta_value = $wpdb->posts.ID + WHERE 1 = 1 {$where_post_status} AND $wpdb->posts.post_type IN ({$post_types}) AND ($wpdb->posts.post_author = %d OR $wpdb->usermeta.user_id = %d) {$search_sql} @@ -1344,4 +1345,195 @@ public static function get_enrolled_courses_by_user( $user_id = 0, $post_status return false; } + + /** + * Get the number of courses created by an instructor within a given date range. + * + * @since 4.0.0 + * + * If no date range is provided, all courses authored by the user are counted. + * Courses without a `_wp_old_date` meta value are considered valid based on + * their publish date. + * + * @param string|null $start_date Start date in Y-m-d format. + * @param string|null $end_date End date in Y-m-d format. + * @param int $user_id Course author (user) ID. + * + * @return int Total number of matching courses. + */ + public static function get_course_count_by_date( $start_date, $end_date, $user_id ) { + + $common_args = array( + 'post_author' => $user_id, + 'posts_per_page' => -1, + 'fields' => 'ids', + ); + + if ( empty( $start_date ) && empty( $end_date ) ) { + return self::get_courses_by_args( $common_args )->post_count; + } + + $by_date = self::get_courses_by_args( + $common_args + + array( + 'date_query' => array( + 'column' => 'post_date_gmt', + 'before' => $end_date, + 'after' => $start_date, + 'inclusive' => true, + ), + ) + ); + + $by_meta = self::get_courses_by_args( + $common_args + + array( + 'meta_key' => '_wp_old_date', + 'meta_value' => array( $start_date, $end_date ), + 'meta_compare' => 'BETWEEN', + 'meta_type' => 'DATE', + ) + ); + + $post_ids = array_unique( array_merge( (array) $by_date->posts, (array) $by_meta->posts ) ); + + $filtered = array_filter( + $post_ids, + function ( int $post_id ) use ( $start_date, $end_date ): bool { + $old_date = get_post_meta( $post_id, '_wp_old_date', true ); // first value. + + if ( empty( $old_date ) ) { + return true; + } + + return strtotime( $start_date ) <= $old_date && strtotime( $end_date ) >= $old_date; + } + ); + + return count( $filtered ); + } + + /** + * Get topic-wise progress data for a course and a student. + * + * @since 4.0.0 + * + * @param int $course_id Course ID. + * @param int $student_id Student ID. + * + * @return array[] Topic progress data. + */ + public static function get_topic_progress_by_course_id( $course_id, $student_id ) { + + $topics_query = tutor_utils()->get_topics( $course_id ); + + $topic_list = array(); + + if ( empty( $topics_query ) || ! $topics_query->have_posts() ) { + return $topic_list; + } + + foreach ( $topics_query->posts as $topic_post ) { + $topic_id = (int) $topic_post->ID; + + $topic = array( + 'topic_id' => $topic_id, + 'topic_summary' => $topic_post->post_content, + 'topic_title' => get_the_title( $topic_id ), + 'items' => array(), + 'topic_completed' => true, + 'topic_started' => false, + ); + + $contents_query = tutor_utils()->get_course_contents_by_topic( $topic_id, -1 ); + + if ( ! empty( $contents_query ) && $contents_query->have_posts() ) { + foreach ( $contents_query->posts as $content_post ) { + $post_id = (int) $content_post->ID; + $post_type = $content_post->post_type; + $is_completed = true; + + if ( tutor()->quiz_post_type === $post_type ) { + + $is_completed = (bool) tutor_utils()->has_attempted_quiz( $student_id, $post_id ); + + $topic['items'][] = array( + 'type' => 'quiz', + 'id' => $post_id, + 'link' => esc_url_raw( get_permalink( $post_id ) ), + 'title' => $content_post->post_title, + 'is_completed' => $is_completed, + 'time_limit' => tutor_utils()->get_quiz_option( $post_id, 'time_limit.time_value' ), + 'time_type' => tutor_utils()->get_quiz_option( $post_id, 'time_limit.time_type' ), + 'label' => __( 'Quiz', 'tutor' ), + 'icon' => Icon::QUIZ_2, + ); + + } elseif ( tutor()->assignment_post_type === $post_type ) { + + $submitted_count = tutor_utils()->get_submitted_assignment_count( $post_id, $student_id ); + $is_completed = $submitted_count > 0; + + $topic['items'][] = array( + 'type' => 'assignment', + 'id' => $post_id, + 'link' => esc_url_raw( get_permalink( $post_id ) ), + 'title' => $content_post->post_title, + 'is_completed' => $is_completed, + 'label' => __( 'Assignment', 'tutor' ), + 'icon' => Icon::BOOK_2, + ); + + } elseif ( tutor()->zoom_post_type === $post_type ) { + $topic['items'][] = array( + 'type' => 'zoom_meeting', + 'id' => $post_id, + 'title' => $content_post->post_title, + 'link' => esc_url_raw( get_permalink( $post_id ) ), + 'label' => __( 'Live Class', 'tutor' ), + 'icon' => Icon::ZOOM, + ); + + } elseif ( tutor()->meet_post_type === $post_type ) { + $topic['items'][] = array( + 'type' => 'google_meet', + 'id' => $post_id, + 'title' => $content_post->post_title, + 'link' => esc_url_raw( get_permalink( $post_id ) ), + 'label' => __( 'Live Class', 'tutor' ), + 'icon' => Icon::GOOGLE_MEET, + ); + + } else { + $video = tutor_utils()->get_video_info( $post_id ); + $is_completed = (bool) tutor_utils()->is_completed_lesson( $post_id, $student_id ); + + $topic['items'][] = array( + 'type' => 'lesson', + 'id' => $post_id, + 'link' => esc_url_raw( get_permalink( $post_id ) ), + 'title' => $content_post->post_title, + 'video' => $video, + 'video_play_time' => isset( $video->playtime ) ? $video->playtime : '', + 'is_completed' => $is_completed, + 'label' => __( 'Reading', 'tutor' ), + 'icon' => Icon::COURSES, + ); + } + + if ( ! $is_completed ) { + $topic['topic_completed'] = false; + } else { + $topic['topic_started'] = true; + } + } + } + + $topic_list[] = $topic; + } + + wp_reset_postdata(); + + return $topic_list; + } } diff --git a/models/OrderModel.php b/models/OrderModel.php index 345793b109..2fcc11fd86 100644 --- a/models/OrderModel.php +++ b/models/OrderModel.php @@ -1138,7 +1138,8 @@ public function get_discounts_by_user( int $user_id, string $period = '', $start 0 ) ) AS total, - o.created_at_gmt AS date_format + o.created_at_gmt AS date_format, + DATE_FORMAT( o.created_at_gmt, '%b' ) AS label_name FROM {$this->table_name} o JOIN @@ -1180,7 +1181,8 @@ public function get_discounts_by_user( int $user_id, string $period = '', $start 0 ) ) AS total, - o.created_at_gmt AS date_format + o.created_at_gmt AS date_format, + DATE_FORMAT( o.created_at_gmt, '%b' ) AS label_name FROM {$this->table_name} AS o WHERE 1 = %d AND o.order_status = 'completed' @@ -1336,7 +1338,8 @@ public function get_refunds_by_user( int $user_id, string $period = '', $start_d $wpdb->prepare( "SELECT COALESCE(SUM(o.refund_amount), 0) AS total, - created_at_gmt AS date_format + created_at_gmt AS date_format, + DATE_FORMAT(o.created_at_gmt, '%b') AS label_name FROM {$this->table_name} AS o -- LEFT JOIN {$item_table} AS i ON i.order_id = o.id -- LEFT JOIN {$wpdb->posts} AS c ON c.id = i.item_id @@ -1909,17 +1912,20 @@ private static function should_active_pay_button( $order, $show_pay_button ) { * Retrieves statements for a specific user. * * @since 3.5.0 + * @since 4.0.0 Added $order_by and $order option. * - * @param string $post_type_in_clause SQL clause to filter the course post types. - * @param string $course_query SQL query string to further filter the courses . - * @param string $date_query SQL query string to filter by date range. - * @param int $user_id The user ID for which the statements are being retrieved. - * @param int $offset The offset for pagination. - * @param int $limit The number of rows to return. + * @param string $post_type_in_clause Prepared SQL IN clause containing allowed course post types. + * @param string $course_query Optional SQL fragment to filter by course ID. + * @param string $date_query Optional SQL fragment to filter by statement date. + * @param int $user_id User (instructor) ID. + * @param int $offset Number of records to skip (pagination offset). + * @param int $limit Maximum number of records to return. + * @param string $order_by Column name to order results by. + * @param string $order Sort direction. Accepts 'ASC' or 'DESC'. * * @return array */ - public function get_statements( $post_type_in_clause, $course_query, $date_query, $user_id, $offset, $limit ): array { + public function get_statements( $post_type_in_clause, $course_query, $date_query, $user_id, $offset, $limit, $order_by, $order ): array { global $wpdb; //phpcs:disable @@ -1943,9 +1949,8 @@ public function get_statements( $post_type_in_clause, $course_query, $date_query WHERE statements.user_id = %d {$course_query} {$date_query} - ORDER BY statements.created_at DESC - LIMIT %d, %d - ", + ORDER BY {$order_by} {$order} + LIMIT %d, %d", $user_id, $offset, $limit @@ -1960,8 +1965,7 @@ public function get_statements( $post_type_in_clause, $course_query, $date_query AND course.post_type IN ({$post_type_in_clause}) WHERE statements.user_id = %d {$course_query} - {$date_query} - ", + {$date_query}", $user_id ) ); diff --git a/models/WithdrawModel.php b/models/WithdrawModel.php index d1ab884cd9..84522ee0c8 100644 --- a/models/WithdrawModel.php +++ b/models/WithdrawModel.php @@ -27,13 +27,22 @@ class WithdrawModel { * Get withdraw summary info for an user * * @since 2.0.7 + * @since 4.0.0 $args parameter added. * * @param int $instructor_id instructor id. + * @param array $args Optional query. * @return array|object|null|void */ - public static function get_withdraw_summary( $instructor_id ) { + public static function get_withdraw_summary( $instructor_id, $args = array() ) { global $wpdb; + $args = tutor_utils()->sanitize_array( $args ); + $where_clause = ''; + + if ( ! empty( $args['where'] ) ) { + $where_clause = ' AND ' . $args['where']; + } + $maturity_days = tutor_utils()->get_option( 'minimum_days_for_balance_to_be_available' ); //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder @@ -49,25 +58,25 @@ public static function get_withdraw_summary( $instructor_id ) { FROM ( SELECT ID,display_name, - COALESCE((SELECT SUM(instructor_amount) FROM {$wpdb->prefix}tutor_earnings WHERE order_status='%s' GROUP BY user_id HAVING user_id=u.ID),0) total_income, + COALESCE((SELECT SUM(instructor_amount) FROM {$wpdb->prefix}tutor_earnings WHERE order_status='%s' {$where_clause} GROUP BY user_id HAVING user_id=u.ID),0) total_income, COALESCE(( SELECT sum(amount) total_withdraw FROM {$wpdb->prefix}tutor_withdraws - WHERE status='%s' + WHERE status='%s' {$where_clause} GROUP BY user_id HAVING user_id=u.ID ),0) total_withdraw, COALESCE(( SELECT sum(amount) total_pending FROM {$wpdb->prefix}tutor_withdraws - WHERE status='pending' + WHERE status='pending' {$where_clause} GROUP BY user_id HAVING user_id=u.ID ),0) total_pending, COALESCE(( SELECT SUM(instructor_amount) FROM( - SELECT user_id, instructor_amount, created_at, DATEDIFF(NOW(),created_at) AS days_old FROM {$wpdb->prefix}tutor_earnings WHERE order_status='%s' + SELECT user_id, instructor_amount, created_at, DATEDIFF(NOW(),created_at) AS days_old FROM {$wpdb->prefix}tutor_earnings WHERE order_status='%s' {$where_clause} ) a WHERE days_old >= %d GROUP BY user_id diff --git a/templates/dashboard/account/profile.php b/templates/dashboard/account/profile.php index fd1c23063c..728a234ec6 100644 --- a/templates/dashboard/account/profile.php +++ b/templates/dashboard/account/profile.php @@ -10,6 +10,44 @@ */ defined( 'ABSPATH' ) || exit; + +use TUTOR\Icon; +use TUTOR\User; +use Tutor\Models\CourseModel; +use Tutor\Models\WithdrawModel; + +$statistics = array(); +$user = wp_get_current_user(); +$show_statistics = User::is_instructor() ? true : false; + +if ( $show_statistics ) { + $statistics = array( + array( + 'value' => WithdrawModel::get_withdraw_summary( $user->ID )->total_income ?? 0, + 'icon' => Icon::EARNING_FILL, + 'label' => __( 'Total Earnings', 'tutor' ), + 'icon_class' => 'tutor-actions-success-primary', + ), + array( + 'value' => CourseModel::get_course_count_by_instructor( $user->ID ), + 'icon' => Icon::COURSES_FILL, + 'label' => __( 'Total Courses', 'tutor' ), + 'icon_class' => 'tutor-icon-brand', + ), + array( + 'value' => tutor_utils()->get_instructor_ratings( $user->ID )->rating_avg ?? 0, + 'icon' => Icon::STAR_FILL, + 'label' => __( 'Ratings', 'tutor' ), + 'icon_class' => 'tutor-text-exception4', + ), + array( + 'value' => tutor_utils()->get_total_students_by_instructor( $user->ID ) ?? 0, + 'icon' => Icon::PASSED_FILL, + 'label' => __( 'Total Students', 'tutor' ), + 'icon_class' => 'tutor-text-exception5', + ), + ); +} ?>
@@ -17,6 +55,18 @@
+
diff --git a/templates/dashboard/dashboard.php b/templates/dashboard/dashboard.php index 39b7ba4100..a339d8bc71 100644 --- a/templates/dashboard/dashboard.php +++ b/templates/dashboard/dashboard.php @@ -9,8 +9,7 @@ * @since 1.4.3 */ -use Tutor\Models\CourseModel; -use Tutor\Models\WithdrawModel; +defined( 'ABSPATH' ) || exit; if ( tutor_utils()->get_option( 'enable_profile_completion' ) ) { $profile_completion = tutor_utils()->user_profile_completion(); @@ -19,7 +18,7 @@ $incomplete_count = count( array_filter( $profile_completion, - function( $data ) { + function ( $data ) { return ! $data['is_set']; } ) @@ -109,8 +108,7 @@ function( $data ) {
@@ -128,270 +126,10 @@ function( $data ) { ); echo $alert_message; //phpcs:ignore - } + } } ?> -
-
- get_completed_courses_ids_by_user(); - $total_students = tutor_utils()->get_total_students_by_instructor( $user_id ); - $my_courses = CourseModel::get_courses_by_instructor( $user_id, CourseModel::STATUS_PUBLISH ); - $earning_sum = WithdrawModel::get_withdraw_summary( $user_id ); - $active_courses = CourseModel::get_active_courses_by_user( $user_id ); - - $enrolled_course_count = $enrolled_course ? $enrolled_course->post_count : 0; - $completed_course_count = count( $completed_courses ); - $active_course_count = is_object( $active_courses ) && $active_courses->have_posts() ? $active_courses->post_count : 0; - - $status_translations = array( - 'publish' => __( 'Published', 'tutor' ), - 'pending' => __( 'Pending', 'tutor' ), - 'trash' => __( 'Trash', 'tutor' ), - ); - - ?> -
-
-
-
- - - -
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- instructor_role ) ) : - ?> -
-
-
- - - -
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
- -
-
-
- - - -
tutor_price( $earning_sum->total_income ) ); ?>
-
-
tutor_price( $earning_sum->total_income ) ); ?>
-
-
-
- -
-
- -url . 'assets/images/placeholder.svg'; -$courses_in_progress = CourseModel::get_active_courses_by_user( get_current_user_id() ); -?> - -have_posts() ) : ?> -
-
- -
- have_posts() ) : - $courses_in_progress->the_post(); - $tutor_course_img = get_tutor_course_thumbnail_src(); - $course_rating = tutor_utils()->get_course_rating( get_the_ID() ); - $course_progress = tutor_utils()->get_course_completed_percent( get_the_ID(), 0, true ); - $completed_number = 0 === (int) $course_progress['completed_count'] ? 1 : (int) $course_progress['completed_count']; - ?> -
-
-
-
- <?php the_title(); ?> -
-
- -
-
- -
- star_rating_generator( $course_rating->rating_avg ); ?> -
- rating_avg, 2 ) ); ?> -
-
- - -
- -
- -
- - - - - - - - - - - -
- -
-
-
-
- -
- - - - - -
-
-
-
- -
-
- - -
- - -get_courses_for_instructors( get_current_user_id() ); - -if ( count( $instructor_course ) ) { - $course_badges = array( - 'publish' => 'success', - 'pending' => 'warning', - 'trash' => 'danger', - ); - - ?> - - -
-
- - - - - - - - - - - - count_enrolled_users_by_course( $course->ID ); - $course_status = isset( $status_translations[ $course->post_status ] ) ? $status_translations[ $course->post_status ] : __( $course->post_status, 'tutor' ); //phpcs:ignore - $course_rating = tutor_utils()->get_course_rating( $course->ID ); - $course_badge = isset( $course_badges[ $course->post_status ] ) ? $course_badges[ $course->post_status ] : 'dark'; - ?> - - - - - - - - - - - - - -
-
- + diff --git a/templates/dashboard/instructor/analytics/stat-card-hover.php b/templates/dashboard/instructor/analytics/stat-card-hover.php new file mode 100644 index 0000000000..b98a0eebca --- /dev/null +++ b/templates/dashboard/instructor/analytics/stat-card-hover.php @@ -0,0 +1,52 @@ + + +
+
+ + + render_svg_icon( $hover_content['icon'], 16, 16, array( 'class' => $hover_content['icon_class'] ?? '' ) ); ?> + + +
+
+ + - + + + + + - + + +
+
+ + + + + + render_svg_icon( $hover_content['icon'], 16, 16, array( 'class' => $hover_content['icon_class'] ?? '' ) ); ?> + +
+
+
+
+ diff --git a/templates/dashboard/instructor/analytics/stat-card.php b/templates/dashboard/instructor/analytics/stat-card.php new file mode 100644 index 0000000000..1663781e1a --- /dev/null +++ b/templates/dashboard/instructor/analytics/stat-card.php @@ -0,0 +1,81 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +use TUTOR\Input; +use TUTOR\Instructor; + + +defined( 'ABSPATH' ) || exit; + +// Default values. +$icon_size = $icon_size ?? 24; +$value = $value ?? 0; +$content_display = $content ?? ''; +$show_graph = $show_graph ?? false; +$data = $data ?? array( 0, 0, 0 ); +$hover_content = $hover_content ?? array(); +$variation = empty( $variation ) ? $icon : $variation; + +// Required fields validation. +if ( ! isset( $card_title ) || empty( $card_title ) ) { + return; +} +if ( ! isset( $icon ) || empty( $icon ) ) { + return; +} + +if ( ! empty( $hover_content ) ) { + $start_date = Input::has( 'start_date' ) ? tutor_get_formated_date( 'Y-m-d', Input::get( 'start_date' ) ) : ''; + $end_date = Input::has( 'end_date' ) ? tutor_get_formated_date( 'Y-m-d', Input::get( 'end_date' ) ) : ''; + $template_path = tutor()->path . 'templates/dashboard/instructor/analytics/stat-card-hover.php'; + $hover_template = Instructor::get_template_output( + $template_path, + array( + 'start_date' => $start_date, + 'end_date' => $end_date, + 'hover_content' => $hover_content, + 'hover_amount' => $value, + ), + false + ); +} + +?> +
+
+
+ +
+
+ render_svg_icon( $icon, $icon_size, $icon_size ); ?> +
+
+
+
+ +
+ + +

+ +

+ + + + + +
+ +
+ +
+ +
diff --git a/templates/dashboard/instructor/home.php b/templates/dashboard/instructor/home.php new file mode 100644 index 0000000000..2931bc3eaa --- /dev/null +++ b/templates/dashboard/instructor/home.php @@ -0,0 +1,584 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use TUTOR\Icon; +use TUTOR\Input; +use TUTOR\Instructor; +use TUTOR_REPORT\Analytics; +use Tutor\Models\CourseModel; +use Tutor\Helpers\QueryHelper; +use Tutor\Models\WithdrawModel; +use Tutor\Components\DateFilter; +use Tutor\Components\InputField; +use Tutor\Components\Constants\InputType; + +$sortable_sections = array( + array( + 'id' => 'current_stats', + 'label' => esc_html__( 'Current Stats', 'tutor' ), + 'is_active' => true, + 'order' => 0, + ), + array( + 'id' => 'overview_chart', + 'label' => esc_html__( 'Earning Over Time', 'tutor' ), + 'is_active' => true, + 'order' => 1, + ), + array( + 'id' => 'course_completion_and_leader', + 'label' => esc_html__( 'Course Completion and Leader', 'tutor' ), + 'is_active' => true, + 'order' => 2, + ), + array( + 'id' => 'top_performing_courses', + 'label' => esc_html__( 'Top Performing Courses', 'tutor' ), + 'is_active' => true, + 'order' => 3, + ), + array( + 'id' => 'upcoming_tasks_and_activity', + 'label' => esc_html__( 'Upcoming Tasks and Recent Activity', 'tutor' ), + 'is_active' => true, + 'order' => 4, + ), + array( + 'id' => 'recent_reviews', + 'label' => esc_html__( 'Recent Student Reviews', 'tutor' ), + 'is_active' => true, + 'order' => 6, + ), +); + +$sortable_sections_defaults = array_reduce( + $sortable_sections, + function ( $carry, $section ) { + $carry[ $section['id'] ] = $section['is_active'] ?? false; + return $carry; + }, + array() +); + +$sortable_sections_ids = array_reduce( + $sortable_sections, + function ( $carry, $section ) { + $carry[] = $section['id']; + return $carry; + }, + array() +); + +$upcoming_tasks = array(); +$get_upcoming_live_tasks = array(); +$overview_chart_data = array(); +$recent_reviews = array(); + +$user = wp_get_current_user(); +$instructor_course_ids = CourseModel::get_courses_by_args( + array( + 'post_author' => $user->ID, + 'posts_per_page' => -1, + 'fields' => 'ids', + ) +)->posts; + +$tutor_pro_enabled = tutor_utils()->is_plugin_active( 'tutor-pro/tutor-pro.php' ); +$is_pro_reports = $tutor_pro_enabled && tutor_utils()->is_addon_enabled( 'tutor-report' ); + +$start_date = Input::has( 'start_date' ) ? tutor_get_formated_date( 'Y-m-d', Input::get( 'start_date' ) ) : ''; +$end_date = Input::has( 'end_date' ) ? tutor_get_formated_date( 'Y-m-d', Input::get( 'end_date' ) ) : ''; +$is_all_time = empty( $start_date ) && empty( $end_date ); +$previous_dates = $is_all_time ? array() : Instructor::get_comparison_date_range( $start_date, $end_date ); + +$date_range = fn( $from, $to, $column ) => array( + $column => array( 'BETWEEN', array( $from, $to ) ), +); + +$stat = function ( $current, $previous, $previous_dates ) { + return array_merge( $previous_dates, Instructor::get_stat_card_details( (float) $current, (float) $previous ) ); +}; + +// Total Earnings. +if ( $is_pro_reports ) { + $earnings = Analytics::get_earnings_by_user( $user->ID, '', $start_date, $end_date ); + $total_earnings = $earnings['total_earnings'] ?? 0; + + if ( ! $is_all_time ) { + $previous_period_earnings = Analytics::get_earnings_by_user( + $user->ID, + '', + $previous_dates['previous_start_date'], + $previous_dates['previous_end_date'] + )['total_earnings'] ?? 0; + } +} else { + $total_earnings = WithdrawModel::get_withdraw_summary( $user->ID )->total_income ?? 0; + + if ( ! $is_all_time ) { + $previous_period_earnings = WithdrawModel::get_withdraw_summary( + $user->ID, + $date_range( $previous_dates['previous_start_date'], $previous_dates['previous_end_date'], 'created_at' ) + )->total_income ?? 0; + } +} + +// Total Courses. +$total_courses = CourseModel::get_course_count_by_date( $start_date, $end_date, $user->ID ); +$previous_period_courses = ! $is_all_time + ? CourseModel::get_course_count_by_date( $previous_dates['previous_start_date'], $previous_dates['previous_end_date'], $user->ID ) + : 0; + +// Total Students. +$total_students = Instructor::get_instructor_total_students_by_date_range( $start_date, $end_date, $user->ID ); +$previous_period_students = ! $is_all_time + ? Instructor::get_instructor_total_students_by_date_range( $previous_dates['previous_start_date'], $previous_dates['previous_end_date'], $user->ID ) + : 0; + +// Total Ratings. +$total_ratings_where = ! $is_all_time ? $date_range( $start_date, $end_date, 'reviews.comment_date' ) : array(); +$total_ratings = tutor_utils()->get_instructor_ratings( $user->ID, $total_ratings_where ); +$previous_period_ratings = ! $is_all_time + ? tutor_utils()->get_instructor_ratings( $user->ID, $date_range( $previous_dates['previous_start_date'], $previous_dates['previous_end_date'], 'reviews.comment_date' ) ) + : (object) array( 'rating_avg' => 0 ); + + +/** + * ------------------------- + * Hover (comparison) data + * Only when pro reports + date range and if all-time is not selected. + * ------------------------- + */ +$total_earnings_state_card_details = array(); +$total_courses_state_card_details = array(); +$total_students_state_card_details = array(); +$total_ratings_state_card_details = array(); + +if ( $tutor_pro_enabled && ! $is_all_time ) { + $total_earnings_state_card_details = $stat( $total_earnings, $previous_period_earnings, $previous_dates ); + $total_courses_state_card_details = $stat( $total_courses, $previous_period_courses, $previous_dates ); + $total_students_state_card_details = $stat( $total_students, $previous_period_students, $previous_dates ); + $total_ratings_state_card_details = $stat( $total_ratings->rating_avg, $previous_period_ratings->rating_avg, $previous_dates ); +} + +/** + * ------------------------- + * Stat cards + * ------------------------- + */ +$stat_cards = array( + array( + 'variation' => 'success', + 'title' => esc_html__( 'Total Earnings', 'tutor' ), + 'icon' => Icon::EARNING, + 'value' => wp_kses_post( tutor_utils()->tutor_price( $total_earnings ?? 0 ) ), + 'hover_content' => $total_earnings_state_card_details, + ), + array( + 'variation' => 'exception1', + 'title' => esc_html__( 'Total Courses', 'tutor' ), + 'icon' => Icon::COURSES, + 'value' => $total_courses, + 'hover_content' => $total_courses_state_card_details, + ), + array( + 'variation' => 'exception5', + 'title' => esc_html__( 'Total Students', 'tutor' ), + 'icon' => Icon::PASSED, + 'value' => $total_students, + 'hover_content' => $total_students_state_card_details, + ), + array( + 'variation' => 'exception4', + 'title' => esc_html__( 'Avg. Rating', 'tutor' ), + 'icon' => Icon::STAR_LINE, + 'value' => $total_ratings->rating_avg, + 'hover_content' => $total_ratings_state_card_details, + ), +); + + +/** + * ------------------------- + * Graph data (only for pro) + * ------------------------- + */ +if ( $is_pro_reports ) { + $labels = wp_list_pluck( $earnings['earnings'], 'label_name' ); + $graph_earnings = array_map( 'intval', wp_list_pluck( $earnings['earnings'], 'total' ) ); + $enrollments = Analytics::get_total_students_by_user( $user->ID, '', $start_date, $end_date ); + $graph_enrollments = array_map( 'intval', wp_list_pluck( $enrollments['enrollments'], 'total' ) ); + $overview_chart_data = array( + 'earnings' => array_merge( array( 0 ), $graph_earnings, array( 0 ) ), + 'enrolled' => array_merge( array( 0 ), $graph_enrollments, array( 0 ) ), + 'labels' => array_merge( array( '' ), $labels, array( '' ) ), + ); +} + +// Course Completion Distribution. +$distribution = Instructor::get_course_completion_distribution_data_by_instructor( $instructor_course_ids ); + +$course_completion_data = array( + 'enrolled' => array( + 'label' => esc_html__( 'Enrolled', 'tutor' ), + 'value' => $distribution['enrolled'], + ), + 'completed' => array( + 'label' => esc_html__( 'Completed', 'tutor' ), + 'value' => $distribution['completed'], + ), + 'in_progress' => array( + 'label' => esc_html__( 'In Progress', 'tutor' ), + 'value' => $distribution['inprogress'], + ), + 'inactive' => array( + 'label' => esc_html__( 'Inactive', 'tutor' ), + 'value' => $distribution['inactive'], + ), + 'cancelled' => array( + 'label' => esc_html__( 'Cancelled', 'tutor' ), + 'value' => $distribution['cancelled'], + ), +); + +// @todo Will be added on later. +// $leaderboard_data = array( +// array( +// 'name' => esc_html__( 'John Doe', 'tutor' ), +// 'avatar' => 'https://i.pravatar.cc/300?u=a04258a2462d826712d', +// 'no_of_courses' => 10, +// 'completion_percentage' => 50, +// ), +// array( +// 'name' => esc_html__( 'Jane Doe', 'tutor' ), +// 'avatar' => 'https://i.pravatar.cc/300?u=a042581f4e29026704d', +// 'no_of_courses' => 20, +// 'completion_percentage' => 30, +// ), +// array( +// 'name' => esc_html__( 'Bob Doe', 'tutor' ), +// 'avatar' => 'https://i.pravatar.cc/300?u=a04258a2462d826732d', +// 'no_of_courses' => 30, +// 'completion_percentage' => 70, +// ), +// ); + +// Top Performing Courses. +$args = array( + 'start_date' => $start_date, + 'end_date' => $end_date, + 'order_by' => Input::get( 'type', 'revenue' ), +); + +$top_performing_courses = Instructor::format_instructor_top_performing_courses( + Instructor::get_top_performing_courses_by_instructor( $user->ID, $args ) +); + +// Upcoming Live Tasks (all-time + pro only). +if ( $is_all_time && $tutor_pro_enabled ) { + $upcoming_tasks = Instructor::format_instructor_upcoming_live_tasks( + Instructor::get_instructor_upcoming_live_tasks( $user->ID ) + ); +} + +// @todo will be added later. +// $recent_activity = array( +// array( +// 'course_name' => 'Complete Web Development Bootcamp', +// 'course_url' => '#', +// 'date' => '2022-01-01 10:00 AM', +// 'meta' => 'enrolled in', +// 'user' => array( +// 'name' => 'John Doe', +// 'avatar' => 'https://i.pravatar.cc/300?u=a04258a2462d826712d', +// ), +// ), +// array( +// 'course_name' => 'Complete Web Development Bootcamp', +// 'course_url' => '#', +// 'date' => '2022-01-01 10:00 AM', +// 'meta' => 'enrolled in', +// 'user' => array( +// 'name' => 'John Doe', +// 'avatar' => 'https://i.pravatar.cc/300?u=a04258a2462d826712d', +// ), +// ), +// array( +// 'course_name' => 'Complete Web Development Bootcamp', +// 'course_url' => '#', +// 'date' => '2022-01-01 10:00 AM', +// 'meta' => 'enrolled in', +// 'user' => array( +// 'name' => 'John Doe', +// 'avatar' => 'https://i.pravatar.cc/300?u=a04258a2462d826712d', +// ), +// ), +// ); + +// Recent Reviews. +$review_where = array( 'comment_post_ID' => array( 'IN', $instructor_course_ids ) ); +if ( ! $is_all_time ) { + $review_where['comment_date'] = array( 'BETWEEN', array( $start_date, $end_date ) ); +} +$review_args = array( 'where' => QueryHelper::prepare_where_clause( $review_where ) ); +$reviews = tutor_utils()->get_reviews_by_instructor( $user->ID, 0, 3, '', '', $review_args ); +$recent_reviews = Instructor::format_instructor_recent_reviews( $reviews->results ); +?> + +
+ +
+ + type( DateFilter::TYPE_RANGE )->placement( 'bottom-start' )->render(); ?> + + +
+ + +
+
+ +
+ + type( InputType::CHECKBOX ) + ->name( "$section[id]" ) + ->label( $section['label'] ) + ->attr( 'x-bind', "register('$section[id]')" ) + ->render(); + ?> +
+ +
+
+
+
+ + +
+ +
+ $card['variation'] ?? 'enrolled', + 'card_title' => $card['title'] ?? '', + 'icon' => $card['icon'] ?? '', + 'value' => $card['value'] ?? '', + 'content' => $card['content'] ?? '', + 'hover_content' => $card['hover_content'] ?? array(), + ) + ); + ?> +
+ +
+ + + $overview_chart_data, + ) + ); + ?> + +
+ + $course_completion_data, + ) + ); + ?> + + + + +
+ + + +
+
+
+ +
+ + + array( + 'revenue' => __( 'Revenue', 'tutor' ), + 'student' => __( 'Student', 'tutor' ), + ), + 'selected' => Input::get( 'type', 'revenue' ), + ); + tutor_load_template( + 'dashboard.instructor.home.top-performing-course-filter', + $data, + ); + ?> +
+ +
+ $item ) : ?> + $item_key, + 'item' => $item, + ), + ) + ?> + +
+
+ + + + +
+ +
+
+ +
+ +
+ + $item ) + ); + ?> + +
+
+ + + + +
+ + + + +
+
+ +
+ +
+ + $review ), + ); + ?> + +
+
+ +
diff --git a/templates/dashboard/instructor/home/course-completion-chart.php b/templates/dashboard/instructor/home/course-completion-chart.php new file mode 100644 index 0000000000..c2164e842a --- /dev/null +++ b/templates/dashboard/instructor/home/course-completion-chart.php @@ -0,0 +1,36 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; +?> + + +
+
+ +
+ + + +
+ $value ) : ?> +
+
+
+ +
+
+ +
+
+
+ +
+
diff --git a/templates/dashboard/instructor/home/overview-chart.php b/templates/dashboard/instructor/home/overview-chart.php new file mode 100644 index 0000000000..7a6646619c --- /dev/null +++ b/templates/dashboard/instructor/home/overview-chart.php @@ -0,0 +1,36 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; +?> + + + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+ diff --git a/templates/dashboard/instructor/home/recent-student-review-item.php b/templates/dashboard/instructor/home/recent-student-review-item.php new file mode 100644 index 0000000000..2e691272a9 --- /dev/null +++ b/templates/dashboard/instructor/home/recent-student-review-item.php @@ -0,0 +1,52 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Avatar; +use Tutor\Components\StarRating; +use Tutor\Components\Constants\Size; + +?> + +
+
+ +
+ src( $review['user']['avatar'] )->initials( $review['user']['name'] )->size( Size::SIZE_48 )->render(); ?> +
+ + +
+ +
+
+
+ +
+
+ + +
+
+ + +
+ rating( $review['rating'] ?? 0 )->render(); ?> +
+
+ + +
+ +
+
+
+
diff --git a/templates/dashboard/instructor/home/top-performing-course-filter.php b/templates/dashboard/instructor/home/top-performing-course-filter.php new file mode 100644 index 0000000000..c2c2829c84 --- /dev/null +++ b/templates/dashboard/instructor/home/top-performing-course-filter.php @@ -0,0 +1,45 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use TUTOR\Icon; +?> + +
+ +
+
+ $option ) : ?> + + + + +
+
+
diff --git a/templates/dashboard/instructor/home/top-performing-course-item.php b/templates/dashboard/instructor/home/top-performing-course-item.php new file mode 100644 index 0000000000..f6c23edc9a --- /dev/null +++ b/templates/dashboard/instructor/home/top-performing-course-item.php @@ -0,0 +1,56 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use TUTOR\Icon; +?> + +
+
+
+ # +
+
+ +
+
+ +
+ +
+
+ render_svg_icon( Icon::DOLLAR, 12, 12, array( 'class' => 'tutor-icon-secondary' ) ); ?> +
+ +
+
+ +
+ +
+
+ + +
+
+ + render_svg_icon( Icon::STUDENT, 12, 12, array( 'class' => 'tutor-icon-secondary' ) ); ?> +
+ +
+
+ +
+ +
+
+
+
diff --git a/templates/dashboard/instructor/home/upcoming-task-item.php b/templates/dashboard/instructor/home/upcoming-task-item.php new file mode 100644 index 0000000000..99179f9b41 --- /dev/null +++ b/templates/dashboard/instructor/home/upcoming-task-item.php @@ -0,0 +1,70 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use TUTOR\Icon; +use Tutor\Components\Badge; + +$get_icon_by_post_type = function ( $post_type ) { + switch ( $post_type ) { + case 'tutor_assignments': + return Icon::ASSIGNMENT; + case 'tutor-google-meet': + return Icon::GOOGLE_MEET_COLORIZE; + case 'tutor_quiz': + return Icon::QUIZ; + case 'tutor_zoom_meeting': + return Icon::ZOOM_COLORIZE; + case 'lesson': + return Icon::LESSON; + } +}; + +$label = __( 'Live Session', 'tutor' ); +?> + +
+
+ render_svg_icon( Icon::VIDEO_CAMERA ); ?> +
+
+
+ + + + + + + + + + + +
+
+ +
+
+
+ icon( $get_icon_by_post_type( $item['post_type'] ) ) + ->label( $label ) + ->rounded() + ->render(); + ?> +
+
+ + + render_svg_icon( Icon::CHEVRON_RIGHT_2 ); ?> + +
+
diff --git a/templates/dashboard/instructor/profile-statistics.php b/templates/dashboard/instructor/profile-statistics.php new file mode 100644 index 0000000000..5b302e3667 --- /dev/null +++ b/templates/dashboard/instructor/profile-statistics.php @@ -0,0 +1,38 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +use TUTOR\Icon; +?> + +
+ +
+ + +
+ +
+
+ render_svg_icon( + $stat['icon'], + 24, + 24, + array( 'class' => $stat['icon_class'] ) + ); + ?> +
+
+

+
+
+
+ +
diff --git a/templates/demo-components/dashboard/components/star-rating.php b/templates/demo-components/dashboard/components/star-rating.php index f1840c4523..1a44ec7fe8 100644 --- a/templates/demo-components/dashboard/components/star-rating.php +++ b/templates/demo-components/dashboard/components/star-rating.php @@ -1,4 +1,6 @@
- - = $i ) { - echo $star_fill; // phpcs:ignore -- already escaped inside template file - } elseif ( ( $rating - $i ) >= -0.5 ) { - echo $star_half; // phpcs:ignore -- already escaped inside template file - } else { - echo $star; // phpcs:ignore -- already escaped inside template file - } - ?> - + = $i; + $is_half = ! $is_full && ( $rating >= ( $i - 0.5 ) ); + + $icon_name = $is_full + ? Icon::STAR_FILL + : ( $is_half ? Icon::STAR_LINE : Icon::STAR_LINE ); // Todo: Half star icon. + + $icon_html = tutor_utils()->render_svg_icon( $icon_name, 16, 16, array(), true ); // phpcs:ignore + ?> +
+ +
diff --git a/templates/demo-components/dashboard/components/stat-card.php b/templates/demo-components/dashboard/components/stat-card.php index 034773cad3..2d25ba8b93 100644 --- a/templates/demo-components/dashboard/components/stat-card.php +++ b/templates/demo-components/dashboard/components/stat-card.php @@ -10,6 +10,7 @@ */ // Default values. +$icon_size = $icon_size ?? 24; $variation = isset( $variation ) ? $variation : 'enrolled'; $value = isset( $value ) ? $value : 0; $change = isset( $change ) ? $change : ''; @@ -24,9 +25,7 @@ return; } -$change_display = ! empty( $change ) - ? $change . ' ' . esc_html__( 'this month', 'tutor' ) - : ''; +$change_display = ! empty( $change ) ? $change : ''; ?>
@@ -35,7 +34,7 @@
- render_svg_icon( $icon, 20, 20 ); ?> + render_svg_icon( $icon, $icon_size, $icon_size ); ?>
diff --git a/templates/user-profile.php b/templates/user-profile.php new file mode 100644 index 0000000000..5eaf73a576 --- /dev/null +++ b/templates/user-profile.php @@ -0,0 +1,75 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +use TUTOR\Icon; +use TUTOR\User; + +$user = wp_get_current_user(); +$student_details = get_userdata( $user->ID ); +$student_meta = get_user_meta( $user->ID ); +$edit_profile_url = tutor_utils()->tutor_dashboard_url( 'account/settings' ); +?> + +
+ +
+ +
+ + +
+
+ render_svg_icon( Icon::MEMBER ); ?> + + user_registered, 'F j, Y' ) ); ?> +
+
+