From c0b798daf3b95c8259d446497a5544e5bea73e5d Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 13 Mar 2026 08:27:38 -0500 Subject: [PATCH] feat(web): task management with list view and kanban board Add task management page with dual view modes (list table and kanban columns). Tasks are fetched from the gateway API and displayed with status badges, priority indicators, and due dates. Kanban columns map to task statuses: not-started, in-progress, blocked, done. Components: TaskCard, KanbanBoard, TaskListView Types: Task, TaskStatus, TaskPriority, Project, ProjectStatus Refs #29 Co-Authored-By: Claude Opus 4.6 --- 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 | -- 2.49.1