import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { cancelTodo, completeTodo, createTodo, fetchTodos } from "../api/todos"; import { fetchProjects } from "../api/projects"; import { PageHeader } from "../components/common/PageHeader"; import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace"; import { MetricCard } from "../components/patterns/MetricCard"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { toast } from "sonner"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { TEST_IDS } from "../lib/testIds"; function errorMessage(error: unknown, fallback: string) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } return error instanceof Error ? error.message : fallback; } export function TodosPage() { const queryClient = useQueryClient(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [projectId, setProjectId] = useState(""); const [statusFilter, setStatusFilter] = useState<"todo" | "doing" | "done" | "cancelled" | "">(""); const todosQuery = useQuery({ queryKey: ["todos", statusFilter], queryFn: () => fetchTodos({ status: statusFilter || undefined }), }); const projectsQuery = useQuery({ queryKey: ["projects", "todo-options"], queryFn: () => fetchProjects({}), }); const todoItems = todosQuery.data?.items ?? []; const todoStats = useMemo(() => { return todoItems.reduce( (acc, item) => { acc.total += 1; if (item.status === "todo") acc.todo += 1; if (item.status === "doing") acc.doing += 1; if (item.status === "done") acc.done += 1; return acc; }, { total: 0, todo: 0, doing: 0, done: 0 }, ); }, [todoItems]); const createMutation = useMutation({ mutationFn: () => createTodo({ title, description: description || undefined, projectId: projectId || null }), onSuccess: async () => { setTitle(""); setDescription(""); setProjectId(""); await Promise.all([ queryClient.invalidateQueries({ queryKey: ["todos"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), ]); toast.success("待办已创建"); }, onError: (error) => toast.error(errorMessage(error, "创建待办失败")), }); const completeMutation = useMutation({ mutationFn: (id: string) => completeTodo(id), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["todos"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), ]); toast.success("待办已完成"); }, onError: (error) => toast.error(errorMessage(error, "完成待办失败")), }); const cancelMutation = useMutation({ mutationFn: (id: string) => cancelTodo(id), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["todos"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), ]); toast.success("待办已取消"); }, onError: (error) => toast.error(errorMessage(error, "取消待办失败")), }); return (
0 ? "warning" : "neutral"} />
按状态筛选后,列表只保留当前关注的工作项。
{todosQuery.isError ?
{errorMessage(todosQuery.error, "加载待办失败")}
: null} 标题 项目 负责人 优先级 状态 操作 {todosQuery.isLoading ? ( 待办加载中... ) : todoItems.length ? ( todoItems.map((item) => (
{item.title}
{item.description ?? "-"}
{item.project ? `${item.project.projectNo} · ${item.project.name}` : "-"} {item.assignee?.name ?? "-"} {item.priority} {item.status}
)) ) : ( 当前没有符合条件的待办。 )}
setTitle(event.target.value)} />
setDescription(event.target.value)} />
); } export default TodosPage;