fixes and more ui
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { PlusIcon, HorizontaLDots } from "../../icons";
|
import { PlusIcon, HorizontaLDots } from "../../icons";
|
||||||
|
import RelationshipMap from "./RelationshipMap";
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,6 +16,13 @@ export interface Task {
|
|||||||
};
|
};
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
// Relationship data
|
||||||
|
clusterId?: number | null;
|
||||||
|
clusterName?: string | null;
|
||||||
|
ideaId?: number | null;
|
||||||
|
ideaTitle?: string | null;
|
||||||
|
keywordIds?: number[];
|
||||||
|
keywordNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KanbanBoardProps {
|
interface KanbanBoardProps {
|
||||||
@@ -34,6 +42,8 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All");
|
const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All");
|
||||||
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
|
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
|
||||||
|
const [draggedTask, setDraggedTask] = useState<Task | null>(null);
|
||||||
|
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredTasks = selectedFilter === "All"
|
const filteredTasks = selectedFilter === "All"
|
||||||
? tasks
|
? tasks
|
||||||
@@ -61,6 +71,32 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
setOpenDropdowns(prev => ({ ...prev, [id]: false }));
|
setOpenDropdowns(prev => ({ ...prev, [id]: false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent, task: Task) => {
|
||||||
|
setDraggedTask(task);
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
setDragOverColumn(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setDragOverColumn(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent, targetStatus: "todo" | "in_progress" | "completed") => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragOverColumn(null);
|
||||||
|
|
||||||
|
if (draggedTask && draggedTask.status !== targetStatus) {
|
||||||
|
onTaskMove?.(draggedTask.id, targetStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraggedTask(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||||
{/* Task header Start */}
|
{/* Task header Start */}
|
||||||
@@ -180,6 +216,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
onDropdownToggle={toggleDropdown}
|
onDropdownToggle={toggleDropdown}
|
||||||
onDropdownClose={closeDropdown}
|
onDropdownClose={closeDropdown}
|
||||||
openDropdown={openDropdowns["todo"] || false}
|
openDropdown={openDropdowns["todo"] || false}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === "todo"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Progress list */}
|
{/* Progress list */}
|
||||||
@@ -193,6 +234,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
onDropdownClose={closeDropdown}
|
onDropdownClose={closeDropdown}
|
||||||
openDropdown={openDropdowns["in_progress"] || false}
|
openDropdown={openDropdowns["in_progress"] || false}
|
||||||
borderX
|
borderX
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === "in_progress"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Completed list */}
|
{/* Completed list */}
|
||||||
@@ -205,6 +251,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
onDropdownToggle={toggleDropdown}
|
onDropdownToggle={toggleDropdown}
|
||||||
onDropdownClose={closeDropdown}
|
onDropdownClose={closeDropdown}
|
||||||
openDropdown={openDropdowns["completed"] || false}
|
openDropdown={openDropdowns["completed"] || false}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === "completed"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Task wrapper End */}
|
{/* Task wrapper End */}
|
||||||
@@ -222,6 +273,11 @@ interface KanbanColumnProps {
|
|||||||
onDropdownClose: (id: string) => void;
|
onDropdownClose: (id: string) => void;
|
||||||
openDropdown: boolean;
|
openDropdown: boolean;
|
||||||
borderX?: boolean;
|
borderX?: boolean;
|
||||||
|
onDragStart: (e: React.DragEvent, task: Task) => void;
|
||||||
|
onDragOver: (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => void;
|
||||||
|
onDragLeave: () => void;
|
||||||
|
onDrop: (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => void;
|
||||||
|
isDragOver?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
||||||
@@ -234,6 +290,11 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
onDropdownClose,
|
onDropdownClose,
|
||||||
openDropdown,
|
openDropdown,
|
||||||
borderX = false,
|
borderX = false,
|
||||||
|
onDragStart,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
onDrop,
|
||||||
|
isDragOver = false,
|
||||||
}) => {
|
}) => {
|
||||||
const getCountBadgeClass = () => {
|
const getCountBadgeClass = () => {
|
||||||
if (status === "in_progress") {
|
if (status === "in_progress") {
|
||||||
@@ -246,7 +307,14 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`swim-lane flex flex-col gap-5 p-4 xl:p-6 ${borderX ? "border-x border-gray-200 dark:border-gray-800" : ""}`}>
|
<div
|
||||||
|
className={`swim-lane flex flex-col gap-5 p-4 xl:p-6 ${borderX ? "border-x border-gray-200 dark:border-gray-800" : ""} ${
|
||||||
|
isDragOver ? "bg-brand-50/50 dark:bg-brand-500/10 border-brand-300 dark:border-brand-500/30" : ""
|
||||||
|
} transition-colors`}
|
||||||
|
onDragOver={(e) => onDragOver(e, status)}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={(e) => onDrop(e, status)}
|
||||||
|
>
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<h3 className="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
|
<h3 className="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
|
||||||
{title}
|
{title}
|
||||||
@@ -285,7 +353,12 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<TaskCard key={task.id} task={task} onClick={() => onTaskClick?.(task)} />
|
<TaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
onClick={() => onTaskClick?.(task)}
|
||||||
|
onDragStart={(e) => onDragStart(e, task)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -294,9 +367,10 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
|
const TaskCard: React.FC<TaskCardProps> = ({ task, onClick, onDragStart }) => {
|
||||||
const getTagClass = (color?: string) => {
|
const getTagClass = (color?: string) => {
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case "brand":
|
case "brand":
|
||||||
@@ -315,8 +389,9 @@ const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable
|
draggable
|
||||||
|
onDragStart={onDragStart}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5 cursor-pointer hover:shadow-md transition-shadow"
|
className="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5 cursor-move hover:shadow-md transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-6">
|
<div className="flex items-start justify-between gap-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -377,6 +452,24 @@ const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Relationship Mapping */}
|
||||||
|
{(task.clusterId || task.ideaId || (task.keywordNames && task.keywordNames.length > 0)) && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<RelationshipMap
|
||||||
|
task={{
|
||||||
|
taskId: Number(task.id),
|
||||||
|
taskTitle: task.title,
|
||||||
|
clusterId: task.clusterId,
|
||||||
|
clusterName: task.clusterName,
|
||||||
|
ideaId: task.ideaId,
|
||||||
|
ideaTitle: task.ideaTitle,
|
||||||
|
keywordIds: task.keywordIds,
|
||||||
|
keywordNames: task.keywordNames,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{task.assignee?.avatar && (
|
{task.assignee?.avatar && (
|
||||||
|
|||||||
131
frontend/src/components/tasks/RelationshipMap.tsx
Normal file
131
frontend/src/components/tasks/RelationshipMap.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ArrowRightIcon } from "../../icons";
|
||||||
|
|
||||||
|
export interface RelationshipData {
|
||||||
|
taskId: number;
|
||||||
|
taskTitle: string;
|
||||||
|
clusterId?: number | null;
|
||||||
|
clusterName?: string | null;
|
||||||
|
ideaId?: number | null;
|
||||||
|
ideaTitle?: string | null;
|
||||||
|
keywordIds?: number[];
|
||||||
|
keywordNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationshipMapProps {
|
||||||
|
task: RelationshipData;
|
||||||
|
onNavigate?: (type: "cluster" | "idea" | "keyword", id: number) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelationshipMap: React.FC<RelationshipMapProps> = ({
|
||||||
|
task,
|
||||||
|
onNavigate,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
const hasRelationships = task.clusterId || task.ideaId || (task.keywordIds && task.keywordIds.length > 0);
|
||||||
|
|
||||||
|
if (!hasRelationships) {
|
||||||
|
return (
|
||||||
|
<div className={`text-xs text-gray-500 dark:text-gray-400 ${className}`}>
|
||||||
|
No relationships mapped
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-2 ${className}`}>
|
||||||
|
{/* Keywords → Cluster → Idea → Task Flow */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
{/* Keywords */}
|
||||||
|
{task.keywordIds && task.keywordIds.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => onNavigate?.("keyword", task.keywordIds![idx])}
|
||||||
|
className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
{keyword}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{task.keywordIds.length > 3 && (
|
||||||
|
<span className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||||
|
+{task.keywordIds.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{task.clusterId && <ArrowRightIcon className="size-3 text-gray-400" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cluster */}
|
||||||
|
{task.clusterId && (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/planner/clusters?highlight=${task.clusterId}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (onNavigate) {
|
||||||
|
e.preventDefault();
|
||||||
|
onNavigate("cluster", task.clusterId!);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-2 py-0.5 rounded bg-brand-50 dark:bg-brand-500/15 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-500/25 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
{task.clusterName || `Cluster #${task.clusterId}`}
|
||||||
|
</Link>
|
||||||
|
{task.ideaId && <ArrowRightIcon className="size-3 text-gray-400" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Idea */}
|
||||||
|
{task.ideaId && (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/planner/ideas?highlight=${task.ideaId}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (onNavigate) {
|
||||||
|
e.preventDefault();
|
||||||
|
onNavigate("idea", task.ideaId!);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-2 py-0.5 rounded bg-success-50 dark:bg-success-500/15 text-success-600 dark:text-success-400 hover:bg-success-100 dark:hover:bg-success-500/25 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
{task.ideaTitle || `Idea #${task.ideaId}`}
|
||||||
|
</Link>
|
||||||
|
<ArrowRightIcon className="size-3 text-gray-400" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Task (current) */}
|
||||||
|
<span className="px-2 py-0.5 rounded bg-warning-50 dark:bg-warning-500/15 text-warning-600 dark:text-warning-400 font-medium">
|
||||||
|
Task
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Relationship Summary */}
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 pt-1 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
{task.clusterId && (
|
||||||
|
<span>
|
||||||
|
Cluster: <span className="font-medium text-gray-700 dark:text-gray-300">{task.clusterName || `#${task.clusterId}`}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.ideaId && (
|
||||||
|
<span>
|
||||||
|
Idea: <span className="font-medium text-gray-700 dark:text-gray-300">{task.ideaTitle || `#${task.ideaId}`}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.keywordIds && task.keywordIds.length > 0 && (
|
||||||
|
<span>
|
||||||
|
Keywords: <span className="font-medium text-gray-700 dark:text-gray-300">{task.keywordIds.length}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelationshipMap;
|
||||||
|
|
||||||
@@ -31,8 +31,12 @@ import { useSectorStore } from '../../store/sectorStore';
|
|||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
import ViewToggle, { ViewType } from '../../components/common/ViewToggle';
|
import ViewToggle, { ViewType } from '../../components/common/ViewToggle';
|
||||||
import { KanbanBoard, TaskList, Task as KanbanTask } from '../../components/tasks';
|
import { KanbanBoard, TaskList, Task as KanbanTask } from '../../components/tasks';
|
||||||
|
import WorkflowPipeline, { WorkflowStep } from '../../components/dashboard/WorkflowPipeline';
|
||||||
|
import ComponentCard from '../../components/common/ComponentCard';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
export default function Tasks() {
|
export default function Tasks() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { activeSector } = useSectorStore();
|
const { activeSector } = useSectorStore();
|
||||||
const { pageSize } = usePageSizeStore();
|
const { pageSize } = usePageSizeStore();
|
||||||
@@ -210,6 +214,9 @@ export default function Tasks() {
|
|||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
}) : undefined;
|
}) : undefined;
|
||||||
|
|
||||||
|
// Parse keywords if available (comma-separated string)
|
||||||
|
const keywordNames = task.keywords ? task.keywords.split(',').map(k => k.trim()).filter(k => k) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(task.id),
|
id: String(task.id),
|
||||||
title: task.title || 'Untitled Task',
|
title: task.title || 'Untitled Task',
|
||||||
@@ -220,6 +227,13 @@ export default function Tasks() {
|
|||||||
tags: task.cluster_name ? [{ label: task.cluster_name, color: 'brand' as const }] : undefined,
|
tags: task.cluster_name ? [{ label: task.cluster_name, color: 'brand' as const }] : undefined,
|
||||||
description: task.description || undefined,
|
description: task.description || undefined,
|
||||||
assignee: undefined, // Add if you have assignee data
|
assignee: undefined, // Add if you have assignee data
|
||||||
|
// Relationship data
|
||||||
|
clusterId: task.cluster_id || null,
|
||||||
|
clusterName: task.cluster_name || null,
|
||||||
|
ideaId: task.idea_id || null,
|
||||||
|
ideaTitle: task.idea_title || null,
|
||||||
|
keywordIds: undefined, // API doesn't return keyword IDs directly, would need to fetch
|
||||||
|
keywordNames: keywordNames,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -626,8 +640,62 @@ export default function Tasks() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate workflow steps for Tasks page
|
||||||
|
const todoCount = tasks.filter(t => t.status === 'queued' || t.status === 'draft').length;
|
||||||
|
const inProgressCount = tasks.filter(t => t.status === 'in_progress' || t.status === 'generating' || t.status === 'review').length;
|
||||||
|
const completedCount = tasks.filter(t => t.status === 'completed' || t.status === 'published').length;
|
||||||
|
const contentCount = tasks.filter(t => t.content && t.content.length > 0).length;
|
||||||
|
|
||||||
|
const workflowSteps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
title: "Queue Tasks",
|
||||||
|
status: todoCount > 0 ? "completed" : "pending",
|
||||||
|
count: todoCount,
|
||||||
|
path: "/writer/tasks",
|
||||||
|
description: "Tasks queued for content generation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 2,
|
||||||
|
title: "Generate Content",
|
||||||
|
status: inProgressCount > 0 ? "in_progress" : contentCount > 0 ? "completed" : "pending",
|
||||||
|
count: contentCount,
|
||||||
|
path: "/writer/tasks",
|
||||||
|
description: "AI content generation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 3,
|
||||||
|
title: "Review & Edit",
|
||||||
|
status: tasks.filter(t => t.status === 'review').length > 0 ? "in_progress" : "pending",
|
||||||
|
count: tasks.filter(t => t.status === 'review').length,
|
||||||
|
path: "/writer/content",
|
||||||
|
description: "Content review and editing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 4,
|
||||||
|
title: "Publish",
|
||||||
|
status: completedCount > 0 ? "completed" : "pending",
|
||||||
|
count: completedCount,
|
||||||
|
path: "/writer/content",
|
||||||
|
description: "Published content",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Workflow Pipeline - Show for all views */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<ComponentCard title="Content Creation Workflow" desc="Track your content creation progress">
|
||||||
|
<WorkflowPipeline
|
||||||
|
steps={workflowSteps}
|
||||||
|
onStepClick={(step) => {
|
||||||
|
navigate(step.path);
|
||||||
|
}}
|
||||||
|
showConnections={true}
|
||||||
|
/>
|
||||||
|
</ComponentCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* View Toggle - Only show for Kanban/List views */}
|
{/* View Toggle - Only show for Kanban/List views */}
|
||||||
{currentView !== 'table' && (
|
{currentView !== 'table' && (
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
@@ -646,10 +714,11 @@ export default function Tasks() {
|
|||||||
|
|
||||||
{/* Table View */}
|
{/* Table View */}
|
||||||
{currentView === 'table' && (
|
{currentView === 'table' && (
|
||||||
<div className="mb-4 flex justify-end">
|
<>
|
||||||
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
|
<div className="mb-4 flex justify-end">
|
||||||
</div>
|
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
|
||||||
<TablePageTemplate
|
</div>
|
||||||
|
<TablePageTemplate
|
||||||
title="Tasks"
|
title="Tasks"
|
||||||
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
|
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
|
||||||
subtitle="Manage content generation queue and tasks"
|
subtitle="Manage content generation queue and tasks"
|
||||||
@@ -752,6 +821,7 @@ export default function Tasks() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Kanban View */}
|
{/* Kanban View */}
|
||||||
|
|||||||
Reference in New Issue
Block a user