import { useEffect, useState, useMemo, lazy, Suspense } from "react"; import { Link, useNavigate } from "react-router-dom"; import PageMeta from "../../components/common/PageMeta"; import ComponentCard from "../../components/common/ComponentCard"; import { ProgressBar } from "../../components/ui/progress"; import { ApexOptions } from "apexcharts"; import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard"; import PageHeader from "../../components/common/PageHeader"; const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default }))); import { ListIcon, GroupIcon, BoltIcon, PieChartIcon, ArrowRightIcon, CheckCircleIcon, TimeIcon, ArrowUpIcon, ArrowDownIcon, PlugInIcon, ClockIcon, } from "../../icons"; import { fetchKeywords, fetchClusters, fetchContentIdeas, fetchTasks, fetchSiteBlueprints, SiteBlueprint, } from "../../services/api"; import { useSiteStore } from "../../store/siteStore"; import { useSectorStore } from "../../store/sectorStore"; interface DashboardStats { keywords: { total: number; mapped: number; unmapped: number; byStatus: Record; byIntent: Record; }; clusters: { total: number; withIdeas: number; withoutIdeas: number; totalVolume: number; avgKeywords: number; topClusters: Array<{ id: number; name: string; volume: number; keywords_count: number }>; }; ideas: { total: number; queued: number; notQueued: number; byStatus: Record; byContentType: Record; }; workflow: { keywordsReady: boolean; clustersBuilt: boolean; ideasGenerated: boolean; readyForWriter: boolean; }; } export default function PlannerDashboard() { const navigate = useNavigate(); const { activeSite } = useSiteStore(); const { activeSector } = useSectorStore(); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(new Date()); // Fetch real data const fetchDashboardData = async () => { try { setLoading(true); const [keywordsRes, clustersRes, ideasRes, tasksRes, blueprintsRes] = await Promise.all([ fetchKeywords({ page_size: 1000, sector_id: activeSector?.id }), fetchClusters({ page_size: 1000, sector_id: activeSector?.id }), fetchContentIdeas({ page_size: 1000, sector_id: activeSector?.id }), fetchTasks({ page_size: 1000, sector_id: activeSector?.id }), activeSite?.id ? fetchSiteBlueprints({ site_id: activeSite.id, page_size: 100 }) : Promise.resolve({ results: [] }) ]); const keywords = keywordsRes.results || []; const mappedKeywords = keywords.filter(k => k.cluster && k.cluster.length > 0); const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0); const keywordsByStatus: Record = {}; const keywordsByIntent: Record = {}; keywords.forEach(k => { keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1; if (k.intent) { keywordsByIntent[k.intent] = (keywordsByIntent[k.intent] || 0) + 1; } }); const clusters = clustersRes.results || []; const clustersWithIdeas = clusters.filter(c => c.keywords_count > 0); const totalVolume = clusters.reduce((sum, c) => sum + (c.volume || 0), 0); const totalKeywordsInClusters = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0); const avgKeywords = clusters.length > 0 ? Math.round(totalKeywordsInClusters / clusters.length) : 0; const topClusters = [...clusters] .sort((a, b) => (b.volume || 0) - (a.volume || 0)) .slice(0, 5) .map(c => ({ id: c.id, name: c.name || 'Unnamed Cluster', volume: c.volume || 0, keywords_count: c.keywords_count || 0 })); const ideas = ideasRes.results || []; const ideaIds = new Set(ideas.map(i => i.id)); const tasks = tasksRes.results || []; const queuedIdeas = tasks.filter(t => t.idea && ideaIds.has(t.idea)).length; const notQueuedIdeas = ideas.length - queuedIdeas; const ideasByStatus: Record = {}; const ideasByContentType: Record = {}; ideas.forEach(i => { ideasByStatus[i.status || 'new'] = (ideasByStatus[i.status || 'new'] || 0) + 1; if (i.content_type) { ideasByContentType[i.content_type] = (ideasByContentType[i.content_type] || 0) + 1; } }); setStats({ keywords: { total: keywords.length, mapped: mappedKeywords.length, unmapped: unmappedKeywords.length, byStatus: keywordsByStatus, byIntent: keywordsByIntent }, clusters: { total: clusters.length, withIdeas: clustersWithIdeas.length, withoutIdeas: clusters.length - clustersWithIdeas.length, totalVolume, avgKeywords, topClusters }, ideas: { total: ideas.length, queued: queuedIdeas, notQueued: notQueuedIdeas, byStatus: ideasByStatus, byContentType: ideasByContentType }, workflow: { keywordsReady: keywords.length > 0, clustersBuilt: clusters.length > 0, ideasGenerated: ideas.length > 0, readyForWriter: queuedIdeas > 0 } }); setLastUpdated(new Date()); } catch (error) { console.error('Error fetching dashboard data:', error); } finally { setLoading(false); } }; useEffect(() => { fetchDashboardData(); const interval = setInterval(fetchDashboardData, 30000); return () => clearInterval(interval); }, [activeSector?.id, activeSite?.id]); const keywordMappingPct = useMemo(() => { if (!stats || stats.keywords.total === 0) return 0; return Math.round((stats.keywords.mapped / stats.keywords.total) * 100); }, [stats]); const clustersIdeasPct = useMemo(() => { if (!stats || stats.clusters.total === 0) return 0; return Math.round((stats.clusters.withIdeas / stats.clusters.total) * 100); }, [stats]); const ideasQueuedPct = useMemo(() => { if (!stats || stats.ideas.total === 0) return 0; return Math.round((stats.ideas.queued / stats.ideas.total) * 100); }, [stats]); const plannerModules = [ { title: "Keywords", description: "Manage and discover keywords", icon: ListIcon, color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]", path: "/planner/keywords", count: stats?.keywords.total || 0, metric: `${stats?.keywords.mapped || 0} mapped`, }, { title: "Clusters", description: "Keyword clusters and groups", icon: GroupIcon, color: "from-[var(--color-success)] to-[var(--color-success-dark)]", path: "/planner/clusters", count: stats?.clusters.total || 0, metric: `${stats?.clusters.totalVolume.toLocaleString() || 0} volume`, }, { title: "Ideas", description: "Content ideas and concepts", icon: BoltIcon, color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]", path: "/planner/ideas", count: stats?.ideas.total || 0, metric: `${stats?.ideas.queued || 0} queued`, }, { title: "Keyword Opportunities", description: "Discover new keyword opportunities", icon: PieChartIcon, color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]", path: "/planner/keyword-opportunities", count: 0, metric: "Discover new keywords", }, ]; const recentActivity = [ { id: 1, type: "Keywords Clustered", description: `${stats?.clusters.total || 0} new clusters created`, timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), icon: GroupIcon, color: "text-green-600", }, { id: 2, type: "Ideas Generated", description: `${stats?.ideas.total || 0} content ideas created`, timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000), icon: BoltIcon, color: "text-orange-600", }, { id: 3, type: "Keywords Added", description: `${stats?.keywords.total || 0} keywords in database`, timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000), icon: ListIcon, color: "text-blue-600", }, ]; const chartOptions: ApexOptions = { chart: { type: "area", height: 300, toolbar: { show: false }, zoom: { enabled: false }, }, stroke: { curve: "smooth", width: 3, }, xaxis: { categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], labels: { style: { colors: "#6b7280" } }, }, yaxis: { labels: { style: { colors: "#6b7280" } }, }, legend: { position: "top", labels: { colors: "#6b7280" }, }, colors: ["var(--color-primary)", "var(--color-success)", "var(--color-warning)"], grid: { borderColor: "#e5e7eb", }, fill: { type: "gradient", gradient: { opacityFrom: 0.6, opacityTo: 0.1, }, }, }; const chartSeries = [ { name: "Keywords Added", data: [12, 19, 15, 25, 22, 18, 24], }, { name: "Clusters Created", data: [8, 12, 10, 15, 14, 11, 16], }, { name: "Ideas Generated", data: [5, 8, 6, 10, 9, 7, 11], }, ]; const keywordsStatusChart = useMemo(() => { if (!stats) return null; const options: ApexOptions = { chart: { type: 'donut', fontFamily: 'Outfit, sans-serif', toolbar: { show: false } }, labels: Object.keys(stats.keywords.byStatus).filter(key => stats.keywords.byStatus[key] > 0), colors: ['#465FFF', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'], legend: { position: 'bottom', fontFamily: 'Outfit', show: true }, dataLabels: { enabled: false }, plotOptions: { pie: { donut: { size: '70%', labels: { show: true, name: { show: false }, value: { show: true, fontSize: '24px', fontWeight: 700, color: '#465FFF', fontFamily: 'Outfit', formatter: () => { const total = Object.values(stats.keywords.byStatus).reduce((a, b) => a + b, 0); return total > 0 ? total.toString() : '0'; } }, total: { show: false } } } } } }; const series = Object.keys(stats.keywords.byStatus) .filter(key => stats.keywords.byStatus[key] > 0) .map(key => stats.keywords.byStatus[key]); return { options, series }; }, [stats]); const topClustersChart = useMemo(() => { if (!stats || stats.clusters.topClusters.length === 0) return null; const options: ApexOptions = { chart: { type: 'bar', fontFamily: 'Outfit, sans-serif', toolbar: { show: false }, height: 300 }, colors: ['#10B981'], plotOptions: { bar: { horizontal: true, borderRadius: 5, dataLabels: { position: 'top' } } }, dataLabels: { enabled: true, formatter: (val: number) => val.toLocaleString(), offsetX: 10 }, xaxis: { categories: stats.clusters.topClusters.map(c => c.name), labels: { style: { fontFamily: 'Outfit', fontSize: '12px' } } }, yaxis: { labels: { style: { fontFamily: 'Outfit' } } }, tooltip: { y: { formatter: (val: number) => `${val.toLocaleString()} volume` } } }; const series = [{ name: 'Search Volume', data: stats.clusters.topClusters.map(c => c.volume) }]; return { options, series }; }, [stats]); const formatTimeAgo = (date: Date) => { const minutes = Math.floor((Date.now() - date.getTime()) / 60000); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; }; if (loading && !stats) { return ( <>

Loading dashboard data...

); } if (!stats && !loading) { return ( <>

{activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}

); } if (!stats) return null; return ( <>
{/* Key Metrics */}
} accentColor="blue" trend={0} href="/planner/keywords" /> } accentColor="green" trend={0} href="/planner/clusters" /> } accentColor="orange" trend={0} href="/planner/ideas" /> } accentColor="purple" trend={0} href="/planner/keywords" />
{/* Planner Modules */}
{plannerModules.map((module) => { const Icon = module.icon; return (

{module.title}

{module.description}

{module.count}
{module.metric}
); })}
{/* Activity Chart & Recent Activity */}
Loading chart...
}>
{recentActivity.map((activity) => { const Icon = activity.icon; return (

{activity.type}

{formatTimeAgo(activity.timestamp)}

{activity.description}

); })}
{/* Charts */}
{keywordsStatusChart && ( Loading chart...
}> )} {topClustersChart && ( Loading chart...}> )} {/* Progress Summary */}
Keyword Mapping {keywordMappingPct}%

{stats.keywords.mapped} of {stats.keywords.total} keywords mapped

Clusters With Ideas {clustersIdeasPct}%

{stats.clusters.withIdeas} of {stats.clusters.total} clusters have ideas

Ideas Queued to Writer {ideasQueuedPct}%

{stats.ideas.queued} of {stats.ideas.total} ideas queued

{/* Quick Actions */}

Add Keywords

Discover opportunities

Auto Cluster

Group keywords

Generate Ideas

Create content ideas

Setup Automation

Automate workflows

{/* Info Cards */}

Keyword Discovery

Discover high-volume keywords from our global database. Add keywords manually or import from keyword opportunities.

AI Clustering

Automatically group related keywords into strategic clusters. Each cluster represents a content topic with shared search intent.

Idea Generation

Generate content ideas from clusters using AI. Each idea includes title, outline, and target keywords for content creation.

1

Add Keywords

Start by adding keywords from the keyword opportunities page. You can search by volume, difficulty, or intent.

2

Cluster Keywords

Use the auto-cluster feature to group related keywords. Review and refine clusters to match your content strategy.

3

Generate Ideas

Create content ideas from your clusters. Queue ideas to the Writer module to start content creation.

); }