false, 'message' => 'Operation failed']; } try { // Get keyword count and aggregated data $keyword_data = $wpdb->get_row($wpdb->prepare(" SELECT COUNT(*) as keyword_count, COALESCE(SUM(search_volume), 0) as total_volume, COALESCE(AVG(difficulty), 0) as avg_difficulty FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id = %d ", $cluster_id)); // Get mapped pages count from taxonomy relationships $cluster_term_id = $wpdb->get_var($wpdb->prepare(" SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d ", $cluster_id)); $mapped_pages_count = 0; if ($cluster_term_id) { // Count content (posts, pages, products) associated with this cluster term $mapped_pages_count = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT object_id) FROM {$wpdb->prefix}term_relationships tr INNER JOIN {$wpdb->prefix}term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.term_id = %d AND tt.taxonomy = 'clusters' ", $cluster_term_id)); } // Update cluster record $wpdb->update( $wpdb->prefix . 'igny8_clusters', [ 'keyword_count' => intval($keyword_data->keyword_count), 'total_volume' => intval($keyword_data->total_volume), 'avg_difficulty' => floatval($keyword_data->avg_difficulty), 'mapped_pages_count' => intval($mapped_pages_count) ], ['id' => $cluster_id], ['%d', '%d', '%f', '%d'], ['%d'] ); return true; } catch (Exception $e) { error_log("ERROR in igny8_update_cluster_metrics: " . $e->getMessage()); return ['success' => false, 'message' => 'Failed to update cluster metrics: ' . $e->getMessage()]; } } /** * Update campaign metrics */ function igny8_update_campaign_metrics($campaign_id) { global $wpdb; if (!$campaign_id) { return ['success' => false, 'message' => 'Operation failed']; } // Get campaign performance data $performance_data = $wpdb->get_row($wpdb->prepare(" SELECT COUNT(*) as total_tasks, COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_tasks, COALESCE(AVG(seo_score), 0) as avg_seo_score FROM {$wpdb->prefix}igny8_tasks WHERE campaign_id = %d ", $campaign_id)); // Update campaign record $wpdb->update( $wpdb->prefix . 'igny8_campaigns', [ 'total_tasks' => intval($performance_data->total_tasks), 'completed_tasks' => intval($performance_data->completed_tasks), 'avg_seo_score' => floatval($performance_data->avg_seo_score) ], ['id' => $campaign_id], ['%d', '%d', '%f'], ['%d'] ); return true; } /** * Update idea metrics based on related tasks */ function igny8_update_idea_metrics($idea_id) { global $wpdb; if (!$idea_id) { return ['success' => false, 'message' => 'Operation failed']; } // Get tasks count for this idea $tasks_count = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE idea_id = %d ", $idea_id)); // Update idea record $wpdb->update( $wpdb->prefix . 'igny8_content_ideas', [ 'tasks_count' => intval($tasks_count) ], ['id' => $idea_id], ['%d'], ['%d'] ); return true; } /** * Update task metrics based on related variations */ // REMOVED: Task variations functionality - tasks don't need variations /** * Update keyword status when WordPress post is published * * @param int $post_id The WordPress post ID that was published */ function igny8_update_keywords_on_post_publish($post_id) { global $wpdb; if (!$post_id) { return; } // Find tasks that are assigned to this post $tasks = $wpdb->get_results($wpdb->prepare(" SELECT id, cluster_id FROM {$wpdb->prefix}igny8_tasks WHERE assigned_post_id = %d AND cluster_id IS NOT NULL ", $post_id)); if (empty($tasks)) { return; } // Update keyword status to 'mapped' for all keywords in the clusters of published tasks foreach ($tasks as $task) { if ($task->cluster_id) { // Get all keywords in this cluster and update their status to 'mapped' $keyword_ids = $wpdb->get_col($wpdb->prepare(" SELECT id FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id = %d ", $task->cluster_id)); if (!empty($keyword_ids)) { $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); $wpdb->query($wpdb->prepare(" UPDATE {$wpdb->prefix}igny8_keywords SET status = 'mapped' WHERE id IN ({$placeholders}) ", $keyword_ids)); } // Update task status to 'completed' $wpdb->update( $wpdb->prefix . 'igny8_tasks', ['status' => 'completed'], ['id' => $task->id], ['%s'], ['%d'] ); } } // Log the event if (function_exists('igny8_write_log')) { igny8_write_log('post_published', [ 'post_id' => $post_id, 'tasks_updated' => count($tasks), 'message' => 'Keywords updated to mapped status' ]); } } /** * Automatically create clusters from keywords using AI clustering * * @param array $keyword_ids Optional array of keyword IDs to cluster. If empty, uses all unmapped keywords. * @return array Result array with success status and message */ function igny8_auto_create_clusters_from_keywords($keyword_ids = []) { global $wpdb; // If no keyword IDs provided, get all unmapped keywords if (empty($keyword_ids)) { $keyword_ids = $wpdb->get_col(" SELECT id FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id IS NULL "); } if (empty($keyword_ids)) { return ['success' => true, 'message' => 'No unmapped keywords found for clustering']; } // Limit to 20 keywords for AI processing if (count($keyword_ids) > 20) { $keyword_ids = array_slice($keyword_ids, 0, 20); } // Get keywords data $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); $keywords = $wpdb->get_results($wpdb->prepare(" SELECT * FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) ", $keyword_ids)); if (empty($keywords)) { return ['success' => false, 'message' => 'No valid keywords found for clustering']; } // Create clusters using AI (this would call the AI clustering function) // For now, create a simple cluster with all keywords $cluster_name = igny8_generate_cluster_name_from_keywords(array_column($keywords, 'keyword')); // Create cluster $cluster_result = $wpdb->insert( $wpdb->prefix . 'igny8_clusters', [ 'cluster_name' => $cluster_name, 'status' => 'active', 'created_at' => current_time('mysql') ], ['%s', '%s', '%s'] ); if ($cluster_result === false) { return ['success' => false, 'message' => 'Failed to create cluster']; } $cluster_id = $wpdb->insert_id; // Trigger cluster_added action to create taxonomy term do_action('igny8_cluster_added', $cluster_id); // Map all keywords to this cluster $mapped_count = $wpdb->query($wpdb->prepare(" UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = %d, status = 'mapped' WHERE id IN ({$placeholders}) ", array_merge([$cluster_id], $keyword_ids))); // Update cluster metrics igny8_update_cluster_metrics($cluster_id); return [ 'success' => true, 'message' => "Created cluster '{$cluster_name}' with {$mapped_count} keywords", 'cluster_id' => $cluster_id, 'mapped_count' => $mapped_count ]; } /** * Workflow automation trigger system */ function igny8_workflow_triggers($event, $payload = []) { switch ($event) { case 'keywords_imported': if (isset($payload['keyword_ids'])) { return igny8_auto_create_clusters_from_keywords($payload['keyword_ids']); } break; case 'cluster_created': if (isset($payload['cluster_id'])) { // Create ideas from cluster $ideas_result = igny8_auto_create_ideas_from_clusters($payload['cluster_id']); return [ 'success' => true, 'message' => 'Cluster workflow completed', 'ideas' => $ideas_result ]; } break; } return ['success' => true, 'message' => 'No workflow automation for this event']; } /** * Bulk delete keywords with cluster metrics update */ function igny8_bulk_delete_keywords($keyword_ids) { global $wpdb; if (empty($keyword_ids) || !is_array($keyword_ids)) { return ['success' => false, 'message' => 'Operation failed']; } // Get cluster IDs before deletion for metrics update $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); $cluster_ids = $wpdb->get_col($wpdb->prepare(" SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL ", $keyword_ids)); // Delete keywords $result = $wpdb->query($wpdb->prepare(" DELETE FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) ", $keyword_ids)); if ($result !== false) { // Update cluster metrics for affected clusters foreach ($cluster_ids as $cluster_id) { if ($cluster_id) { igny8_update_cluster_metrics($cluster_id); } } return $result; } return ['success' => false, 'message' => 'Operation failed']; } /** * Bulk map keywords to cluster * * @param array $keyword_ids Array of keyword IDs to map * @param int $cluster_id Cluster ID to map keywords to * @return array ['success' => bool, 'message' => string, 'mapped_count' => int] */ function igny8_bulk_map_keywords($keyword_ids, $cluster_id) { global $wpdb; if (empty($keyword_ids) || !is_array($keyword_ids)) { return ['success' => false, 'message' => 'No keywords selected for mapping', 'mapped_count' => 0]; } if (empty($cluster_id) || !is_numeric($cluster_id)) { return ['success' => false, 'message' => 'Invalid cluster ID provided', 'mapped_count' => 0]; } $cluster_id = intval($cluster_id); // Verify cluster exists $cluster_exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", $cluster_id )); if (!$cluster_exists) { return ['success' => false, 'message' => 'Cluster not found', 'mapped_count' => 0]; } // Sanitize and validate IDs $keyword_ids = array_map('intval', $keyword_ids); $keyword_ids = array_filter($keyword_ids, function($id) { return $id > 0; }); if (empty($keyword_ids)) { return ['success' => false, 'message' => 'No valid keyword IDs provided', 'mapped_count' => 0]; } // Get old cluster IDs for metrics update $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); $old_cluster_ids = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL", $keyword_ids )); // Update keywords to new cluster $mapped_count = $wpdb->query($wpdb->prepare( "UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = %d, status = 'mapped', updated_at = CURRENT_TIMESTAMP WHERE id IN ({$placeholders})", array_merge([$cluster_id], $keyword_ids) )); if ($mapped_count === false) { return ['success' => false, 'message' => 'Failed to map keywords to cluster', 'mapped_count' => 0]; } // Update metrics for old clusters (they lost keywords) if (!empty($old_cluster_ids)) { foreach (array_unique($old_cluster_ids) as $old_cluster_id) { if ($old_cluster_id && $old_cluster_id != $cluster_id) { igny8_update_cluster_metrics($old_cluster_id); } } } // Update metrics for new cluster (gained keywords) igny8_update_cluster_metrics($cluster_id); return [ 'success' => true, 'message' => "Successfully mapped {$mapped_count} keyword(s) to cluster", 'mapped_count' => $mapped_count ]; } /** * Bulk unmap keywords from their clusters * * @param array $keyword_ids Array of keyword IDs to unmap * @return array ['success' => bool, 'message' => string, 'unmapped_count' => int] */ function igny8_bulk_unmap_keywords($keyword_ids) { global $wpdb; if (empty($keyword_ids) || !is_array($keyword_ids)) { return ['success' => false, 'message' => 'No keywords selected for unmapping', 'unmapped_count' => 0]; } // Sanitize and validate IDs $keyword_ids = array_map('intval', $keyword_ids); $keyword_ids = array_filter($keyword_ids, function($id) { return $id > 0; }); if (empty($keyword_ids)) { return ['success' => false, 'message' => 'No valid keyword IDs provided', 'unmapped_count' => 0]; } // Get cluster IDs before unmapping for metrics update $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); $cluster_ids = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL", $keyword_ids )); // Update keywords to unmap them $unmapped_count = $wpdb->query($wpdb->prepare( "UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = NULL, status = 'unmapped', updated_at = CURRENT_TIMESTAMP WHERE id IN ({$placeholders})", $keyword_ids )); if ($unmapped_count === false) { return ['success' => false, 'message' => 'Failed to unmap keywords from clusters', 'unmapped_count' => 0]; } // Update metrics for affected clusters (they lost keywords) if (!empty($cluster_ids)) { foreach (array_unique($cluster_ids) as $cluster_id) { if ($cluster_id) { igny8_update_cluster_metrics($cluster_id); } } } return [ 'success' => true, 'message' => "Successfully unmapped {$unmapped_count} keyword(s) from clusters", 'unmapped_count' => $unmapped_count ]; } /** * Automatically create or link a taxonomy term when a new cluster is created. * * @param int $cluster_id The cluster ID from wp_igny8_clusters. */ function igny8_auto_create_cluster_term($cluster_id) { global $wpdb; // 1. Validate cluster $cluster = $wpdb->get_row($wpdb->prepare( "SELECT cluster_name, cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", $cluster_id )); if (!$cluster) { error_log("Igny8: Cluster not found for ID $cluster_id."); return; } // 2. Skip if already mapped if (!empty($cluster->cluster_term_id)) { error_log("Igny8: Cluster $cluster_id already linked to term {$cluster->cluster_term_id}."); return; } // 3. Ensure taxonomy exists if (!taxonomy_exists('clusters') && function_exists('igny8_register_taxonomies')) { igny8_register_taxonomies(); } // 4. Create or find existing term $term_result = wp_insert_term( sanitize_text_field($cluster->cluster_name), 'clusters', ['slug' => sanitize_title($cluster->cluster_name)] ); if (is_wp_error($term_result)) { // If term exists, fetch it $existing = get_term_by('name', $cluster->cluster_name, 'clusters'); $term_id = $existing ? $existing->term_id : 0; } else { $term_id = $term_result['term_id']; } // 5. Save term ID back to cluster table if ($term_id > 0) { $wpdb->update( "{$wpdb->prefix}igny8_clusters", ['cluster_term_id' => $term_id], ['id' => $cluster_id], ['%d'], ['%d'] ); error_log("Igny8: Created and linked taxonomy term $term_id for cluster $cluster_id."); } else { error_log("Igny8: Failed to link taxonomy term for cluster $cluster_id."); } } /** * Automatically update taxonomy term when a cluster name is changed/updated. * * @param int $cluster_id The cluster ID from wp_igny8_clusters. */ function igny8_auto_update_cluster_term($cluster_id) { global $wpdb; // 1. Validate cluster $cluster = $wpdb->get_row($wpdb->prepare( "SELECT cluster_name, cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", $cluster_id )); if (!$cluster) { error_log("Igny8: Cluster not found for ID $cluster_id."); return; } // 2. Skip if no term linked if (empty($cluster->cluster_term_id)) { error_log("Igny8: Cluster $cluster_id has no linked taxonomy term."); return; } // 3. Ensure taxonomy exists if (!taxonomy_exists('clusters') && function_exists('igny8_register_taxonomies')) { igny8_register_taxonomies(); } // 4. Get the existing term $term = get_term($cluster->cluster_term_id, 'clusters'); if (is_wp_error($term) || !$term) { error_log("Igny8: Taxonomy term {$cluster->cluster_term_id} not found for cluster $cluster_id."); return; } // 5. Check if name has changed if ($term->name === $cluster->cluster_name) { error_log("Igny8: Cluster $cluster_id name unchanged, skipping taxonomy update."); return; } // 6. Update the term name and slug $update_result = wp_update_term( $cluster->cluster_term_id, 'clusters', [ 'name' => sanitize_text_field($cluster->cluster_name), 'slug' => sanitize_title($cluster->cluster_name) ] ); if (is_wp_error($update_result)) { error_log("Igny8: Failed to update taxonomy term for cluster $cluster_id: " . $update_result->get_error_message()); } else { error_log("Igny8: Successfully updated taxonomy term {$cluster->cluster_term_id} for cluster $cluster_id."); } } // Hook registration for automatic cluster term creation // Hook moved to sync-hooks.php /** * Handle content association/disassociation with cluster taxonomy terms */ function igny8_handle_content_cluster_association($object_id, $terms, $tt_ids, $taxonomy) { // Only process clusters taxonomy if ($taxonomy !== 'clusters') { return; } global $wpdb; // Get all cluster IDs that have terms in this taxonomy update $cluster_ids = []; foreach ($terms as $term) { if (is_numeric($term)) { $term_id = $term; } else { $term_obj = get_term_by('slug', $term, 'clusters'); $term_id = $term_obj ? $term_obj->term_id : null; } if ($term_id) { // Find cluster that has this term_id $cluster_id = $wpdb->get_var($wpdb->prepare(" SELECT id FROM {$wpdb->prefix}igny8_clusters WHERE cluster_term_id = %d ", $term_id)); if ($cluster_id) { $cluster_ids[] = $cluster_id; } } } // Update metrics for all affected clusters foreach (array_unique($cluster_ids) as $cluster_id) { igny8_update_cluster_metrics($cluster_id); } } // Hook registration for automatic cluster metrics updates when content is associated/disassociated with cluster terms // Hook moved to sync-hooks.php /** * Update task metrics including word count and meta sync */ function igny8_update_task_metrics($task_id) { global $wpdb; $post_id = $wpdb->get_var($wpdb->prepare("SELECT assigned_post_id FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", $task_id)); if ($post_id) { $content = get_post_field('post_content', $post_id); $word_count = str_word_count(strip_tags($content)); update_post_meta($post_id, '_igny8_word_count', $word_count); $wpdb->update("{$wpdb->prefix}igny8_tasks", ['word_count' => $word_count], ['id' => $task_id]); } }