feat: 完成 R13/R14/R15/R16 小团队管理核心能力

这个提交包含在:
2026-03-21 20:31:34 +08:00
父节点 0826693a8f
当前提交 1e52f1c225
共修改 47 个文件,包含 4298 行新增和 306 行删除
+140
查看文件
@@ -0,0 +1,140 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchEmployees,
regularizeEmployee,
startEmployeeOffboarding,
startEmployeeOnboarding,
} from "../api/employees";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
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 { StatusBadge } from "../components/common/StatusBadge";
import { toast } from "sonner";
export function PeoplePage() {
const queryClient = useQueryClient();
const [q, setQ] = useState("");
const employeesQuery = useQuery({
queryKey: ["employees", "people-page", q],
queryFn: () => fetchEmployees({ page: 1, pageSize: 100, q: q || undefined }),
});
const onboardingMutation = useMutation({
mutationFn: (employeeId: string) => startEmployeeOnboarding(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employees"] });
toast.success("已发起入职流程");
},
onError: (error) => {
const message = isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
: error instanceof Error
? error.message
: "发起入职失败";
toast.error(message);
},
});
const regularizeMutation = useMutation({
mutationFn: (employeeId: string) => regularizeEmployee(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employees"] });
toast.success("已完成转正");
},
onError: (error) => {
const message = isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
: error instanceof Error
? error.message
: "转正失败";
toast.error(message);
},
});
const offboardingMutation = useMutation({
mutationFn: (employeeId: string) => startEmployeeOffboarding(employeeId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["employees"] });
toast.success("已发起离职流程");
},
onError: (error) => {
const message = isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
: error instanceof Error
? error.message
: "发起离职失败";
toast.error(message);
},
});
const items = employeesQuery.data?.items ?? [];
return (
<div className="space-y-6">
<PageHeader
title="人员管理"
subtitle="小团队用这里管入职、转正、离职与档案入口。"
actions={
<div className="flex gap-2">
<Button asChild variant="secondary" type="button">
<Link to="/system/users">先新建账号</Link>
</Button>
</div>
}
/>
<div className="card-std p-4">
<Input placeholder="搜索姓名、工号、手机号、邮箱" value={q} onChange={(event) => setQ(event.target.value)} />
</div>
<div className="card-std p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>员工</TableHead>
<TableHead>角色</TableHead>
<TableHead>账号状态</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={item.id}>
<TableCell>
<div className="font-medium text-zinc-900">{item.name}</div>
<div className="text-xs text-zinc-500">{item.employeeNo} · {item.email}</div>
</TableCell>
<TableCell>{item.roleCodes.join(" / ") || "-"}</TableCell>
<TableCell><StatusBadge status={item.status} /></TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
<Button asChild size="sm" variant="secondary" type="button">
<Link to={`/people/${item.id}`}>查看档案</Link>
</Button>
<Button size="sm" type="button" disabled={onboardingMutation.isPending} onClick={() => onboardingMutation.mutate(item.id)}>
发起入职
</Button>
<Button size="sm" variant="secondary" type="button" disabled={regularizeMutation.isPending} onClick={() => regularizeMutation.mutate(item.id)}>
转正
</Button>
<Button size="sm" variant="secondary" type="button" disabled={offboardingMutation.isPending} onClick={() => offboardingMutation.mutate(item.id)}>
发起离职
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
export default PeoplePage;