From a1a1976b38c1d520db7022894c5defc014cf8326 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 13 Mar 2026 13:28:17 +0000 Subject: [PATCH] feat(web): task management with list view and kanban board (#86) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/web/src/app/(dashboard)/tasks/page.tsx | 72 +++++++++++++++++++ .../web/src/components/tasks/kanban-board.tsx | 47 ++++++++++++ apps/web/src/components/tasks/task-card.tsx | 57 +++++++++++++++ .../src/components/tasks/task-list-view.tsx | 67 +++++++++++++++++ apps/web/src/lib/types.ts | 38 ++++++++++ docs/TASKS.md | 4 +- 6 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/tasks/page.tsx create mode 100644 apps/web/src/components/tasks/kanban-board.tsx create mode 100644 apps/web/src/components/tasks/task-card.tsx create mode 100644 apps/web/src/components/tasks/task-list-view.tsx diff --git a/apps/web/src/app/(dashboard)/tasks/page.tsx b/apps/web/src/app/(dashboard)/tasks/page.tsx new file mode 100644 index 0000000..36ee9d4 --- /dev/null +++ b/apps/web/src/app/(dashboard)/tasks/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { api } from '@/lib/api'; +import { cn } from '@/lib/cn'; +import type { Task } from '@/lib/types'; +import { KanbanBoard } from '@/components/tasks/kanban-board'; +import { TaskListView } from '@/components/tasks/task-list-view'; + +type ViewMode = 'list' | 'kanban'; + +export default function TasksPage(): React.ReactElement { + const [tasks, setTasks] = useState([]); + const [view, setView] = useState('kanban'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + api('/api/tasks') + .then(setTasks) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const handleTaskClick = useCallback((task: Task) => { + // Task detail view will be added in future iteration + console.log('Task clicked:', task.id); + }, []); + + return ( +
+
+

Tasks

+
+
+ + +
+
+
+ + {loading ? ( +

Loading tasks...

+ ) : view === 'kanban' ? ( + + ) : ( + + )} +
+ ); +} diff --git a/apps/web/src/components/tasks/kanban-board.tsx b/apps/web/src/components/tasks/kanban-board.tsx new file mode 100644 index 0000000..56ec8f8 --- /dev/null +++ b/apps/web/src/components/tasks/kanban-board.tsx @@ -0,0 +1,47 @@ +'use client'; + +import type { Task, TaskStatus } from '@/lib/types'; +import { TaskCard } from './task-card'; + +interface KanbanBoardProps { + tasks: Task[]; + onTaskClick: (task: Task) => void; +} + +const columns: { id: TaskStatus; label: string }[] = [ + { id: 'not-started', label: 'Not Started' }, + { id: 'in-progress', label: 'In Progress' }, + { id: 'blocked', label: 'Blocked' }, + { id: 'done', label: 'Done' }, +]; + +export function KanbanBoard({ tasks, onTaskClick }: KanbanBoardProps): React.ReactElement { + return ( +
+ {columns.map((col) => { + const columnTasks = tasks.filter((t) => t.status === col.id); + return ( +
+
+

{col.label}

+ + {columnTasks.length} + +
+
+ {columnTasks.length === 0 && ( +

No tasks

+ )} + {columnTasks.map((task) => ( + + ))} +
+
+ ); + })} +
+ ); +} diff --git a/apps/web/src/components/tasks/task-card.tsx b/apps/web/src/components/tasks/task-card.tsx new file mode 100644 index 0000000..2b711d4 --- /dev/null +++ b/apps/web/src/components/tasks/task-card.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { cn } from '@/lib/cn'; +import type { Task } from '@/lib/types'; + +interface TaskCardProps { + task: Task; + onClick: (task: Task) => void; +} + +const priorityColors: Record = { + critical: 'text-error', + high: 'text-warning', + medium: 'text-blue-400', + low: 'text-text-muted', +}; + +const statusBadgeColors: Record = { + 'not-started': 'bg-gray-600/20 text-gray-300', + 'in-progress': 'bg-blue-600/20 text-blue-400', + blocked: 'bg-error/20 text-error', + done: 'bg-success/20 text-success', + cancelled: 'bg-gray-600/20 text-gray-500', +}; + +export function TaskCard({ task, onClick }: TaskCardProps): React.ReactElement { + return ( + + ); +} diff --git a/apps/web/src/components/tasks/task-list-view.tsx b/apps/web/src/components/tasks/task-list-view.tsx new file mode 100644 index 0000000..d29d72e --- /dev/null +++ b/apps/web/src/components/tasks/task-list-view.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { cn } from '@/lib/cn'; +import type { Task } from '@/lib/types'; + +interface TaskListViewProps { + tasks: Task[]; + onTaskClick: (task: Task) => void; +} + +const priorityColors: Record = { + critical: 'text-error', + high: 'text-warning', + medium: 'text-blue-400', + low: 'text-text-muted', +}; + +const statusColors: Record = { + 'not-started': 'text-gray-400', + 'in-progress': 'text-blue-400', + blocked: 'text-error', + done: 'text-success', + cancelled: 'text-gray-500', +}; + +export function TaskListView({ tasks, onTaskClick }: TaskListViewProps): React.ReactElement { + if (tasks.length === 0) { + return

No tasks found

; + } + + return ( +
+ + + + + + + + + + + {tasks.map((task) => ( + onTaskClick(task)} + className="cursor-pointer border-b border-surface-border transition-colors last:border-b-0 hover:bg-surface-elevated" + > + + + + + + ))} + +
TitleStatusPriorityDue
{task.title} + {task.status} + + + {task.priority} + + + {task.dueDate ? new Date(task.dueDate).toLocaleDateString() : '—'} +
+
+ ); +} diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 4d48a84..bfc56a4 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -17,3 +17,41 @@ export interface Message { metadata?: Record; createdAt: string; } + +/** Task statuses. */ +export type TaskStatus = 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled'; + +/** Task priorities. */ +export type TaskPriority = 'critical' | 'high' | 'medium' | 'low'; + +/** Task returned by the gateway API. */ +export interface Task { + id: string; + title: string; + description: string | null; + status: TaskStatus; + priority: TaskPriority; + projectId: string | null; + missionId: string | null; + assignee: string | null; + tags: string[] | null; + dueDate: string | null; + metadata: Record | null; + createdAt: string; + updatedAt: string; +} + +/** Project statuses. */ +export type ProjectStatus = 'active' | 'paused' | 'completed' | 'archived'; + +/** Project returned by the gateway API. */ +export interface Project { + id: string; + name: string; + description: string | null; + status: ProjectStatus; + userId: string; + metadata: Record | null; + createdAt: string; + updatedAt: string; +} diff --git a/docs/TASKS.md b/docs/TASKS.md index 0d80923..f50cc2c 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -31,8 +31,8 @@ | P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 | | P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 | | P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 | -| P3-003 | in-progress | Phase 3 | Chat UI — conversations, messages, streaming | — | #28 | -| P3-004 | not-started | Phase 3 | Task management — list view + kanban board | — | #29 | +| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 | +| P3-004 | in-progress | Phase 3 | Task management — list view + kanban board | — | #29 | | P3-005 | not-started | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 | | P3-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 | | P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |