/** * Dashboard Home - Compact, information-dense dashboard * Shows workflow pipeline, quick actions, AI operations, recent activity, * content velocity, and automation status */ import React, { useEffect, useState, useCallback } from "react"; import PageMeta from "../../components/common/PageMeta"; import WorkflowGuide from "../../components/onboarding/WorkflowGuide"; import { useOnboardingStore } from "../../store/onboardingStore"; import { useBillingStore } from "../../store/billingStore"; import { GridIcon, PlusIcon } from "../../icons"; import { fetchSites, Site, } from "../../services/api"; import { getDashboardStats } from "../../services/billing.api"; import { useSiteStore } from "../../store/siteStore"; import { useSectorStore } from "../../store/sectorStore"; import { useToast } from "../../components/ui/toast/ToastContainer"; import Button from "../../components/ui/button/Button"; import { useAuthStore } from "../../store/authStore"; import { usePageContext } from "../../context/PageContext"; // Dashboard Widgets import NeedsAttentionBar, { AttentionItem } from "../../components/dashboard/NeedsAttentionBar"; import WorkflowPipelineWidget, { PipelineData } from "../../components/dashboard/WorkflowPipelineWidget"; import QuickActionsWidget from "../../components/dashboard/QuickActionsWidget"; import AIOperationsWidget, { AIOperationsData } from "../../components/dashboard/AIOperationsWidget"; import RecentActivityWidget, { ActivityItem } from "../../components/dashboard/RecentActivityWidget"; import ContentVelocityWidget, { ContentVelocityData } from "../../components/dashboard/ContentVelocityWidget"; import AutomationStatusWidget, { AutomationData } from "../../components/dashboard/AutomationStatusWidget"; import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget"; import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget"; import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget"; import { getSubscriptions, Subscription } from "../../services/billing.api"; export default function Home() { const toast = useToast(); const { activeSite, setActiveSite, loadActiveSite } = useSiteStore(); const { activeSector } = useSectorStore(); const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore(); const { user } = useAuthStore(); const { balance, loadBalance } = useBillingStore(); const { setPageInfo } = usePageContext(); // Core state const [sites, setSites] = useState([]); const [sitesLoading, setSitesLoading] = useState(true); const [siteFilter, setSiteFilter] = useState<'all' | number>('all'); const [showAddSite, setShowAddSite] = useState(false); const [loading, setLoading] = useState(true); const [subscription, setSubscription] = useState(null); // Dashboard data state const [attentionItems, setAttentionItems] = useState([]); const [pipelineData, setPipelineData] = useState({ sites: 0, keywords: 0, clusters: 0, ideas: 0, tasks: 0, drafts: 0, published: 0, completionPercentage: 0, }); const [aiOperations, setAIOperations] = useState({ period: '7d', operations: [ { type: 'clustering', count: 0, credits: 0 }, { type: 'ideas', count: 0, credits: 0 }, { type: 'content', count: 0, credits: 0 }, { type: 'images', count: 0, credits: 0 }, ], totals: { count: 0, credits: 0, successRate: 100, avgCreditsPerOp: 0 }, }); const [recentActivity, setRecentActivity] = useState([]); const [contentVelocity, setContentVelocity] = useState({ thisWeek: { articles: 0, words: 0, images: 0 }, thisMonth: { articles: 0, words: 0, images: 0 }, total: { articles: 0, words: 0, images: 0 }, trend: 0, }); const [automationData, setAutomationData] = useState({ status: 'not_configured', }); // Plan limits const maxSites = (user?.account as any)?.plan?.max_sites || (user as any)?.account?.plan?.max_sites || 0; const canAddMoreSites = maxSites === 0 || sites.length < maxSites; // Set page info for AppHeader useEffect(() => { setPageInfo({ title: 'Dashboard', badge: { icon: , color: 'blue' }, siteFilter: siteFilter, onSiteFilterChange: (value) => { setSiteFilter(value); if (typeof value === 'number') { const site = sites.find(s => s.id === value); if (site) { setActiveSite(site); } } }, }); return () => setPageInfo(null); }, [setPageInfo, sites, siteFilter, setActiveSite]); // Load initial data useEffect(() => { loadSites(); loadBalance(); loadSubscription(); loadFromBackend().catch(() => {}); }, [loadFromBackend, loadBalance]); // Load subscription info const loadSubscription = async () => { try { const { results } = await getSubscriptions(); // Get the active subscription const activeSubscription = results.find(s => s.status === 'active') || results[0] || null; setSubscription(activeSubscription); } catch (error) { console.error('Failed to load subscription:', error); } }; // Load active site if not set useEffect(() => { if (!activeSite && sites.length > 0) { loadActiveSite(); } }, [sites, activeSite, loadActiveSite]); // Set initial site filter useEffect(() => { if (sites.length === 0) { setSiteFilter('all'); } else if (sites.length === 1 && sites[0]) { setSiteFilter(sites[0].id); } }, [sites.length]); // Show guide logic useEffect(() => { if (sites.length === 0 || showAddSite) { showGuide(); } }, [sites.length, showAddSite, showGuide]); const loadSites = async () => { try { setSitesLoading(true); const response = await fetchSites(); const activeSites = (response.results || []).filter(site => site.is_active); setSites(activeSites); } catch (error: any) { console.error('Failed to load sites:', error); toast.error(`Failed to load sites: ${error.message}`); } finally { setSitesLoading(false); } }; const fetchDashboardData = useCallback(async () => { try { setLoading(true); const siteId = siteFilter === 'all' ? undefined : siteFilter; // Fetch real dashboard stats from API const stats = await getDashboardStats({ site_id: siteId, days: 7 }); // Update pipeline data from real API data const { pipeline, counts } = stats; const completionPercentage = pipeline.keywords > 0 ? Math.round((pipeline.published / pipeline.keywords) * 100) : 0; setPipelineData({ sites: pipeline.sites, keywords: pipeline.keywords, clusters: pipeline.clusters, ideas: pipeline.ideas, tasks: pipeline.tasks, drafts: pipeline.drafts, published: pipeline.published, completionPercentage: Math.min(completionPercentage, 100), }); // Generate attention items based on real data const attentionList: AttentionItem[] = []; // Check for sites without sectors const sitesWithoutSectors = sites.filter(s => !s.active_sectors_count || s.active_sectors_count === 0); if (sitesWithoutSectors.length > 0) { attentionList.push({ id: 'setup_incomplete', type: 'setup_incomplete', title: 'Setup Incomplete', description: `${sitesWithoutSectors.length} site(s) need industry & sectors configured`, actionLabel: 'Complete Setup', actionHref: '/sites', }); } // Check for content needing images (content in review without all images generated) const contentWithPendingImages = counts.images.pending; if (contentWithPendingImages > 0) { attentionList.push({ id: 'needs_images', type: 'pending_review', title: 'images pending', count: contentWithPendingImages, description: 'Generate images before publishing', actionLabel: 'Generate Images', actionHref: '/writer/images', }); } // Check for content in review if (counts.content.review > 0) { attentionList.push({ id: 'pending_review', type: 'pending_review', title: 'articles ready for review', count: counts.content.review, description: 'Review and publish content', actionLabel: 'Review Content', actionHref: '/writer/content?status=review', }); } setAttentionItems(attentionList); // Update content velocity from real API data setContentVelocity(stats.content_velocity); // Update recent activity from real API data (convert timestamp strings to Date objects) const activityList: ActivityItem[] = stats.recent_activity.map(item => ({ ...item, timestamp: new Date(item.timestamp), })); setRecentActivity(activityList); // Update AI operations from real API data // Map operation types to display types const operationTypeMap: Record = { 'clustering': 'clustering', 'idea_generation': 'ideas', 'content_generation': 'content', 'image_generation': 'images', 'image_prompt_extraction': 'images', }; const mappedOperations = stats.ai_operations.operations.map(op => ({ type: operationTypeMap[op.type] || op.type, count: op.count, credits: op.credits, })); // Ensure all expected types exist const expectedTypes = ['clustering', 'ideas', 'content', 'images']; for (const type of expectedTypes) { if (!mappedOperations.find(op => op.type === type)) { mappedOperations.push({ type, count: 0, credits: 0 }); } } setAIOperations({ period: stats.ai_operations.period, operations: mappedOperations, totals: stats.ai_operations.totals, }); // Set automation status (would come from automation API) setAutomationData({ status: sites.length > 0 ? 'active' : 'not_configured', schedule: sites.length > 0 ? 'Daily 9 AM' : undefined, lastRun: sites.length > 0 && counts.content.total > 0 ? { timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000), clustered: pipeline.clusters, ideas: pipeline.ideas, content: counts.content.total, images: counts.images.total, success: true, } : undefined, nextRun: sites.length > 0 ? new Date(Date.now() + 12 * 60 * 60 * 1000) : undefined, }); } catch (error: any) { if (error?.status === 429) { setTimeout(() => fetchDashboardData(), 2000); } else { console.error('Error fetching dashboard data:', error); toast.error(`Failed to load dashboard: ${error.message}`); } } finally { setLoading(false); } }, [siteFilter, sites, toast]); // Fetch dashboard data when filter changes useEffect(() => { if (!sitesLoading) { fetchDashboardData(); } }, [siteFilter, sitesLoading, fetchDashboardData]); const handleAddSiteClick = () => { setShowAddSite(true); showGuide(); }; const handleSiteAdded = () => { setShowAddSite(false); loadSites(); fetchDashboardData(); }; const handleDismissAttention = (id: string) => { setAttentionItems(items => items.filter(item => item.id !== id)); }; const handlePeriodChange = (period: '7d' | '30d' | '90d') => { setAIOperations(prev => ({ ...prev, period })); // In real implementation, would refetch data for new period }; const handleRunAutomation = () => { toast.info('Starting automation run...'); // In real implementation, would trigger automation via API }; return ( <> {/* Welcome/Guide Screen - Shows for new users or when adding site */} {(sites.length === 0 || showAddSite) && (
)} {/* Main Dashboard Content */} {sites.length > 0 && !showAddSite && (
{/* Needs Attention Bar */} {/* Row 1: Sites Overview + Credits + Account (3 columns) */}
{/* Row 2: Workflow Pipeline (full width) */} {/* Row 3: Quick Actions (full width) */} {/* Row 4: AI Operations + Recent Activity */}
{/* Row 5: Content Velocity + Automation Status */}
{/* Add Site Button - Floating */} {canAddMoreSites && (
)}
)} ); }