From c8ae47c0bc0454c9185730c902efeef2bc4b48fe Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Tue, 3 Mar 2026 20:12:31 +0100 Subject: [PATCH] feat(ui): add task management UI with list, detail, and create pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Task 19: Task List + Task Detail + Status Transitions UI New components: - useTasks hook: TanStack Query hooks (useTasks, useTask, useCreateTask, useUpdateTask) - Task list page: shadcn Table with status filter, pagination, status badges - Task detail page: Full task info with valid status transition buttons - New task form: Create task with title, description, assigneeId, dueDate Key features: - Status transitions match backend logic: Open→Assigned→InProgress→Review→Done - Review status allows back-transition to InProgress (only bidirectional) - Only valid next states shown as buttons (VALID_TRANSITIONS map) - Status badge colors: Open=gray, Assigned=blue, InProgress=yellow, Review=red, Done=green - TanStack Query with automatic cache invalidation on mutations - Next.js 15+ async params pattern (use() hook) TDD: - 3 task list tests (renders rows, status badges, new task button) - 3 task detail tests (Open→Assigned, InProgress→Review, Review→Done+InProgress) All tests pass (31/31). Build succeeds. --- .../notepads/club-work-manager/learnings.md | 13 ++ .sisyphus/plans/club-work-manager.md | 2 +- .../src/app/(protected)/tasks/[id]/page.tsx | 116 ++++++++++++++++ .../src/app/(protected)/tasks/new/page.tsx | 129 +++++++++++++++++ frontend/src/app/(protected)/tasks/page.tsx | 131 ++++++++++++++++++ .../components/__tests__/task-detail.test.tsx | 77 ++++++++++ .../components/__tests__/task-list.test.tsx | 65 +++++++++ frontend/src/hooks/useTasks.ts | 122 ++++++++++++++++ 8 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/(protected)/tasks/[id]/page.tsx create mode 100644 frontend/src/app/(protected)/tasks/new/page.tsx create mode 100644 frontend/src/app/(protected)/tasks/page.tsx create mode 100644 frontend/src/components/__tests__/task-detail.test.tsx create mode 100644 frontend/src/components/__tests__/task-list.test.tsx create mode 100644 frontend/src/hooks/useTasks.ts diff --git a/.sisyphus/notepads/club-work-manager/learnings.md b/.sisyphus/notepads/club-work-manager/learnings.md index de37b1e..54480fa 100644 --- a/.sisyphus/notepads/club-work-manager/learnings.md +++ b/.sisyphus/notepads/club-work-manager/learnings.md @@ -1548,3 +1548,16 @@ frontend/ - **Testing Radix UI DropdownMenu**: When testing Radix UI components like `DropdownMenu` with React Testing Library, you often need to either use complex test setups waiting for portal rendering and pointer events, or simply mock the Radix UI components out to test just the integration logic. Mocking `DropdownMenu`, `DropdownMenuTrigger`, etc., makes checking dropdown logic faster and less prone to portal-related DOM test issues. - **Provider Architecture in Next.js App Router**: Combining multiple providers like `SessionProvider`, `QueryProvider`, and a custom context provider like `TenantProvider` in `app/layout.tsx` is an effective way to handle global state. Custom components needing hooks must have `"use client"` at the top. + + +## Task 19: Task Management UI (2026-03-03) + +### Key Learnings + +- **TanStack Query Patterns**: Successfully used `useQuery` for data fetching and `useMutation` for updates across the task pages, combining them with `useTenant` hook to auto-inject `activeClubId` in API calls and query cache keys. Invalidation happens seamlessly. +- **Next.js 15+ React `use()` Testing**: When page components use `params` as a Promise (e.g., Next.js 15+ convention for dynamic routes), using `use()` in the component causes it to suspend. Vitest tests for such components must either be wrapped in `await act(async () => ...)` or wrapped in a `` boundary while awaiting UI changes with `findByText`. +- **Status Badge Colors**: Implemented mapped `WorkItemStatus` enum values to shadcn Badge colors, ensuring an intuitive UI mapping for transitions (e.g. Open->Assigned->InProgress->Review->Done). +- **Valid Transitions**: Built client-side validation logic that perfectly mirrors the backend `CanTransitionTo` logic (including the back-transition from Review to InProgress). +- **UI Component Usage**: Leveraged shadcn `Table` for the list and `Card` for details and new task forms, alongside raw inputs for simplified creation without needing heavy forms libraries. + + diff --git a/.sisyphus/plans/club-work-manager.md b/.sisyphus/plans/club-work-manager.md index 1c0cd0a..0de3a52 100644 --- a/.sisyphus/plans/club-work-manager.md +++ b/.sisyphus/plans/club-work-manager.md @@ -1769,7 +1769,7 @@ Max Concurrent: 6 (Wave 1) - Files: `frontend/src/app/layout.tsx`, `frontend/src/components/club-switcher.tsx`, `frontend/src/components/auth-guard.tsx` - Pre-commit: `bun run build && bun run test` (in frontend/) -- [ ] 19. Task List + Task Detail + Status Transitions UI +- [x] 19. Task List + Task Detail + Status Transitions UI **What to do**: - Create `/frontend/src/app/(protected)/tasks/page.tsx`: diff --git a/frontend/src/app/(protected)/tasks/[id]/page.tsx b/frontend/src/app/(protected)/tasks/[id]/page.tsx new file mode 100644 index 0000000..8e2b078 --- /dev/null +++ b/frontend/src/app/(protected)/tasks/[id]/page.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { use } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useTask, useUpdateTask } from '@/hooks/useTasks'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +const VALID_TRANSITIONS: Record = { + Open: ['Assigned'], + Assigned: ['InProgress'], + InProgress: ['Review'], + Review: ['Done', 'InProgress'], +}; + +const statusColors: Record = { + Open: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + Assigned: 'bg-primary text-primary-foreground hover:bg-primary/80', + InProgress: 'bg-yellow-500 text-black hover:bg-yellow-500/80', + Review: 'bg-destructive text-destructive-foreground hover:bg-destructive/80', + Done: 'bg-green-500 text-white hover:bg-green-500/80', +}; + +export default function TaskDetailPage({ params }: { params: Promise<{ id: string }> }) { + const resolvedParams = use(params); + const router = useRouter(); + const { data: task, isLoading, error } = useTask(resolvedParams.id); + const { mutate: updateTask, isPending } = useUpdateTask(); + + if (isLoading) return
Loading task...
; + if (error || !task) return
Failed to load task.
; + + const validTransitions = VALID_TRANSITIONS[task.status] || []; + + const handleTransition = (newStatus: string) => { + updateTask({ id: task.id, data: { status: newStatus } }); + }; + + const getTransitionLabel = (status: string, newStatus: string) => { + if (status === 'Review' && newStatus === 'InProgress') return 'Back to InProgress'; + if (newStatus === 'Done') return 'Mark as Done'; + return `Move to ${newStatus}`; + }; + + return ( +
+
+
+ +

Task Details

+
+
+ + + +
+
+ {task.title} + + {task.description || 'No description provided.'} + +
+ + {task.status} + +
+
+ +
+
+

Assignee

+

{task.assigneeId || 'Unassigned'}

+
+
+

Created By

+

{task.createdById}

+
+
+

Created At

+

{new Date(task.createdAt).toLocaleString()}

+
+ {task.dueDate && ( +
+

Due Date

+

{new Date(task.dueDate).toLocaleDateString()}

+
+ )} +
+ + {validTransitions.length > 0 && ( +
+

Actions

+
+ {validTransitions.map((nextStatus) => ( + + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/app/(protected)/tasks/new/page.tsx b/frontend/src/app/(protected)/tasks/new/page.tsx new file mode 100644 index 0000000..1885b1a --- /dev/null +++ b/frontend/src/app/(protected)/tasks/new/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useCreateTask } from '@/hooks/useTasks'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function NewTaskPage() { + const router = useRouter(); + const { mutate: createTask, isPending, error } = useCreateTask(); + + const [formData, setFormData] = useState({ + title: '', + description: '', + assigneeId: '', + dueDate: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + createTask( + { + title: formData.title, + description: formData.description || undefined, + assigneeId: formData.assigneeId || undefined, + dueDate: formData.dueDate ? new Date(formData.dueDate).toISOString() : undefined, + }, + { + onSuccess: (data) => { + router.push(`/tasks/${data.id}`); + }, + } + ); + }; + + return ( +
+
+
+ +

Create New Task

+
+
+ + + + Task Details + Fill in the details for the new task. + + +
+
+ + +
+ +
+ +