159 行
5.8 KiB
TypeScript
159 行
5.8 KiB
TypeScript
import { Link, useSearchParams } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { isApiError } from "../api/types";
|
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
|
import { fetchWorkflowTasks } from "../api/workflow";
|
|
import { PageHeader } from "../components/common/PageHeader";
|
|
import { Pagination } from "../components/common/Pagination";
|
|
import { StatusBadge } from "../components/common/StatusBadge";
|
|
import { TableSkeleton } from "../components/common/TableSkeleton";
|
|
import { Button } from "../components/ui/button";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
|
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
function entityLabel(entityType: string | undefined) {
|
|
if (entityType === "leave") {
|
|
return "请假";
|
|
}
|
|
if (entityType === "reimbursement") {
|
|
return "报销";
|
|
}
|
|
if (entityType === "purchase_request") {
|
|
return "采购";
|
|
}
|
|
if (entityType === "travel_request") {
|
|
return "差旅";
|
|
}
|
|
return entityType || "-";
|
|
}
|
|
|
|
export function WorkflowTasksPage() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const status =
|
|
(searchParams.get("status") as
|
|
| "pending"
|
|
| "completed"
|
|
| "rejected"
|
|
| "cancelled"
|
|
| "skipped"
|
|
| null) ?? "pending";
|
|
const page = Number(searchParams.get("page") ?? "1") || 1;
|
|
|
|
const query = useQuery({
|
|
queryKey: ["workflow", "tasks", status, page],
|
|
queryFn: () => fetchWorkflowTasks({ status, page, pageSize: PAGE_SIZE }),
|
|
});
|
|
|
|
const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / PAGE_SIZE));
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader title="待我处理" subtitle="审批待办列表,覆盖报销、请假、采购与差旅流程。" />
|
|
|
|
<div className="card-std p-6 space-y-4">
|
|
<Tabs
|
|
data-testid="workflow-tasks-status-tabs"
|
|
value={status}
|
|
onValueChange={(value) => setSearchParams({ status: value, page: "1" })}
|
|
>
|
|
<TabsList className="flex w-full flex-wrap justify-start gap-1">
|
|
{[
|
|
{ value: "pending", label: "待处理" },
|
|
{ value: "completed", label: "已通过" },
|
|
{ value: "rejected", label: "已驳回" },
|
|
{ value: "cancelled", label: "已取消" },
|
|
{ value: "skipped", label: "已跳过" },
|
|
].map((item) => (
|
|
<TabsTrigger
|
|
data-testid={`workflow-tasks-tab-${item.value}`}
|
|
key={item.value}
|
|
value={item.value}
|
|
>
|
|
{item.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
|
|
{query.isLoading ? <TableSkeleton columns={5} rows={5} /> : null}
|
|
|
|
{query.isError ? (
|
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
|
{isApiError(query.error)
|
|
? `${query.error.code}: ${mapApiErrorMessage(
|
|
query.error.code,
|
|
query.error.details,
|
|
query.error.message,
|
|
)}`
|
|
: "加载待办失败,请稍后重试"}
|
|
</p>
|
|
) : null}
|
|
|
|
{!query.isLoading && !query.isError ? (
|
|
<div className="rounded-lg border border-zinc-200">
|
|
<Table data-testid="workflow-tasks-table">
|
|
<TableHeader>
|
|
<tr>
|
|
<TableHead>任务</TableHead>
|
|
<TableHead>业务对象</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>创建时间</TableHead>
|
|
<TableHead className="w-24">操作</TableHead>
|
|
</tr>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(query.data?.items ?? []).length ? (
|
|
query.data?.items.map((task) => (
|
|
<TableRow data-testid="workflow-task-row" key={task.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-zinc-900">{task.stepName}</div>
|
|
<div className="text-xs text-zinc-500">{task.id}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="font-medium text-zinc-900">
|
|
{entityLabel(task.workflowInstance?.entityType)}
|
|
</div>
|
|
<div className="font-mono text-xs text-zinc-500">
|
|
{task.workflowInstance?.entityId ?? "-"}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell><StatusBadge status={task.status} /></TableCell>
|
|
<TableCell>{new Date(task.createdAt).toLocaleString()}</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
asChild
|
|
size="sm"
|
|
variant="secondary"
|
|
data-testid="workflow-task-detail-link"
|
|
>
|
|
<Link to={`/workflow/tasks/${task.id}`}>详情</Link>
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell className="py-3 text-zinc-500" colSpan={5}>
|
|
暂无数据
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : null}
|
|
|
|
<Pagination
|
|
page={page}
|
|
totalPages={totalPages}
|
|
onPageChange={(p) => setSearchParams({ status, page: String(p) })}
|
|
testIdPrefix="workflow-tasks"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|