feat(ui): add task management UI with list, detail, and create pages
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.
This commit is contained in:
122
frontend/src/hooks/useTasks.ts
Normal file
122
frontend/src/hooks/useTasks.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTenant } from '@/contexts/tenant-context';
|
||||
import { apiClient } from '@/lib/api';
|
||||
|
||||
export interface TaskListDto {
|
||||
items: TaskListItemDto[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface TaskListItemDto {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
assigneeId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface TaskDetailDto {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
assigneeId: string | null;
|
||||
createdById: string;
|
||||
clubId: string;
|
||||
dueDate: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
title: string;
|
||||
description?: string;
|
||||
clubId: string;
|
||||
assigneeId?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTaskRequest {
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
assigneeId?: string;
|
||||
dueDate?: string;
|
||||
}
|
||||
|
||||
export function useTasks(filters?: { status?: string; page?: number }) {
|
||||
const { activeClubId } = useTenant();
|
||||
|
||||
return useQuery<TaskListDto>({
|
||||
queryKey: ['tasks', activeClubId, filters],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.page) params.append('page', filters.page.toString());
|
||||
|
||||
const res = await apiClient(`/api/tasks?${params}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch tasks');
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!activeClubId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTask(id: string) {
|
||||
const { activeClubId } = useTenant();
|
||||
|
||||
return useQuery<TaskDetailDto>({
|
||||
queryKey: ['tasks', activeClubId, id],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient(`/api/tasks/${id}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch task');
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!activeClubId && !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { activeClubId } = useTenant();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: Omit<CreateTaskRequest, 'clubId'>) => {
|
||||
const payload: CreateTaskRequest = {
|
||||
...data,
|
||||
clubId: activeClubId!,
|
||||
};
|
||||
const res = await apiClient('/api/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create task');
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', activeClubId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateTask() {
|
||||
const queryClient = useQueryClient();
|
||||
const { activeClubId } = useTenant();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, data }: { id: string; data: UpdateTaskRequest }) => {
|
||||
const res = await apiClient(`/api/tasks/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update task');
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', activeClubId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', activeClubId, variables.id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user