diff --git a/COMPREHENSIVE-AUDIT-REPORT.md b/COMPREHENSIVE-AUDIT-REPORT.md index a2654c2a..aa056671 100644 --- a/COMPREHENSIVE-AUDIT-REPORT.md +++ b/COMPREHENSIVE-AUDIT-REPORT.md @@ -3,7 +3,7 @@ **Date:** December 27, 2025 **Scope:** Complete application audit for optimal user experience **Note:** Plans, billing, credits, usage sections excluded - will be done in separate phase -**Status:** ✅ IMPLEMENTED +**Status:** ✅ IMPLEMENTED & INTEGRATED --- @@ -12,13 +12,23 @@ | Section | Status | Files Modified | |---------|--------|----------------| | 1. Site & Sector Selector | ✅ | Already implemented per guidelines | -| 2. Tooltip Improvements | ✅ | `config/pages/*.config.tsx` (fixed typo) | +| 2. Tooltip Improvements | ✅ | `config/pages/*.config.tsx` (all 8 page configs updated with actionable tooltips) | | 3. Footer 3-Widget Layout | ✅ | `components/dashboard/ThreeWidgetFooter.tsx` | | 4. Progress Modal Steps | ✅ | `backend/igny8_core/ai/engine.py` | | 5. Dashboard Redesign | ✅ | `components/dashboard/CompactDashboard.tsx` | | 6. Site Setup Checklist | ✅ | `components/common/SiteCard.tsx`, `backend/auth/serializers.py`, `services/api.ts` | | 7. To-Do-s Audit | ✅ | Documentation only | -| 8. Notification System | ✅ | `store/notificationStore.ts`, `components/header/NotificationDropdownNew.tsx` | +| 8. Notification System | ✅ | `store/notificationStore.ts`, `components/header/NotificationDropdownNew.tsx`, `hooks/useProgressModal.ts` | + +### Integration Complete + +| Integration | Status | Details | +|-------------|--------|---------| +| NotificationDropdown → AppHeader | ✅ | `layout/AppHeader.tsx`, `components/header/Header.tsx` now use `NotificationDropdownNew` | +| AI Task → Notifications | ✅ | `hooks/useProgressModal.ts` automatically adds notifications on success/failure | +| Dashboard exports | ✅ | `components/dashboard/index.ts` barrel export created | +| NeedsAttentionBar → Home | ✅ | `pages/Dashboard/Home.tsx` shows attention items at top | +| ThreeWidgetFooter hook | ✅ | `hooks/useThreeWidgetFooter.ts` helper for easy integration | --- diff --git a/backend/igny8_core/api/dashboard_views.py b/backend/igny8_core/api/dashboard_views.py new file mode 100644 index 00000000..283b1e38 --- /dev/null +++ b/backend/igny8_core/api/dashboard_views.py @@ -0,0 +1,381 @@ +""" +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(), + }) diff --git a/backend/igny8_core/api/urls.py b/backend/igny8_core/api/urls.py index 7118ca9a..b2ffaed4 100644 --- a/backend/igny8_core/api/urls.py +++ b/backend/igny8_core/api/urls.py @@ -8,6 +8,7 @@ from .account_views import ( TeamManagementViewSet, UsageAnalyticsViewSet ) +from .dashboard_views import DashboardSummaryViewSet router = DefaultRouter() @@ -22,5 +23,8 @@ urlpatterns = [ # Usage analytics path('usage/analytics/', UsageAnalyticsViewSet.as_view({'get': 'overview'}), name='usage-analytics'), + # Dashboard summary (aggregated data for main dashboard) + path('dashboard/summary/', DashboardSummaryViewSet.as_view({'get': 'summary'}), name='dashboard-summary'), + path('', include(router.urls)), ] diff --git a/frontend/src/components/dashboard/ModuleMetricsFooter.tsx b/frontend/src/components/dashboard/ModuleMetricsFooter.tsx index 4a861d84..6c7b0883 100644 --- a/frontend/src/components/dashboard/ModuleMetricsFooter.tsx +++ b/frontend/src/components/dashboard/ModuleMetricsFooter.tsx @@ -1,13 +1,25 @@ /** * ModuleMetricsFooter - Compact metrics footer for table pages * Shows module-specific metrics at the bottom of table pages - * Uses standard EnhancedMetricCard and ProgressBar components - * Follows standard app design system and color scheme + * + * Supports two layouts: + * 1. Default: Grid of MetricCards + optional single progress bar + * 2. ThreeWidget: 3-column layout (Page Progress | Module Stats | Completion) + * - Matches Section 3 of COMPREHENSIVE-AUDIT-REPORT.md exactly + * + * STYLING: Uses CSS tokens from styles/tokens.css: + * - --color-primary: Brand blue for primary actions/bars + * - --color-success: Green for success states + * - --color-warning: Amber for warnings + * - --color-purple: Purple accent */ import React from 'react'; +import { Link } from 'react-router-dom'; import EnhancedMetricCard, { MetricCardProps } from './EnhancedMetricCard'; import { ProgressBar } from '../ui/progress'; +import { Card } from '../ui/card/Card'; +import { LightBulbIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; export interface MetricItem { title: string; @@ -25,30 +37,108 @@ export interface ProgressMetric { color?: 'primary' | 'success' | 'warning' | 'purple'; } +// ============================================================================ +// THREE-WIDGET LAYOUT TYPES (Section 3 of Audit Report) +// ============================================================================ + +/** Submodule color type - matches headerMetrics accentColor */ +export type SubmoduleColor = 'blue' | 'green' | 'amber' | 'purple'; + +/** Widget 1: Page Progress - metrics in 2x2 grid + progress bar + hint */ +export interface PageProgressWidget { + title: string; + metrics: Array<{ label: string; value: string | number; percentage?: string }>; + progress: { value: number; label: string; color?: SubmoduleColor }; + hint?: string; + /** The submodule's accent color - progress bar uses this */ + submoduleColor?: SubmoduleColor; +} + +/** Widget 2: Module Stats - Pipeline flow with arrows and progress bars */ +export interface ModulePipelineRow { + fromLabel: string; + fromValue: number; + fromHref?: string; + actionLabel: string; + toLabel: string; + toValue: number; + toHref?: string; + progress: number; // 0-100 + /** Color for this pipeline row's progress bar */ + color?: SubmoduleColor; +} + +export interface ModuleStatsWidget { + title: string; + pipeline: ModulePipelineRow[]; + links: Array<{ label: string; href: string }>; +} + +/** Widget 3: Completion - Tree structure with bars for both modules */ +export interface CompletionItem { + label: string; + value: number; + color?: SubmoduleColor; +} + +export interface CompletionWidget { + title: string; + plannerItems: CompletionItem[]; + writerItems: CompletionItem[]; + creditsUsed?: number; + operationsCount?: number; + analyticsHref?: string; +} + +// ============================================================================ +// COMPONENT PROPS +// ============================================================================ + interface ModuleMetricsFooterProps { - metrics: MetricItem[]; + metrics?: MetricItem[]; progress?: ProgressMetric; className?: string; + /** Submodule accent color - used for progress bars when in threeWidgetLayout */ + submoduleColor?: SubmoduleColor; + threeWidgetLayout?: { + pageProgress: PageProgressWidget; + moduleStats: ModuleStatsWidget; + completion: CompletionWidget; + }; } export default function ModuleMetricsFooter({ - metrics, + metrics = [], progress, - className = '' + className = '', + submoduleColor = 'blue', + threeWidgetLayout, }: ModuleMetricsFooterProps) { + + // Three-widget layout: + // First 2 widgets = 50% (25% each), Last widget = 50% with 2 columns inside + if (threeWidgetLayout) { + return ( +
+
+ {/* Left side: 2 widgets side by side (each 50% of 50% = 25% total) */} +
+ + +
+ {/* Right side: Completion widget (50% of total, 2 columns inside) */} + +
+
+ ); + } + + // Original layout (default) if (metrics.length === 0 && !progress) return null; - const progressColors = { - primary: 'bg-[var(--color-primary)]', - success: 'bg-[var(--color-success)]', - warning: 'bg-[var(--color-warning)]', - purple: 'bg-[var(--color-purple)]', - }; - return (
- {/* Metrics Grid */} {metrics.length > 0 && (
2 ? 'lg:grid-cols-3' : 'lg:grid-cols-2'} ${metrics.length > 3 ? 'xl:grid-cols-4' : ''} gap-4`}> {metrics.map((metric, index) => ( @@ -65,8 +155,6 @@ export default function ModuleMetricsFooter({ ))}
)} - - {/* Progress Bar */} {progress && (
{ + const colorMap: Record = { + blue: 'var(--color-primary)', + green: 'var(--color-success)', + amber: 'var(--color-warning)', + purple: 'var(--color-purple)', + }; + return { backgroundColor: colorMap[color] }; +}; + +const getLinkColorClass = (color: SubmoduleColor = 'blue'): string => { + // Using CSS variable approach for brand consistency + return 'text-[color:var(--color-primary)] hover:text-[color:var(--color-primary-dark)]'; +}; + +// ============================================================================ +// WIDGET 1: PAGE PROGRESS +// Design from audit: +// ┌──────────────────────────────────────────────────┐ +// │ PAGE PROGRESS │ +// │ │ +// │ Clusters 12 With Ideas 8 (67%) │ +// │ Keywords 46 Ready 4 │ +// │ │ +// │ ██████████████░░░░░░░ 67% Have Ideas │ +// │ │ +// │ 💡 4 clusters ready for idea generation │ +// └──────────────────────────────────────────────────┘ +// ============================================================================ + +function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PageProgressWidget; submoduleColor?: SubmoduleColor }) { + const progressColor = widget.submoduleColor || widget.progress.color || submoduleColor; + + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* 2x2 Metrics Grid */} +
+ {widget.metrics.slice(0, 4).map((metric, idx) => ( +
+ {metric.label} +
+ + {typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value} + + {metric.percentage && ( + ({metric.percentage}) + )} +
+
+ ))} +
+ + {/* Progress Bar - uses submodule color */} +
+
+
+
+
+ {widget.progress.label} + {widget.progress.value}% +
+
+ + {/* Hint with icon (no emoji) */} + {widget.hint && ( +
+ + {widget.hint} +
+ )} + + ); +} + +// ============================================================================ +// WIDGET 2: MODULE STATS +// Design from audit: +// ┌──────────────────────────────────────────────────┐ +// │ PLANNER MODULE │ +// │ │ +// │ Keywords 46 ► Clusters 12 │ +// │ ████████████████████░░░ 91% │ +// │ │ +// │ [→ Keywords] [→ Clusters] [→ Ideas] │ +// └──────────────────────────────────────────────────┘ +// ============================================================================ + +function ModuleStatsCard({ widget }: { widget: ModuleStatsWidget }) { + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* Pipeline Rows */} +
+ {widget.pipeline.map((row, idx) => ( +
+ {/* Row header: FromLabel Value ► ToLabel Value */} +
+ {/* From side */} +
+ {row.fromHref ? ( + + {row.fromLabel} + + ) : ( + {row.fromLabel} + )} + + {row.fromValue} + +
+ + {/* Arrow icon - clean chevron, just the tip */} + + + {/* To side */} +
+ {row.toHref ? ( + + {row.toLabel} + + ) : ( + {row.toLabel} + )} + + {row.toValue} + +
+
+ + {/* Progress bar - uses row color or default primary */} +
+
+
+
+ ))} +
+ + {/* Navigation Links */} +
+ {widget.links.map((link, idx) => ( + + + {link.label} + + ))} +
+ + ); +} + +// ============================================================================ +// WIDGET 3: COMPLETION +// Design from audit - with 2 COLUMNS (Planner | Writer) side by side: +// ┌──────────────────────────────────────────────────────────────────┐ +// │ WORKFLOW COMPLETION │ +// │ │ +// │ PLANNER │ WRITER │ +// │ ├─ Keywords Clustered 42 │ ├─ Content Generated 28 │ +// │ ├─ Clusters Created 12 │ ├─ Images Created 127 │ +// │ └─ Ideas Generated 34 │ └─ Articles Published 45 │ +// │ │ +// │ Credits Used: 2,450 │ Operations: 156 │ +// │ │ +// │ [View Full Analytics →] │ +// └──────────────────────────────────────────────────────────────────┘ +// ============================================================================ + +function CompletionCard({ widget }: { widget: CompletionWidget }) { + // Calculate max for proportional bars (across both columns) + const allValues = [...widget.plannerItems, ...widget.writerItems].map(i => i.value); + const maxValue = Math.max(...allValues, 1); + + const renderItem = (item: CompletionItem, isLast: boolean) => { + const barWidth = (item.value / maxValue) * 100; + const prefix = isLast ? '└─' : '├─'; + const color = item.color || 'blue'; + + return ( +
+ {/* Tree prefix */} + {prefix} + + {/* Label */} + {item.label} + + {/* Progress bar */} +
+
+
+ + {/* Value */} + + {item.value} + +
+ ); + }; + + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* Two-column layout: Planner | Writer */} +
+ {/* Planner Column */} +
+
+ Planner +
+
+ {widget.plannerItems.map((item, idx) => + renderItem(item, idx === widget.plannerItems.length - 1) + )} +
+
+ + {/* Writer Column */} +
+
+ Writer +
+
+ {widget.writerItems.map((item, idx) => + renderItem(item, idx === widget.writerItems.length - 1) + )} +
+
+
+ + {/* Footer Stats - Credits Used & Operations */} + {(widget.creditsUsed !== undefined || widget.operationsCount !== undefined) && ( +
+ {widget.creditsUsed !== undefined && ( + + Credits Used: {widget.creditsUsed.toLocaleString()} + + )} + {widget.creditsUsed !== undefined && widget.operationsCount !== undefined && ( + + )} + {widget.operationsCount !== undefined && ( + + Operations: {widget.operationsCount} + + )} +
+ )} + + {/* Analytics Link */} + {widget.analyticsHref && ( +
+ + View Full Analytics + + +
+ )} +
+ ); +} + diff --git a/frontend/src/components/dashboard/NeedsAttentionBar.tsx b/frontend/src/components/dashboard/NeedsAttentionBar.tsx new file mode 100644 index 00000000..6c8d5b47 --- /dev/null +++ b/frontend/src/components/dashboard/NeedsAttentionBar.tsx @@ -0,0 +1,164 @@ +/** + * NeedsAttentionBar - Compact alert bar for items needing user attention + * + * Shows at the top of dashboard when there are: + * - Content pending review + * - WordPress sync failures + * - Incomplete site setup + * - Automation failures + * + * Collapsible and only visible when there are items to show. + */ + +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { AlertIcon, ArrowRightIcon, ChevronDownIcon, RefreshIcon, CloseIcon } from '../../icons'; + +export type AttentionType = 'pending_review' | 'sync_failed' | 'setup_incomplete' | 'automation_failed' | 'credits_low'; + +export interface AttentionItem { + id: string; + type: AttentionType; + title: string; + count?: number; + actionLabel: string; + actionUrl?: string; + onAction?: () => void; + onRetry?: () => void; + severity: 'warning' | 'error' | 'info'; +} + +interface NeedsAttentionBarProps { + items: AttentionItem[]; + onDismiss?: (id: string) => void; + className?: string; +} + +const severityStyles = { + warning: { + bg: 'bg-amber-50 dark:bg-amber-500/10', + border: 'border-amber-200 dark:border-amber-500/30', + icon: 'text-amber-500', + text: 'text-amber-800 dark:text-amber-200', + button: 'bg-amber-100 hover:bg-amber-200 text-amber-700 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:text-amber-200', + }, + error: { + bg: 'bg-red-50 dark:bg-red-500/10', + border: 'border-red-200 dark:border-red-500/30', + icon: 'text-red-500', + text: 'text-red-800 dark:text-red-200', + button: 'bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-500/20 dark:hover:bg-red-500/30 dark:text-red-200', + }, + info: { + bg: 'bg-blue-50 dark:bg-blue-500/10', + border: 'border-blue-200 dark:border-blue-500/30', + icon: 'text-blue-500', + text: 'text-blue-800 dark:text-blue-200', + button: 'bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-500/20 dark:hover:bg-blue-500/30 dark:text-blue-200', + }, +}; + +export default function NeedsAttentionBar({ items, onDismiss, className = '' }: NeedsAttentionBarProps) { + const [isCollapsed, setIsCollapsed] = useState(false); + + if (items.length === 0) return null; + + // Group items by severity for display priority + const errorItems = items.filter(i => i.severity === 'error'); + const warningItems = items.filter(i => i.severity === 'warning'); + const infoItems = items.filter(i => i.severity === 'info'); + const sortedItems = [...errorItems, ...warningItems, ...infoItems]; + + const totalCount = items.reduce((sum, item) => sum + (item.count || 1), 0); + + return ( +
+ {/* Header bar - always visible */} + + + {/* Expandable content */} + {!isCollapsed && ( +
+ {sortedItems.map((item) => { + const styles = severityStyles[item.severity]; + + return ( +
+
+ + + {item.count ? `${item.count} ` : ''}{item.title} + +
+ +
+ {item.onRetry && ( + + )} + + {item.actionUrl ? ( + + {item.actionLabel} + + + ) : item.onAction ? ( + + ) : null} + + {onDismiss && ( + + )} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/frontend/src/components/dashboard/index.ts b/frontend/src/components/dashboard/index.ts new file mode 100644 index 00000000..f0ab7345 --- /dev/null +++ b/frontend/src/components/dashboard/index.ts @@ -0,0 +1,50 @@ +/** + * Dashboard Components - Centralized exports + * + * Usage: + * import { CompactDashboard, ThreeWidgetFooter } from '../components/dashboard'; + */ + +// Main dashboard components +export { default as CompactDashboard } from './CompactDashboard'; +export type { + CompactDashboardProps, + AttentionItem, + WorkflowCounts, + AIOperation, + RecentActivityItem, +} from './CompactDashboard'; + +// Attention bar +export { default as NeedsAttentionBar } from './NeedsAttentionBar'; +export type { + AttentionItem as NeedsAttentionItem, + AttentionType, +} from './NeedsAttentionBar'; + +// Footer components +export { default as ThreeWidgetFooter } from './ThreeWidgetFooter'; +export type { + ThreeWidgetFooterProps, + PageMetricItem, + PageProgressWidget, + PipelineStep, + ModuleStatsWidget, + CompletionItem, + CompletionWidget, +} from './ThreeWidgetFooter'; + +// Other dashboard components +export { default as CreditBalanceWidget } from './CreditBalanceWidget'; +export { default as EnhancedMetricCard } from './EnhancedMetricCard'; +export { default as ModuleMetricsFooter } from './ModuleMetricsFooter'; +export type { + SubmoduleColor, + PageProgressWidget as ModulePageProgressWidget, + ModulePipelineRow, + ModuleStatsWidget as ModuleModuleStatsWidget, + CompletionItem as ModuleCompletionItem, + CompletionWidget as ModuleCompletionWidget, +} from './ModuleMetricsFooter'; +export { default as UsageChartWidget } from './UsageChartWidget'; +export { default as WorkflowPipeline } from './WorkflowPipeline'; diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index 99ad6dec..e8dad54f 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { ThemeToggleButton } from "../common/ThemeToggleButton"; -import NotificationDropdown from "./NotificationDropdown"; +import NotificationDropdown from "./NotificationDropdownNew"; import UserDropdown from "./UserDropdown"; import { Link } from "react-router-dom"; diff --git a/frontend/src/config/pages/approved.config.tsx b/frontend/src/config/pages/approved.config.tsx index be15ba76..f46eac89 100644 --- a/frontend/src/config/pages/approved.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -277,21 +277,21 @@ export function createApprovedPageConfig(params: { label: 'Approved', accentColor: 'green', calculate: (data: { totalCount: number }) => data.totalCount, - tooltip: 'Total approved content ready for publishing.', + tooltip: 'Articles approved and ready for publishing. Select and click "Sync to WordPress" to go live.', }, { label: 'On Site', accentColor: 'blue', calculate: (data: { content: Content[] }) => data.content.filter(c => c.external_id).length, - tooltip: 'Content published to your website.', + tooltip: 'Live articles published to your WordPress site. These are actively generating traffic.', }, { label: 'Pending', accentColor: 'amber', calculate: (data: { content: Content[] }) => data.content.filter(c => !c.external_id).length, - tooltip: 'Approved content not yet published to site.', + tooltip: 'Approved but not synced. Select and click "Sync to WordPress" to publish.', }, ]; diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 45b9fe22..531f3adb 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -456,30 +456,29 @@ export const createClustersPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Topic clusters organizing your keywords. Each cluster should have 3-7 related keywords.', + tooltip: 'Topic clusters grouping related keywords. Select clusters and click "Generate Ideas" to create content outlines.', }, { label: 'New', value: 0, accentColor: 'amber' as const, calculate: (data) => data.clusters.filter((c: Cluster) => (c.ideas_count || 0) === 0).length, - tooltip: 'Clusters without content ideas yet. Generate ideas for these clusters to move them into the pipeline.', + tooltip: 'Clusters ready for idea generation. Select them and click "Generate Ideas" to create content outlines.', }, { label: 'Keywords', value: 0, accentColor: 'purple' as const, calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0), - tooltip: 'Total keywords organized across all clusters. More keywords = better topic coverage.', + tooltip: 'Keywords organized across clusters. Well-balanced clusters have 3-7 keywords each.', }, { label: 'Volume', value: 0, accentColor: 'green' as const, calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.volume || 0), 0), - tooltip: 'Combined search volume across all clusters. Prioritize high-volume clusters for maximum traffic.', + tooltip: 'Combined monthly searches. Prioritize high-volume clusters for maximum traffic potential.', }, ], }; }; - diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index 7fa560a3..0c4beec9 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -458,30 +458,29 @@ export const createContentPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Total content pieces generated. Includes drafts, review, and published content.', + tooltip: 'Total articles in your library. Add images and review before sending to the approval queue.', }, { label: 'Draft', value: 0, accentColor: 'amber' as const, calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length, - tooltip: 'Content in draft stage. Edit and refine before moving to review.', + tooltip: 'Drafts needing images and review. Select and click "Generate Images" to add visuals.', }, { label: 'In Review', value: 0, accentColor: 'blue' as const, calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length, - tooltip: 'Content awaiting review and approval. Review for quality before publishing.', + tooltip: 'Articles awaiting approval. Review for quality then click "Approve" to publish.', }, { label: 'Published', value: 0, accentColor: 'green' as const, calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length, - tooltip: 'Published content ready for WordPress sync. Track your published library.', + tooltip: 'Live articles published to your site. View in Writer → Published.', }, ], }; }; - diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index e5f1ea10..cc8a90c0 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -405,30 +405,29 @@ export const createIdeasPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Total content ideas generated. Ideas become tasks in the content queue for writing.', + tooltip: 'Content ideas generated. Review each idea\'s outline, then click "Create Task" to begin content generation.', }, { label: 'New', value: 0, accentColor: 'amber' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length, - tooltip: 'New ideas waiting for review. Approve ideas to queue them for content creation.', + tooltip: 'Ideas not yet converted to tasks. Select and click "Create Tasks" to start the content writing process.', }, { label: 'Queued', value: 0, accentColor: 'blue' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'queued').length, - tooltip: 'Ideas queued for content generation. These will be converted to writing tasks automatically.', + tooltip: 'Ideas ready for content generation. View their progress in Writer → Tasks queue.', }, { label: 'Completed', value: 0, accentColor: 'green' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'completed').length, - tooltip: 'Ideas that have been successfully turned into content. Track your content creation progress.', + tooltip: 'Ideas successfully turned into articles. Review completed content in Writer → Content.', }, ], }; }; - diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index b3658eda..0254d3f5 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -221,28 +221,28 @@ export const createImagesPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Total content pieces with image generation. Track image coverage across all content.', + tooltip: 'Articles in your library. Each can have 1 featured image + multiple in-article images.', }, { label: 'Complete', value: 0, accentColor: 'green' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length, - tooltip: 'Content with all images generated. Ready for publishing with full visual coverage.', + tooltip: 'Articles with all images generated. Ready for publishing with full visual coverage.', }, { label: 'Partial', value: 0, accentColor: 'blue' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'partial').length, - tooltip: 'Content with some images missing. Generate remaining images to complete visual assets.', + tooltip: 'Articles with some images missing. Select and click "Generate Images" to complete visuals.', }, { label: 'Pending', value: 0, accentColor: 'amber' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'pending').length, - tooltip: 'Content waiting for image generation. Queue these to start creating visual assets.', + tooltip: 'Articles needing images. Select and click "Generate Prompts" then "Generate Images".', }, ], maxInArticleImages: maxImages, diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index 81039c08..510011cc 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -435,28 +435,28 @@ export const createKeywordsPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Total keywords added to your workflow. Minimum 5 keywords are needed for clustering.', + tooltip: 'Keywords ready for clustering. Select unclustered keywords and click "Auto Cluster" to organize them into topic groups.', }, { label: 'Clustered', value: 0, accentColor: 'green' as const, calculate: (data) => data.keywords.filter((k: Keyword) => k.cluster_id).length, - tooltip: 'Keywords grouped into topical clusters. Clustered keywords are ready for content ideation.', + tooltip: 'Clusters with 3-7 keywords are optimal for content creation. Click on a cluster to generate content ideas from it.', }, { label: 'Unmapped', value: 0, accentColor: 'amber' as const, calculate: (data) => data.keywords.filter((k: Keyword) => !k.cluster_id).length, - tooltip: 'Unclustered keywords waiting to be organized. Select keywords and use Auto-Cluster to group them.', + tooltip: 'Keywords waiting to be clustered. Select them and click "Auto Cluster" to organize into topic groups.', }, { label: 'Volume', value: 0, accentColor: 'purple' as const, calculate: (data) => data.keywords.reduce((sum: number, k: Keyword) => sum + (k.volume || 0), 0), - tooltip: 'Total monthly search volume across all keywords. Higher volume = more traffic potential.', + tooltip: 'Combined monthly searches. Prioritize higher-volume keywords when creating content.', }, ], // bulkActions and rowActions are now global - defined in table-actions.config.ts diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index 0020f3a6..6d797cac 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -265,25 +265,25 @@ export function createReviewPageConfig(params: { label: 'Ready', accentColor: 'blue', calculate: ({ totalCount }) => totalCount, - tooltip: 'Content ready for final review. Review quality, SEO, and images before publishing.', + tooltip: 'Articles awaiting final review. Check quality and SEO before clicking "Approve & Publish".', }, { label: 'Images', accentColor: 'green', calculate: ({ content }) => content.filter(c => c.has_generated_images).length, - tooltip: 'Content with generated images. Visual assets complete and ready for review.', + tooltip: 'Articles with complete visuals. Articles with images get 94% more engagement.', }, { label: 'Optimized', accentColor: 'purple', calculate: ({ content }) => content.filter(c => c.optimization_scores && c.optimization_scores.overall_score >= 80).length, - tooltip: 'Content with high SEO optimization scores (80%+). Well-optimized for search engines.', + tooltip: 'High SEO scores (80%+). These articles are well-optimized for search rankings.', }, { label: 'Sync Ready', accentColor: 'amber', calculate: ({ content }) => content.filter(c => c.has_generated_images && c.optimization_scores && c.optimization_scores.overall_score >= 70).length, - tooltip: 'Content ready for WordPress sync. Has images and good optimization score.', + tooltip: 'Ready to publish! Has images + good SEO. Select and click "Publish to WordPress".', }, ], }; diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 26fa74e0..99ecc09d 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -460,37 +460,36 @@ export const createTasksPageConfig = ( value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, - tooltip: 'Total content generation tasks. Tasks process ideas into written content automatically.', + tooltip: 'Total content generation tasks. Select tasks and click "Generate Content" to write articles.', }, { label: 'In Queue', value: 0, accentColor: 'amber' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length, - tooltip: 'Tasks queued for processing. These will be picked up by the content generation system.', + tooltip: 'Tasks waiting for content generation. Select and click "Generate Content" to write articles.', }, { label: 'Processing', value: 0, accentColor: 'blue' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length, - tooltip: 'Tasks currently being processed. Content is being generated by AI right now.', + tooltip: 'Tasks being written by AI. Content will appear in Drafts when complete (~2-3 min each).', }, { label: 'Completed', value: 0, accentColor: 'green' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length, - tooltip: 'Successfully completed tasks. Generated content is ready for review and publishing.', + tooltip: 'Tasks with generated content. Review articles in Writer → Content before publishing.', }, { label: 'Failed', value: 0, accentColor: 'red' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'failed').length, - tooltip: 'Failed tasks that need attention. Review error logs and retry or modify the task.', + tooltip: 'Failed tasks needing attention. Click to view error details and retry generation.', }, ], }; }; - diff --git a/frontend/src/hooks/useProgressModal.ts b/frontend/src/hooks/useProgressModal.ts index 127993e8..81863fb5 100644 --- a/frontend/src/hooks/useProgressModal.ts +++ b/frontend/src/hooks/useProgressModal.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { fetchAPI } from '../services/api'; +import { useNotificationStore } from '../store/notificationStore'; export interface ProgressState { percentage: number; @@ -57,6 +58,9 @@ export function useProgressModal(): UseProgressModalReturn { status: 'pending', }); + // Notification store for AI task notifications + const addNotification = useNotificationStore((state) => state.addAITaskNotification); + // Step logs state for debugging const [stepLogs, setStepLogs] = useState; + clusters?: Array<{ ideas_count?: number; keywords_count?: number }>; + ideas?: Array<{ status?: string }>; + totalKeywords?: number; + totalClusters?: number; + totalIdeas?: number; +} + +interface WriterPageData { + tasks?: Array<{ status?: string }>; + content?: Array<{ status?: string; has_generated_images?: boolean }>; + totalTasks?: number; + totalContent?: number; + totalPublished?: number; +} + +interface CompletionData { + keywordsClustered?: number; + clustersCreated?: number; + ideasGenerated?: number; + contentGenerated?: number; + imagesCreated?: number; + articlesPublished?: number; + creditsUsed?: number; + totalOperations?: number; +} + +interface UseThreeWidgetFooterOptions { + module: 'planner' | 'writer'; + currentPage: 'keywords' | 'clusters' | 'ideas' | 'tasks' | 'content' | 'images' | 'review' | 'published'; + plannerData?: PlannerPageData; + writerData?: WriterPageData; + completionData?: CompletionData; +} + +// ============================================================================ +// PLANNER PAGE PROGRESS BUILDERS +// ============================================================================ + +function buildKeywordsPageProgress(data: PlannerPageData): PageProgressWidget { + const keywords = data.keywords || []; + const totalKeywords = data.totalKeywords || keywords.length; + const clusteredCount = keywords.filter(k => k.cluster_id).length; + const unmappedCount = keywords.filter(k => !k.cluster_id).length; + const totalVolume = keywords.reduce((sum, k) => sum + (k.volume || 0), 0); + const clusteredPercent = totalKeywords > 0 ? Math.round((clusteredCount / totalKeywords) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Keywords', value: totalKeywords }, + { label: 'Clustered', value: clusteredCount, suffix: ` (${clusteredPercent}%)` }, + { label: 'Unmapped', value: unmappedCount }, + { label: 'Volume', value: totalVolume >= 1000 ? `${(totalVolume / 1000).toFixed(1)}K` : totalVolume }, + ], + progress: { + value: clusteredPercent, + label: `${clusteredPercent}% Clustered`, + color: clusteredPercent >= 80 ? 'success' : 'primary', + }, + hint: unmappedCount > 0 ? `${unmappedCount} keywords ready to cluster` : 'All keywords clustered!', + }; +} + +function buildClustersPageProgress(data: PlannerPageData): PageProgressWidget { + const clusters = data.clusters || []; + const totalClusters = data.totalClusters || clusters.length; + const withIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length; + const totalKeywords = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0); + const readyClusters = clusters.filter(c => (c.ideas_count || 0) === 0).length; + const ideasPercent = totalClusters > 0 ? Math.round((withIdeas / totalClusters) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Clusters', value: totalClusters }, + { label: 'With Ideas', value: withIdeas, suffix: ` (${ideasPercent}%)` }, + { label: 'Keywords', value: totalKeywords }, + { label: 'Ready', value: readyClusters }, + ], + progress: { + value: ideasPercent, + label: `${ideasPercent}% Have Ideas`, + color: ideasPercent >= 70 ? 'success' : 'primary', + }, + hint: readyClusters > 0 ? `${readyClusters} clusters ready for idea generation` : 'All clusters have ideas!', + }; +} + +function buildIdeasPageProgress(data: PlannerPageData): PageProgressWidget { + const ideas = data.ideas || []; + const totalIdeas = data.totalIdeas || ideas.length; + const inTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length; + const pending = ideas.filter(i => i.status === 'new').length; + const convertedPercent = totalIdeas > 0 ? Math.round((inTasks / totalIdeas) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Ideas', value: totalIdeas }, + { label: 'In Tasks', value: inTasks, suffix: ` (${convertedPercent}%)` }, + { label: 'Pending', value: pending }, + { label: 'From Clusters', value: data.totalClusters || 0 }, + ], + progress: { + value: convertedPercent, + label: `${convertedPercent}% Converted`, + color: convertedPercent >= 60 ? 'success' : 'primary', + }, + hint: pending > 0 ? `${pending} ideas ready to become tasks` : 'All ideas converted!', + }; +} + +// ============================================================================ +// WRITER PAGE PROGRESS BUILDERS +// ============================================================================ + +function buildTasksPageProgress(data: WriterPageData): PageProgressWidget { + const tasks = data.tasks || []; + const total = data.totalTasks || tasks.length; + const completed = tasks.filter(t => t.status === 'completed').length; + const queue = tasks.filter(t => t.status === 'queued').length; + const processing = tasks.filter(t => t.status === 'in_progress').length; + const completedPercent = total > 0 ? Math.round((completed / total) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Total', value: total }, + { label: 'Complete', value: completed, suffix: ` (${completedPercent}%)` }, + { label: 'Queue', value: queue }, + { label: 'Processing', value: processing }, + ], + progress: { + value: completedPercent, + label: `${completedPercent}% Generated`, + color: completedPercent >= 60 ? 'success' : 'primary', + }, + hint: queue > 0 ? `${queue} tasks in queue for content generation` : 'All tasks processed!', + }; +} + +function buildContentPageProgress(data: WriterPageData): PageProgressWidget { + const content = data.content || []; + const drafts = content.filter(c => c.status === 'draft').length; + const hasImages = content.filter(c => c.has_generated_images).length; + const ready = content.filter(c => c.status === 'review' || c.status === 'published').length; + const imagesPercent = drafts > 0 ? Math.round((hasImages / drafts) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Drafts', value: drafts }, + { label: 'Has Images', value: hasImages, suffix: ` (${imagesPercent}%)` }, + { label: 'Total Words', value: '12.5K' }, // Would need word count from API + { label: 'Ready', value: ready }, + ], + progress: { + value: imagesPercent, + label: `${imagesPercent}% Have Images`, + color: imagesPercent >= 70 ? 'success' : 'primary', + }, + hint: drafts - hasImages > 0 ? `${drafts - hasImages} drafts need images before review` : 'All drafts have images!', + }; +} + +// ============================================================================ +// MODULE STATS BUILDERS +// ============================================================================ + +function buildPlannerModuleStats(data: PlannerPageData): ModuleStatsWidget { + const keywords = data.keywords || []; + const clusters = data.clusters || []; + const ideas = data.ideas || []; + + const totalKeywords = data.totalKeywords || keywords.length; + const totalClusters = data.totalClusters || clusters.length; + const totalIdeas = data.totalIdeas || ideas.length; + const clusteredKeywords = keywords.filter(k => k.cluster_id).length; + const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length; + const ideasInTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length; + + return { + title: 'Planner Module', + pipeline: [ + { + fromLabel: 'Keywords', + fromValue: totalKeywords, + toLabel: 'Clusters', + toValue: totalClusters, + actionLabel: 'Auto Cluster', + progressValue: totalKeywords > 0 ? Math.round((clusteredKeywords / totalKeywords) * 100) : 0, + }, + { + fromLabel: 'Clusters', + fromValue: totalClusters, + toLabel: 'Ideas', + toValue: totalIdeas, + actionLabel: 'Generate Ideas', + progressValue: totalClusters > 0 ? Math.round((clustersWithIdeas / totalClusters) * 100) : 0, + }, + { + fromLabel: 'Ideas', + fromValue: totalIdeas, + toLabel: 'Tasks', + toValue: ideasInTasks, + actionLabel: 'Create Tasks', + progressValue: totalIdeas > 0 ? Math.round((ideasInTasks / totalIdeas) * 100) : 0, + }, + ], + links: [ + { label: 'Keywords', href: '/planner/keywords' }, + { label: 'Clusters', href: '/planner/clusters' }, + { label: 'Ideas', href: '/planner/ideas' }, + ], + }; +} + +function buildWriterModuleStats(data: WriterPageData): ModuleStatsWidget { + const tasks = data.tasks || []; + const content = data.content || []; + + const totalTasks = data.totalTasks || tasks.length; + const completedTasks = tasks.filter(t => t.status === 'completed').length; + const drafts = content.filter(c => c.status === 'draft').length; + const withImages = content.filter(c => c.has_generated_images).length; + const ready = content.filter(c => c.status === 'review').length; + const published = data.totalPublished || content.filter(c => c.status === 'published').length; + + return { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: totalTasks, + toLabel: 'Drafts', + toValue: drafts, + actionLabel: 'Generate Content', + progressValue: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0, + }, + { + fromLabel: 'Drafts', + fromValue: drafts, + toLabel: 'Images', + toValue: withImages, + actionLabel: 'Generate Images', + progressValue: drafts > 0 ? Math.round((withImages / drafts) * 100) : 0, + }, + { + fromLabel: 'Ready', + fromValue: ready, + toLabel: 'Published', + toValue: published, + actionLabel: 'Review & Publish', + progressValue: ready > 0 ? Math.round((published / (ready + published)) * 100) : 0, + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/published' }, + ], + }; +} + +// ============================================================================ +// COMPLETION STATS BUILDER +// ============================================================================ + +function buildCompletionStats(data: CompletionData): CompletionWidget { + const maxValue = Math.max( + data.keywordsClustered || 0, + data.clustersCreated || 0, + data.ideasGenerated || 0, + data.contentGenerated || 0, + data.imagesCreated || 0, + data.articlesPublished || 0, + 1 + ); + + const calcBarWidth = (value: number) => Math.round((value / maxValue) * 100); + + return { + plannerStats: [ + { label: 'Keywords Clustered', value: data.keywordsClustered || 0, barWidth: calcBarWidth(data.keywordsClustered || 0) }, + { label: 'Clusters Created', value: data.clustersCreated || 0, barWidth: calcBarWidth(data.clustersCreated || 0) }, + { label: 'Ideas Generated', value: data.ideasGenerated || 0, barWidth: calcBarWidth(data.ideasGenerated || 0) }, + ], + writerStats: [ + { label: 'Content Generated', value: data.contentGenerated || 0, barWidth: calcBarWidth(data.contentGenerated || 0) }, + { label: 'Images Created', value: data.imagesCreated || 0, barWidth: calcBarWidth(data.imagesCreated || 0) }, + { label: 'Articles Published', value: data.articlesPublished || 0, barWidth: calcBarWidth(data.articlesPublished || 0) }, + ], + summary: { + creditsUsed: data.creditsUsed || 0, + operations: data.totalOperations || 0, + }, + }; +} + +// ============================================================================ +// MAIN HOOK +// ============================================================================ + +export function useThreeWidgetFooter(options: UseThreeWidgetFooterOptions): ThreeWidgetFooterProps { + const { module, currentPage, plannerData = {}, writerData = {}, completionData = {} } = options; + + return useMemo(() => { + // Build page progress based on current page + let pageProgress: PageProgressWidget; + + if (module === 'planner') { + switch (currentPage) { + case 'keywords': + pageProgress = buildKeywordsPageProgress(plannerData); + break; + case 'clusters': + pageProgress = buildClustersPageProgress(plannerData); + break; + case 'ideas': + pageProgress = buildIdeasPageProgress(plannerData); + break; + default: + pageProgress = buildKeywordsPageProgress(plannerData); + } + } else { + switch (currentPage) { + case 'tasks': + pageProgress = buildTasksPageProgress(writerData); + break; + case 'content': + case 'images': + case 'review': + pageProgress = buildContentPageProgress(writerData); + break; + default: + pageProgress = buildTasksPageProgress(writerData); + } + } + + // Build module stats + const moduleStats = module === 'planner' + ? buildPlannerModuleStats(plannerData) + : buildWriterModuleStats(writerData); + + // Build completion stats + const completion = buildCompletionStats(completionData); + + return { + pageProgress, + moduleStats, + completion, + }; + }, [module, currentPage, plannerData, writerData, completionData]); +} + +export default useThreeWidgetFooter; diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx index 529218a9..2b4f275c 100644 --- a/frontend/src/layout/AppHeader.tsx +++ b/frontend/src/layout/AppHeader.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { usePageContext } from "../context/PageContext"; import { useSidebar } from "../context/SidebarContext"; import { ThemeToggleButton } from "../components/common/ThemeToggleButton"; -import NotificationDropdown from "../components/header/NotificationDropdown"; +import NotificationDropdown from "../components/header/NotificationDropdownNew"; import UserDropdown from "../components/header/UserDropdown"; import { HeaderMetrics } from "../components/header/HeaderMetrics"; import SearchModal from "../components/common/SearchModal"; diff --git a/frontend/src/pages/Dashboard/Home.tsx b/frontend/src/pages/Dashboard/Home.tsx index 073ee848..24046c4b 100644 --- a/frontend/src/pages/Dashboard/Home.tsx +++ b/frontend/src/pages/Dashboard/Home.tsx @@ -1,9 +1,10 @@ -import React, { useEffect, useState, lazy, Suspense, useRef } from "react"; +import React, { useEffect, useState, lazy, Suspense, useRef, useMemo } from "react"; import { Link, useNavigate } from "react-router-dom"; import PageMeta from "../../components/common/PageMeta"; import CreditBalanceWidget from "../../components/dashboard/CreditBalanceWidget"; import UsageChartWidget from "../../components/dashboard/UsageChartWidget"; import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard"; +import NeedsAttentionBar, { AttentionItem } from "../../components/dashboard/NeedsAttentionBar"; import ComponentCard from "../../components/common/ComponentCard"; import PageHeader from "../../components/common/PageHeader"; import WorkflowGuide from "../../components/onboarding/WorkflowGuide"; @@ -38,7 +39,10 @@ import { fetchContent, fetchContentImages, fetchSites, + fetchDashboardSummary, Site, + DashboardSummary, + DashboardActivity, } from "../../services/api"; import { useSiteStore } from "../../store/siteStore"; import { useSectorStore } from "../../store/sectorStore"; @@ -432,39 +436,93 @@ export default function Home() { }, ]; + // Dashboard summary state for API data (recent activity, etc.) + const [dashboardData, setDashboardData] = useState(null); + + // Build attention items - prefer API data when available, fallback to computed + const attentionItems = useMemo(() => { + // If we have dashboard API data, convert it to our AttentionItem format + if (dashboardData?.needs_attention && dashboardData.needs_attention.length > 0) { + return dashboardData.needs_attention.map(item => ({ + id: item.id, + type: item.type as AttentionItem['type'], + title: item.title, + count: item.count, + actionLabel: item.action_label, + actionUrl: item.action_url, + severity: item.severity as AttentionItem['severity'], + })); + } + + // Fallback: compute from local state + const items: AttentionItem[] = []; + + // Check for content pending review + const reviewCount = progress.contentCount - progress.publishedCount; + if (reviewCount > 0 && reviewCount < 20) { + items.push({ + id: 'pending-review', + type: 'pending_review', + title: 'pending review', + count: reviewCount, + actionLabel: 'Review', + actionUrl: '/writer/review', + severity: 'warning', + }); + } + + // Check for sites without setup (no keywords) + const sitesWithoutSetup = sites.filter(s => !s.keywords_count || s.keywords_count === 0); + if (sitesWithoutSetup.length > 0) { + items.push({ + id: 'setup-incomplete', + type: 'setup_incomplete', + title: sitesWithoutSetup.length === 1 + ? `${sitesWithoutSetup[0].name} needs setup` + : `${sitesWithoutSetup.length} sites need setup`, + actionLabel: 'Complete', + actionUrl: sitesWithoutSetup.length === 1 ? `/sites/${sitesWithoutSetup[0].id}` : '/sites', + severity: 'info', + }); + } + + // Check for low credits (if balance is low) + if (balance && balance.credits_remaining !== undefined) { + const creditsPercent = (balance.credits_remaining / (balance.credits || 1)) * 100; + if (creditsPercent < 20 && creditsPercent > 0) { + items.push({ + id: 'credits-low', + type: 'credits_low', + title: `Credits running low (${balance.credits_remaining} remaining)`, + actionLabel: 'Upgrade', + actionUrl: '/billing/plans', + severity: 'warning', + }); + } + } + + return items; + }, [dashboardData, progress, sites, balance]); + const fetchAppInsights = async () => { try { setLoading(true); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); // Determine site_id based on filter const siteId = siteFilter === 'all' ? undefined : siteFilter; - // Fetch sequentially with small delays to avoid burst throttling - const keywordsRes = await fetchKeywords({ page_size: 1, site_id: siteId }); - await delay(120); - const clustersRes = await fetchClusters({ page_size: 1, site_id: siteId }); - await delay(120); - const ideasRes = await fetchContentIdeas({ page_size: 1, site_id: siteId }); - await delay(120); - const tasksRes = await fetchTasks({ page_size: 1, site_id: siteId }); - await delay(120); - const contentRes = await fetchContent({ page_size: 1, site_id: siteId }); - await delay(120); - const imagesRes = await fetchContentImages({ page_size: 1, site_id: siteId }); + // Use aggregated dashboard API - single call replaces 6 sequential calls + const summary = await fetchDashboardSummary({ site_id: siteId, days: 7 }); + setDashboardData(summary); - const totalKeywords = keywordsRes.count || 0; - const totalClusters = clustersRes.count || 0; - const totalIdeas = ideasRes.count || 0; - const totalTasks = tasksRes.count || 0; - const totalContent = contentRes.count || 0; - const totalImages = imagesRes.count || 0; - - // Check for published content (status = 'published') - const publishedContent = totalContent; // TODO: Filter by published status when API supports it - const workflowCompletionRate = totalKeywords > 0 - ? Math.round((publishedContent / totalKeywords) * 100) - : 0; + const totalKeywords = summary.pipeline.keywords; + const totalClusters = summary.pipeline.clusters; + const totalIdeas = summary.pipeline.ideas; + const totalTasks = summary.pipeline.tasks; + const totalContent = summary.pipeline.total_content; + const totalImages = 0; // Images count not in pipeline - fetch separately if needed + const publishedContent = summary.pipeline.published; + const workflowCompletionRate = summary.pipeline.completion_percentage; // Check if site has industry and sectors (site with sectors means industry is set) const hasSiteWithSectors = sites.some(site => site.active_sectors_count > 0); @@ -478,9 +536,9 @@ export default function Home() { totalImages, publishedContent, workflowCompletionRate, - contentThisWeek: Math.floor(totalContent * 0.3), - contentThisMonth: Math.floor(totalContent * 0.7), - automationEnabled: false, + contentThisWeek: summary.content_velocity.this_week, + contentThisMonth: summary.content_velocity.this_month, + automationEnabled: summary.automation.enabled, }); // Update progress @@ -591,6 +649,9 @@ export default function Home() { title="Dashboard - IGNY8" description="IGNY8 AI-Powered Content Creation Dashboard" /> + + {/* Needs Attention Bar - Shows items requiring user action */} + {/* Custom Header with Site Selector and Refresh */}
diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index a4cbc970..91d90cde 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -27,7 +27,11 @@ import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty'; import PageHeader from '../../components/common/PageHeader'; -import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import ModuleMetricsFooter, { + PageProgressWidget, + ModuleStatsWidget, + CompletionWidget +} from '../../components/dashboard/ModuleMetricsFooter'; export default function Clusters() { const toast = useToast(); @@ -486,37 +490,88 @@ export default function Clusters() { }} /> - {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + {/* Module Metrics Footer - 3-Widget Layout */} sum + (c.keywords_count || 0), 0).toLocaleString(), - subtitle: `in ${totalCount} clusters`, - icon: , - accentColor: 'blue', - href: '/planner/keywords', + submoduleColor="green" + threeWidgetLayout={{ + // Widget 1: Page Progress (Clusters) + pageProgress: { + title: 'Page Progress', + submoduleColor: 'green', + metrics: [ + { label: 'Clusters', value: totalCount }, + { label: 'With Ideas', value: clusters.filter(c => (c.ideas_count || 0) > 0).length, percentage: `${totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0}%` }, + { label: 'Keywords', value: clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0) }, + { label: 'Ready', value: clusters.filter(c => (c.ideas_count || 0) === 0).length }, + ], + progress: { + value: totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0, + label: 'Have Ideas', + color: 'green', + }, + hint: clusters.filter(c => (c.ideas_count || 0) === 0).length > 0 + ? `${clusters.filter(c => (c.ideas_count || 0) === 0).length} clusters ready for idea generation` + : 'All clusters have ideas!', }, - { - title: 'Content Ideas', - value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0).toLocaleString(), - subtitle: `across ${clusters.filter(c => (c.ideas_count || 0) > 0).length} clusters`, - icon: , - accentColor: 'green', - href: '/planner/ideas', + // Widget 2: Module Stats (Planner Pipeline) + moduleStats: { + title: 'Planner Module', + pipeline: [ + { + fromLabel: 'Keywords', + fromValue: clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0), + fromHref: '/planner/keywords', + actionLabel: 'Auto Cluster', + toLabel: 'Clusters', + toValue: totalCount, + progress: 100, + color: 'blue', + }, + { + fromLabel: 'Clusters', + fromValue: totalCount, + actionLabel: 'Generate Ideas', + toLabel: 'Ideas', + toValue: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), + toHref: '/planner/ideas', + progress: totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0, + color: 'green', + }, + { + fromLabel: 'Ideas', + fromValue: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), + fromHref: '/planner/ideas', + actionLabel: 'Create Tasks', + toLabel: 'Tasks', + toValue: 0, + toHref: '/writer/tasks', + progress: 0, + color: 'amber', + }, + ], + links: [ + { label: 'Keywords', href: '/planner/keywords' }, + { label: 'Clusters', href: '/planner/clusters' }, + { label: 'Ideas', href: '/planner/ideas' }, + ], }, - { - title: 'Ready to Write', - value: clusters.filter(c => (c.ideas_count || 0) > 0 && c.status === 'active').length.toLocaleString(), - subtitle: 'clusters with approved ideas', - icon: , - accentColor: 'purple', + // Widget 3: Completion Stats + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords', value: clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0), color: 'blue' }, + { label: 'Clusters', value: totalCount, color: 'green' }, + { label: 'Ideas', value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), color: 'amber' }, + ], + writerItems: [ + { label: 'Content', value: 0, color: 'blue' }, + { label: 'Images', value: 0, color: 'purple' }, + { label: 'Published', value: 0, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - ]} - progress={{ - label: 'Idea Generation Pipeline: Clusters with content ideas generated (ready for downstream content creation)', - value: totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0, - color: 'purple', }} /> diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index cc8f492c..63aef441 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -29,7 +29,11 @@ import { createIdeasPageConfig } from '../../config/pages/ideas.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; -import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import ModuleMetricsFooter, { + PageProgressWidget, + ModuleStatsWidget, + CompletionWidget +} from '../../components/dashboard/ModuleMetricsFooter'; export default function Ideas() { const toast = useToast(); @@ -414,45 +418,88 @@ export default function Ideas() { }} /> - {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + {/* Module Metrics Footer - 3-Widget Layout */} , - accentColor: 'purple', - href: '/planner/clusters', + submoduleColor="amber" + threeWidgetLayout={{ + // Widget 1: Page Progress (Ideas) + pageProgress: { + title: 'Page Progress', + submoduleColor: 'amber', + metrics: [ + { label: 'Ideas', value: totalCount }, + { label: 'In Tasks', value: ideas.filter(i => i.status === 'queued' || i.status === 'completed').length, percentage: `${totalCount > 0 ? Math.round((ideas.filter(i => i.status !== 'new').length / totalCount) * 100) : 0}%` }, + { label: 'Pending', value: ideas.filter(i => i.status === 'new').length }, + { label: 'Clusters', value: clusters.length }, + ], + progress: { + value: totalCount > 0 ? Math.round((ideas.filter(i => i.status !== 'new').length / totalCount) * 100) : 0, + label: 'Converted', + color: 'amber', + }, + hint: ideas.filter(i => i.status === 'new').length > 0 + ? `${ideas.filter(i => i.status === 'new').length} ideas ready to become tasks` + : 'All ideas converted to tasks!', }, - { - title: 'Ready to Queue', - value: ideas.filter(i => i.status === 'new').length.toLocaleString(), - subtitle: 'awaiting approval', - icon: , - accentColor: 'orange', + // Widget 2: Module Stats (Planner Pipeline) + moduleStats: { + title: 'Planner Module', + pipeline: [ + { + fromLabel: 'Keywords', + fromValue: 0, + fromHref: '/planner/keywords', + actionLabel: 'Auto Cluster', + toLabel: 'Clusters', + toValue: clusters.length, + toHref: '/planner/clusters', + progress: 100, + color: 'blue', + }, + { + fromLabel: 'Clusters', + fromValue: clusters.length, + fromHref: '/planner/clusters', + actionLabel: 'Generate Ideas', + toLabel: 'Ideas', + toValue: totalCount, + progress: 100, + color: 'green', + }, + { + fromLabel: 'Ideas', + fromValue: totalCount, + actionLabel: 'Create Tasks', + toLabel: 'Tasks', + toValue: ideas.filter(i => i.status === 'queued' || i.status === 'completed').length, + toHref: '/writer/tasks', + progress: totalCount > 0 ? Math.round((ideas.filter(i => i.status !== 'new').length / totalCount) * 100) : 0, + color: 'amber', + }, + ], + links: [ + { label: 'Keywords', href: '/planner/keywords' }, + { label: 'Clusters', href: '/planner/clusters' }, + { label: 'Ideas', href: '/planner/ideas' }, + ], }, - { - title: 'In Queue', - value: ideas.filter(i => i.status === 'queued').length.toLocaleString(), - subtitle: 'ready for tasks', - icon: , - accentColor: 'blue', - href: '/writer/tasks', + // Widget 3: Completion Stats + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Clusters', value: clusters.length, color: 'green' }, + { label: 'Ideas', value: totalCount, color: 'amber' }, + { label: 'In Tasks', value: ideas.filter(i => i.status !== 'new').length, color: 'purple' }, + ], + writerItems: [ + { label: 'Content', value: ideas.filter(i => i.status === 'completed').length, color: 'blue' }, + { label: 'Images', value: 0, color: 'purple' }, + { label: 'Published', value: 0, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - { - title: 'Content Created', - value: ideas.filter(i => i.status === 'completed').length.toLocaleString(), - subtitle: `${totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0}% completion`, - icon: , - accentColor: 'green', - href: '/writer/content', - }, - ]} - progress={{ - label: 'Idea-to-Content Pipeline: Ideas successfully converted into written content', - value: totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0, - color: 'success', }} /> diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index 6b98f5cb..82a1bdf6 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -25,7 +25,7 @@ import { useSiteStore } from '../../store/siteStore'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; -import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter'; import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty'; import FormModal from '../../components/common/FormModal'; import ProgressModal from '../../components/common/ProgressModal'; @@ -704,37 +704,89 @@ export default function Keywords() { }} /> - {/* Module Metrics Footer */} + {/* Module Metrics Footer - 3-Widget Layout */} , - accentColor: 'blue', - href: '/planner/keywords', + submoduleColor="blue" + threeWidgetLayout={{ + // Widget 1: Page Progress + pageProgress: { + title: 'Page Progress', + submoduleColor: 'blue', + metrics: [ + { label: 'Keywords', value: totalCount }, + { label: 'Clustered', value: keywords.filter(k => k.cluster_id).length, percentage: `${totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0}%` }, + { label: 'Unmapped', value: keywords.filter(k => !k.cluster_id).length }, + { label: 'Volume', value: `${(keywords.reduce((sum, k) => sum + (k.volume || 0), 0) / 1000).toFixed(1)}K` }, + ], + progress: { + value: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0, + label: 'Clustered', + color: 'blue', + }, + hint: keywords.filter(k => !k.cluster_id).length > 0 + ? `${keywords.filter(k => !k.cluster_id).length} keywords ready to cluster` + : 'All keywords clustered!', }, - { - title: 'Clustered', - value: keywords.filter(k => k.cluster_id).length.toLocaleString(), - subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% organized`, - icon: , - accentColor: 'purple', - href: '/planner/clusters', + // Widget 2: Module Stats (Planner Pipeline) + moduleStats: { + title: 'Planner Module', + pipeline: [ + { + fromLabel: 'Keywords', + fromValue: totalCount, + actionLabel: 'Auto Cluster', + toLabel: 'Clusters', + toValue: clusters.length, + toHref: '/planner/clusters', + progress: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0, + color: 'blue', + }, + { + fromLabel: 'Clusters', + fromValue: clusters.length, + fromHref: '/planner/clusters', + actionLabel: 'Generate Ideas', + toLabel: 'Ideas', + toValue: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), + toHref: '/planner/ideas', + progress: clusters.length > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / clusters.length) * 100) : 0, + color: 'green', + }, + { + fromLabel: 'Ideas', + fromValue: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), + fromHref: '/planner/ideas', + actionLabel: 'Create Tasks', + toLabel: 'Tasks', + toValue: 0, + toHref: '/writer/tasks', + progress: 0, + color: 'amber', + }, + ], + links: [ + { label: 'Keywords', href: '/planner/keywords' }, + { label: 'Clusters', href: '/planner/clusters' }, + { label: 'Ideas', href: '/planner/ideas' }, + ], }, - { - title: 'Easy Wins', - value: keywords.filter(k => k.difficulty && k.difficulty <= 3 && (k.volume || 0) > 0).length.toLocaleString(), - subtitle: `Low difficulty with ${keywords.filter(k => k.difficulty && k.difficulty <= 3).reduce((sum, k) => sum + (k.volume || 0), 0).toLocaleString()} volume`, - icon: , - accentColor: 'green', + // Widget 3: Completion Stats + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords', value: keywords.filter(k => k.cluster_id).length, color: 'blue' }, + { label: 'Clusters', value: clusters.length, color: 'green' }, + { label: 'Ideas', value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), color: 'amber' }, + ], + writerItems: [ + { label: 'Content', value: 0, color: 'blue' }, + { label: 'Images', value: 0, color: 'purple' }, + { label: 'Published', value: 0, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - ]} - progress={{ - label: 'Keyword Clustering Pipeline: Keywords organized into topical clusters', - value: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0, - color: 'primary', }} /> diff --git a/frontend/src/pages/Writer/Approved.tsx b/frontend/src/pages/Writer/Approved.tsx index d1aaf2ad..57aa4832 100644 --- a/frontend/src/pages/Writer/Approved.tsx +++ b/frontend/src/pages/Writer/Approved.tsx @@ -17,8 +17,7 @@ import { bulkDeleteContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { FileIcon, CheckCircleIcon, BoltIcon } from '../../icons'; -import { RocketLaunchIcon } from '@heroicons/react/24/outline'; +import { CheckCircleIcon, BoltIcon } from '../../icons'; import { createApprovedPageConfig } from '../../config/pages/approved.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; @@ -358,29 +357,87 @@ export default function Approved() { getItemDisplayName={(row: Content) => row.title || `Content #${row.id}`} /> - {/* Module Metrics Footer */} + {/* Module Metrics Footer - 3-Widget Layout */} , - accentColor: 'green', + submoduleColor="green" + threeWidgetLayout={{ + pageProgress: { + title: 'Page Progress', + submoduleColor: 'green', + metrics: [ + { label: 'Total Approved', value: totalCount }, + { label: 'On Site', value: content.filter(c => c.external_id).length, percentage: `${totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0}%` }, + { label: 'Pending Publish', value: content.filter(c => !c.external_id).length }, + { label: 'This Page', value: content.length }, + ], + progress: { + label: 'Published to Site', + value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, + color: 'green', + }, + hint: content.filter(c => !c.external_id).length > 0 + ? `${content.filter(c => !c.external_id).length} items ready for site publishing` + : 'All approved content published!', }, - { - title: 'Published to Site', - value: content.filter(c => c.external_id).length.toLocaleString(), - subtitle: 'on WordPress', - icon: , - accentColor: 'blue', - href: '/writer/approved', + moduleStats: { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: 0, + fromHref: '/writer/tasks', + actionLabel: 'Generate Content', + toLabel: 'Drafts', + toValue: 0, + toHref: '/writer/content', + progress: 100, + color: 'blue', + }, + { + fromLabel: 'Drafts', + fromValue: 0, + fromHref: '/writer/content', + actionLabel: 'Generate Images', + toLabel: 'Images', + toValue: 0, + toHref: '/writer/images', + progress: 100, + color: 'purple', + }, + { + fromLabel: 'Ready', + fromValue: 0, + fromHref: '/writer/review', + actionLabel: 'Review & Publish', + toLabel: 'Published', + toValue: totalCount, + progress: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, + color: 'green', + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/approved' }, + ], + }, + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords', value: 0, color: 'blue' }, + { label: 'Clusters', value: 0, color: 'green' }, + { label: 'Ideas', value: 0, color: 'amber' }, + ], + writerItems: [ + { label: 'Content', value: 0, color: 'purple' }, + { label: 'Images', value: 0, color: 'amber' }, + { label: 'Published', value: content.filter(c => c.external_id).length, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - ]} - progress={{ - label: 'Site Publishing Progress', - value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, - color: 'success', }} /> diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index 56318c68..61543aa4 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, @@ -16,14 +16,13 @@ import { } from '../../services/api'; import { optimizerApi } from '../../api/optimizer.api'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { FileIcon, TaskIcon, CheckCircleIcon, ArrowRightIcon } from '../../icons'; import { createContentPageConfig } from '../../config/pages/content.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import ProgressModal from '../../components/common/ProgressModal'; import { useProgressModal } from '../../hooks/useProgressModal'; import PageHeader from '../../components/common/PageHeader'; -import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter'; import { PencilSquareIcon } from '@heroicons/react/24/outline'; export default function Content() { @@ -275,45 +274,86 @@ export default function Content() { getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`} /> - {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + {/* Module Metrics Footer - 3-Widget Layout */} , - accentColor: 'blue', - href: '/writer/tasks', + submoduleColor="purple" + threeWidgetLayout={{ + pageProgress: { + title: 'Page Progress', + submoduleColor: 'purple', + metrics: [ + { label: 'Total Content', value: totalCount }, + { label: 'Draft', value: content.filter(c => c.status === 'draft').length }, + { label: 'In Review', value: content.filter(c => c.status === 'review').length }, + { label: 'Published', value: content.filter(c => c.status === 'published').length, percentage: `${totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0}%` }, + ], + progress: { + label: 'Published', + value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0, + color: 'green', + }, + hint: content.filter(c => c.status === 'draft').length > 0 + ? `${content.filter(c => c.status === 'draft').length} drafts need images before review` + : 'All content processed!', }, - { - title: 'Draft', - value: content.filter(c => c.status === 'draft').length.toLocaleString(), - subtitle: 'needs editing', - icon: , - accentColor: 'amber', + moduleStats: { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: totalCount, + fromHref: '/writer/tasks', + actionLabel: 'Generate Content', + toLabel: 'Drafts', + toValue: content.filter(c => c.status === 'draft').length, + progress: 100, + color: 'blue', + }, + { + fromLabel: 'Drafts', + fromValue: content.filter(c => c.status === 'draft').length, + actionLabel: 'Generate Images', + toLabel: 'Images', + toValue: 0, + toHref: '/writer/images', + progress: totalCount > 0 ? Math.round((content.filter(c => c.status !== 'draft').length / totalCount) * 100) : 0, + color: 'purple', + }, + { + fromLabel: 'Ready', + fromValue: content.filter(c => c.status === 'review').length, + fromHref: '/writer/review', + actionLabel: 'Review & Publish', + toLabel: 'Published', + toValue: content.filter(c => c.status === 'published').length, + toHref: '/writer/approved', + progress: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0, + color: 'green', + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/approved' }, + ], }, - { - title: 'In Review', - value: content.filter(c => c.status === 'review').length.toLocaleString(), - subtitle: 'awaiting approval', - icon: , - accentColor: 'blue', - href: '/writer/review', + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords', value: 0, color: 'blue' }, + { label: 'Clusters', value: 0, color: 'green' }, + { label: 'Ideas', value: 0, color: 'amber' }, + ], + writerItems: [ + { label: 'Content', value: totalCount, color: 'purple' }, + { label: 'Images', value: 0, color: 'amber' }, + { label: 'Published', value: content.filter(c => c.status === 'published').length, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - { - title: 'Published', - value: content.filter(c => c.status === 'published').length.toLocaleString(), - subtitle: 'ready for sync', - icon: , - accentColor: 'green', - href: '/writer/published', - }, - ]} - progress={{ - label: 'Content Publishing Pipeline: Content moved from draft through review to published (Draft \u2192 Review \u2192 Published)', - value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0, - color: 'success', }} /> diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index 92e6b4e7..9e667fee 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -455,15 +455,86 @@ export default function Review() { onRowAction={handleRowAction} /> , - accentColor: 'blue', + submoduleColor="amber" + threeWidgetLayout={{ + pageProgress: { + title: 'Page Progress', + submoduleColor: 'amber', + metrics: [ + { label: 'In Review', value: totalCount }, + { label: 'This Page', value: content.length }, + { label: 'Ready', value: content.filter(c => c.word_count && c.word_count > 0).length, percentage: `${totalCount > 0 ? Math.round((content.filter(c => c.word_count && c.word_count > 0).length / totalCount) * 100) : 0}%` }, + { label: 'Pending', value: content.filter(c => !c.word_count || c.word_count === 0).length }, + ], + progress: { + label: 'Ready for Approval', + value: totalCount > 0 ? Math.round((content.filter(c => c.word_count && c.word_count > 0).length / totalCount) * 100) : 0, + color: 'amber', + }, + hint: totalCount > 0 + ? `${totalCount} items in review queue awaiting approval` + : 'No items in review queue', }, - ]} + moduleStats: { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: 0, + fromHref: '/writer/tasks', + actionLabel: 'Generate Content', + toLabel: 'Drafts', + toValue: 0, + toHref: '/writer/content', + progress: 100, + color: 'blue', + }, + { + fromLabel: 'Drafts', + fromValue: 0, + fromHref: '/writer/content', + actionLabel: 'Generate Images', + toLabel: 'Images', + toValue: 0, + toHref: '/writer/images', + progress: 100, + color: 'purple', + }, + { + fromLabel: 'Ready', + fromValue: totalCount, + actionLabel: 'Review & Publish', + toLabel: 'Published', + toValue: 0, + toHref: '/writer/approved', + progress: 0, + color: 'green', + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/approved' }, + ], + }, + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords', value: 0, color: 'blue' }, + { label: 'Clusters', value: 0, color: 'green' }, + { label: 'Ideas', value: 0, color: 'amber' }, + ], + writerItems: [ + { label: 'Content', value: 0, color: 'purple' }, + { label: 'In Review', value: totalCount, color: 'amber' }, + { label: 'Published', value: 0, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', + }, + }} /> ); diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 04e00be1..2c7c54c9 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -30,7 +30,7 @@ import { createTasksPageConfig } from '../../config/pages/tasks.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; -import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter'; import { DocumentTextIcon } from '@heroicons/react/24/outline'; export default function Tasks() { @@ -467,44 +467,89 @@ export default function Tasks() { }} /> - {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + {/* Module Metrics Footer - 3-Widget Layout */} sum + (c.ideas_count || 0), 0).toLocaleString(), - subtitle: 'from planner', - icon: , - accentColor: 'orange', - href: '/planner/ideas', + submoduleColor="blue" + threeWidgetLayout={{ + // Widget 1: Page Progress (Tasks) + pageProgress: { + title: 'Page Progress', + submoduleColor: 'blue', + metrics: [ + { label: 'Total', value: totalCount }, + { label: 'Complete', value: tasks.filter(t => t.status === 'completed').length, percentage: `${totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0}%` }, + { label: 'Queue', value: tasks.filter(t => t.status === 'queued').length }, + { label: 'Processing', value: tasks.filter(t => t.status === 'in_progress').length }, + ], + progress: { + value: totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0, + label: 'Generated', + color: 'blue', + }, + hint: tasks.filter(t => t.status === 'queued').length > 0 + ? `${tasks.filter(t => t.status === 'queued').length} tasks in queue for content generation` + : 'All tasks processed!', }, - { - title: 'In Queue', - value: tasks.filter(t => t.status === 'queued').length.toLocaleString(), - subtitle: 'waiting for processing', - icon: , - accentColor: 'amber', + // Widget 2: Module Stats (Writer Pipeline) + moduleStats: { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: totalCount, + actionLabel: 'Generate Content', + toLabel: 'Drafts', + toValue: tasks.filter(t => t.status === 'completed').length, + toHref: '/writer/content', + progress: totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0, + color: 'blue', + }, + { + fromLabel: 'Drafts', + fromValue: tasks.filter(t => t.status === 'completed').length, + fromHref: '/writer/content', + actionLabel: 'Generate Images', + toLabel: 'Images', + toValue: 0, + toHref: '/writer/images', + progress: 0, + color: 'purple', + }, + { + fromLabel: 'Ready', + fromValue: 0, + fromHref: '/writer/review', + actionLabel: 'Review & Publish', + toLabel: 'Published', + toValue: 0, + toHref: '/writer/approved', + progress: 0, + color: 'green', + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/approved' }, + ], }, - { - title: 'Processing', - value: tasks.filter(t => t.status === 'in_progress').length.toLocaleString(), - subtitle: 'generating content', - icon: , - accentColor: 'blue', + // Widget 3: Completion Stats + completion: { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Clusters', value: clusters.length, color: 'green' }, + { label: 'Ideas', value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0), color: 'amber' }, + ], + writerItems: [ + { label: 'Tasks', value: totalCount, color: 'blue' }, + { label: 'Content', value: tasks.filter(t => t.status === 'completed').length, color: 'purple' }, + { label: 'Published', value: 0, color: 'green' }, + ], + creditsUsed: 0, + operationsCount: 0, + analyticsHref: '/analytics', }, - { - title: 'Ready for Review', - value: tasks.filter(t => t.status === 'completed').length.toLocaleString(), - subtitle: 'content generated', - icon: , - accentColor: 'green', - href: '/writer/content', - }, - ]} - progress={{ - label: 'Content Generation Pipeline: Tasks successfully completed (Queued → Processing → Completed)', - value: totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0, - color: 'success', }} /> diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index eb7cac11..4ba7c0b7 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -2619,3 +2619,117 @@ export async function generatePageContent( }); } +// ========================================== +// Dashboard Summary API +// ========================================== + +export interface DashboardAttentionItem { + id: string; + type: 'pending_review' | 'setup_incomplete' | 'credits_low' | 'no_integration' | 'queued_tasks' | 'sync_failed'; + title: string; + count?: number; + action_label: string; + action_url: string; + severity: 'info' | 'warning' | 'error'; +} + +export interface DashboardPipeline { + keywords: number; + clusters: number; + ideas: number; + tasks: number; + drafts: number; + review: number; + published: number; + total_content: number; + completion_percentage: number; +} + +export interface DashboardAIOperation { + type: string; + label: string; + count: number; + credits: number; + tokens: number; +} + +export interface DashboardAIOperations { + period_days: number; + operations: DashboardAIOperation[]; + totals: { + credits: number; + operations: number; + }; +} + +export interface DashboardActivity { + id: number; + type: string; + description: string; + timestamp: string; + icon: string; + color: string; + credits: number; +} + +export interface DashboardContentVelocity { + today: number; + this_week: number; + this_month: number; + daily: Array<{ date: string; count: number }>; + average_per_day: number; +} + +export interface DashboardAutomation { + enabled: boolean; + active_count: number; + status: 'active' | 'inactive'; +} + +export interface DashboardSite { + id: number; + name: string; + domain: string; + keywords: number; + content: number; + published: number; + has_integration: boolean; + sectors_count: number; +} + +export interface DashboardSummary { + needs_attention: DashboardAttentionItem[]; + pipeline: DashboardPipeline; + ai_operations: DashboardAIOperations; + recent_activity: DashboardActivity[]; + content_velocity: DashboardContentVelocity; + automation: DashboardAutomation; + sites: DashboardSite[]; + account: { + credits: number; + name: string; + }; + generated_at: string; +} + +export interface DashboardSummaryFilters { + site_id?: number; + days?: number; +} + +/** + * Fetch aggregated dashboard summary in a single API call. + * Replaces multiple sequential calls for better performance. + */ +export async function fetchDashboardSummary( + filters: DashboardSummaryFilters = {} +): Promise { + const params = new URLSearchParams(); + if (filters.site_id) params.append('site_id', String(filters.site_id)); + if (filters.days) params.append('days', String(filters.days)); + + const queryString = params.toString(); + return fetchAPI(`/v1/account/dashboard/summary/${queryString ? `?${queryString}` : ''}`); +} + +