From 408b12b60752c3e060511990e751c90ced49a171 Mon Sep 17 00:00:00 2001 From: Desktop Date: Wed, 12 Nov 2025 21:52:22 +0500 Subject: [PATCH] fixes and more ui --- frontend/src/components/tasks/KanbanBoard.tsx | 101 +++++++++++++- .../src/components/tasks/RelationshipMap.tsx | 131 ++++++++++++++++++ frontend/src/pages/Writer/Tasks.tsx | 78 ++++++++++- 3 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/tasks/RelationshipMap.tsx diff --git a/frontend/src/components/tasks/KanbanBoard.tsx b/frontend/src/components/tasks/KanbanBoard.tsx index 35dfdacc..9e61d5e9 100644 --- a/frontend/src/components/tasks/KanbanBoard.tsx +++ b/frontend/src/components/tasks/KanbanBoard.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { PlusIcon, HorizontaLDots } from "../../icons"; +import RelationshipMap from "./RelationshipMap"; export interface Task { id: string; @@ -15,6 +16,13 @@ export interface Task { }; description?: string; image?: string; + // Relationship data + clusterId?: number | null; + clusterName?: string | null; + ideaId?: number | null; + ideaTitle?: string | null; + keywordIds?: number[]; + keywordNames?: string[]; } interface KanbanBoardProps { @@ -34,6 +42,8 @@ const KanbanBoard: React.FC = ({ }) => { const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All"); const [openDropdowns, setOpenDropdowns] = useState>({}); + const [draggedTask, setDraggedTask] = useState(null); + const [dragOverColumn, setDragOverColumn] = useState(null); const filteredTasks = selectedFilter === "All" ? tasks @@ -61,6 +71,32 @@ const KanbanBoard: React.FC = ({ 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 (
{/* Task header Start */} @@ -180,6 +216,11 @@ const KanbanBoard: React.FC = ({ onDropdownToggle={toggleDropdown} onDropdownClose={closeDropdown} openDropdown={openDropdowns["todo"] || false} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + isDragOver={dragOverColumn === "todo"} /> {/* Progress list */} @@ -193,6 +234,11 @@ const KanbanBoard: React.FC = ({ onDropdownClose={closeDropdown} openDropdown={openDropdowns["in_progress"] || false} borderX + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + isDragOver={dragOverColumn === "in_progress"} /> {/* Completed list */} @@ -205,6 +251,11 @@ const KanbanBoard: React.FC = ({ onDropdownToggle={toggleDropdown} onDropdownClose={closeDropdown} openDropdown={openDropdowns["completed"] || false} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + isDragOver={dragOverColumn === "completed"} />
{/* Task wrapper End */} @@ -222,6 +273,11 @@ interface KanbanColumnProps { onDropdownClose: (id: string) => void; openDropdown: 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 = ({ @@ -234,6 +290,11 @@ const KanbanColumn: React.FC = ({ onDropdownClose, openDropdown, borderX = false, + onDragStart, + onDragOver, + onDragLeave, + onDrop, + isDragOver = false, }) => { const getCountBadgeClass = () => { if (status === "in_progress") { @@ -246,7 +307,14 @@ const KanbanColumn: React.FC = ({ }; return ( -
+
onDragOver(e, status)} + onDragLeave={onDragLeave} + onDrop={(e) => onDrop(e, status)} + >

{title} @@ -285,7 +353,12 @@ const KanbanColumn: React.FC = ({

{tasks.map((task) => ( - onTaskClick?.(task)} /> + onTaskClick?.(task)} + onDragStart={(e) => onDragStart(e, task)} + /> ))}
); @@ -294,9 +367,10 @@ const KanbanColumn: React.FC = ({ interface TaskCardProps { task: Task; onClick?: () => void; + onDragStart?: (e: React.DragEvent) => void; } -const TaskCard: React.FC = ({ task, onClick }) => { +const TaskCard: React.FC = ({ task, onClick, onDragStart }) => { const getTagClass = (color?: string) => { switch (color) { case "brand": @@ -315,8 +389,9 @@ const TaskCard: React.FC = ({ task, onClick }) => { return (
@@ -377,6 +452,24 @@ const TaskCard: React.FC = ({ task, onClick }) => { ))}
)} + + {/* Relationship Mapping */} + {(task.clusterId || task.ideaId || (task.keywordNames && task.keywordNames.length > 0)) && ( +
+ +
+ )}
{task.assignee?.avatar && ( diff --git a/frontend/src/components/tasks/RelationshipMap.tsx b/frontend/src/components/tasks/RelationshipMap.tsx new file mode 100644 index 00000000..fd3f0027 --- /dev/null +++ b/frontend/src/components/tasks/RelationshipMap.tsx @@ -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 = ({ + task, + onNavigate, + className = "", +}) => { + const hasRelationships = task.clusterId || task.ideaId || (task.keywordIds && task.keywordIds.length > 0); + + if (!hasRelationships) { + return ( +
+ No relationships mapped +
+ ); + } + + return ( +
+ {/* Keywords → Cluster → Idea → Task Flow */} +
+ {/* Keywords */} + {task.keywordIds && task.keywordIds.length > 0 && ( + <> +
+ {task.keywordNames?.slice(0, 3).map((keyword, idx) => ( + + ))} + {task.keywordIds.length > 3 && ( + + +{task.keywordIds.length - 3} + + )} +
+ {task.clusterId && } + + )} + + {/* Cluster */} + {task.clusterId && ( + <> + { + 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}`} + + {task.ideaId && } + + )} + + {/* Idea */} + {task.ideaId && ( + <> + { + 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}`} + + + + )} + + {/* Task (current) */} + + Task + +
+ + {/* Relationship Summary */} +
+ {task.clusterId && ( + + Cluster: {task.clusterName || `#${task.clusterId}`} + + )} + {task.ideaId && ( + + Idea: {task.ideaTitle || `#${task.ideaId}`} + + )} + {task.keywordIds && task.keywordIds.length > 0 && ( + + Keywords: {task.keywordIds.length} + + )} +
+
+ ); +}; + +export default RelationshipMap; + diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 19404f6b..93663525 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -31,8 +31,12 @@ 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'; +import WorkflowPipeline, { WorkflowStep } from '../../components/dashboard/WorkflowPipeline'; +import ComponentCard from '../../components/common/ComponentCard'; +import { useNavigate } from 'react-router'; export default function Tasks() { + const navigate = useNavigate(); const toast = useToast(); const { activeSector } = useSectorStore(); const { pageSize } = usePageSizeStore(); @@ -210,6 +214,9 @@ export default function Tasks() { year: 'numeric' }) : undefined; + // Parse keywords if available (comma-separated string) + const keywordNames = task.keywords ? task.keywords.split(',').map(k => k.trim()).filter(k => k) : undefined; + return { id: String(task.id), 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, description: task.description || undefined, 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 ( <> + {/* Workflow Pipeline - Show for all views */} +
+ + { + navigate(step.path); + }} + showConnections={true} + /> + +
+ {/* View Toggle - Only show for Kanban/List views */} {currentView !== 'table' && (
@@ -646,10 +714,11 @@ export default function Tasks() { {/* Table View */} {currentView === 'table' && ( -
- -
- +
+ +
+ } subtitle="Manage content generation queue and tasks" @@ -752,6 +821,7 @@ export default function Tasks() { setCurrentPage(1); }} /> + )} {/* Kanban View */}