|null */ private static $data = null; /** * Enqueue core dashboard scripts and register widgets. */ public static function on_load() { if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'manage_options' ) ) { return; } wp_enqueue_script( 'dashboard' ); wp_enqueue_style( 'dashboard' ); if ( wp_is_mobile() ) { wp_enqueue_script( 'jquery-touch-punch' ); } wp_enqueue_style( 'forwp-faq-dashboard', FORWP_FAQ_PLUGIN_URL . 'assets/admin-dashboard.css', [ 'dashboard' ], FORWP_FAQ_VERSION ); wp_add_inline_script( 'dashboard', self::category_tree_script() ); $screen = get_current_screen(); if ( ! $screen ) { return; } add_filter( 'screen_settings', [ __CLASS__, 'filter_screen_settings' ], 10, 2 ); add_filter( 'admin_body_class', [ __CLASS__, 'body_class' ] ); add_meta_box( 'forwp_faq_status', __( 'FAQ Status', '4wp-faq' ), [ __CLASS__, 'render_status' ], $screen->id, 'normal', 'high' ); add_meta_box( 'forwp_faq_by_category', __( 'By category', '4wp-faq' ), [ __CLASS__, 'render_by_category' ], $screen->id, 'column3', 'default' ); add_meta_box( 'forwp_faq_top_reused', __( 'Most reused', '4wp-faq' ), [ __CLASS__, 'render_top_reused' ], $screen->id, 'side', 'high' ); add_meta_box( 'forwp_faq_uncategorized', __( 'Uncategorized', '4wp-faq' ), [ __CLASS__, 'render_uncategorized' ], $screen->id, 'side', 'default' ); self::migrate_category_box( $screen ); } /** * Dashboard page markup (core metabox holder). */ public static function render() { if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Access denied.', '4wp-faq' ) ); } $screen = get_current_screen(); if ( ! $screen ) { return; } $columns = self::layout_columns( $screen ); echo '
'; echo '

' . esc_html__( 'Dashboard', '4wp-faq' ) . '

'; if ( isset( $_GET['forwp_faq_scan_done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

'; echo esc_html__( 'Registry scan completed.', '4wp-faq' ); echo '

'; } echo '
'; echo '
'; echo '
'; do_meta_boxes( $screen->id, 'normal', '' ); echo '
'; echo '
'; do_meta_boxes( $screen->id, 'side', '' ); echo '
'; echo '
'; do_meta_boxes( $screen->id, 'column3', '' ); echo '
'; echo '
'; do_meta_boxes( $screen->id, 'column4', '' ); echo '
'; echo '
'; wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); echo '
'; } /** * @param string $classes Body classes. * @return string */ public static function body_class( $classes ) { return $classes . ' forwp-faq-dashboard-page'; } /** * Screen Options: 50/50, 33/33/33, 33/66. * * @param string $settings Existing extra settings HTML. * @param \WP_Screen $screen Current screen. * @return string */ public static function filter_screen_settings( $settings, $screen ) { if ( ! $screen instanceof \WP_Screen || false === strpos( $screen->id, 'forwp-faq-dashboard' ) ) { return $settings; } $columns = self::layout_columns( $screen ); ob_start(); ?>
id ); if ( $columns < 2 || $columns > 4 ) { return 2; } return $columns; } /** * One-time: move By category from the old 2-column order into column 3. * * @param \WP_Screen $screen Current screen. */ private static function migrate_category_box( $screen ) { $user_id = get_current_user_id(); if ( $user_id < 1 || get_user_meta( $user_id, 'forwp_faq_dash_boxes_v2', true ) ) { return; } update_user_meta( $user_id, 'forwp_faq_dash_boxes_v2', '1' ); $key = 'meta-box-order_' . $screen->id; $order = get_user_option( $key ); if ( ! is_array( $order ) ) { return; } $found = false; foreach ( [ 'normal', 'side', 'advanced', 'column4' ] as $ctx ) { if ( empty( $order[ $ctx ] ) || ! is_string( $order[ $ctx ] ) ) { continue; } $ids = array_values( array_filter( explode( ',', $order[ $ctx ] ) ) ); if ( ! in_array( 'forwp_faq_by_category', $ids, true ) ) { continue; } $order[ $ctx ] = implode( ',', array_values( array_diff( $ids, [ 'forwp_faq_by_category' ] ) ) ); $found = true; } if ( ! $found ) { return; } $col3 = []; if ( ! empty( $order['column3'] ) && is_string( $order['column3'] ) ) { $col3 = array_values( array_filter( explode( ',', $order['column3'] ) ) ); } if ( ! in_array( 'forwp_faq_by_category', $col3, true ) ) { $col3[] = 'forwp_faq_by_category'; } $order['column3'] = implode( ',', $col3 ); update_user_meta( $user_id, $key, $order ); } /** * @return array */ private static function data() { if ( null === self::$data ) { self::$data = Plugin::get_dashboard_data(); } return self::$data; } /** * Status widget — WC-style metric rows. */ public static function render_status() { $data = self::data(); $stats = isset( $data['stats'] ) && is_array( $data['stats'] ) ? $data['stats'] : []; $list = admin_url( 'edit.php?post_type=' . rawurlencode( Settings::get_post_type() ) ); $cats = isset( $data['categories_url'] ) ? (string) $data['categories_url'] : ''; $setup = ! empty( $data['setup_complete'] ); if ( ! $setup ) { $setup_url = isset( $data['setup_url'] ) ? (string) $data['setup_url'] : ''; $action = ''; if ( $setup_url ) { $action = '' . esc_html__( 'Open setup', '4wp-faq' ) . ''; } self::render_empty( __( 'Complete registry setup to see dashboard slices.', '4wp-faq' ), $action ); return; } $rows = [ [ 'url' => $list, 'value' => (int) ( $stats['total_questions'] ?? 0 ), 'label' => __( 'Questions', '4wp-faq' ), ], [ 'url' => $cats, 'value' => (int) ( $stats['faq_categories'] ?? 0 ), 'label' => __( 'Categories', '4wp-faq' ), ], [ 'url' => $list, 'value' => (int) ( $stats['reused_questions'] ?? 0 ), 'label' => __( 'Reused', '4wp-faq' ), ], [ 'url' => $list, 'value' => (int) ( $stats['single_use_questions'] ?? 0 ), 'label' => __( 'Used once', '4wp-faq' ), ], [ 'url' => admin_url( 'edit.php?post_type=page' ), 'value' => (int) ( $stats['pages_with_faq'] ?? 0 ), 'label' => __( 'Pages with FAQ', '4wp-faq' ), ], [ 'url' => admin_url( 'edit.php' ), 'value' => (int) ( $stats['posts_with_faq'] ?? 0 ), 'label' => __( 'Posts with FAQ', '4wp-faq' ), ], [ 'url' => $list, 'value' => (int) ( $stats['uncategorized'] ?? 0 ), 'label' => __( 'Uncategorized', '4wp-faq' ), ], [ 'url' => $list, 'value' => (int) ( $stats['unused_questions'] ?? 0 ), 'label' => __( 'Unused', '4wp-faq' ), ], ]; self::render_language_note( $data ); echo '
'; foreach ( $rows as $row ) { echo ''; echo '' . esc_html( number_format_i18n( $row['value'] ) ) . ''; echo '' . esc_html( $row['label'] ) . ''; echo ''; } echo '
'; echo '
'; echo ''; if ( ! empty( $data['last_scan_label'] ) ) { echo esc_html( sprintf( /* translators: %s: date/time */ __( 'Last scan: %s', '4wp-faq' ), $data['last_scan_label'] ) ); } else { echo esc_html__( 'Not scanned yet.', '4wp-faq' ); } echo ''; if ( current_user_can( 'manage_options' ) ) { echo '
'; echo ''; echo ''; wp_nonce_field( 'forwp_faq_scan' ); echo ''; echo '
'; } echo '
'; } /** * Which language the numbers are for; per-language breakdown when "All languages" is selected. * * @param array $data Dashboard data. */ public static function render_language_note( $data ) { $name = isset( $data['language']['name'] ) ? (string) $data['language']['name'] : ''; if ( '' === $name ) { return; } echo '

'; echo esc_html( sprintf( /* translators: %s: language name or "All languages" */ __( 'Language: %s', '4wp-faq' ), $name ) ); echo '

'; $rows = isset( $data['by_language'] ) && is_array( $data['by_language'] ) ? $data['by_language'] : []; if ( empty( $rows ) ) { return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $rows as $row ) { echo ''; echo ''; foreach ( [ 'questions', 'categories', 'uncategorized', 'pages' ] as $key ) { echo ''; } echo ''; } echo '
' . esc_html__( 'Language', '4wp-faq' ) . '' . esc_html__( 'Questions', '4wp-faq' ) . '' . esc_html__( 'Categories', '4wp-faq' ) . '' . esc_html__( 'Uncategorized', '4wp-faq' ) . '' . esc_html__( 'Pages with FAQ', '4wp-faq' ) . '
' . esc_html( (string) ( $row['name'] ?? '' ) ) . '' . esc_html( number_format_i18n( (int) ( $row[ $key ] ?? 0 ) ) ) . '
'; } /** * Category slice widget — saved parent → child order, collapsible. */ public static function render_by_category() { $data = self::data(); $items = isset( $data['by_category'] ) && is_array( $data['by_category'] ) ? $data['by_category'] : []; if ( empty( $items ) ) { self::render_empty( __( 'No FAQ categories yet.', '4wp-faq' ) ); return; } echo ''; } /** * Render category rows recursively — collapsible when children exist. * * @param list> $items Tree nodes. * @param int $depth Nesting depth. */ private static function render_category_rows( $items, $depth = 0 ) { foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $name = isset( $item['name'] ) ? (string) $item['name'] : ''; $count = isset( $item['count'] ) ? (int) $item['count'] : 0; $url = isset( $item['url'] ) ? (string) $item['url'] : ''; $children = isset( $item['children'] ) && is_array( $item['children'] ) ? $item['children'] : []; $term_id = isset( $item['id'] ) ? (int) $item['id'] : 0; if ( ! empty( $children ) ) { echo '
  • '; echo '
    '; echo ''; if ( $url ) { echo '' . esc_html( $name ) . ''; } else { echo '' . esc_html( $name ) . ''; } echo '' . esc_html( number_format_i18n( $count ) ) . ''; echo ''; echo '
      '; self::render_category_rows( $children, $depth + 1 ); echo '
    '; echo '
    '; echo '
  • '; } else { echo '
  • '; if ( $url ) { echo '' . esc_html( $name ) . ''; } else { echo '' . esc_html( $name ) . ''; } echo '' . esc_html( number_format_i18n( $count ) ) . ''; echo '
  • '; } } } /** * Persist collapsed/expanded category state in the browser. * * @return string */ private static function category_tree_script() { return <<<'JS' jQuery(function($){ const storageKey = 'forwpFaqDashboardCategoryState'; const $details = $('#forwp_faq_by_category details[data-term-id]'); if (!$details.length) { return; } let state = {}; try { state = JSON.parse(window.localStorage.getItem(storageKey) || '{}') || {}; } catch (e) { state = {}; } $details.each(function(){ const termId = String($(this).data('term-id') || ''); if (!termId) { return; } if (Object.prototype.hasOwnProperty.call(state, termId)) { this.open = !!state[termId]; } else { this.open = false; } }); $details.on('toggle', function(){ const termId = String($(this).data('term-id') || ''); if (!termId) { return; } state[termId] = this.open; try { window.localStorage.setItem(storageKey, JSON.stringify(state)); } catch (e) { // Ignore storage failures. } }); }); JS; } /** * Most reused questions. */ public static function render_top_reused() { $data = self::data(); $items = isset( $data['top_reused'] ) && is_array( $data['top_reused'] ) ? $data['top_reused'] : []; if ( empty( $items ) ) { self::render_empty( __( 'No reused questions yet.', '4wp-faq' ) ); return; } echo '
      '; foreach ( $items as $item ) { $title = isset( $item['title'] ) ? (string) $item['title'] : ''; $count = isset( $item['count'] ) ? (int) $item['count'] : 0; $url = isset( $item['edit_url'] ) ? (string) $item['edit_url'] : ''; echo '
    • '; if ( $url ) { echo '' . esc_html( $title ) . ''; } else { echo '' . esc_html( $title ) . ''; } echo '' . esc_html( number_format_i18n( $count ) ) . ''; echo '
    • '; } echo '
    '; } /** * Questions with no category. */ public static function render_uncategorized() { $data = self::data(); $items = isset( $data['uncategorized'] ) && is_array( $data['uncategorized'] ) ? $data['uncategorized'] : []; if ( empty( $items ) ) { self::render_empty( __( 'Every question has a category.', '4wp-faq' ) ); return; } echo '
      '; foreach ( $items as $item ) { $title = isset( $item['title'] ) ? (string) $item['title'] : ''; $url = isset( $item['edit_url'] ) ? (string) $item['edit_url'] : ''; echo '
    • '; if ( $url ) { echo '' . esc_html( $title ) . ''; } else { echo '' . esc_html( $title ) . ''; } echo '
    • '; } echo '
    '; } /** * Native empty-state block for dashboard widgets. * * @param string $message Empty copy. * @param string $action Optional HTML (already escaped). */ private static function render_empty( $message, $action = '' ) { echo '
    '; echo '

    ' . esc_html( $message ) . '

    '; if ( '' !== $action ) { echo '

    ' . $action . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped markup. } echo '
    '; } }