From fa47cfa7ff86b2fd18da07cc324c7fb37bb5dde6 Mon Sep 17 00:00:00 2001 From: Desktop Date: Wed, 12 Nov 2025 21:37:41 +0500 Subject: [PATCH] enhanced ui --- frontend/src/components/common/ViewToggle.tsx | 45 ++ .../dashboard/EnhancedMetricCard.tsx | 169 ++++++ .../components/dashboard/WorkflowPipeline.tsx | 150 +++++ ...anban-sample-with-ready-touse-classes.html | 546 ++++++++++++++++++ .../sample-componeents/tasks-list-sample.html | 0 frontend/src/components/tasks/KanbanBoard.tsx | 393 +++++++++++++ frontend/src/components/tasks/TaskList.tsx | 410 +++++++++++++ frontend/src/components/tasks/index.ts | 4 + .../components/ui/tooltip/EnhancedTooltip.tsx | 88 +++ frontend/src/pages/Planner/Dashboard.tsx | 295 +++++----- frontend/src/pages/Writer/Dashboard.tsx | 301 +++++----- frontend/src/pages/Writer/Tasks.tsx | 368 ++++++++---- 12 files changed, 2341 insertions(+), 428 deletions(-) create mode 100644 frontend/src/components/common/ViewToggle.tsx create mode 100644 frontend/src/components/dashboard/EnhancedMetricCard.tsx create mode 100644 frontend/src/components/dashboard/WorkflowPipeline.tsx create mode 100644 frontend/src/components/sample-componeents/kanban-sample-with-ready-touse-classes.html create mode 100644 frontend/src/components/sample-componeents/tasks-list-sample.html create mode 100644 frontend/src/components/tasks/KanbanBoard.tsx create mode 100644 frontend/src/components/tasks/TaskList.tsx create mode 100644 frontend/src/components/tasks/index.ts create mode 100644 frontend/src/components/ui/tooltip/EnhancedTooltip.tsx diff --git a/frontend/src/components/common/ViewToggle.tsx b/frontend/src/components/common/ViewToggle.tsx new file mode 100644 index 00000000..e8691337 --- /dev/null +++ b/frontend/src/components/common/ViewToggle.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { GridIcon, ListIcon, TableIcon } from "../../icons"; + +export type ViewType = "table" | "kanban" | "list"; + +interface ViewToggleProps { + currentView: ViewType; + onViewChange: (view: ViewType) => void; + className?: string; +} + +const ViewToggle: React.FC = ({ + currentView, + onViewChange, + className = "", +}) => { + const views: { type: ViewType; icon: React.ReactNode; label: string }[] = [ + { type: "table", icon: , label: "Table" }, + { type: "kanban", icon: , label: "Kanban" }, + { type: "list", icon: , label: "List" }, + ]; + + return ( +
+ {views.map((view) => ( + + ))} +
+ ); +}; + +export default ViewToggle; + diff --git a/frontend/src/components/dashboard/EnhancedMetricCard.tsx b/frontend/src/components/dashboard/EnhancedMetricCard.tsx new file mode 100644 index 00000000..df3541b7 --- /dev/null +++ b/frontend/src/components/dashboard/EnhancedMetricCard.tsx @@ -0,0 +1,169 @@ +import { ReactNode, useState } from "react"; +import { Link } from "react-router"; +import { ArrowUpIcon, ArrowDownIcon } from "../../icons"; +import { EnhancedTooltip } from "../ui/tooltip/EnhancedTooltip"; + +export interface MetricCardProps { + title: string; + value: string | number; + subtitle?: string; + trend?: number; // Positive or negative trend value + icon: ReactNode; + accentColor: "blue" | "green" | "orange" | "purple" | "red"; + href?: string; + onClick?: () => void; + tooltip?: string | ReactNode; // Enhanced tooltip content + details?: Array<{ label: string; value: string | number }>; // Breakdown for tooltip + className?: string; +} + +const accentColors = { + blue: { + bg: "bg-blue-50 dark:bg-blue-500/10", + hover: "hover:bg-blue-100 dark:hover:bg-blue-500/20", + border: "bg-brand-500", + icon: "text-brand-500", + }, + green: { + bg: "bg-green-50 dark:bg-green-500/10", + hover: "hover:bg-green-100 dark:hover:bg-green-500/20", + border: "bg-success-500", + icon: "text-success-500", + }, + orange: { + bg: "bg-amber-50 dark:bg-amber-500/10", + hover: "hover:bg-amber-100 dark:hover:bg-amber-500/20", + border: "bg-warning-500", + icon: "text-warning-500", + }, + purple: { + bg: "bg-purple-50 dark:bg-purple-500/10", + hover: "hover:bg-purple-100 dark:hover:bg-purple-500/20", + border: "bg-purple-500", + icon: "text-purple-500", + }, + red: { + bg: "bg-red-50 dark:bg-red-500/10", + hover: "hover:bg-red-100 dark:hover:bg-red-500/20", + border: "bg-error-500", + icon: "text-error-500", + }, +}; + +export default function EnhancedMetricCard({ + title, + value, + subtitle, + trend, + icon, + accentColor, + href, + onClick, + tooltip, + details, + className = "", +}: MetricCardProps) { + const [isHovered, setIsHovered] = useState(false); + const colors = accentColors[accentColor]; + + const formatValue = (val: string | number): string => { + if (typeof val === "number") { + return val.toLocaleString(); + } + return val; + }; + + const tooltipContent = tooltip || ( + details ? ( +
+ {details.map((detail, idx) => ( +
+ {detail.label}: + {detail.value} +
+ ))} +
+ ) : null + ); + + const cardContent = ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + {/* Accent Border */} +
+ +
+
+

{title}

+
+

+ {formatValue(value)} +

+ {trend !== undefined && trend !== 0 && ( +
0 ? "text-success-500" : "text-error-500" + }`} + > + {trend > 0 ? ( + + ) : ( + + )} + {Math.abs(trend)} +
+ )} +
+ {subtitle && ( +

+ {subtitle} +

+ )} +
+
+
{icon}
+
+
+ + {/* Hover Effect */} + {isHovered && tooltipContent && ( +
+ )} +
+ ); + + // Wrap with tooltip if provided + if (tooltipContent) { + const wrappedContent = href ? ( + + {cardContent} + + ) : ( + cardContent + ); + + return ( + + {wrappedContent} + + ); + } + + return href ? {cardContent} : cardContent; +} + diff --git a/frontend/src/components/dashboard/WorkflowPipeline.tsx b/frontend/src/components/dashboard/WorkflowPipeline.tsx new file mode 100644 index 00000000..fcf71765 --- /dev/null +++ b/frontend/src/components/dashboard/WorkflowPipeline.tsx @@ -0,0 +1,150 @@ +import { ReactNode } from "react"; +import { Link } from "react-router"; +import { CheckCircleIcon, TimeIcon, ArrowRightIcon } from "../../icons"; +import { Tooltip } from "../ui/tooltip"; + +export interface WorkflowStep { + number: number; + title: string; + status: "completed" | "in_progress" | "pending"; + count: number; + path: string; + description?: string; + details?: string; // Additional details for tooltip +} + +interface WorkflowPipelineProps { + steps: WorkflowStep[]; + onStepClick?: (step: WorkflowStep) => void; + showConnections?: boolean; + className?: string; +} + +export default function WorkflowPipeline({ + steps, + onStepClick, + showConnections = true, + className = "", +}: WorkflowPipelineProps) { + const getStatusColor = (status: WorkflowStep["status"]) => { + switch (status) { + case "completed": + return "bg-success-500 border-success-500 text-white"; + case "in_progress": + return "bg-warning-500 border-warning-500 text-white"; + case "pending": + return "bg-gray-200 border-gray-300 text-gray-600 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400"; + } + }; + + const getStatusIcon = (status: WorkflowStep["status"], stepNumber: number) => { + switch (status) { + case "completed": + return ; + case "in_progress": + return ; + case "pending": + return {stepNumber}; + } + }; + + const getConnectionColor = (currentStatus: WorkflowStep["status"], nextStatus?: WorkflowStep["status"]) => { + if (currentStatus === "completed" && nextStatus === "completed") { + return "bg-success-500"; + } + if (currentStatus === "completed") { + return "bg-success-500"; + } + return "bg-gray-300 dark:bg-gray-600"; + }; + + return ( +
+
+ {steps.map((step, index) => { + const isLast = index === steps.length - 1; + const nextStep = !isLast ? steps[index + 1] : null; + + return ( +
+ {/* Step Node */} +
+ + { + if (onStepClick) { + e.preventDefault(); + onStepClick(step); + } + }} + className={` + relative flex flex-col items-center justify-center + w-20 h-20 rounded-full border-2 transition-all duration-300 + ${getStatusColor(step.status)} + hover:scale-110 hover:shadow-lg + cursor-pointer group + `} + > +
+ {getStatusIcon(step.status, step.number)} +
+ {step.status === "in_progress" && ( +
+ )} + + + + {/* Step Label */} +
+

+ {step.title} +

+

+ {step.count} {step.title.toLowerCase().includes('keyword') ? 'keywords' : + step.title.toLowerCase().includes('cluster') ? 'clusters' : + step.title.toLowerCase().includes('idea') ? 'ideas' : + step.title.toLowerCase().includes('task') ? 'tasks' : 'items'} +

+ {step.status === "pending" && ( + { + if (onStepClick) { + e.preventDefault(); + onStepClick(step); + } + }} + > + Start Now + + )} +
+
+ + {/* Connection Line */} + {!isLast && showConnections && ( +
+
+ +
+
+ )} +
+ ); + })} +
+
+ ); +} + diff --git a/frontend/src/components/sample-componeents/kanban-sample-with-ready-touse-classes.html b/frontend/src/components/sample-componeents/kanban-sample-with-ready-touse-classes.html new file mode 100644 index 00000000..9763b1ee --- /dev/null +++ b/frontend/src/components/sample-componeents/kanban-sample-with-ready-touse-classes.html @@ -0,0 +1,546 @@ +
+ +
+
+
+ + + + + + + +
+ +
+ + + +
+
+
+ + + +
+ +
+
+

+ To Do + + 3 + +

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

+ Finish user onboarding +

+ +
+ + + + + Tomorrow + + + + + + + + 1 + +
+
+ +
+ user +
+
+
+ + +
+
+
+

+ Solve the Dribbble prioritisation issue with the team +

+ +
+ + + + + Jan 8, 2027 + + + + + + + + 2 + + + + + + + + 1 + +
+ + + Marketing + +
+ +
+ user +
+
+
+ + +
+
+
+

+ Change license and remove products +

+ +
+ + + + + Jan 8, 2027 + +
+ + + Dev + +
+ +
+ user +
+
+
+
+ + +
+
+

+ In Progress + + 5 + +

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

+ Work In Progress (WIP) Dashboard +

+ +
+ + + + + Today + + + + + + + + 1 + +
+
+ +
+ user +
+
+
+ + +
+
+
+

+ Kanban Flow Manager +

+ +
+ + + + + Feb 12, 2027 + + + + + + + + 8 + + + + + + + + 2 + +
+ + + Template + +
+ +
+ user +
+
+
+ + +
+
+

+ Product Update - Q4 2024 +

+ +

+ Dedicated form for a category of users that will perform + actions. +

+ +
+ task +
+ +
+
+ + + + + Feb 12, 2027 + + + + + + + + 8 + +
+ +
+ user +
+
+
+
+ + +
+
+
+

+ Make figbot send comment when ticket is auto-moved + back to inbox +

+ +
+ + + + + Mar 08, 2027 + + + + + + + + 1 + +
+
+ +
+ user +
+
+
+
+ + +
+
+

+ Completed + + 4 + +

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

+ Manage internal feedback +

+ +
+ + + + + Tomorrow + + + + + + + + 1 + +
+
+ +
+ user +
+
+
+ + +
+
+
+

+ Do some projects on React Native with Flutter +

+ +
+ + + + + Jan 8, 2027 + +
+ + + Development + +
+ +
+ user +
+
+
+ + +
+
+
+

+ Design marketing assets +

+ +
+ + + + + Jan 8, 2027 + + + + + + + + 2 + + + + + + + + 1 + +
+ + + Marketing + +
+ +
+ user +
+
+
+ + +
+
+
+

+ Kanban Flow Manager +

+ +
+ + + + + Feb 12, 2027 + + + + + + + + 8 + +
+ + + Template + +
+ +
+ user +
+
+
+
+
+ +
\ No newline at end of file diff --git a/frontend/src/components/sample-componeents/tasks-list-sample.html b/frontend/src/components/sample-componeents/tasks-list-sample.html new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/components/tasks/KanbanBoard.tsx b/frontend/src/components/tasks/KanbanBoard.tsx new file mode 100644 index 00000000..35dfdacc --- /dev/null +++ b/frontend/src/components/tasks/KanbanBoard.tsx @@ -0,0 +1,393 @@ +import React, { useState } from "react"; +import { PlusIcon, HorizontaLDots } from "../../icons"; + +export interface Task { + id: string; + title: string; + status: "todo" | "in_progress" | "completed"; + dueDate?: string; + commentsCount?: number; + attachmentsCount?: number; + tags?: { label: string; color?: "brand" | "success" | "warning" | "orange" | "gray" }[]; + assignee?: { + avatar?: string; + name?: string; + }; + description?: string; + image?: string; +} + +interface KanbanBoardProps { + tasks: Task[]; + onTaskClick?: (task: Task) => void; + onTaskMove?: (taskId: string, newStatus: "todo" | "in_progress" | "completed") => void; + onAddTask?: () => void; + onFilterChange?: (filter: "All" | "Todo" | "InProgress" | "Completed") => void; +} + +const KanbanBoard: React.FC = ({ + tasks, + onTaskClick, + onTaskMove, + onAddTask, + onFilterChange, +}) => { + const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All"); + const [openDropdowns, setOpenDropdowns] = useState>({}); + + const filteredTasks = selectedFilter === "All" + ? tasks + : tasks.filter(task => { + if (selectedFilter === "Todo") return task.status === "todo"; + if (selectedFilter === "InProgress") return task.status === "in_progress"; + if (selectedFilter === "Completed") return task.status === "completed"; + return true; + }); + + const todoTasks = filteredTasks.filter(t => t.status === "todo"); + const inProgressTasks = filteredTasks.filter(t => t.status === "in_progress"); + const completedTasks = filteredTasks.filter(t => t.status === "completed"); + + const handleFilterChange = (filter: "All" | "Todo" | "InProgress" | "Completed") => { + setSelectedFilter(filter); + onFilterChange?.(filter); + }; + + const toggleDropdown = (id: string) => { + setOpenDropdowns(prev => ({ ...prev, [id]: !prev[id] })); + }; + + const closeDropdown = (id: string) => { + setOpenDropdowns(prev => ({ ...prev, [id]: false })); + }; + + return ( +
+ {/* Task header Start */} +
+
+
+ + + + + + + +
+ +
+ + + +
+
+
+ {/* Task header End */} + + {/* Task wrapper Start */} +
+ {/* To do list */} + + + {/* Progress list */} + + + {/* Completed list */} + +
+ {/* Task wrapper End */} +
+ ); +}; + +interface KanbanColumnProps { + title: string; + tasks: Task[]; + count: number; + status: "todo" | "in_progress" | "completed"; + onTaskClick?: (task: Task) => void; + onDropdownToggle: (id: string) => void; + onDropdownClose: (id: string) => void; + openDropdown: boolean; + borderX?: boolean; +} + +const KanbanColumn: React.FC = ({ + title, + tasks, + count, + status, + onTaskClick, + onDropdownToggle, + onDropdownClose, + openDropdown, + borderX = false, +}) => { + const getCountBadgeClass = () => { + if (status === "in_progress") { + return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400"; + } + if (status === "completed") { + return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500"; + } + return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80"; + }; + + return ( +
+
+

+ {title} + + {count} + +

+ +
+ + {openDropdown && ( + <> +
onDropdownClose(status)} + /> +
+ + + +
+ + )} +
+
+ + {tasks.map((task) => ( + onTaskClick?.(task)} /> + ))} +
+ ); +}; + +interface TaskCardProps { + task: Task; + onClick?: () => void; +} + +const TaskCard: React.FC = ({ task, onClick }) => { + const getTagClass = (color?: string) => { + switch (color) { + case "brand": + return "bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400"; + case "success": + return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500"; + case "warning": + return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400"; + case "orange": + return "bg-orange-400/10 text-orange-400"; + default: + return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80"; + } + }; + + return ( +
+
+
+ {task.image && ( +
+ task +
+ )} +

+ {task.title} +

+ {task.description && ( +

{task.description}

+ )} + +
+ {task.dueDate && ( + + + + + {task.dueDate} + + )} + + {task.commentsCount !== undefined && ( + + + + + {task.commentsCount} + + )} + + {task.attachmentsCount !== undefined && ( + + + + + {task.attachmentsCount} + + )} +
+ + {task.tags && task.tags.length > 0 && ( +
+ {task.tags.map((tag, index) => ( + + {tag.label} + + ))} +
+ )} +
+ + {task.assignee?.avatar && ( +
+ {task.assignee.name +
+ )} +
+
+ ); +}; + +export default KanbanBoard; + diff --git a/frontend/src/components/tasks/TaskList.tsx b/frontend/src/components/tasks/TaskList.tsx new file mode 100644 index 00000000..d31c3b17 --- /dev/null +++ b/frontend/src/components/tasks/TaskList.tsx @@ -0,0 +1,410 @@ +import React, { useState } from "react"; +import { PlusIcon, HorizontaLDots } from "../../icons"; +import { Task } from "./KanbanBoard"; + +interface TaskListProps { + tasks: Task[]; + onTaskClick?: (task: Task) => void; + onTaskToggle?: (taskId: string, completed: boolean) => void; + onAddTask?: () => void; + onFilterChange?: (filter: "All" | "Todo" | "InProgress" | "Completed") => void; +} + +const TaskList: React.FC = ({ + tasks, + onTaskClick, + onTaskToggle, + onAddTask, + onFilterChange, +}) => { + const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All"); + const [openDropdowns, setOpenDropdowns] = useState>({}); + const [checkedTasks, setCheckedTasks] = useState>(new Set()); + + const filteredTasks = selectedFilter === "All" + ? tasks + : tasks.filter(task => { + if (selectedFilter === "Todo") return task.status === "todo"; + if (selectedFilter === "InProgress") return task.status === "in_progress"; + if (selectedFilter === "Completed") return task.status === "completed"; + return true; + }); + + const todoTasks = filteredTasks.filter(t => t.status === "todo"); + const inProgressTasks = filteredTasks.filter(t => t.status === "in_progress"); + const completedTasks = filteredTasks.filter(t => t.status === "completed"); + + const handleFilterChange = (filter: "All" | "Todo" | "InProgress" | "Completed") => { + setSelectedFilter(filter); + onFilterChange?.(filter); + }; + + const toggleDropdown = (id: string) => { + setOpenDropdowns(prev => ({ ...prev, [id]: !prev[id] })); + }; + + const closeDropdown = (id: string) => { + setOpenDropdowns(prev => ({ ...prev, [id]: false })); + }; + + const handleCheckboxChange = (taskId: string, checked: boolean) => { + if (checked) { + setCheckedTasks(prev => new Set(prev).add(taskId)); + } else { + setCheckedTasks(prev => { + const newSet = new Set(prev); + newSet.delete(taskId); + return newSet; + }); + } + onTaskToggle?.(taskId, checked); + }; + + return ( +
+ {/* Task header Start */} +
+
+
+ + + + + + + +
+ +
+ + + +
+
+
+ {/* Task header End */} + + {/* Task wrapper Start */} +
+ {/* To do list */} + + + {/* Progress list */} + + + {/* Completed list */} + +
+ {/* Task wrapper End */} +
+ ); +}; + +interface TaskListSectionProps { + title: string; + tasks: Task[]; + count: number; + checkedTasks: Set; + onTaskClick?: (task: Task) => void; + onCheckboxChange: (taskId: string, checked: boolean) => void; + onDropdownToggle: (id: string) => void; + onDropdownClose: (id: string) => void; + openDropdown: boolean; +} + +const TaskListSection: React.FC = ({ + title, + tasks, + count, + checkedTasks, + onTaskClick, + onCheckboxChange, + onDropdownToggle, + onDropdownClose, + openDropdown, +}) => { + const getCountBadgeClass = () => { + if (title === "In Progress") { + return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400"; + } + if (title === "Completed") { + return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500"; + } + return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80"; + }; + + return ( +
+
+

+ {title} + + {count} + +

+ +
+ + {openDropdown && ( + <> +
onDropdownClose(title.toLowerCase().replace(" ", "_"))} + /> +
+ + + +
+ + )} +
+
+ + {tasks.map((task) => ( + onTaskClick?.(task)} + onCheckboxChange={(checked) => onCheckboxChange(task.id, checked)} + /> + ))} +
+ ); +}; + +interface TaskListItemProps { + task: Task; + checked: boolean; + onClick?: () => void; + onCheckboxChange: (checked: boolean) => void; +} + +const TaskListItem: React.FC = ({ task, checked, onClick, onCheckboxChange }) => { + const getTagClass = (color?: string) => { + switch (color) { + case "brand": + return "bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400"; + case "success": + return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500"; + case "warning": + return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400"; + case "orange": + return "bg-orange-400/10 text-orange-400"; + default: + return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80"; + } + }; + + return ( +
+
+
+ + + + + + + +
+ +
+ {task.tags && task.tags.length > 0 && ( + + {task.tags[0].label} + + )} + +
+
+ {task.dueDate && ( + + + + + {task.dueDate} + + )} + + {task.commentsCount !== undefined && ( + + + + + {task.commentsCount} + + )} + + {task.attachmentsCount !== undefined && ( + + + + + {task.attachmentsCount} + + )} +
+ + {task.assignee?.avatar && ( +
+ {task.assignee.name +
+ )} +
+
+
+
+ ); +}; + +export default TaskList; + diff --git a/frontend/src/components/tasks/index.ts b/frontend/src/components/tasks/index.ts new file mode 100644 index 00000000..e053a74f --- /dev/null +++ b/frontend/src/components/tasks/index.ts @@ -0,0 +1,4 @@ +export { default as KanbanBoard } from "./KanbanBoard"; +export { default as TaskList } from "./TaskList"; +export type { Task } from "./KanbanBoard"; + diff --git a/frontend/src/components/ui/tooltip/EnhancedTooltip.tsx b/frontend/src/components/ui/tooltip/EnhancedTooltip.tsx new file mode 100644 index 00000000..73faf165 --- /dev/null +++ b/frontend/src/components/ui/tooltip/EnhancedTooltip.tsx @@ -0,0 +1,88 @@ +import { ReactNode, useState, useRef, useEffect } from "react"; + +interface EnhancedTooltipProps { + children: ReactNode; + content: ReactNode | string; + placement?: "top" | "bottom" | "left" | "right"; + className?: string; + delay?: number; +} + +export const EnhancedTooltip: React.FC = ({ + children, + content, + placement = "top", + className = "", + delay = 200, +}) => { + const [isVisible, setIsVisible] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0 }); + const tooltipRef = useRef(null); + const triggerRef = useRef(null); + const timeoutRef = useRef(); + + const placementClasses = { + top: "bottom-full left-1/2 mb-2 -translate-x-1/2 before:top-full before:left-1/2 before:-translate-x-1/2 before:-mt-1 before:border-t-gray-900", + bottom: + "top-full left-1/2 mt-2 -translate-x-1/2 before:bottom-full before:left-1/2 before:-translate-x-1/2 before:-mb-1 before:border-b-gray-900", + left: "right-full top-1/2 mr-2 -translate-y-1/2 before:left-full before:top-1/2 before:-translate-y-1/2 before:-ml-1 before:border-l-gray-900", + right: + "left-full top-1/2 ml-2 -translate-y-1/2 before:right-full before:top-1/2 before:-translate-y-1/2 before:-mr-1 before:border-r-gray-900", + }; + + const handleMouseEnter = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsVisible(true); + }, delay); + }; + + const handleMouseLeave = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setIsVisible(false); + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + return ( +
+ {children} + {isVisible && ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {typeof content === "string" ? ( + {content} + ) : ( +
{content}
+ )} +
+ )} +
+ ); +}; + diff --git a/frontend/src/pages/Planner/Dashboard.tsx b/frontend/src/pages/Planner/Dashboard.tsx index 0a025737..4ff1547a 100644 --- a/frontend/src/pages/Planner/Dashboard.tsx +++ b/frontend/src/pages/Planner/Dashboard.tsx @@ -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() {
- {/* Top Status Cards */} -
- -
-
-
-

Keywords Ready

-
-

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

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

- {stats.keywords.mapped} mapped • {stats.keywords.unmapped} unmapped + {/* Hero Section - Key Metric */} +

+
+
+

Planning Progress

+

+ {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 + + )} +

+

+ {stats.keywords.total} keywords • {stats.clusters.total} clusters • {stats.ideas.total} ideas

-
- -
-
- - - -
-
-
-

Clusters Built

-
-

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

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

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

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

Ideas Generated

-
-

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

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

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

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

Mapping Progress

-

- {keywordMappingPct}% -

-

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

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

{step.title}

+
+
+
{keywordMappingPct}%
+
Mapped
-
-
- {step.status === "completed" ? ( - <> - - Completed - - ) : ( - <> - - Pending - - )} -
+
+
{clustersIdeasPct}%
+
With Ideas
- {step.count !== null && ( -

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

- )} - {step.status === "pending" && ( - - )} - - ))} +
+
{ideasQueuedPct}%
+
Queued
+
+
+
+ + {/* Enhanced Metric Cards */} +
+ } + 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 }, + ]} + /> + + } + 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 }, + ]} + /> + + } + 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 }, + ]} + /> + + } + 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 }, + ]} + /> +
+ + {/* Interactive Workflow Pipeline */} + + ({ + 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} + />
diff --git a/frontend/src/pages/Writer/Dashboard.tsx b/frontend/src/pages/Writer/Dashboard.tsx index 14c6801f..553c8c60 100644 --- a/frontend/src/pages/Writer/Dashboard.tsx +++ b/frontend/src/pages/Writer/Dashboard.tsx @@ -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() {
- {/* Top Status Cards */} -
- -
-
-
-

Total Tasks

-
-

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

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

Content Creation Progress

+

+ {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 + + )} +

+

+ {stats.tasks.total} tasks • {stats.content.total} content pieces • {stats.images.generated} images generated +

+
+
+
+
{completionRate}%
+
Complete
+
+
+
{stats.productivity.publishRate}%
+
Published
+
+
+
+ {stats.images.generated > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
-

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

-
-
- +
Images
- - - -
-
-
-

Content Pieces

-
-

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

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

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

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

Images Generated

-
-

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

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

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

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

Publish Rate

-

- {stats.productivity.publishRate}% -

-

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

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

{step.title}

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

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

- )} - {step.status === "pending" && ( - - )} - - ))} -
+ {/* Enhanced Metric Cards */} +
+ } + 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 }, + ]} + /> + + } + 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() }, + ]} + /> + + } + 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 }, + ]} + /> + + } + 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 }, + ]} + /> +
+ + {/* Interactive Workflow Pipeline */} + + ({ + 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} + />
diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 6bf03530..19404f6b 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -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('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 = { + '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 = { + '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 ( <> - } - 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={} - 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' && ( +
+
+

+ + Tasks +

+

+ Manage content generation queue and tasks +

+
+ +
+ )} + + {/* Table View */} + {currentView === 'table' && ( +
+ +
+ } + 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={} - 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={} + 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={} + 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' && ( + { + resetForm(); + setIsModalOpen(true); + }} + onFilterChange={(filter) => { + // Map filter to status filter + const statusMap: Record = { + 'All': '', + 'Todo': 'queued', + 'InProgress': 'in_progress', + 'Completed': 'completed', + }; + setStatusFilter(statusMap[filter] || ''); + setCurrentPage(1); + loadTasks(); + }} + /> + )} + + {/* List View */} + {currentView === 'list' && ( + { + 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 = { + 'All': '', + 'Todo': 'queued', + 'InProgress': 'in_progress', + 'Completed': 'completed', + }; + setStatusFilter(statusMap[filter] || ''); + setCurrentPage(1); + loadTasks(); + }} + /> + )} {/* Progress Modal for AI Functions */}