readiness_score = $readiness_score; } /** * Register the meta box hooks. * * @return void */ public function register(): void { add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) ); add_action( 'save_post_product', array( $this, 'on_product_save' ), 20 ); } /** * Add the AI Readiness meta box to the product edit screen. * * @return void */ public function add_meta_box(): void { add_meta_box( 'aicommerce-readiness', __( 'AI Readiness Score', 'ai-discoverability-kit' ), array( $this, 'render' ), 'product', 'side', 'high' ); add_meta_box( 'aicommerce-identifiers', __( 'Tyralix AI Discoverability — Product Identifiers', 'ai-discoverability-kit' ), array( $this, 'render_identifiers' ), 'product', 'normal', 'default' ); } /** * Render the meta box content. * * @param \WP_Post $post The post object. * * @return void */ public function render( \WP_Post $post ): void { $product = wc_get_product( $post->ID ); if ( ! $product instanceof \WC_Product ) { printf( '

%s

', esc_html__( 'Save the product first to see your AI Readiness Score.', 'ai-discoverability-kit' ) ); return; } $cached = $this->readiness_score->get_cached( $product->get_id() ); $score = $cached ?? $this->readiness_score->calculate( $product ); $this->render_score_display( $score ); $this->render_sub_scores( $score ); $this->render_fix_queue( $score ); // Nonce for AJAX refresh. wp_nonce_field( 'aicommerce_meta_box', 'aicommerce_meta_nonce' ); } /** * Render the main score circle. * * @param array $score Score data. * * @return void */ private function render_score_display( array $score ): void { $overall = $score['overall'] ?? 0; $grade = $score['grade'] ?? 'F'; $color = $this->grade_color( $grade ); printf( '
%2$d
%3$s %4$s
', esc_attr( $color ), (int) $overall, esc_html__( 'Grade:', 'ai-discoverability-kit' ), esc_html( $grade ) ); } /** * Render sub-score bars. * * @param array $score Score data. * * @return void */ private function render_sub_scores( array $score ): void { if ( empty( $score['sub_scores'] ) ) { return; } echo '
'; foreach ( $score['sub_scores'] as $sub ) { $bar_color = $sub['score'] >= 80 ? '#00a32a' : ( $sub['score'] >= 50 ? '#dba617' : '#d63638' ); printf( '
%1$s %2$d%%
', esc_html( $sub['label'] ), (int) $sub['score'], esc_attr( $bar_color ) ); } echo '
'; } /** * Render the priority fix queue. * * @param array $score Score data. * * @return void */ private function render_fix_queue( array $score ): void { if ( empty( $score['fix_queue'] ) ) { printf( '

✓ %s

', esc_html__( 'All checks passing!', 'ai-discoverability-kit' ) ); return; } $fixes = array_slice( $score['fix_queue'], 0, 5 ); // Show top 5. printf( '
%s', esc_html__( 'Priority Fixes:', 'ai-discoverability-kit' ) ); echo '
    '; foreach ( $fixes as $fix ) { printf( '
  • %s
  • ', esc_html( $fix['action'] ) ); } echo '
'; } /** * Render the product identifiers meta box. * * @param \WP_Post $post The post object. * * @return void */ public function render_identifiers( \WP_Post $post ): void { $product = wc_get_product( $post->ID ); $brand = ''; $gtin = ''; $mpn = ''; if ( $product instanceof \WC_Product ) { $brand = $product->get_meta( '_aicommerce_brand' ); $gtin = $product->get_meta( '_aicommerce_gtin' ); $mpn = $product->get_meta( '_aicommerce_mpn' ); } wp_nonce_field( 'aicommerce_identifiers', 'aicommerce_identifiers_nonce' ); printf( '

%3$s

%6$s

%9$s

', esc_html__( 'Brand', 'ai-discoverability-kit' ), esc_attr( $brand ), esc_html__( 'Product brand name. Overrides the default brand from settings.', 'ai-discoverability-kit' ), esc_html__( 'GTIN / UPC / EAN', 'ai-discoverability-kit' ), esc_attr( $gtin ), esc_html__( 'Global Trade Item Number (barcode). Used in Product schema for Google and AI agents.', 'ai-discoverability-kit' ), esc_html__( 'MPN', 'ai-discoverability-kit' ), esc_attr( $mpn ), esc_html__( 'Manufacturer Part Number. Relevant for branded/manufactured products.', 'ai-discoverability-kit' ) ); } /** * Recalculate and cache score on product save. * * @param int $post_id The post ID. * * @return void */ public function on_product_save( int $post_id ): void { // Skip autosaves and revisions. if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( wp_is_post_revision( $post_id ) ) { return; } $product = wc_get_product( $post_id ); if ( ! $product instanceof \WC_Product ) { return; } // Save product identifiers if the nonce is valid. if ( isset( $_POST['aicommerce_identifiers_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aicommerce_identifiers_nonce'] ) ), 'aicommerce_identifiers' ) ) { $fields = array( 'aicommerce_brand' => '_aicommerce_brand', 'aicommerce_gtin' => '_aicommerce_gtin', 'aicommerce_mpn' => '_aicommerce_mpn', ); foreach ( $fields as $post_key => $meta_key ) { if ( isset( $_POST[ $post_key ] ) ) { $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ); $product->update_meta_data( $meta_key, $value ); } } $product->save_meta_data(); } $this->readiness_score->calculate_and_cache( $product ); } /** * Get the CSS color for a letter grade. * * @param string $grade The letter grade. * * @return string Hex color code. */ private function grade_color( string $grade ): string { return match ( $grade ) { 'A+', 'A' => '#00a32a', 'B' => '#2271b1', 'C' => '#dba617', 'D' => '#d63638', default => '#a7aaad', }; } }