diff --git a/dist/index.html b/dist/index.html index 9780a955..ff7d2115 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,7 +6,7 @@ Todo App - + diff --git a/src/components/CompletedSection.tsx b/src/components/CompletedSection.tsx new file mode 100644 index 00000000..37824e87 --- /dev/null +++ b/src/components/CompletedSection.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { ChevronRight, ChevronDown, Check } from 'lucide-react'; +import type { Task } from '@/types'; +import { cn } from '@/lib/utils'; +import { useTasksStore } from '@/stores/tasks'; + +interface CompletedSectionProps { + tasks: Task[]; +} + +export function CompletedSection({ tasks }: CompletedSectionProps) { + const [isOpen, setIsOpen] = useState(false); + const { toggleComplete, setSelectedTask } = useTasksStore(); + + if (tasks.length === 0) return null; + + return ( +
+ + + {isOpen && ( +
+ {tasks.map((task) => ( +
setSelectedTask(task)} + > + + + {task.title} + + {task.project && ( + + + {task.project.name} + + )} +
+ ))} +
+ )} +
+ ); +} diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index 4513816c..5981ac31 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -20,6 +20,7 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { const [dueDate, setDueDate] = useState(task.dueDate || ''); const [priority, setPriority] = useState(task.priority); const [assigneeId, setAssigneeId] = useState(task.assigneeId || ''); + const [projectId, setProjectId] = useState(task.projectId || ''); const [comments, setComments] = useState([]); const [isDeleting, setIsDeleting] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -32,6 +33,7 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { setDueDate(task.dueDate || ''); setPriority(task.priority); setAssigneeId(task.assigneeId || ''); + setProjectId(task.projectId || ''); }, [task]); useEffect(() => { @@ -72,6 +74,11 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { updateTask(task.id, { assigneeId: value || undefined }); }; + const handleProjectChange = (value: string) => { + setProjectId(value); + updateTask(task.id, { projectId: value || undefined }); + }; + const handleDelete = async () => { setIsDeleting(true); try { @@ -85,6 +92,10 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { const handleComplete = async () => { await toggleComplete(task.id); + if (!task.isCompleted) { + // Task was just completed — close detail panel after brief delay + setTimeout(() => onClose(), 500); + } }; // Close on Escape @@ -176,14 +187,26 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { Project
{taskProject && ( - <> - - {taskProject.name} - + )} +
diff --git a/src/pages/Inbox.tsx b/src/pages/Inbox.tsx index 87956ce5..065fdfaf 100644 --- a/src/pages/Inbox.tsx +++ b/src/pages/Inbox.tsx @@ -3,15 +3,17 @@ import { Inbox as InboxIcon } from 'lucide-react'; import { useTasksStore } from '@/stores/tasks'; import { TaskItem } from '@/components/TaskItem'; import { AddTask } from '@/components/AddTask'; +import { CompletedSection } from '@/components/CompletedSection'; export function InboxPage() { - const { tasks, projects, isLoading, fetchTasks, setSelectedTask } = useTasksStore(); + const { tasks, completedTasks, projects, isLoading, fetchTasks, fetchCompletedTasks, setSelectedTask } = useTasksStore(); const inbox = projects.find(p => p.isInbox); useEffect(() => { if (inbox) { fetchTasks({ projectId: inbox.id, completed: false }); + fetchCompletedTasks({ projectId: inbox.id }); } }, [inbox?.id]); @@ -50,6 +52,9 @@ export function InboxPage() {
+ + {/* Completed tasks */} + ); } diff --git a/src/pages/Project.tsx b/src/pages/Project.tsx index 065b41cf..f2ee5307 100644 --- a/src/pages/Project.tsx +++ b/src/pages/Project.tsx @@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'; import { useTasksStore } from '@/stores/tasks'; import { TaskItem } from '@/components/TaskItem'; import { AddTask } from '@/components/AddTask'; +import { CompletedSection } from '@/components/CompletedSection'; import { BoardView } from '@/pages/Board'; import { api } from '@/lib/api'; import type { Project as ProjectType, Section } from '@/types'; @@ -13,7 +14,7 @@ export function ProjectPage() { const { id } = useParams<{ id: string }>(); const location = useLocation(); const navigate = useNavigate(); - const { tasks, isLoading, fetchTasks, setSelectedTask } = useTasksStore(); + const { tasks, completedTasks, isLoading, fetchTasks, fetchCompletedTasks, setSelectedTask } = useTasksStore(); const [project, setProject] = useState(null); const [sections, setSections] = useState([]); @@ -22,6 +23,7 @@ export function ProjectPage() { useEffect(() => { if (!id) return; fetchTasks({ projectId: id, completed: false }); + fetchCompletedTasks({ projectId: id }); api.getProject(id).then((p) => { setProject(p); setSections(p.sections || []); @@ -138,6 +140,9 @@ export function ProjectPage() { ))} + + {/* Completed tasks */} + )} diff --git a/src/pages/Today.tsx b/src/pages/Today.tsx index 58f79b9d..f3ce7d01 100644 --- a/src/pages/Today.tsx +++ b/src/pages/Today.tsx @@ -3,12 +3,14 @@ import { Calendar } from 'lucide-react'; import { useTasksStore } from '@/stores/tasks'; import { TaskItem } from '@/components/TaskItem'; import { AddTask } from '@/components/AddTask'; +import { CompletedSection } from '@/components/CompletedSection'; export function TodayPage() { - const { tasks, isLoading, fetchTasks, setSelectedTask } = useTasksStore(); + const { tasks, completedTasks, isLoading, fetchTasks, fetchCompletedTasks, setSelectedTask } = useTasksStore(); useEffect(() => { fetchTasks({ today: true, completed: false }); + fetchCompletedTasks({ today: true }); }, []); const today = new Date(); @@ -104,6 +106,9 @@ export function TodayPage() {
+ + {/* Completed tasks */} + ); } diff --git a/src/pages/Upcoming.tsx b/src/pages/Upcoming.tsx index 490c0b34..0dd68d5c 100644 --- a/src/pages/Upcoming.tsx +++ b/src/pages/Upcoming.tsx @@ -3,13 +3,15 @@ import { CalendarDays } from 'lucide-react'; import { useTasksStore } from '@/stores/tasks'; import { TaskItem } from '@/components/TaskItem'; import { AddTask } from '@/components/AddTask'; +import { CompletedSection } from '@/components/CompletedSection'; import { format, addDays, startOfDay, isSameDay } from 'date-fns'; export function UpcomingPage() { - const { tasks, isLoading, fetchTasks, setSelectedTask } = useTasksStore(); + const { tasks, completedTasks, isLoading, fetchTasks, fetchCompletedTasks, setSelectedTask } = useTasksStore(); useEffect(() => { fetchTasks({ upcoming: true, completed: false }); + fetchCompletedTasks({ upcoming: true }); }, []); // Group tasks by date @@ -112,6 +114,9 @@ export function UpcomingPage() {
+ + {/* Completed tasks */} + ); } diff --git a/src/stores/__tests__/tasks.test.ts b/src/stores/__tests__/tasks.test.ts index 6a2c9dbc..ec1984de 100644 --- a/src/stores/__tests__/tasks.test.ts +++ b/src/stores/__tests__/tasks.test.ts @@ -62,6 +62,7 @@ describe('useTasksStore', () => { vi.clearAllMocks(); useTasksStore.setState({ tasks: [], + completedTasks: [], projects: [], labels: [], users: [], @@ -75,6 +76,7 @@ describe('useTasksStore', () => { it('has correct initial state', () => { const state = useTasksStore.getState(); expect(state.tasks).toEqual([]); + expect(state.completedTasks).toEqual([]); expect(state.projects).toEqual([]); expect(state.labels).toEqual([]); expect(state.users).toEqual([]); @@ -157,14 +159,37 @@ describe('useTasksStore', () => { }); describe('toggleComplete', () => { - it('flips isCompleted', async () => { + it('moves task to completedTasks when completing', async () => { const task = makeTask({ id: '1', isCompleted: false }); - useTasksStore.setState({ tasks: [task] }); + useTasksStore.setState({ tasks: [task], completedTasks: [] }); mockApi.updateTask.mockResolvedValueOnce({ ...task, isCompleted: true, completedAt: '2025-06-15' }); await useTasksStore.getState().toggleComplete('1'); - expect(useTasksStore.getState().tasks[0].isCompleted).toBe(true); + expect(useTasksStore.getState().tasks).toHaveLength(0); + expect(useTasksStore.getState().completedTasks[0].isCompleted).toBe(true); + }); + + it('moves task back to tasks when uncompleting', async () => { + const task = makeTask({ id: '1', isCompleted: true, completedAt: '2025-06-15' }); + useTasksStore.setState({ tasks: [], completedTasks: [task] }); + mockApi.updateTask.mockResolvedValueOnce({ ...task, isCompleted: false, completedAt: undefined }); + + await useTasksStore.getState().toggleComplete('1'); + + expect(useTasksStore.getState().completedTasks).toHaveLength(0); + expect(useTasksStore.getState().tasks[0].isCompleted).toBe(false); + }); + }); + + describe('fetchCompletedTasks', () => { + it('populates completedTasks', async () => { + const tasks = [makeTask({ id: '1', isCompleted: true })]; + mockApi.getTasks.mockResolvedValueOnce(tasks); + + await useTasksStore.getState().fetchCompletedTasks(); + + expect(useTasksStore.getState().completedTasks).toEqual(tasks); }); }); diff --git a/src/stores/tasks.ts b/src/stores/tasks.ts index 9a9099cb..c44fa785 100644 --- a/src/stores/tasks.ts +++ b/src/stores/tasks.ts @@ -4,6 +4,7 @@ import { api } from '@/lib/api'; interface TasksState { tasks: Task[]; + completedTasks: Task[]; projects: Project[]; labels: Label[]; users: User[]; @@ -16,6 +17,7 @@ interface TasksState { // Actions fetchTasks: (params?: Parameters[0]) => Promise; + fetchCompletedTasks: (params?: Parameters[0]) => Promise; fetchProjects: () => Promise; fetchLabels: () => Promise; fetchUsers: () => Promise; @@ -33,6 +35,7 @@ interface TasksState { export const useTasksStore = create((set, get) => ({ tasks: [], + completedTasks: [], projects: [], labels: [], users: [], @@ -52,6 +55,15 @@ export const useTasksStore = create((set, get) => ({ } }, + fetchCompletedTasks: async (params) => { + try { + const completedTasks = await api.getTasks({ ...params, completed: true }); + set({ completedTasks }); + } catch (error) { + console.error('Failed to fetch completed tasks:', error); + } + }, + fetchProjects: async () => { try { const projects = await api.getProjects(); @@ -110,15 +122,25 @@ export const useTasksStore = create((set, get) => ({ }, toggleComplete: async (id) => { - const task = get().tasks.find((t) => t.id === id); + const task = get().tasks.find((t) => t.id === id) || get().completedTasks.find((t) => t.id === id); if (!task) return; const updated = await api.updateTask(id, { isCompleted: !task.isCompleted }); - set((state) => ({ - tasks: state.tasks.map((t) => - t.id === id ? { ...t, isCompleted: !task.isCompleted, completedAt: updated.completedAt } : t - ), - })); + const updatedTask = { ...task, isCompleted: !task.isCompleted, completedAt: updated.completedAt }; + + if (updatedTask.isCompleted) { + // Move from tasks to completedTasks + set((state) => ({ + tasks: state.tasks.filter((t) => t.id !== id), + completedTasks: [updatedTask, ...state.completedTasks], + })); + } else { + // Move from completedTasks to tasks + set((state) => ({ + tasks: [updatedTask, ...state.tasks], + completedTasks: state.completedTasks.filter((t) => t.id !== id), + })); + } }, setSelectedTask: (task) => set({ selectedTask: task }),