diff --git a/backend/igny8_core/ai/functions/generate_content.py b/backend/igny8_core/ai/functions/generate_content.py index 6206994b..ce0eda0f 100644 --- a/backend/igny8_core/ai/functions/generate_content.py +++ b/backend/igny8_core/ai/functions/generate_content.py @@ -198,7 +198,7 @@ class GenerateContentFunction(BaseAIFunction): tags = parsed.get('tags', []) categories = parsed.get('categories', []) # Content status should always be 'draft' for newly generated content - # Status can only be changed manually to 'review' or 'published' + # Status can only be changed manually to 'review' or 'publish' content_status = 'draft' else: # Plain text response (legacy) diff --git a/backend/igny8_core/modules/planner/cluster_serializers.py b/backend/igny8_core/modules/planner/cluster_serializers.py index 0a845727..bf9607a2 100644 --- a/backend/igny8_core/modules/planner/cluster_serializers.py +++ b/backend/igny8_core/modules/planner/cluster_serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Clusters, Keywords +from .models import Clusters, Keywords, ContentIdeas from django.db.models import Count, Sum, Avg @@ -9,6 +9,8 @@ class ClusterSerializer(serializers.ModelSerializer): volume = serializers.SerializerMethodField() difficulty = serializers.SerializerMethodField() sector_name = serializers.SerializerMethodField() + ideas_count = serializers.SerializerMethodField() + content_count = serializers.SerializerMethodField() site_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(write_only=True, required=False) @@ -22,6 +24,8 @@ class ClusterSerializer(serializers.ModelSerializer): 'volume', 'difficulty', 'mapped_pages', + 'ideas_count', + 'content_count', 'status', 'sector_name', 'site_id', @@ -89,6 +93,22 @@ class ClusterSerializer(serializers.ModelSerializer): ) ) return round(result['avg_difficulty'] or 0, 1) # Round to 1 decimal place + + def get_ideas_count(self, obj): + """Count content ideas linked to this cluster""" + if hasattr(obj, '_ideas_count'): + return obj._ideas_count + + return ContentIdeas.objects.filter(keyword_cluster_id=obj.id).count() + + def get_content_count(self, obj): + """Count generated content items linked to this cluster via tasks""" + if hasattr(obj, '_content_count'): + return obj._content_count + + from igny8_core.modules.writer.models import Content + + return Content.objects.filter(task__cluster_id=obj.id).count() @classmethod def prefetch_keyword_stats(cls, clusters): @@ -137,12 +157,34 @@ class ClusterSerializer(serializers.ModelSerializer): for stat in keyword_stats } + # Prefetch idea counts + idea_counts = ( + ContentIdeas.objects + .filter(keyword_cluster_id__in=cluster_ids) + .values('keyword_cluster_id') + .annotate(count=Count('id')) + ) + idea_stats = {item['keyword_cluster_id']: item['count'] for item in idea_counts} + + # Prefetch content counts (through writer.Tasks -> Content) + from igny8_core.modules.writer.models import Content + + content_counts = ( + Content.objects + .filter(task__cluster_id__in=cluster_ids) + .values('task__cluster_id') + .annotate(count=Count('id')) + ) + content_stats = {item['task__cluster_id']: item['count'] for item in content_counts} + # Attach stats to each cluster object for cluster in clusters: cluster_stats = stats_dict.get(cluster.id, {'count': 0, 'volume': 0, 'difficulty': 0}) cluster._keywords_count = cluster_stats['count'] cluster._volume = cluster_stats['volume'] cluster._difficulty = cluster_stats['difficulty'] + cluster._ideas_count = idea_stats.get(cluster.id, 0) + cluster._content_count = content_stats.get(cluster.id, 0) return clusters diff --git a/backend/igny8_core/modules/writer/migrations/0002_update_status_choices.py b/backend/igny8_core/modules/writer/migrations/0002_update_status_choices.py new file mode 100644 index 00000000..858ce34f --- /dev/null +++ b/backend/igny8_core/modules/writer/migrations/0002_update_status_choices.py @@ -0,0 +1,45 @@ +from django.db import migrations, models + + +def migrate_content_status_forward(apps, schema_editor): + Content = apps.get_model('writer', 'Content') + Content.objects.filter(status='published').update(status='publish') + + +def migrate_content_status_backward(apps, schema_editor): + Content = apps.get_model('writer', 'Content') + Content.objects.filter(status='publish').update(status='published') + + +class Migration(migrations.Migration): + + dependencies = [ + ('writer', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='tasks', + name='status', + field=models.CharField( + choices=[('queued', 'Queued'), ('completed', 'Completed')], + default='queued', + max_length=50, + ), + ), + migrations.AlterField( + model_name='content', + name='status', + field=models.CharField( + choices=[('draft', 'Draft'), ('review', 'Review'), ('publish', 'Publish')], + default='draft', + help_text='Content workflow status (draft, review, publish)', + max_length=50, + ), + ), + migrations.RunPython( + migrate_content_status_forward, + migrate_content_status_backward, + ), + ] + diff --git a/backend/igny8_core/modules/writer/models.py b/backend/igny8_core/modules/writer/models.py index d55fbd9f..b1e8bca8 100644 --- a/backend/igny8_core/modules/writer/models.py +++ b/backend/igny8_core/modules/writer/models.py @@ -109,9 +109,9 @@ class Content(SiteSectorBaseModel): STATUS_CHOICES = [ ('draft', 'Draft'), ('review', 'Review'), - ('published', 'Published'), + ('publish', 'Publish'), ] - status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, published)") + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, publish)") generated_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index a76d21fa..f9494a82 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -263,8 +263,8 @@ class TasksViewSet(SiteSectorModelViewSet): # Tasks module not available - update status only try: queryset = self.get_queryset() - tasks = queryset.filter(id__in=ids, status__in=['queued', 'in_progress']) - updated_count = tasks.update(status='draft', content='[AI content generation not available]') + tasks = queryset.filter(id__in=ids, status='queued') + updated_count = tasks.update(status='completed', content='[AI content generation not available]') logger.info(f"auto_generate_content: Updated {updated_count} tasks (AI generation not available)") return Response({ diff --git a/frontend/src/config/pages/bulk-action-modal.config.ts b/frontend/src/config/pages/bulk-action-modal.config.ts index e648879f..fb21b0a3 100644 --- a/frontend/src/config/pages/bulk-action-modal.config.ts +++ b/frontend/src/config/pages/bulk-action-modal.config.ts @@ -93,8 +93,6 @@ export const bulkActionModalConfigs: Record = { itemNamePlural: 'tasks', statusOptions: [ { value: 'queued', label: 'Queued' }, - { value: 'draft', label: 'Draft' }, - { value: 'in_progress', label: 'In Progress' }, { value: 'completed', label: 'Completed' }, ], }, @@ -114,7 +112,7 @@ export const bulkActionModalConfigs: Record = { statusOptions: [ { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Review' }, - { value: 'published', label: 'Published' }, + { value: 'publish', label: 'Publish' }, ], }, }, @@ -131,8 +129,9 @@ export const bulkActionModalConfigs: Record = { confirmText: 'Update Status', itemNamePlural: 'published content items', statusOptions: [ - { value: 'published', label: 'Published' }, - { value: 'archived', label: 'Archived' }, + { value: 'publish', label: 'Publish' }, + { value: 'review', label: 'Review' }, + { value: 'draft', label: 'Draft' }, ], }, }, diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 68eae269..12b28504 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -120,6 +120,13 @@ export const createClustersPageConfig = ( width: '120px', render: (value: number) => value.toLocaleString(), }, + { + key: 'ideas_count', + label: 'Ideas', + sortable: false, + width: '120px', + render: (value: number) => value.toLocaleString(), + }, { key: 'volume', label: 'Volume', @@ -170,8 +177,8 @@ export const createClustersPageConfig = ( }, }, { - key: 'mapped_pages', - label: 'Mapped Pages', + key: 'content_count', + label: 'Content', sortable: false, width: '120px', render: (value: number) => value.toLocaleString(), diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 1fc68636..bac44f7c 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -148,20 +148,18 @@ export const createTasksPageConfig = ( sortable: true, sortField: 'status', render: (value: string) => { - const statusColors: Record = { - 'queued': 'warning', - 'in_progress': 'info', - 'draft': 'warning', - 'review': 'info', - 'published': 'success', - 'completed': 'success', + const statusColors: Record = { + queued: 'warning', + completed: 'success', }; + const label = value ? value.replace('_', ' ') : ''; + const formatted = label ? label.charAt(0).toUpperCase() + label.slice(1) : ''; return ( - {value?.replace('_', ' ') || value} + {formatted} ); }, @@ -193,10 +191,6 @@ export const createTasksPageConfig = ( options: [ { value: '', label: 'All Status' }, { value: 'queued', label: 'Queued' }, - { value: 'in_progress', label: 'In Progress' }, - { value: 'draft', label: 'Draft' }, - { value: 'review', label: 'Review' }, - { value: 'published', label: 'Published' }, { value: 'completed', label: 'Completed' }, ], }, @@ -318,10 +312,6 @@ export const createTasksPageConfig = ( handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'queued', label: 'Queued' }, - { value: 'in_progress', label: 'In Progress' }, - { value: 'draft', label: 'Draft' }, - { value: 'review', label: 'Review' }, - { value: 'published', label: 'Published' }, { value: 'completed', label: 'Completed' }, ], }, @@ -340,16 +330,10 @@ export const createTasksPageConfig = ( calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length, }, { - label: 'In Progress', - value: 0, - accentColor: 'blue' as const, - calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length, - }, - { - label: 'Published', + label: 'Completed', value: 0, accentColor: 'green' as const, - calculate: (data) => data.tasks.filter((t: Task) => t.status === 'published').length, + calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length, }, ], }; diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index ec6aff02..ec5a77b3 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -8,7 +8,7 @@ import HTMLContentRenderer from '../../components/common/HTMLContentRenderer'; const statusColors: Record = { draft: 'warning', review: 'info', - published: 'success', + publish: 'success', }; export default function Content() { @@ -176,7 +176,7 @@ export default function Content() { size="sm" variant="light" > - {item.status?.replace('_', ' ') || 'draft'} + {(item.status || 'draft').replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase())} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 93cc3d7a..f788e59b 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -477,6 +477,8 @@ export interface Cluster { volume: number; difficulty: number; // Average difficulty of keywords in cluster mapped_pages: number; + ideas_count: number; + content_count: number; status: string; sector_name?: string | null; // Optional: populated by serializer created_at: string;