enhanced ui

This commit is contained in:
Desktop
2025-11-12 21:37:41 +05:00
parent 9692a5ed2e
commit fa47cfa7ff
12 changed files with 2341 additions and 428 deletions

View File

@@ -4,6 +4,8 @@ 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 {
@@ -444,174 +446,141 @@ export default function PlannerDashboard() {
</button>
</div>
{/* Top Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<Link
to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Keywords Ready</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.keywords.total.toLocaleString()}
</h4>
{trends.keywords !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.keywords > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.keywords > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.keywords)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} mapped {stats.keywords.unmapped} unmapped
{/* 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">Planning Progress</p>
<h3 className="mt-2 text-3xl font-bold text-gray-800 dark:text-white/90">
{stats.ideas.queued > 0 ? (
<>
{stats.ideas.queued} Ideas Ready for Content Generation
</>
) : stats.ideas.total > 0 ? (
<>
{stats.ideas.total} Ideas Generated
</>
) : stats.clusters.total > 0 ? (
<>
{stats.clusters.total} Clusters Built
</>
) : (
<>
{stats.keywords.total} Keywords Ready
</>
)}
</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{stats.keywords.total} keywords {stats.clusters.total} clusters {stats.ideas.total} ideas
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<ListIcon className="text-brand-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/clusters"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Clusters Built</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.clusters.total.toLocaleString()}
</h4>
{trends.clusters !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.clusters > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.clusters > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.clusters)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.clusters.totalVolume.toLocaleString()} total volume {stats.clusters.avgKeywords} avg keywords
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<GroupIcon className="text-success-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/ideas"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Ideas Generated</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.ideas.total.toLocaleString()}
</h4>
{trends.ideas !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.ideas > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.ideas > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.ideas)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.ideas.queued} queued {stats.ideas.notQueued} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoltIcon className="text-warning-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Mapping Progress</p>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
{keywordMappingPct}%
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} of {stats.keywords.total} keywords mapped
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
<PieChartIcon className="text-purple-500 size-6" />
</div>
</div>
</Link>
</div>
{/* Planner Workflow Steps */}
<ComponentCard title="Planner Workflow Steps" desc="Track your planning progress">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{workflowSteps.map((step) => (
<Link
key={step.number}
to={step.path}
className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group"
>
<div className="flex items-center gap-3 mb-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${
step.status === "completed"
? "bg-success-500 text-white"
: step.status === "in_progress"
? "bg-warning-500 text-white"
: "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}`}>
{step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number}
</div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
<div className="hidden md:flex items-center gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-brand-500">{keywordMappingPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Mapped</div>
</div>
<div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-1.5">
{step.status === "completed" ? (
<>
<CheckCircleIcon className="size-4 text-success-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span>
</>
) : (
<>
<TimeIcon className="size-4 text-amber-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span>
</>
)}
</div>
<div className="text-center">
<div className="text-2xl font-bold text-success-500">{clustersIdeasPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">With Ideas</div>
</div>
{step.count !== null && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : step.title.includes("Ideas") ? "ideas" : "items"}
</p>
)}
{step.status === "pending" && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(step.path);
}}
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform"
>
Start Now <ArrowRightIcon className="size-3" />
</button>
)}
</Link>
))}
<div className="text-center">
<div className="text-2xl font-bold text-warning-500">{ideasQueuedPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Queued</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="Keywords Ready"
value={stats.keywords.total}
subtitle={`${stats.keywords.mapped} mapped • ${stats.keywords.unmapped} unmapped`}
trend={trends.keywords}
icon={<ListIcon className="size-6" />}
accentColor="blue"
href="/planner/keywords"
details={[
{ label: "Total Keywords", value: stats.keywords.total },
{ label: "Mapped", value: stats.keywords.mapped },
{ label: "Unmapped", value: stats.keywords.unmapped },
{ label: "Active", value: stats.keywords.byStatus.active || 0 },
{ label: "Pending", value: stats.keywords.byStatus.pending || 0 },
]}
/>
<EnhancedMetricCard
title="Clusters Built"
value={stats.clusters.total}
subtitle={`${stats.clusters.totalVolume.toLocaleString()} total volume • ${stats.clusters.avgKeywords} avg keywords`}
trend={trends.clusters}
icon={<GroupIcon className="size-6" />}
accentColor="green"
href="/planner/clusters"
details={[
{ label: "Total Clusters", value: stats.clusters.total },
{ label: "With Ideas", value: stats.clusters.withIdeas },
{ label: "Without Ideas", value: stats.clusters.withoutIdeas },
{ label: "Total Volume", value: stats.clusters.totalVolume.toLocaleString() },
{ label: "Avg Keywords", value: stats.clusters.avgKeywords },
]}
/>
<EnhancedMetricCard
title="Ideas Generated"
value={stats.ideas.total}
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} pending`}
trend={trends.ideas}
icon={<BoltIcon className="size-6" />}
accentColor="orange"
href="/planner/ideas"
details={[
{ label: "Total Ideas", value: stats.ideas.total },
{ label: "Queued", value: stats.ideas.queued },
{ label: "Not Queued", value: stats.ideas.notQueued },
{ label: "New", value: stats.ideas.byStatus.new || 0 },
{ label: "Scheduled", value: stats.ideas.byStatus.scheduled || 0 },
]}
/>
<EnhancedMetricCard
title="Mapping Progress"
value={`${keywordMappingPct}%`}
subtitle={`${stats.keywords.mapped} of ${stats.keywords.total} keywords mapped`}
icon={<PieChartIcon className="size-6" />}
accentColor="purple"
href="/planner/keywords"
details={[
{ label: "Mapping Progress", value: `${keywordMappingPct}%` },
{ label: "Mapped Keywords", value: stats.keywords.mapped },
{ label: "Total Keywords", value: stats.keywords.total },
{ label: "Unmapped", value: stats.keywords.unmapped },
]}
/>
</div>
{/* Interactive Workflow Pipeline */}
<ComponentCard title="Planner Workflow Pipeline" desc="Track your planning 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">

View File

@@ -4,6 +4,8 @@ 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 {
@@ -523,174 +525,147 @@ export default function WriterDashboard() {
</button>
</div>
{/* Top Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<Link
to="/writer/tasks"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Total Tasks</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.tasks.total.toLocaleString()}
</h4>
{trends.tasks !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.tasks > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.tasks > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.tasks)}</span>
</div>
)}
{/* 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>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.tasks.completed} completed {stats.tasks.pending} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<FileTextIcon className="text-brand-500 size-6" />
<div className="text-xs text-gray-500 dark:text-gray-400">Images</div>
</div>
</div>
</Link>
<Link
to="/writer/content"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Content Pieces</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.content.total.toLocaleString()}
</h4>
{trends.content !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.content > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.content > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.content)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.content.published} published {stats.content.drafts} drafts
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<PencilIcon className="text-success-500 size-6" />
</div>
</div>
</Link>
<Link
to="/writer/images"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Images Generated</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.images.generated.toLocaleString()}
</h4>
{trends.images !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.images > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.images > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.images)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.images.total} total {stats.images.pending} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoxIcon className="text-warning-500 size-6" />
</div>
</div>
</Link>
<Link
to="/writer/published"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Publish Rate</p>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.productivity.publishRate}%
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.content.published} of {stats.content.total} published
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
<BoltIcon className="text-purple-500 size-6" />
</div>
</div>
</Link>
</div>
</div>
{/* Writer Workflow Steps */}
<ComponentCard title="Writer Workflow Steps" desc="Track your content creation progress">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{workflowSteps.map((step) => (
<Link
key={step.number}
to={step.path}
className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group"
>
<div className="flex items-center gap-3 mb-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${
step.status === "completed"
? "bg-success-500 text-white"
: step.status === "in_progress"
? "bg-warning-500 text-white"
: "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}`}>
{step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number}
</div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
</div>
<div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-1.5">
{step.status === "completed" ? (
<>
<CheckCircleIcon className="size-4 text-success-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span>
</>
) : (
<>
<ClockIcon className="size-4 text-amber-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span>
</>
)}
</div>
</div>
{step.count !== null && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{step.count} {step.title.includes("Tasks") ? "tasks" : step.title.includes("Content") ? "pieces" : step.title.includes("Images") ? "images" : "items"}
</p>
)}
{step.status === "pending" && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(step.path);
}}
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform"
>
Start Now <ArrowRightIcon className="size-3" />
</button>
)}
</Link>
))}
</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">

View File

@@ -29,6 +29,8 @@ import { TaskIcon, PlusIcon, DownloadIcon } from '../../icons';
import { createTasksPageConfig } from '../../config/pages/tasks.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import ViewToggle, { ViewType } from '../../components/common/ViewToggle';
import { KanbanBoard, TaskList, Task as KanbanTask } from '../../components/tasks';
export default function Tasks() {
const toast = useToast();
@@ -58,6 +60,9 @@ export default function Tasks() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// View state
const [currentView, setCurrentView] = useState<ViewType>('table');
// Modal state
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
@@ -183,6 +188,43 @@ export default function Tasks() {
setCurrentPage(1);
}, [pageSize]);
// Map API Task to KanbanBoard Task
const mapTaskToKanbanTask = (task: Task): KanbanTask => {
// Map status from API to Kanban status
const statusMap: Record<string, "todo" | "in_progress" | "completed"> = {
'queued': 'todo',
'in_progress': 'in_progress',
'generating': 'in_progress',
'completed': 'completed',
'published': 'completed',
'draft': 'todo',
'review': 'in_progress',
};
const kanbanStatus = statusMap[task.status] || 'todo';
// Format due date if available (you might need to add this field to Task)
const dueDate = task.created_at ? new Date(task.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
}) : undefined;
return {
id: String(task.id),
title: task.title || 'Untitled Task',
status: kanbanStatus,
dueDate,
commentsCount: 0, // Add if you have this data
attachmentsCount: 0, // Add if you have this data
tags: task.cluster_name ? [{ label: task.cluster_name, color: 'brand' as const }] : undefined,
description: task.description || undefined,
assignee: undefined, // Add if you have assignee data
};
};
const kanbanTasks = useMemo(() => tasks.map(mapTaskToKanbanTask), [tasks]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
@@ -545,111 +587,233 @@ export default function Tasks() {
}
};
const handleTaskClick = (kanbanTask: KanbanTask) => {
const apiTask = tasks.find(t => String(t.id) === kanbanTask.id);
if (apiTask) {
setEditingTask(apiTask);
setFormData({
title: apiTask.title || '',
description: apiTask.description || '',
keywords: apiTask.keywords || '',
cluster_id: apiTask.cluster_id || null,
content_structure: apiTask.content_structure || 'blog_post',
content_type: apiTask.content_type || 'blog_post',
status: apiTask.status || 'queued',
word_count: apiTask.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}
};
const handleTaskMove = async (taskId: string, newStatus: "todo" | "in_progress" | "completed") => {
const apiTask = tasks.find(t => String(t.id) === taskId);
if (!apiTask) return;
// Map Kanban status back to API status
const statusMap: Record<string, string> = {
'todo': 'queued',
'in_progress': 'in_progress',
'completed': 'completed',
};
try {
await updateTask(apiTask.id, { status: statusMap[newStatus] || 'queued' });
toast.success('Task status updated');
loadTasks();
} catch (error: any) {
toast.error(`Failed to update task: ${error.message}`);
}
};
return (
<>
<TablePageTemplate
title="Tasks"
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
subtitle="Manage content generation queue and tasks"
columns={pageConfig.columns}
data={tasks}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
} else if (key === 'cluster_id') {
setClusterFilter(stringValue);
} else if (key === 'content_structure') {
setStructureFilter(stringValue);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
}
setCurrentPage(1);
}}
onEdit={(row) => {
setEditingTask(row);
setFormData({
title: row.title || '',
description: row.description || '',
keywords: row.keywords || '',
cluster_id: row.cluster_id || null,
content_structure: row.content_structure || 'blog_post',
content_type: row.content_type || 'blog_post',
status: row.status || 'queued',
word_count: row.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}}
onCreate={() => {
resetForm();
setIsModalOpen(true);
}}
createLabel="Add Task"
onCreateIcon={<PlusIcon />}
onDelete={async (id: number) => {
await deleteTask(id);
loadTasks();
}}
onBulkDelete={async (ids: number[]) => {
const result = await bulkDeleteTasks(ids);
// Clear selection first
setSelectedIds([]);
// Reset to page 1 if we deleted all items on current page
if (currentPage > 1 && tasks.length <= ids.length) {
{/* View Toggle - Only show for Kanban/List views */}
{currentView !== 'table' && (
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<TaskIcon className="text-brand-500 size-6" />
Tasks
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 hidden sm:block">
Manage content generation queue and tasks
</p>
</div>
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
</div>
)}
{/* Table View */}
{currentView === 'table' && (
<div className="mb-4 flex justify-end">
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
</div>
<TablePageTemplate
title="Tasks"
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
subtitle="Manage content generation queue and tasks"
columns={pageConfig.columns}
data={tasks}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
} else if (key === 'cluster_id') {
setClusterFilter(stringValue);
} else if (key === 'content_structure') {
setStructureFilter(stringValue);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
}
setCurrentPage(1);
}
// Always reload data to refresh the table
await loadTasks();
return result;
}}
onBulkExport={handleBulkExport}
onBulkUpdateStatus={handleBulkUpdateStatus}
onBulkAction={handleBulkAction}
onRowAction={handleRowAction}
getItemDisplayName={(row: Task) => row.title}
onExport={async () => {
toast.info('Export functionality coming soon');
}}
onExportIcon={<DownloadIcon />}
selectionLabel="task"
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onFilterReset={() => {
setSearchTerm('');
setStatusFilter('');
setClusterFilter('');
setStructureFilter('');
setTypeFilter('');
setCurrentPage(1);
}}
/>
}}
onEdit={(row) => {
setEditingTask(row);
setFormData({
title: row.title || '',
description: row.description || '',
keywords: row.keywords || '',
cluster_id: row.cluster_id || null,
content_structure: row.content_structure || 'blog_post',
content_type: row.content_type || 'blog_post',
status: row.status || 'queued',
word_count: row.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}}
onCreate={() => {
resetForm();
setIsModalOpen(true);
}}
createLabel="Add Task"
onCreateIcon={<PlusIcon />}
onDelete={async (id: number) => {
await deleteTask(id);
loadTasks();
}}
onBulkDelete={async (ids: number[]) => {
const result = await bulkDeleteTasks(ids);
// Clear selection first
setSelectedIds([]);
// Reset to page 1 if we deleted all items on current page
if (currentPage > 1 && tasks.length <= ids.length) {
setCurrentPage(1);
}
// Always reload data to refresh the table
await loadTasks();
return result;
}}
onBulkExport={handleBulkExport}
onBulkUpdateStatus={handleBulkUpdateStatus}
onBulkAction={handleBulkAction}
onRowAction={handleRowAction}
getItemDisplayName={(row: Task) => row.title}
onExport={async () => {
toast.info('Export functionality coming soon');
}}
onExportIcon={<DownloadIcon />}
selectionLabel="task"
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onFilterReset={() => {
setSearchTerm('');
setStatusFilter('');
setClusterFilter('');
setStructureFilter('');
setTypeFilter('');
setCurrentPage(1);
}}
/>
)}
{/* Kanban View */}
{currentView === 'kanban' && (
<KanbanBoard
tasks={kanbanTasks}
onTaskClick={handleTaskClick}
onTaskMove={handleTaskMove}
onAddTask={() => {
resetForm();
setIsModalOpen(true);
}}
onFilterChange={(filter) => {
// Map filter to status filter
const statusMap: Record<string, string> = {
'All': '',
'Todo': 'queued',
'InProgress': 'in_progress',
'Completed': 'completed',
};
setStatusFilter(statusMap[filter] || '');
setCurrentPage(1);
loadTasks();
}}
/>
)}
{/* List View */}
{currentView === 'list' && (
<TaskList
tasks={kanbanTasks}
onTaskClick={handleTaskClick}
onTaskToggle={async (taskId, completed) => {
const apiTask = tasks.find(t => String(t.id) === taskId);
if (apiTask) {
try {
await updateTask(apiTask.id, { status: completed ? 'completed' : 'queued' });
toast.success('Task updated');
loadTasks();
} catch (error: any) {
toast.error(`Failed to update task: ${error.message}`);
}
}
}}
onAddTask={() => {
resetForm();
setIsModalOpen(true);
}}
onFilterChange={(filter) => {
// Map filter to status filter
const statusMap: Record<string, string> = {
'All': '',
'Todo': 'queued',
'InProgress': 'in_progress',
'Completed': 'completed',
};
setStatusFilter(statusMap[filter] || '');
setCurrentPage(1);
loadTasks();
}}
/>
)}
{/* Progress Modal for AI Functions */}
<ProgressModal