R38: 完成员工管理筛选与部门树试跑
这个提交包含在:
+91
-27
@@ -7,6 +7,7 @@ import {
|
||||
startEmployeeOffboarding,
|
||||
startEmployeeOnboarding,
|
||||
} from "../api/employees";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
@@ -19,10 +20,24 @@ import { toast } from "sonner";
|
||||
export function PeoplePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [q, setQ] = useState("");
|
||||
const [status, setStatus] = useState<"" | "active" | "archived">("");
|
||||
const [departmentId, setDepartmentId] = useState("");
|
||||
|
||||
const departmentsQuery = useQuery({
|
||||
queryKey: ["departments", "people-page", "active"],
|
||||
queryFn: () => fetchDepartments({ includeArchived: false }),
|
||||
});
|
||||
|
||||
const employeesQuery = useQuery({
|
||||
queryKey: ["employees", "people-page", q],
|
||||
queryFn: () => fetchEmployees({ page: 1, pageSize: 100, q: q || undefined }),
|
||||
queryKey: ["employees", "people-page", q, status, departmentId],
|
||||
queryFn: () =>
|
||||
fetchEmployees({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
q: q || undefined,
|
||||
status: status || undefined,
|
||||
departmentId: departmentId || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
const onboardingMutation = useMutation({
|
||||
@@ -74,6 +89,7 @@ export function PeoplePage() {
|
||||
});
|
||||
|
||||
const items = employeesQuery.data?.items ?? [];
|
||||
const departments = departmentsQuery.data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -90,10 +106,48 @@ export function PeoplePage() {
|
||||
/>
|
||||
|
||||
<div className="card-std p-4">
|
||||
<Input placeholder="搜索姓名、工号、手机号、邮箱" value={q} onChange={(event) => setQ(event.target.value)} />
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
placeholder="搜索姓名、工号、手机号、邮箱"
|
||||
value={q}
|
||||
onChange={(event) => setQ(event.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="select-std w-auto"
|
||||
value={departmentId}
|
||||
onChange={(event) => setDepartmentId(event.target.value)}
|
||||
>
|
||||
<option value="">全部部门</option>
|
||||
{departments.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="select-std w-auto"
|
||||
value={status}
|
||||
onChange={(event) => setStatus(event.target.value as "" | "active" | "archived")}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="active">生效中</option>
|
||||
<option value="archived">已归档</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std p-6">
|
||||
{employeesQuery.isError ? (
|
||||
<p className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(employeesQuery.error)
|
||||
? `${employeesQuery.error.code}: ${mapApiErrorMessage(
|
||||
employeesQuery.error.code,
|
||||
employeesQuery.error.details,
|
||||
employeesQuery.error.message,
|
||||
)}`
|
||||
: "加载人员列表失败"}
|
||||
</p>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -104,32 +158,42 @@ export function PeoplePage() {
|
||||
</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>
|
||||
{items.length ? (
|
||||
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>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-3 text-zinc-500" colSpan={4}>
|
||||
暂无匹配员工
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
在新工单中引用
屏蔽一个用户