856 lines
30 KiB
TypeScript
856 lines
30 KiB
TypeScript
import { useEffect, useState, useMemo, lazy, Suspense } 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 { ApexOptions } from "apexcharts";
|
|
import WorkflowPipeline, { WorkflowStep } from "../../components/dashboard/WorkflowPipeline";
|
|
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
|
|
|
|
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
|
|
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";
|
|
import PageHeader from "../../components/common/PageHeader";
|
|
|
|
interface WriterStats {
|
|
tasks: {
|
|
total: number;
|
|
byStatus: Record<string, number>;
|
|
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<string, number>;
|
|
};
|
|
images: {
|
|
total: number;
|
|
generated: number;
|
|
pending: number;
|
|
failed: number;
|
|
byType: Record<string, number>;
|
|
};
|
|
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<WriterStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [lastUpdated, setLastUpdated] = useState<Date>(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<string, number> = {};
|
|
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<string, number> = {};
|
|
const contentByType: Record<string, number> = {};
|
|
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<string, number> = {};
|
|
const imagesByType: Record<string, number> = {};
|
|
|
|
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(() => {
|
|
// Allow loading for all sectors (when activeSector is null) or specific sector
|
|
fetchDashboardData();
|
|
|
|
// Refresh every 30 seconds
|
|
const interval = setInterval(fetchDashboardData, 30000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [activeSector?.id, activeSite?.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).filter(key => stats.tasks.byStatus[key] > 0),
|
|
colors: ['#465FFF', '#F59E0B', '#10B981', '#EF4444', '#8B5CF6'],
|
|
legend: {
|
|
position: 'bottom',
|
|
fontFamily: 'Outfit',
|
|
show: true
|
|
},
|
|
dataLabels: {
|
|
enabled: false // Disable labels on pie slices
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
y: {
|
|
formatter: (val: number, { seriesIndex, w }: any) => {
|
|
const label = w.globals.labels[seriesIndex] || '';
|
|
return `${label}: ${val}`;
|
|
}
|
|
}
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '70%',
|
|
labels: {
|
|
show: true,
|
|
name: {
|
|
show: false // Hide "Total" label
|
|
},
|
|
value: {
|
|
show: true,
|
|
fontSize: '24px',
|
|
fontWeight: 700,
|
|
color: '#465FFF',
|
|
fontFamily: 'Outfit',
|
|
formatter: () => {
|
|
const total = Object.values(stats.tasks.byStatus).reduce((a, b) => a + b, 0);
|
|
return total > 0 ? total.toString() : '0';
|
|
}
|
|
},
|
|
total: {
|
|
show: false // Hide total label
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const series = Object.keys(stats.tasks.byStatus)
|
|
.filter(key => stats.tasks.byStatus[key] > 0)
|
|
.map(key => stats.tasks.byStatus[key]);
|
|
|
|
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 (
|
|
<>
|
|
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-brand-500 border-t-transparent"></div>
|
|
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (!stats && !loading) {
|
|
return (
|
|
<>
|
|
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
{activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
|
|
</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (!stats) {
|
|
return null; // Still loading
|
|
}
|
|
|
|
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 (
|
|
<>
|
|
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
|
|
|
|
<div className="space-y-5 sm:space-y-6">
|
|
{/* Header with site/sector info and controls */}
|
|
<PageHeader
|
|
title="Writer Dashboard"
|
|
lastUpdated={lastUpdated}
|
|
showRefresh={true}
|
|
onRefresh={fetchDashboardData}
|
|
badge={{ icon: <PencilIcon />, color: 'blue' }}
|
|
/>
|
|
|
|
{/* Hero Section - Key Metric */}
|
|
<div className="rounded-2xl border border-gray-200 bg-gradient-to-br from-brand-50 to-white dark:from-brand-500/10 dark:to-gray-800/50 dark:border-gray-800 p-6 md:p-8">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Content Creation Progress</p>
|
|
<h3 className="mt-2 text-3xl font-bold text-gray-800 dark:text-white/90">
|
|
{stats.content.published > 0 ? (
|
|
<>
|
|
{stats.content.published} Content Pieces Published
|
|
</>
|
|
) : stats.content.review > 0 ? (
|
|
<>
|
|
{stats.content.review} Pieces Ready to Publish
|
|
</>
|
|
) : stats.content.drafts > 0 ? (
|
|
<>
|
|
{stats.content.drafts} Drafts Ready for Review
|
|
</>
|
|
) : stats.tasks.total > 0 ? (
|
|
<>
|
|
{stats.tasks.total} Tasks Created
|
|
</>
|
|
) : (
|
|
<>
|
|
Ready to Create Content
|
|
</>
|
|
)}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
{stats.tasks.total} tasks • {stats.content.total} content pieces • {stats.images.generated} images generated
|
|
</p>
|
|
</div>
|
|
<div className="hidden md:flex items-center gap-4">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-brand-500">{completionRate}%</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">Complete</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-success-500">{stats.productivity.publishRate}%</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">Published</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-warning-500">
|
|
{stats.images.generated > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">Images</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Enhanced Metric Cards */}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
|
|
<EnhancedMetricCard
|
|
title="Total Tasks"
|
|
value={stats.tasks.total}
|
|
subtitle={`${stats.tasks.completed} completed • ${stats.tasks.pending} pending`}
|
|
trend={trends.tasks}
|
|
icon={<FileTextIcon className="size-6" />}
|
|
accentColor="blue"
|
|
href="/writer/tasks"
|
|
details={[
|
|
{ label: "Total Tasks", value: stats.tasks.total },
|
|
{ label: "Completed", value: stats.tasks.completed },
|
|
{ label: "Pending", value: stats.tasks.pending },
|
|
{ label: "In Progress", value: stats.tasks.inProgress },
|
|
{ label: "Avg Word Count", value: stats.tasks.avgWordCount },
|
|
]}
|
|
/>
|
|
|
|
<EnhancedMetricCard
|
|
title="Content Pieces"
|
|
value={stats.content.total}
|
|
subtitle={`${stats.content.published} published • ${stats.content.drafts} drafts`}
|
|
trend={trends.content}
|
|
icon={<PencilIcon className="size-6" />}
|
|
accentColor="green"
|
|
href="/writer/content"
|
|
details={[
|
|
{ label: "Total Content", value: stats.content.total },
|
|
{ label: "Published", value: stats.content.published },
|
|
{ label: "In Review", value: stats.content.review },
|
|
{ label: "Drafts", value: stats.content.drafts },
|
|
{ label: "Avg Word Count", value: stats.content.avgWordCount.toLocaleString() },
|
|
]}
|
|
/>
|
|
|
|
<EnhancedMetricCard
|
|
title="Images Generated"
|
|
value={stats.images.generated}
|
|
subtitle={`${stats.images.total} total • ${stats.images.pending} pending`}
|
|
trend={trends.images}
|
|
icon={<BoxIcon className="size-6" />}
|
|
accentColor="orange"
|
|
href="/writer/images"
|
|
details={[
|
|
{ label: "Generated", value: stats.images.generated },
|
|
{ label: "Total Images", value: stats.images.total },
|
|
{ label: "Pending", value: stats.images.pending },
|
|
{ label: "Failed", value: stats.images.failed },
|
|
]}
|
|
/>
|
|
|
|
<EnhancedMetricCard
|
|
title="Publish Rate"
|
|
value={`${stats.productivity.publishRate}%`}
|
|
subtitle={`${stats.content.published} of ${stats.content.total} published`}
|
|
icon={<BoltIcon className="size-6" />}
|
|
accentColor="purple"
|
|
href="/writer/published"
|
|
details={[
|
|
{ label: "Publish Rate", value: `${stats.productivity.publishRate}%` },
|
|
{ label: "Published", value: stats.content.published },
|
|
{ label: "Total Content", value: stats.content.total },
|
|
{ label: "This Week", value: stats.productivity.contentThisWeek },
|
|
{ label: "This Month", value: stats.productivity.contentThisMonth },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{/* Interactive Workflow Pipeline */}
|
|
<ComponentCard title="Writer Workflow Pipeline" desc="Track your content creation progress through each stage">
|
|
<WorkflowPipeline
|
|
steps={workflowSteps.map(step => ({
|
|
number: step.number,
|
|
title: step.title,
|
|
status: step.status === "completed" ? "completed" : step.status === "in_progress" ? "in_progress" : "pending",
|
|
count: step.count || 0,
|
|
path: step.path,
|
|
description: step.title,
|
|
details: step.status === "completed"
|
|
? `✓ ${step.title} completed with ${step.count} items`
|
|
: step.status === "pending"
|
|
? `→ ${step.title} pending - ${step.count} items ready`
|
|
: `⟳ ${step.title} in progress`,
|
|
}))}
|
|
onStepClick={(step) => {
|
|
navigate(step.path);
|
|
}}
|
|
showConnections={true}
|
|
/>
|
|
</ComponentCard>
|
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Productivity Metrics */}
|
|
<ComponentCard title="Productivity Metrics" desc="Content creation performance" className="lg:col-span-1">
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Task Completion</span>
|
|
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{completionRate}%</span>
|
|
</div>
|
|
<ProgressBar value={completionRate} color="primary" size="md" />
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{stats.tasks.completed} of {stats.tasks.total} tasks completed
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Publish Rate</span>
|
|
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{stats.productivity.publishRate}%</span>
|
|
</div>
|
|
<ProgressBar value={stats.productivity.publishRate} color="success" size="md" />
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{stats.content.published} of {stats.content.total} content published
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">This Week</p>
|
|
<p className="text-lg font-bold text-gray-800 dark:text-white/90">{stats.productivity.contentThisWeek}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">This Month</p>
|
|
<p className="text-lg font-bold text-gray-800 dark:text-white/90">{stats.productivity.contentThisMonth}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Avg Word Count</p>
|
|
<p className="text-lg font-bold text-gray-800 dark:text-white/90">
|
|
{stats.content.avgWordCount.toLocaleString()}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{stats.content.totalWordCount.toLocaleString()} total words
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ComponentCard>
|
|
|
|
{/* Content Status Chart */}
|
|
{contentStatusChart && (
|
|
<ComponentCard title="Content by Status" desc="Distribution across workflow stages" className="lg:col-span-2">
|
|
<Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
|
|
<Chart
|
|
options={contentStatusChart.options}
|
|
series={contentStatusChart.series}
|
|
type="bar"
|
|
height={300}
|
|
/>
|
|
</Suspense>
|
|
</ComponentCard>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
{/* Tasks by Status */}
|
|
{tasksStatusChart && (
|
|
<ComponentCard title="Tasks by Status" desc="Task distribution across statuses">
|
|
<Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
|
|
<Chart
|
|
options={tasksStatusChart.options}
|
|
series={tasksStatusChart.series}
|
|
type="donut"
|
|
height={300}
|
|
/>
|
|
</Suspense>
|
|
</ComponentCard>
|
|
)}
|
|
|
|
{/* Images by Type */}
|
|
{imagesTypeChart ? (
|
|
<ComponentCard title="Images by Type" desc="Image generation breakdown">
|
|
<Suspense fallback={<div className="flex items-center justify-center h-[300px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
|
|
<Chart
|
|
options={imagesTypeChart.options}
|
|
series={imagesTypeChart.series}
|
|
type="bar"
|
|
height={300}
|
|
/>
|
|
</Suspense>
|
|
</ComponentCard>
|
|
) : (
|
|
<ComponentCard title="Images Overview" desc="Image generation status">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Generated</span>
|
|
<span className="text-lg font-bold text-success-500">{stats.images.generated}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Pending</span>
|
|
<span className="text-lg font-bold text-warning-500">{stats.images.pending}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-900/50">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Failed</span>
|
|
<span className="text-lg font-bold text-error-500">{stats.images.failed}</span>
|
|
</div>
|
|
</div>
|
|
</ComponentCard>
|
|
)}
|
|
</div>
|
|
|
|
{/* Productivity Chart */}
|
|
{productivityChart && (
|
|
<ComponentCard title="Content Creation Trend" desc="Content created this week and month">
|
|
<Suspense fallback={<div className="flex items-center justify-center h-[200px]"><div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-500 border-t-transparent"></div></div>}>
|
|
<Chart
|
|
options={productivityChart.options}
|
|
series={productivityChart.series}
|
|
type="area"
|
|
height={200}
|
|
/>
|
|
</Suspense>
|
|
</ComponentCard>
|
|
)}
|
|
|
|
{/* Next Actions */}
|
|
{nextActions.length > 0 && (
|
|
<ComponentCard title="Next Actions" desc="Actionable items requiring attention">
|
|
<div className="space-y-3">
|
|
{nextActions.map((action, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-gray-50 to-white dark:from-gray-900/50 dark:to-gray-800/50 border border-gray-200 dark:border-gray-800 hover:border-brand-300 dark:hover:border-brand-500/30 transition-all group"
|
|
>
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{action.text}</span>
|
|
<Link
|
|
to={action.path}
|
|
className="inline-flex items-center gap-2 text-sm font-medium text-brand-500 hover:text-brand-600 group-hover:translate-x-1 transition-transform"
|
|
>
|
|
{action.action}
|
|
<ArrowRightIcon className="size-4" />
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ComponentCard>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|