""" Dashboard API Views Provides aggregated data for the frontend dashboard in a single call. Replaces multiple sequential API calls for better performance. """ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.db.models import Count, Sum, Q, F from django.utils import timezone from datetime import timedelta from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter from igny8_core.auth.models import Site, Sector from igny8_core.business.planning.models import Keywords, Clusters, ContentIdeas from igny8_core.business.content.models import Tasks, Content from igny8_core.business.billing.models import CreditUsageLog from igny8_core.ai.models import AITaskLog @extend_schema_view( summary=extend_schema( tags=['Dashboard'], summary='Get dashboard summary', description='Returns aggregated dashboard data including pipeline counts, AI operations, recent activity, and items needing attention.', parameters=[ OpenApiParameter( name='site_id', description='Filter by specific site ID', required=False, type=int ), OpenApiParameter( name='days', description='Number of days for recent activity and AI operations (default: 7)', required=False, type=int ), ] ), ) class DashboardSummaryViewSet(viewsets.ViewSet): """Dashboard summary providing aggregated data for the main dashboard.""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['get']) def summary(self, request): """ Get comprehensive dashboard summary in a single API call. Returns: - needs_attention: Items requiring user action - pipeline: Workflow pipeline counts (keywords → published) - ai_operations: Recent AI usage stats - recent_activity: Latest activity log - content_velocity: Content creation trends - automation: Automation status summary """ account = request.user.account site_id = request.query_params.get('site_id') days = int(request.query_params.get('days', 7)) start_date = timezone.now() - timedelta(days=days) # Build base filters site_filter = Q(site__account=account) if site_id: site_filter &= Q(site_id=site_id) # ========================================== # 1. PIPELINE COUNTS # ========================================== keywords_count = Keywords.objects.filter(site_filter).count() clusters_count = Clusters.objects.filter(site_filter).count() ideas_count = ContentIdeas.objects.filter(site_filter).count() tasks_count = Tasks.objects.filter(site_filter).count() content_filter = site_filter drafts_count = Content.objects.filter(content_filter, status='draft').count() review_count = Content.objects.filter(content_filter, status='review').count() published_count = Content.objects.filter(content_filter, status='published').count() total_content = drafts_count + review_count + published_count # Calculate completion percentage based on workflow milestones milestones = [ keywords_count > 0, clusters_count > 0, ideas_count > 0, tasks_count > 0, total_content > 0, published_count > 0, ] completion_percentage = int((sum(milestones) / len(milestones)) * 100) if milestones else 0 pipeline = { 'keywords': keywords_count, 'clusters': clusters_count, 'ideas': ideas_count, 'tasks': tasks_count, 'drafts': drafts_count, 'review': review_count, 'published': published_count, 'total_content': total_content, 'completion_percentage': completion_percentage, } # ========================================== # 2. NEEDS ATTENTION # ========================================== needs_attention = [] # Content pending review if review_count > 0: needs_attention.append({ 'id': 'pending-review', 'type': 'pending_review', 'title': 'pending review', 'count': review_count, 'action_label': 'Review', 'action_url': '/writer/review', 'severity': 'warning', }) # Sites without keywords (incomplete setup) sites = Site.objects.filter(account=account, is_active=True) sites_without_keywords = [] for site in sites: kw_count = Keywords.objects.filter(site=site).count() if kw_count == 0: sites_without_keywords.append(site) if sites_without_keywords: if len(sites_without_keywords) == 1: needs_attention.append({ 'id': 'setup-incomplete', 'type': 'setup_incomplete', 'title': f'{sites_without_keywords[0].name} needs setup', 'action_label': 'Complete', 'action_url': f'/sites/{sites_without_keywords[0].id}', 'severity': 'info', }) else: needs_attention.append({ 'id': 'setup-incomplete', 'type': 'setup_incomplete', 'title': f'{len(sites_without_keywords)} sites need setup', 'action_label': 'Complete', 'action_url': '/sites', 'severity': 'info', }) # Sites without integrations sites_without_integration = sites.filter(has_integration=False).count() if sites_without_integration > 0: needs_attention.append({ 'id': 'no-integration', 'type': 'no_integration', 'title': f'{sites_without_integration} site{"s" if sites_without_integration > 1 else ""} without WordPress', 'action_label': 'Connect', 'action_url': '/integrations', 'severity': 'info', }) # Low credits warning if account.credits < 100: needs_attention.append({ 'id': 'credits-low', 'type': 'credits_low', 'title': f'Credits running low ({account.credits} remaining)', 'action_label': 'Upgrade', 'action_url': '/billing/plans', 'severity': 'warning' if account.credits > 20 else 'error', }) # Queued tasks not processed queued_tasks = Tasks.objects.filter(site_filter, status='queued').count() if queued_tasks > 10: needs_attention.append({ 'id': 'queued-tasks', 'type': 'queued_tasks', 'title': f'{queued_tasks} tasks waiting to be generated', 'action_label': 'Generate', 'action_url': '/writer/tasks', 'severity': 'info', }) # ========================================== # 3. AI OPERATIONS (last N days) # ========================================== ai_usage = CreditUsageLog.objects.filter( account=account, created_at__gte=start_date ) # Group by operation type operations_by_type = ai_usage.values('operation_type').annotate( count=Count('id'), credits=Sum('credits_used'), tokens=Sum('tokens_input') + Sum('tokens_output') ).order_by('-count') # Format operation names operation_display = { 'clustering': 'Clustering', 'idea_generation': 'Ideas', 'content_generation': 'Content', 'image_generation': 'Images', 'image_prompt_extraction': 'Image Prompts', 'linking': 'Linking', 'optimization': 'Optimization', 'reparse': 'Reparse', 'site_page_generation': 'Site Pages', 'site_structure_generation': 'Site Structure', 'ideas': 'Ideas', 'content': 'Content', 'images': 'Images', } operations = [] for op in operations_by_type[:5]: # Top 5 operations operations.append({ 'type': op['operation_type'], 'label': operation_display.get(op['operation_type'], op['operation_type'].replace('_', ' ').title()), 'count': op['count'], 'credits': op['credits'] or 0, 'tokens': op['tokens'] or 0, }) total_credits_used = ai_usage.aggregate(total=Sum('credits_used'))['total'] or 0 total_operations = ai_usage.count() ai_operations = { 'period_days': days, 'operations': operations, 'totals': { 'credits': total_credits_used, 'operations': total_operations, } } # ========================================== # 4. RECENT ACTIVITY # ========================================== recent_logs = AITaskLog.objects.filter( account=account, status='success', created_at__gte=start_date ).order_by('-created_at')[:10] activity_icons = { 'run_clustering': 'group', 'generate_content_ideas': 'bolt', 'generate_content': 'file-text', 'generate_images': 'image', 'publish_content': 'paper-plane', 'optimize_content': 'sparkles', 'link_content': 'link', } activity_colors = { 'run_clustering': 'purple', 'generate_content_ideas': 'orange', 'generate_content': 'blue', 'generate_images': 'pink', 'publish_content': 'green', 'optimize_content': 'cyan', 'link_content': 'indigo', } recent_activity = [] for log in recent_logs: # Parse friendly message from the log message = log.message or f'{log.function_name} completed' recent_activity.append({ 'id': log.id, 'type': log.function_name, 'description': message, 'timestamp': log.created_at.isoformat(), 'icon': activity_icons.get(log.function_name, 'bolt'), 'color': activity_colors.get(log.function_name, 'gray'), 'credits': float(log.cost) if log.cost else 0, }) # ========================================== # 5. CONTENT VELOCITY # ========================================== # Content created in different periods now = timezone.now() content_today = Content.objects.filter( content_filter, created_at__date=now.date() ).count() content_this_week = Content.objects.filter( content_filter, created_at__gte=now - timedelta(days=7) ).count() content_this_month = Content.objects.filter( content_filter, created_at__gte=now - timedelta(days=30) ).count() # Daily breakdown for last 7 days daily_content = [] for i in range(7): day = now - timedelta(days=6-i) count = Content.objects.filter( content_filter, created_at__date=day.date() ).count() daily_content.append({ 'date': day.date().isoformat(), 'count': count, }) content_velocity = { 'today': content_today, 'this_week': content_this_week, 'this_month': content_this_month, 'daily': daily_content, 'average_per_day': round(content_this_week / 7, 1) if content_this_week else 0, } # ========================================== # 6. AUTOMATION STATUS # ========================================== # Check automation settings from igny8_core.business.automation.models import AutomationSettings automation_enabled = AutomationSettings.objects.filter( account=account, enabled=True ).exists() active_automations = AutomationSettings.objects.filter( account=account, enabled=True ).count() automation = { 'enabled': automation_enabled, 'active_count': active_automations, 'status': 'active' if automation_enabled else 'inactive', } # ========================================== # 7. SITES SUMMARY # ========================================== sites_data = [] for site in sites[:5]: # Top 5 sites site_keywords = Keywords.objects.filter(site=site).count() site_content = Content.objects.filter(site=site).count() site_published = Content.objects.filter(site=site, status='published').count() sites_data.append({ 'id': site.id, 'name': site.name, 'domain': site.url, 'keywords': site_keywords, 'content': site_content, 'published': site_published, 'has_integration': site.has_integration, 'sectors_count': site.sectors.filter(is_active=True).count(), }) return Response({ 'needs_attention': needs_attention, 'pipeline': pipeline, 'ai_operations': ai_operations, 'recent_activity': recent_activity, 'content_velocity': content_velocity, 'automation': automation, 'sites': sites_data, 'account': { 'credits': account.credits, 'name': account.name, }, 'generated_at': timezone.now().isoformat(), })