From b07d0f518a5920f76ac230e7663b01f29deb497d Mon Sep 17 00:00:00 2001 From: Desktop Date: Wed, 12 Nov 2025 20:37:56 +0500 Subject: [PATCH] Planner Writer Dashboard --- frontend/src/pages/Planner/Dashboard.tsx | 961 ++++++++++++++++------- frontend/src/pages/Writer/Dashboard.tsx | 849 +++++++++++++++++++- 2 files changed, 1509 insertions(+), 301 deletions(-) diff --git a/frontend/src/pages/Planner/Dashboard.tsx b/frontend/src/pages/Planner/Dashboard.tsx index b34dbd2a..a6b10ac3 100644 --- a/frontend/src/pages/Planner/Dashboard.tsx +++ b/frontend/src/pages/Planner/Dashboard.tsx @@ -1,51 +1,425 @@ +import { useEffect, useState, useMemo } from "react"; import { Link, useNavigate } from "react-router"; import PageMeta from "../../components/common/PageMeta"; import ComponentCard from "../../components/common/ComponentCard"; import { ProgressBar } from "../../components/ui/progress"; -import { ListIcon, GroupIcon, BoltIcon, PieChartIcon, ArrowRightIcon, CheckCircleIcon, TimeIcon } from "../../icons"; +import Chart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import { + ListIcon, + GroupIcon, + BoltIcon, + PieChartIcon, + ArrowRightIcon, + CheckCircleIcon, + TimeIcon, + ArrowUpIcon, + ArrowDownIcon +} from "../../icons"; +import { + fetchKeywords, + fetchClusters, + fetchContentIdeas, + fetchTasks +} 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(); - // Mock data - will be replaced with API calls - const stats = { - keywords: 245, - clusters: 18, - ideas: 52, - mappedKeywords: 180, - clustersWithIdeas: 12, - queuedIdeas: 35, + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(new Date()); + const [trends, setTrends] = useState<{ + keywords: number; + clusters: number; + ideas: number; + }>({ keywords: 0, clusters: 0, ideas: 0 }); + + // Fetch real data + const fetchDashboardData = async () => { + try { + setLoading(true); + + // Fetch all data in parallel + const [keywordsRes, clustersRes, ideasRes, tasksRes] = 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 }) + ]); + + // Process keywords + 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; + } + }); + + // Process clusters + 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; + + // Top clusters by volume + 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 + })); + + // Process ideas + 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; + } + }); + + // Calculate trends (compare with previous state) + if (stats) { + setTrends({ + keywords: keywords.length - stats.keywords.total, + clusters: clusters.length - stats.clusters.total, + ideas: ideas.length - stats.ideas.total + }); + } + + 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); + } }; - const keywordMappingPct = stats.keywords > 0 ? Math.round((stats.mappedKeywords / stats.keywords) * 100) : 0; - const clustersIdeasPct = stats.clusters > 0 ? Math.round((stats.clustersWithIdeas / stats.clusters) * 100) : 0; - const ideasQueuedPct = stats.ideas > 0 ? Math.round((stats.queuedIdeas / stats.ideas) * 100) : 0; + // Initial load and periodic refresh + useEffect(() => { + if (!activeSector?.id) return; + + fetchDashboardData(); + + // Refresh every 30 seconds + const interval = setInterval(fetchDashboardData, 30000); + + return () => clearInterval(interval); + }, [activeSector?.id]); + + // Calculate percentages + 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]); + + // Chart data for keywords by status + 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), + colors: ['#465FFF', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'], + legend: { + position: 'bottom', + fontFamily: 'Outfit' + }, + dataLabels: { + enabled: true, + formatter: (val: number) => `${Math.round(val)}%` + }, + plotOptions: { + pie: { + donut: { + size: '65%' + } + } + } + }; + + const series = Object.values(stats.keywords.byStatus); + + return { options, series }; + }, [stats]); + + // Chart data for ideas by status + const ideasStatusChart = useMemo(() => { + if (!stats) return null; + + const options: ApexOptions = { + chart: { + type: 'bar', + fontFamily: 'Outfit, sans-serif', + toolbar: { show: false }, + height: 250 + }, + colors: ['#465FFF'], + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 5 + } + }, + dataLabels: { + enabled: true + }, + xaxis: { + categories: Object.keys(stats.ideas.byStatus), + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + yaxis: { + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + grid: { + strokeDashArray: 4 + } + }; + + const series = [{ + name: 'Ideas', + data: Object.values(stats.ideas.byStatus) + }]; + + return { options, series }; + }, [stats]); + + // Chart data for top clusters volume + 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]); + + if (loading && !stats) { + return ( + <> + +
+
+
+

Loading dashboard data...

+
+
+ + ); + } + + if (!stats) { + return ( + <> + +
+

No data available. Please select a sector.

+
+ + ); + } const workflowSteps = [ - { number: 1, title: "Add Keywords", status: "completed", count: stats.keywords, path: "/planner/keywords" }, - { number: 2, title: "Select Sector", status: "completed", count: null, path: "/planner" }, - { number: 3, title: "Auto Cluster", status: "pending", count: stats.clusters, path: "/planner/clusters" }, - { number: 4, title: "Generate Ideas", status: "pending", count: stats.ideas, path: "/planner/ideas" }, - ]; - - const topClusters = [ - { name: "SEO Optimization", volume: 45800, keywords: 24 }, - { name: "Content Marketing", volume: 32100, keywords: 18 }, - { name: "Link Building", volume: 28700, keywords: 15 }, - { name: "Keyword Research", volume: 24100, keywords: 12 }, - { name: "Analytics", volume: 18900, keywords: 9 }, - ]; - - const ideasByStatus = [ - { status: "New", count: 20, color: "blue" }, - { status: "Scheduled", count: 15, color: "amber" }, - { status: "Published", count: 17, color: "green" }, + { + number: 1, + title: "Add Keywords", + status: stats.workflow.keywordsReady ? "completed" : "pending", + count: stats.keywords.total, + path: "/planner/keywords" + }, + { + number: 2, + title: "Auto Cluster", + status: stats.workflow.clustersBuilt ? "completed" : "pending", + count: stats.clusters.total, + path: "/planner/clusters" + }, + { + number: 3, + title: "Generate Ideas", + status: stats.workflow.ideasGenerated ? "completed" : "pending", + count: stats.ideas.total, + path: "/planner/ideas" + }, + { + number: 4, + title: "Queue to Writer", + status: stats.workflow.readyForWriter ? "completed" : "pending", + count: stats.ideas.queued, + path: "/writer/tasks" + }, ]; const nextActions = [ - { text: "65 keywords unmapped", action: "Map Keywords", path: "/planner/keywords" }, - { text: "6 clusters without ideas", action: "Generate Ideas", path: "/planner/ideas" }, - { text: "17 ideas not queued to writer", action: "Queue to Writer", path: "/writer/tasks" }, + ...(stats.keywords.unmapped > 0 ? [{ + text: `${stats.keywords.unmapped} keywords unmapped`, + action: "Map Keywords", + path: "/planner/keywords" + }] : []), + ...(stats.clusters.withoutIdeas > 0 ? [{ + text: `${stats.clusters.withoutIdeas} clusters without ideas`, + action: "Generate Ideas", + path: "/planner/clusters" + }] : []), + ...(stats.ideas.notQueued > 0 ? [{ + text: `${stats.ideas.notQueued} ideas not queued to writer`, + action: "Queue to Writer", + path: "/planner/ideas" + }] : []) ]; return ( @@ -53,261 +427,296 @@ export default function PlannerDashboard() {
+ {/* Header with last updated */} +
+
+

Planner Dashboard

+

+ Last updated: {lastUpdated.toLocaleTimeString()} +

+
+ +
+ {/* Top Status Cards */}
- -
-
-
-

Keywords Ready

-

- {stats.keywords.toLocaleString()} -

-

- Research, analyze, and manage keywords strategy -

-
-
- -
-
- - - -
-
-
-

Clusters Built

-

- {stats.clusters.toLocaleString()} -

-

- Organize keywords into strategic topical clusters -

-
-
- -
-
- - - -
-
-
-

Ideas Generated

-

- {stats.ideas.toLocaleString()} -

-

- Generate creative content ideas based on semantic strategy -

-
-
- -
-
- - - -
-
-
-

Mapped Keywords

-

- {stats.mappedKeywords.toLocaleString()} -

-

- Keywords successfully mapped to content pages -

-
-
- -
-
- -
- - {/* Planner Workflow Steps */} - -
- {workflowSteps.map((step) => ( - -
-
- {step.number} -
-

{step.title}

-
-
-
- {step.status === "completed" ? ( - <> - - Completed - - ) : ( - <> - - Pending - + +
+
+
+

Keywords Ready

+
+

+ {stats.keywords.total.toLocaleString()} +

+ {trends.keywords !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.keywords > 0 ? : } + {Math.abs(trends.keywords)} +
)}
-
- {step.count !== null && ( -

- {step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : "ideas"}{" "} - {step.status === "completed" ? "added" : ""} +

+ {stats.keywords.mapped} mapped • {stats.keywords.unmapped} unmapped

- )} - {step.status === "pending" && ( - - )} - - ))} +
+
+ +
+
+ + + +
+
+
+

Clusters Built

+
+

+ {stats.clusters.total.toLocaleString()} +

+ {trends.clusters !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.clusters > 0 ? : } + {Math.abs(trends.clusters)} +
+ )} +
+

+ {stats.clusters.totalVolume.toLocaleString()} total volume • {stats.clusters.avgKeywords} avg keywords +

+
+
+ +
+
+ + + +
+
+
+

Ideas Generated

+
+

+ {stats.ideas.total.toLocaleString()} +

+ {trends.ideas !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.ideas > 0 ? : } + {Math.abs(trends.ideas)} +
+ )} +
+

+ {stats.ideas.queued} queued • {stats.ideas.notQueued} pending +

+
+
+ +
+
+ + + +
+
+
+

Mapping Progress

+

+ {keywordMappingPct}% +

+

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

+
+
+ +
+
+
- -
- {/* Progress Summary */} - -
-
-
- Keyword Mapping - {keywordMappingPct}% -
- -

- {stats.mappedKeywords} of {stats.keywords} keywords mapped -

-
- -
-
- Clusters With Ideas - {clustersIdeasPct}% -
- -

- {stats.clustersWithIdeas} of {stats.clusters} clusters have ideas -

-
- -
-
- Ideas Queued to Writer - {ideasQueuedPct}% -
- -

- {stats.queuedIdeas} of {stats.ideas} ideas queued -

-
-
-
- - {/* Top 5 Clusters */} - -
- {topClusters.map((cluster, index) => { - const maxVolume = topClusters[0].volume; - const percentage = Math.round((cluster.volume / maxVolume) * 100); - return ( -
-
- {cluster.name} - - {cluster.volume.toLocaleString()} - -
- -
- ); - })} -
-
- - {/* Ideas by Status */} - -
- {ideasByStatus.map((item, index) => { - const total = ideasByStatus.reduce((sum, i) => sum + i.count, 0); - const percentage = Math.round((item.count / total) * 100); - return ( -
-
- {item.status} - {item.count} -
- -
- ); - })} -
-
-
- - {/* Next Actions */} - -
- {nextActions.map((action, index) => ( -
- {action.text} + {/* Planner Workflow Steps */} + +
+ {workflowSteps.map((step) => ( - {action.action} - +
+
+ {step.status === "completed" ? : step.number} +
+

{step.title}

+
+
+
+ {step.status === "completed" ? ( + <> + + Completed + + ) : ( + <> + + Pending + + )} +
+
+ {step.count !== null && ( +

+ {step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : step.title.includes("Ideas") ? "ideas" : "items"} +

+ )} + {step.status === "pending" && ( + + )} + ))} +
+
+ +
+ {/* 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 +

+
- ))} +
+ + {/* Top 5 Clusters */} + + {topClustersChart ? ( + + ) : ( +
+ No clusters data available +
+ )} +
- + +
+ {/* Keywords by Status */} + {keywordsStatusChart && ( + + + + )} + + {/* Ideas by Status */} + {ideasStatusChart && ( + + + + )} +
+ + {/* Next Actions */} + {nextActions.length > 0 && ( + +
+ {nextActions.map((action, index) => ( +
+ {action.text} + + {action.action} + + +
+ ))} +
+
+ )}
); diff --git a/frontend/src/pages/Writer/Dashboard.tsx b/frontend/src/pages/Writer/Dashboard.tsx index 5af38dfb..1dbdce03 100644 --- a/frontend/src/pages/Writer/Dashboard.tsx +++ b/frontend/src/pages/Writer/Dashboard.tsx @@ -1,42 +1,841 @@ +import { useEffect, useState, useMemo } from "react"; +import { Link, useNavigate } from "react-router"; import PageMeta from "../../components/common/PageMeta"; import ComponentCard from "../../components/common/ComponentCard"; +import { ProgressBar } from "../../components/ui/progress"; +import Chart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import { + FileTextIcon, + BoxIcon, + CheckCircleIcon, + ClockIcon, + PencilIcon, + BoltIcon, + ArrowUpIcon, + ArrowDownIcon, + ArrowRightIcon +} from "../../icons"; +import { + fetchTasks, + fetchContent, + fetchImages +} from "../../services/api"; +import { useSiteStore } from "../../store/siteStore"; +import { useSectorStore } from "../../store/sectorStore"; + +interface WriterStats { + tasks: { + total: number; + byStatus: Record; + pending: number; + inProgress: number; + completed: number; + avgWordCount: number; + totalWordCount: number; + }; + content: { + total: number; + drafts: number; + review: number; + published: number; + totalWordCount: number; + avgWordCount: number; + byContentType: Record; + }; + images: { + total: number; + generated: number; + pending: number; + failed: number; + byType: Record; + }; + workflow: { + tasksCreated: boolean; + contentGenerated: boolean; + imagesGenerated: boolean; + readyToPublish: boolean; + }; + productivity: { + contentThisWeek: number; + contentThisMonth: number; + avgGenerationTime: number; + publishRate: number; + }; +} export default function WriterDashboard() { + 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()); + const [trends, setTrends] = useState<{ + tasks: number; + content: number; + images: number; + }>({ tasks: 0, content: 0, images: 0 }); + + // Fetch real data + const fetchDashboardData = async () => { + try { + setLoading(true); + + // Fetch all data in parallel + const [tasksRes, contentRes, imagesRes] = await Promise.all([ + fetchTasks({ page_size: 1000, sector_id: activeSector?.id }), + fetchContent({ page_size: 1000, sector_id: activeSector?.id }), + fetchImages({ page_size: 1000, sector_id: activeSector?.id }) + ]); + + // Process tasks + const tasks = tasksRes.results || []; + const tasksByStatus: Record = {}; + let totalWordCount = 0; + let wordCountCount = 0; + + tasks.forEach(t => { + tasksByStatus[t.status || 'draft'] = (tasksByStatus[t.status || 'draft'] || 0) + 1; + if (t.word_count) { + totalWordCount += t.word_count; + wordCountCount++; + } + }); + + const pendingTasks = tasks.filter(t => t.status === 'draft' || t.status === 'pending').length; + const inProgressTasks = tasks.filter(t => t.status === 'in_progress' || t.status === 'review').length; + const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'published').length; + const avgWordCount = wordCountCount > 0 ? Math.round(totalWordCount / wordCountCount) : 0; + + // Process content + const content = contentRes.results || []; + const contentByStatus: Record = {}; + const contentByType: Record = {}; + let contentTotalWords = 0; + let contentWordCount = 0; + + content.forEach(c => { + contentByStatus[c.status || 'draft'] = (contentByStatus[c.status || 'draft'] || 0) + 1; + if (c.word_count) { + contentTotalWords += c.word_count; + contentWordCount++; + } + }); + + const drafts = content.filter(c => c.status === 'draft').length; + const review = content.filter(c => c.status === 'review').length; + const published = content.filter(c => c.status === 'published').length; + const contentAvgWordCount = contentWordCount > 0 ? Math.round(contentTotalWords / contentWordCount) : 0; + + // Process images + const images = imagesRes.results || []; + const imagesByStatus: Record = {}; + const imagesByType: Record = {}; + + images.forEach(img => { + imagesByStatus[img.status || 'pending'] = (imagesByStatus[img.status || 'pending'] || 0) + 1; + if (img.image_type) { + imagesByType[img.image_type] = (imagesByType[img.image_type] || 0) + 1; + } + }); + + const generatedImages = images.filter(img => img.status === 'generated' && img.image_url).length; + const pendingImages = images.filter(img => (img.status === 'pending' || img.status === 'draft') || !img.image_url).length; + const failedImages = images.filter(img => img.status === 'failed' || img.status === 'error').length; + + // Calculate productivity metrics + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const contentThisWeek = content.filter(c => { + if (!c.generated_at) return false; + const created = new Date(c.generated_at); + return created >= weekAgo; + }).length; + + const contentThisMonth = content.filter(c => { + if (!c.generated_at) return false; + const created = new Date(c.generated_at); + return created >= monthAgo; + }).length; + + const publishRate = content.length > 0 ? Math.round((published / content.length) * 100) : 0; + + // Calculate trends + if (stats) { + setTrends({ + tasks: tasks.length - stats.tasks.total, + content: content.length - stats.content.total, + images: images.length - stats.images.total + }); + } + + setStats({ + tasks: { + total: tasks.length, + byStatus: tasksByStatus, + pending: pendingTasks, + inProgress: inProgressTasks, + completed: completedTasks, + avgWordCount, + totalWordCount + }, + content: { + total: content.length, + drafts, + review, + published, + totalWordCount: contentTotalWords, + avgWordCount: contentAvgWordCount, + byContentType: contentByType + }, + images: { + total: images.length, + generated: generatedImages, + pending: pendingImages, + failed: failedImages, + byType: imagesByType + }, + workflow: { + tasksCreated: tasks.length > 0, + contentGenerated: content.length > 0, + imagesGenerated: generatedImages > 0, + readyToPublish: published > 0 + }, + productivity: { + contentThisWeek, + contentThisMonth, + avgGenerationTime: 0, // Would need task timestamps to calculate + publishRate + } + }); + + setLastUpdated(new Date()); + } catch (error) { + console.error('Error fetching dashboard data:', error); + } finally { + setLoading(false); + } + }; + + // Initial load and periodic refresh + useEffect(() => { + if (!activeSector?.id) return; + + fetchDashboardData(); + + // Refresh every 30 seconds + const interval = setInterval(fetchDashboardData, 30000); + + return () => clearInterval(interval); + }, [activeSector?.id]); + + // Chart data for tasks by status + const tasksStatusChart = useMemo(() => { + if (!stats) return null; + + const options: ApexOptions = { + chart: { + type: 'donut', + fontFamily: 'Outfit, sans-serif', + toolbar: { show: false } + }, + labels: Object.keys(stats.tasks.byStatus), + colors: ['#465FFF', '#F59E0B', '#10B981', '#EF4444', '#8B5CF6'], + legend: { + position: 'bottom', + fontFamily: 'Outfit' + }, + dataLabels: { + enabled: true, + formatter: (val: number) => `${Math.round(val)}%` + }, + plotOptions: { + pie: { + donut: { + size: '65%' + } + } + } + }; + + const series = Object.values(stats.tasks.byStatus); + + return { options, series }; + }, [stats]); + + // Chart data for content by status + const contentStatusChart = useMemo(() => { + if (!stats) return null; + + const options: ApexOptions = { + chart: { + type: 'bar', + fontFamily: 'Outfit, sans-serif', + toolbar: { show: false }, + height: 250 + }, + colors: ['#465FFF', '#F59E0B', '#10B981'], + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 5 + } + }, + dataLabels: { + enabled: true + }, + xaxis: { + categories: ['Drafts', 'In Review', 'Published'], + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + yaxis: { + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + grid: { + strokeDashArray: 4 + } + }; + + const series = [{ + name: 'Content', + data: [stats.content.drafts, stats.content.review, stats.content.published] + }]; + + return { options, series }; + }, [stats]); + + // Chart data for images by type + const imagesTypeChart = useMemo(() => { + if (!stats || Object.keys(stats.images.byType).length === 0) return null; + + const options: ApexOptions = { + chart: { + type: 'bar', + fontFamily: 'Outfit, sans-serif', + toolbar: { show: false }, + height: 250 + }, + colors: ['#10B981'], + plotOptions: { + bar: { + horizontal: true, + borderRadius: 5 + } + }, + dataLabels: { + enabled: true + }, + xaxis: { + categories: Object.keys(stats.images.byType), + labels: { + style: { + fontFamily: 'Outfit', + fontSize: '12px' + } + } + }, + yaxis: { + labels: { + style: { + fontFamily: 'Outfit' + } + } + } + }; + + const series = [{ + name: 'Images', + data: Object.values(stats.images.byType) + }]; + + return { options, series }; + }, [stats]); + + // Productivity chart (content over time - simplified) + const productivityChart = useMemo(() => { + if (!stats) return null; + + const options: ApexOptions = { + chart: { + type: 'area', + fontFamily: 'Outfit, sans-serif', + toolbar: { show: false }, + height: 200 + }, + colors: ['#465FFF'], + stroke: { + curve: 'smooth', + width: 2 + }, + fill: { + type: 'gradient', + gradient: { + opacityFrom: 0.6, + opacityTo: 0.1 + } + }, + xaxis: { + categories: ['Week', 'Month'], + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + yaxis: { + labels: { + style: { + fontFamily: 'Outfit' + } + } + }, + grid: { + strokeDashArray: 4 + }, + dataLabels: { + enabled: true + } + }; + + const series = [{ + name: 'Content Created', + data: [stats.productivity.contentThisWeek, stats.productivity.contentThisMonth] + }]; + + return { options, series }; + }, [stats]); + + if (loading && !stats) { + return ( + <> + +
+
+
+

Loading dashboard data...

+
+
+ + ); + } + + if (!stats) { + return ( + <> + +
+

No data available. Please select a sector.

+
+ + ); + } + + const workflowSteps = [ + { + number: 1, + title: "Create Tasks", + status: stats.workflow.tasksCreated ? "completed" : "pending", + count: stats.tasks.total, + path: "/writer/tasks" + }, + { + number: 2, + title: "Generate Content", + status: stats.workflow.contentGenerated ? "completed" : "pending", + count: stats.content.total, + path: "/writer/content" + }, + { + number: 3, + title: "Generate Images", + status: stats.workflow.imagesGenerated ? "completed" : "pending", + count: stats.images.generated, + path: "/writer/images" + }, + { + number: 4, + title: "Publish", + status: stats.workflow.readyToPublish ? "completed" : "pending", + count: stats.content.published, + path: "/writer/published" + }, + ]; + + const completionRate = stats.tasks.total > 0 + ? Math.round((stats.tasks.completed / stats.tasks.total) * 100) + : 0; + + const nextActions = [ + ...(stats.tasks.pending > 0 ? [{ + text: `${stats.tasks.pending} tasks pending content generation`, + action: "Generate Content", + path: "/writer/tasks" + }] : []), + ...(stats.content.drafts > 0 ? [{ + text: `${stats.content.drafts} drafts ready for review`, + action: "Review Content", + path: "/writer/content" + }] : []), + ...(stats.images.pending > 0 ? [{ + text: `${stats.images.pending} images pending generation`, + action: "Generate Images", + path: "/writer/images" + }] : []), + ...(stats.content.review > 0 ? [{ + text: `${stats.content.review} content pieces ready to publish`, + action: "Publish Content", + path: "/writer/published" + }] : []) + ]; + return ( <> -
-
- Tasks -

-

-

Queued tasks

+
+ {/* Header with last updated */} +
+
+

Writer Dashboard

+

+ Last updated: {lastUpdated.toLocaleTimeString()} +

+
+
-
- Drafts -

-

-

Draft content

+ {/* Top Status Cards */} +
+ +
+
+
+

Total Tasks

+
+

+ {stats.tasks.total.toLocaleString()} +

+ {trends.tasks !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.tasks > 0 ? : } + {Math.abs(trends.tasks)} +
+ )} +
+

+ {stats.tasks.completed} completed • {stats.tasks.pending} pending +

+
+
+ +
+
+ + + +
+
+
+

Content Pieces

+
+

+ {stats.content.total.toLocaleString()} +

+ {trends.content !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.content > 0 ? : } + {Math.abs(trends.content)} +
+ )} +
+

+ {stats.content.published} published • {stats.content.drafts} drafts +

+
+
+ +
+
+ + + +
+
+
+

Images Generated

+
+

+ {stats.images.generated.toLocaleString()} +

+ {trends.images !== 0 && ( +
0 ? 'text-success-500' : 'text-error-500'}`}> + {trends.images > 0 ? : } + {Math.abs(trends.images)} +
+ )} +
+

+ {stats.images.total} total • {stats.images.pending} pending +

+
+
+ +
+
+ + + +
+
+
+

Publish Rate

+

+ {stats.productivity.publishRate}% +

+

+ {stats.content.published} of {stats.content.total} published +

+
+
+ +
+
+
-
- Published -

-

-

Published content

+ {/* Writer Workflow Steps */} + +
+ {workflowSteps.map((step) => ( + +
+
+ {step.status === "completed" ? : step.number} +
+

{step.title}

+
+
+
+ {step.status === "completed" ? ( + <> + + Completed + + ) : ( + <> + + Pending + + )} +
+
+ {step.count !== null && ( +

+ {step.count} {step.title.includes("Tasks") ? "tasks" : step.title.includes("Content") ? "pieces" : step.title.includes("Images") ? "images" : "items"} +

+ )} + {step.status === "pending" && ( + + )} + + ))} +
+
+ +
+ {/* Productivity Metrics */} + +
+
+
+ Task Completion + {completionRate}% +
+ +

+ {stats.tasks.completed} of {stats.tasks.total} tasks completed +

+
+ +
+
+ Publish Rate + {stats.productivity.publishRate}% +
+ +

+ {stats.content.published} of {stats.content.total} content published +

+
+ +
+
+
+

This Week

+

{stats.productivity.contentThisWeek}

+
+
+

This Month

+

{stats.productivity.contentThisMonth}

+
+
+
+ +
+
+

Avg Word Count

+

+ {stats.content.avgWordCount.toLocaleString()} +

+

+ {stats.content.totalWordCount.toLocaleString()} total words +

+
+
+
+
+ + {/* Content Status Chart */} + {contentStatusChart && ( + + + + )}
+ +
+ {/* Tasks by Status */} + {tasksStatusChart && ( + + + + )} + + {/* Images by Type */} + {imagesTypeChart ? ( + + + + ) : ( + +
+
+ Generated + {stats.images.generated} +
+
+ Pending + {stats.images.pending} +
+
+ Failed + {stats.images.failed} +
+
+
+ )} +
+ + {/* Productivity Chart */} + {productivityChart && ( + + + + )} + + {/* Next Actions */} + {nextActions.length > 0 && ( + +
+ {nextActions.map((action, index) => ( +
+ {action.text} + + {action.action} + + +
+ ))} +
+
+ )}
- - -
-

- Writer Dashboard - Coming Soon -

-

- Overview of content tasks and workflow will be displayed here -

-
-
); } -