[ 'queued' => 'Queued', 'in_progress' => 'In Progress', 'draft' => 'Draft', 'review' => 'Review', 'published' => 'Published' ], 'priority' => [ 'low' => 'Low', 'medium' => 'Medium', 'high' => 'High', 'urgent' => 'Urgent' ], 'content_type' => [ 'blog_post' => 'Blog Post', 'landing_page' => 'Landing Page', 'product_page' => 'Product Page', 'guide_tutorial' => 'Guide Tutorial', 'news_article' => 'News Article', 'review' => 'Review', 'comparison' => 'Comparison', 'email' => 'Email', 'social_media' => 'Social Media' ] ]; // Check for specific field mapping first if (isset($field_mappings[$field_name][$value])) { return $field_mappings[$field_name][$value]; } // Fallback: convert snake_case to Title Case return ucwords(str_replace('_', ' ', $value)); } /** * Get proper case for enum values * * @param string $value The snake_case value * @return string The proper case value */ function igny8_get_proper_case_enum($value) { $enum_mapping = [ // Content Type values 'blog_post' => 'Blog Post', 'landing_page' => 'Landing Page', 'product_page' => 'Product Page', 'guide_tutorial' => 'Guide Tutorial', 'news_article' => 'News Article', 'review' => 'Review', 'comparison' => 'Comparison', 'page' => 'Page', 'product' => 'Product', 'product_description' => 'Product Description', 'email' => 'Email', 'social_media' => 'Social Media', // Status values 'unmapped' => 'Unmapped', 'mapped' => 'Mapped', 'queued' => 'Queued', 'published' => 'Published', 'active' => 'Active', 'inactive' => 'Inactive', 'archived' => 'Archived', 'draft' => 'Draft', 'in_progress' => 'In Progress', 'completed' => 'Completed', 'cancelled' => 'Cancelled', 'pending' => 'Pending', 'failed' => 'Failed', 'lost' => 'Lost', 'approved' => 'Approved', 'rejected' => 'Rejected', 'needs_revision' => 'Needs Revision', 'planning' => 'Planning', 'paused' => 'Paused', 'review' => 'Review', // Intent values 'informational' => 'Informational', 'navigational' => 'Navigational', 'transactional' => 'Transactional', 'commercial' => 'Commercial', // Link type values 'dofollow' => 'Dofollow', 'nofollow' => 'Nofollow', 'sponsored' => 'Sponsored', 'ugc' => 'UGC', // Coverage status values 'fully_mapped' => 'Fully Mapped', 'partially_mapped' => 'Partially Mapped', 'not_mapped' => 'Not Mapped', // Suggestion type values 'title_optimization' => 'Title Optimization', 'meta_description' => 'Meta Description', 'heading_structure' => 'Heading Structure', 'content_improvement' => 'Content Improvement', 'internal_linking' => 'Internal Linking', // Tone values 'professional' => 'Professional', 'casual' => 'Casual', 'friendly' => 'Friendly', 'authoritative' => 'Authoritative', 'conversational' => 'Conversational', // Category values 'business' => 'Business', 'creative' => 'Creative', 'technical' => 'Technical', 'marketing' => 'Marketing', 'educational' => 'Educational' ]; return $enum_mapping[$value] ?? ucwords(str_replace('_', ' ', $value)); } /** * Apply proper case transformation to a value * * @param string $value The value to transform * @param string $column_name The column name for special handling * @return string The transformed value */ function igny8_apply_proper_case($value, $column_name) { // Apply global proper case transformation to all enum fields return igny8_get_proper_case_enum($value); } /** * Format date for created_at column (tasks table) * Shows hours/days ago if less than 30 days, month/day if greater */ function igny8_format_created_date($date_string) { if (empty($date_string)) { return 'Never'; } try { // Use WordPress timezone $wp_timezone = wp_timezone(); $date = new DateTime($date_string, $wp_timezone); $now = new DateTime('now', $wp_timezone); $diff = $now->diff($date); // Calculate total days difference $total_days = $diff->days; // If less than 30 days, show relative time if ($total_days < 30) { if ($total_days == 0) { if ($diff->h > 0) { $result = $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; } elseif ($diff->i > 0) { $result = $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; } else { $result = 'Just now'; } } else { $result = $total_days . ' day' . ($total_days > 1 ? 's' : '') . ' ago'; } } else { // If 30+ days, show month and day $result = $date->format('M j'); } return $result; } catch (Exception $e) { // Fallback to original date if parsing fails $fallback = wp_date('M j', strtotime($date_string)); return $fallback; } } /** * Format date for updated_at column (drafts/published tables) * Shows hours/days ago if less than 30 days, month/day if greater */ function igny8_format_updated_date($date_string) { if (empty($date_string)) { return 'Never'; } try { // Use WordPress timezone $wp_timezone = wp_timezone(); $date = new DateTime($date_string, $wp_timezone); $now = new DateTime('now', $wp_timezone); $diff = $now->diff($date); // Calculate total days difference $total_days = $diff->days; // If less than 30 days, show relative time if ($total_days < 30) { if ($total_days == 0) { if ($diff->h > 0) { return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; } elseif ($diff->i > 0) { return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; } else { return 'Just now'; } } else { return $total_days . ' day' . ($total_days > 1 ? 's' : '') . ' ago'; } } else { // If 30+ days, show month and day return $date->format('M j'); } } catch (Exception $e) { // Fallback to original date if parsing fails return wp_date('M j', strtotime($date_string)); } } /** * Display image prompts in a formatted way for table display * * @param string $image_prompts JSON string of image prompts * @return string Formatted display string */ function igny8_display_image_prompts($image_prompts) { if (empty($image_prompts)) { return 'No prompts'; } // Try to decode JSON $prompts = json_decode($image_prompts, true); if (!$prompts || !is_array($prompts)) { return 'Invalid format'; } $output = '
'; $count = 0; foreach ($prompts as $key => $prompt) { if (!empty($prompt)) { $count++; $label = ucfirst(str_replace('_', ' ', $key)); $truncated = strlen($prompt) > 50 ? substr($prompt, 0, 50) . '...' : $prompt; $output .= '
'; $output .= '' . esc_html($label) . ': '; $output .= '' . esc_html($truncated) . ''; $output .= '
'; } } if ($count === 0) { $output = 'No prompts'; } else { $output .= '
'; } return $output; } /** * Format structured description for display */ function igny8_format_structured_description_for_display($structured_description) { if (!is_array($structured_description)) { return 'No structured outline available'; } $formatted = "
"; $formatted .= "

Content Outline

"; // Handle introduction section with hook if (!empty($structured_description['introduction'])) { $formatted .= "
"; // Add hook if it exists if (!empty($structured_description['introduction']['hook'])) { $formatted .= "
Hook: " . esc_html($structured_description['introduction']['hook']) . "
"; } // Add paragraphs if they exist if (!empty($structured_description['introduction']['paragraphs']) && is_array($structured_description['introduction']['paragraphs'])) { $paragraph_count = 1; foreach ($structured_description['introduction']['paragraphs'] as $paragraph) { if (!empty($paragraph['details'])) { $formatted .= "
Intro Paragraph " . $paragraph_count . ": " . esc_html($paragraph['details']) . "
"; $paragraph_count++; } } } $formatted .= "
"; } // Handle H2 sections if they exist if (!empty($structured_description['H2']) && is_array($structured_description['H2'])) { foreach ($structured_description['H2'] as $h2_section) { $formatted .= "
"; $formatted .= "
" . esc_html($h2_section['heading']) . "
"; if (!empty($h2_section['subsections'])) { $formatted .= ""; } $formatted .= "
"; } } $formatted .= "
"; return $formatted; } /** * Fetch real table data from database * Phase-2: Real Data Loading from Config * * @param string $tableId The table identifier * @param array $filters Optional filters to apply * @param int $page Page number for pagination * @return array Real data structure */ function igny8_fetch_table_data($tableId, $filters = [], $page = 1, $per_page = null) { global $wpdb; // Sanitize all inputs $tableId = sanitize_text_field($tableId); $page = intval($page); $per_page = $per_page ? intval($per_page) : get_option('igny8_records_per_page', 20); // Sanitize filters array if (is_array($filters)) { $filters = array_map('sanitize_text_field', $filters); } else { $filters = []; } // Get table configuration to apply default filters $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; $GLOBALS['igny8_tables_config'] = $tables_config; $table_config = igny8_get_dynamic_table_config($tableId); // Apply default filters - merge with existing filters if (isset($table_config['default_filter'])) { $default_filters = $table_config['default_filter']; foreach ($default_filters as $key => $value) { // Only apply default filter if not already set by user if (!isset($filters[$key]) || empty($filters[$key])) { $filters[$key] = $value; } } } // Force completed status filter for writer_published table if ($tableId === 'writer_published') { $filters['status'] = ['completed']; } // Get table name from table ID $table_name = igny8_get_table_name($tableId); // Check if table exists if (!igny8_table_exists($table_name)) { // Return empty data if table doesn't exist return [ 'rows' => [], 'pagination' => [ 'page' => $page, 'total' => 0, 'per_page' => $per_page, 'total_pages' => 0 ] ]; } // Build WHERE clause for filters $where_conditions = []; $where_values = []; if (!empty($filters)) { foreach ($filters as $key => $value) { if (!empty($value)) { // Check if this is a range filter (min/max) if (strpos($key, '-min') !== false) { $field_name = str_replace('-min', '', $key); $where_conditions[] = "`{$field_name}` >= %d"; $where_values[] = intval($value); } elseif (strpos($key, '-max') !== false) { $field_name = str_replace('-max', '', $key); $where_conditions[] = "`{$field_name}` <= %d"; $where_values[] = intval($value); } elseif (in_array($key, ['status', 'intent'])) { // For dropdown filters (status, intent), handle both single values and arrays if (is_array($value)) { // For array values (like ['draft'] from default filters) $placeholders = implode(',', array_fill(0, count($value), '%s')); $where_conditions[] = "`{$key}` IN ({$placeholders})"; $where_values = array_merge($where_values, $value); } else { // For single values $where_conditions[] = "`{$key}` = %s"; $where_values[] = $value; } } elseif ($key === 'difficulty') { // For difficulty, convert text label to numeric range $difficulty_range = igny8_get_difficulty_numeric_range($value); if ($difficulty_range) { $where_conditions[] = "`{$key}` >= %d AND `{$key}` <= %d"; $where_values[] = $difficulty_range['min']; $where_values[] = $difficulty_range['max']; } } else { // For keyword search, use LIKE $where_conditions[] = "`{$key}` LIKE %s"; $where_values[] = '%' . $wpdb->esc_like($value) . '%'; } } } } $where_clause = ''; if (!empty($where_conditions)) { $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); } // Build JOIN queries from column configurations $join_queries = []; foreach ($config_columns as $col_key => $col_config) { if (isset($col_config['join_query'])) { $join_query = str_replace(['{prefix}', '{table_name}'], [$wpdb->prefix, $table_name], $col_config['join_query']); if (!in_array($join_query, $join_queries)) { $join_queries[] = $join_query; } } } // Get total count for pagination (with JOINs) $join_clause = ''; if (!empty($join_queries)) { $join_clause = implode(' ', $join_queries); } $count_query = "SELECT COUNT(*) FROM `{$table_name}` {$join_clause} {$where_clause}"; if (!empty($where_values)) { $total_count = $wpdb->get_var($wpdb->prepare($count_query, $where_values)); } else { $total_count = $wpdb->get_var($count_query); } // Calculate pagination $total_pages = ceil($total_count / $per_page); $offset = ($page - 1) * $per_page; // Build main query with JOINs and proper field selection $select_fields = []; // Add base table fields $select_fields[] = "{$table_name}.*"; // Process column configurations to build select fields foreach ($config_columns as $col_key => $col_config) { if (isset($col_config['select_field'])) { $select_fields[] = $col_config['select_field']; } } // Build final query with JOINs $select_clause = implode(', ', $select_fields); $query = "SELECT {$select_clause} FROM `{$table_name}` {$join_clause} {$where_clause} ORDER BY {$table_name}.id DESC LIMIT %d OFFSET %d"; $query_values = array_merge($where_values, [$per_page, $offset]); // Execute query if (!empty($where_values)) { $final_query = $wpdb->prepare($query, $query_values); $results = $wpdb->get_results($final_query, ARRAY_A); } else { $final_query = $wpdb->prepare($query, $per_page, $offset); $results = $wpdb->get_results($final_query, ARRAY_A); } // Format results for frontend - return complete table body HTML using PHP templating $table_body_html = ''; // Load table configuration once (needed for both results and empty state) $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; $GLOBALS['igny8_tables_config'] = $tables_config; $table_config = igny8_get_dynamic_table_config($tableId); $config_columns = $table_config['columns'] ?? []; // Get column keys from humanize_columns, including date columns $column_keys = $table_config['humanize_columns'] ?? array_keys($config_columns); if ($results) { foreach ($results as $row) { $id = $row['id'] ?? 0; // Use PHP templating instead of string concatenation ob_start(); ?> prefix, $table_name], $calculation_query); $query = str_replace('{table_name}.id', $row['id'], $query); $value = $wpdb->get_var($query) ?? 0; } else { $value = 0; } } elseif ($col === 'cluster_id' || $col === 'keyword_cluster_id') { // Check if this column has a display_field from JOIN query $display_field = $column_config['display_field'] ?? null; if ($display_field && isset($row[$display_field])) { $value = $row[$display_field]; } else { // Fallback to fetching cluster name by ID $cluster_id = isset($row[$col]) ? $row[$col] : ''; $value = igny8_get_cluster_term_name($cluster_id); } } elseif ($col === 'source') { // Handle source column - use actual field value $value = isset($row[$col]) ? $row[$col] : ''; } elseif ($col === 'sector_id') { $sector_id = isset($row[$col]) ? $row[$col] : ''; $value = igny8_get_sector_name($sector_id); } elseif ($col === 'difficulty' || $col === 'avg_difficulty') { $difficulty = isset($row[$col]) ? $row[$col] : ''; $value = igny8_get_difficulty_range_name($difficulty); } elseif (in_array($col, ['created_at', 'created_date', 'last_audit', 'discovered_date', 'start_date'])) { // Format created/audit/discovered/start date columns for all tables $date_value = isset($row[$col]) ? $row[$col] : ''; $value = igny8_format_created_date($date_value); } elseif (in_array($col, ['updated_at', 'updated_date', 'next_audit', 'end_date'])) { // Format updated/next audit/end date columns for all tables $date_value = isset($row[$col]) ? $row[$col] : ''; $value = igny8_format_updated_date($date_value); } else { $value = isset($row[$col]) ? $row[$col] : ''; // Apply proper case transformation to eligible columns if (!empty($value) && igny8_should_apply_proper_case($col, $column_config)) { $value = igny8_apply_proper_case($value, $col); } } // Special handling for idea_title column in planner_ideas table if ($col === 'idea_title' && $tableId === 'planner_ideas') { $description = isset($row['idea_description']) ? $row['idea_description'] : ''; $image_prompts = isset($row['image_prompts']) ? $row['image_prompts'] : ''; // Format description for display (handle JSON vs plain text) $display_description = ''; if (!empty($description)) { $decoded = json_decode($description, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { // Format structured description for display $display_description = igny8_format_structured_description_for_display($decoded); } else { // Use as plain text $display_description = $description; } } ?>
Manual
No records found $page, 'total_items' => intval($total_count), 'per_page' => $per_page, 'total_pages' => $total_pages ]; return [ 'table_body_html' => $table_body_html, 'pagination' => $pagination_data ]; } // Render table function function igny8_render_table($table_id, $columns = []) { // Load table configuration with AI-specific modifications $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; $GLOBALS['igny8_tables_config'] = $tables_config; $table_config = igny8_get_dynamic_table_config($table_id); // Set variables for component $module = explode('_', $table_id)[0]; $tab = explode('_', $table_id)[1] ?? ''; // Use config columns if provided, otherwise use passed columns $config_columns = $table_config['columns'] ?? []; // Convert associative array to indexed array with 'key' field, including date columns if (!empty($config_columns)) { $columns = []; foreach ($config_columns as $key => $column_config) { // Check if this column should be humanized $humanize_columns = $table_config['humanize_columns'] ?? []; $should_humanize = in_array($key, $humanize_columns); $columns[] = [ 'key' => $key, 'label' => $column_config['label'] ?? ($should_humanize ? igny8_humanize_label($key) : $key), 'sortable' => $column_config['sortable'] ?? false, 'type' => $column_config['type'] ?? 'text' ]; } } // Start output buffering to capture HTML ob_start(); ?>
↕' : ''; ?>
Keyword Volume KD CPC Intent Status Cluster Actions
'content', 'label' => 'Content', 'sortable' => true], ['key' => 'status', 'label' => 'Status', 'sortable' => true], ['key' => 'date', 'label' => 'Date', 'sortable' => true] ]; $module = $module ?? ''; $tab = $tab ?? ''; // Debug state: Table HTML rendered if (function_exists('igny8_debug_state')) { igny8_debug_state('TABLE_HTML_RENDERED', true, 'Table HTML rendered for ' . $table_id); } ?>