125 行
5.2 KiB
TypeScript
125 行
5.2 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { cancelTodo, completeTodo, createTodo, fetchTodos } from "../api/todos";
|
|
import { PageHeader } from "../components/common/PageHeader";
|
|
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";
|
|
|
|
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 [statusFilter, setStatusFilter] = useState<"todo" | "doing" | "done" | "cancelled" | "">("");
|
|
const todosQuery = useQuery({
|
|
queryKey: ["todos", statusFilter],
|
|
queryFn: () => fetchTodos({ status: statusFilter || undefined }),
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () => createTodo({ title, description: description || undefined }),
|
|
onSuccess: async () => {
|
|
setTitle("");
|
|
setDescription("");
|
|
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 (
|
|
<div className="space-y-6">
|
|
<PageHeader title="待办" subtitle="员工维护个人待办,老板看高优先级和未完成项。" />
|
|
|
|
<div className="card-std p-6 space-y-4">
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<Input placeholder="待办标题" value={title} onChange={(event) => setTitle(event.target.value)} />
|
|
<Input placeholder="补充说明(可选)" value={description} onChange={(event) => setDescription(event.target.value)} />
|
|
<div className="flex gap-2">
|
|
<select className="select-std" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as typeof statusFilter)}>
|
|
<option value="">全部状态</option>
|
|
<option value="todo">todo</option>
|
|
<option value="doing">doing</option>
|
|
<option value="done">done</option>
|
|
<option value="cancelled">cancelled</option>
|
|
</select>
|
|
<Button type="button" onClick={() => createMutation.mutate()} disabled={createMutation.isPending || !title.trim()}>新建待办</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-std p-6">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>标题</TableHead>
|
|
<TableHead>负责人</TableHead>
|
|
<TableHead>优先级</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(todosQuery.data?.items ?? []).map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-zinc-900">{item.title}</div>
|
|
<div className="text-xs text-zinc-500">{item.description ?? "-"}</div>
|
|
</TableCell>
|
|
<TableCell>{item.assignee?.name ?? "-"}</TableCell>
|
|
<TableCell>{item.priority}</TableCell>
|
|
<TableCell>{item.status}</TableCell>
|
|
<TableCell>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" type="button" disabled={completeMutation.isPending || item.status === "done"} onClick={() => completeMutation.mutate(item.id)}>完成</Button>
|
|
<Button size="sm" variant="secondary" type="button" disabled={cancelMutation.isPending || item.status === "cancelled"} onClick={() => cancelMutation.mutate(item.id)}>取消</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TodosPage;
|