import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { AcademicCapIcon, ExclamationTriangleIcon, ClockIcon, FolderIcon, ChevronDownIcon, ChevronRightIcon, } from '@heroicons/react/24/outline'; import { Task } from '../../entities/Task'; import { Project } from '../../entities/Project'; import TaskModal from '../Task/TaskModal'; import { fetchTaskById, updateTask, deleteTask, } from '../../utils/tasksService'; import { createProject } from '../../utils/projectsService'; import { useToast } from '../Shared/ToastContext'; import { getVagueTasks } from '../../utils/taskIntelligenceService'; interface ProductivityInsight { type: | 'stalled_projects' | 'completed_no_next' | 'tasks_are_projects' | 'vague_tasks' | 'overdue_tasks' | 'stuck_projects'; title: string; description: string; items: (Task | Project)[]; icon: React.ComponentType; color: string; } interface ProductivityAssistantProps { tasks: Task[]; projects: Project[]; } const ProductivityAssistant: React.FC = ({ tasks, projects, }) => { const { t } = useTranslation(); const navigate = useNavigate(); const { showSuccessToast, showErrorToast } = useToast(); const [isExpanded, setIsExpanded] = useState(false); const [insights, setInsights] = useState([]); const [expandedInsights, setExpandedInsights] = useState>( new Set() ); // Modal states const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [allProjects, setAllProjects] = useState(projects); const [loading, setLoading] = useState(false); const PROJECT_VERBS = [ 'plan', 'organize', 'set up', 'setup', 'fix', 'review', 'implement', 'create', 'build', 'develop', ]; const OVERDUE_THRESHOLD_DAYS = 30; useEffect(() => { const generateInsights = () => { const newInsights: ProductivityInsight[] = []; // Filter to only include non-completed tasks const activeTasks = tasks.filter( (task) => task.status !== 'done' && task.status !== 'archived' ); // 1. Stalled Projects (no tasks/actions) const stalledProjects = projects.filter( (project) => project.active && !activeTasks.some((task) => task.project_id === project.id) ); if (stalledProjects.length > 0) { newInsights.push({ type: 'stalled_projects', title: t( 'productivity.stalledProjects', 'Stalled Projects' ), description: t( 'productivity.stalledProjectsDesc', 'These projects have no tasks or actions' ), items: stalledProjects, icon: FolderIcon, color: 'text-red-500', }); } // 2. Projects with completed tasks but no next action const projectsNeedingNextAction = projects.filter((project) => { const projectTasks = tasks.filter( (task) => task.project_id === project.id ); const hasCompletedTasks = projectTasks.some( (task) => task.status === 'done' || task.status === 'archived' ); const hasNextAction = activeTasks.some( (task) => task.project_id === project.id && (task.status === 'not_started' || task.status === 'in_progress') ); return project.active && hasCompletedTasks && !hasNextAction; }); if (projectsNeedingNextAction.length > 0) { newInsights.push({ type: 'completed_no_next', title: t( 'productivity.needsNextAction', 'Projects Need Next Action' ), description: t( 'productivity.needsNextActionDesc', 'These projects have completed tasks but no next action' ), items: projectsNeedingNextAction, icon: ExclamationTriangleIcon, color: 'text-yellow-500', }); } // 3. Tasks that are actually projects const tasksAreProjects = activeTasks.filter((task) => { const taskName = task.name.toLowerCase(); return ( PROJECT_VERBS.some((verb) => taskName.includes(verb)) && taskName.length > 30 ); // Longer tasks are more likely to be projects }); if (tasksAreProjects.length > 0) { newInsights.push({ type: 'tasks_are_projects', title: t( 'productivity.tasksAreProjects', 'Tasks That Look Like Projects' ), description: t( 'productivity.tasksAreProjectsDesc', 'These tasks might need to be broken down' ), items: tasksAreProjects, icon: AcademicCapIcon, color: 'text-blue-500', }); } // 4. Tasks without clear verbs const vagueTasks = getVagueTasks(activeTasks); if (vagueTasks.length > 0) { newInsights.push({ type: 'vague_tasks', title: t( 'productivity.vagueTasks', 'Tasks Without Clear Action' ), description: t( 'productivity.vagueTasksDesc', 'These tasks need clearer action verbs' ), items: vagueTasks, icon: ExclamationTriangleIcon, color: 'text-orange-500', }); } // 5. Overdue or stale tasks const now = new Date(); const thresholdDate = new Date( now.getTime() - OVERDUE_THRESHOLD_DAYS * 24 * 60 * 60 * 1000 ); const staleTasks = activeTasks.filter((task) => { // Only use created_at since updated_at doesn't exist in the interface const taskDate = task.created_at ? new Date(task.created_at) : null; return taskDate && taskDate < thresholdDate; }); if (staleTasks.length > 0) { newInsights.push({ type: 'overdue_tasks', title: t('productivity.staleTasks', 'Stale Tasks'), description: t( 'productivity.staleTasksDesc', 'Tasks not updated in {{days}} days', { days: OVERDUE_THRESHOLD_DAYS } ), items: staleTasks, icon: ClockIcon, color: 'text-gray-500', }); } // 6. Stuck projects (not updated in a month) const stuckProjects = projects.filter((project) => { if (!project.active) return false; // Projects don't have date fields in the interface, so we'll check if they have recent tasks const projectTasks = activeTasks.filter( (task) => task.project_id === project.id ); if (projectTasks.length === 0) return false; // Empty projects are handled by "stalled projects" // Find the most recent task date for this project const mostRecentTaskDate = projectTasks.reduce( (latest, task) => { const taskDate = task.created_at ? new Date(task.created_at) : null; if (!taskDate) return latest; return !latest || taskDate > latest ? taskDate : latest; }, null as Date | null ); return mostRecentTaskDate && mostRecentTaskDate < thresholdDate; }); if (stuckProjects.length > 0) { newInsights.push({ type: 'stuck_projects', title: t('productivity.stuckProjects', 'Stuck Projects'), description: t( 'productivity.stuckProjectsDesc', 'Projects not updated recently' ), items: stuckProjects, icon: FolderIcon, color: 'text-purple-500', }); } setInsights(newInsights); }; generateInsights(); }, [tasks, projects, t]); const totalIssues = insights.reduce( (sum, insight) => sum + insight.items.length, 0 ); const toggleInsightExpansion = (index: number) => { const newExpanded = new Set(expandedInsights); if (newExpanded.has(index)) { newExpanded.delete(index); } else { newExpanded.add(index); } setExpandedInsights(newExpanded); }; const handleItemClick = async (item: Task | Project) => { const isTask = 'status' in item; if (isTask) { // Handle task click - open task modal try { setLoading(true); const fullTask = await fetchTaskById(item.id!); setSelectedTask(fullTask); setIsTaskModalOpen(true); } catch (error) { console.error('Failed to fetch task:', error); showErrorToast( t('errors.failedToLoadTask', 'Failed to load task') ); } finally { setLoading(false); } } else { // Handle project click - navigate to project page if (item.uid) { const slug = item.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); navigate(`/project/${item.uid}-${slug}`); } else { navigate(`/project/${item.id}`); } } }; const handleTaskSave = async (updatedTask: Task) => { try { if (updatedTask.id) { await updateTask(updatedTask.id, updatedTask); setIsTaskModalOpen(false); setSelectedTask(null); showSuccessToast( t('task.updateSuccess', 'Task updated successfully') ); // Optionally refresh the parent component data } } catch (error) { console.error('Failed to update task:', error); showErrorToast(t('task.updateError', 'Failed to update task')); } }; const handleTaskDelete = async () => { try { if (selectedTask?.id) { await deleteTask(selectedTask.id); setIsTaskModalOpen(false); setSelectedTask(null); showSuccessToast( t('task.deleteSuccess', 'Task deleted successfully') ); // Optionally refresh the parent component data } } catch (error) { console.error('Failed to delete task:', error); showErrorToast(t('task.deleteError', 'Failed to delete task')); } }; const handleCreateProject = async (name: string): Promise => { try { const project = await createProject({ name, active: true }); setAllProjects((prev) => [...prev, project]); return project; } catch (error) { console.error('Failed to create project:', error); throw error; } }; // Use projects passed as props instead of making additional API calls useEffect(() => { setAllProjects(projects); }, [projects]); if (totalIssues === 0) { return null; } return (
{isExpanded && (
{insights.map((insight, index) => (

{insight.title} ( {insight.items.length})

{insight.description}

{(expandedInsights.has(index) ? insight.items : insight.items.slice(0, 3) ).map((item, itemIndex) => { return (
); })} {insight.items.length > 3 && ( )}
))}

{t( 'productivity.suggestion', 'Click on any item above to open it and make improvements.' )}

)} {/* Task Modal */} {selectedTask && ( { setIsTaskModalOpen(false); setSelectedTask(null); }} task={selectedTask} onSave={handleTaskSave} onDelete={handleTaskDelete} projects={allProjects} onCreateProject={handleCreateProject} /> )}
); }; export default ProductivityAssistant;