feat(client): 部门管理与员工创建增强(F32)

- 新增部门管理页:列表/创建/归档
- 用户创建支持选择部门与直属上级
- 补齐 departments/employees API 封装与导航入口
这个提交包含在:
2026-02-09 18:24:07 +08:00
父节点 1a5d3b1d52
当前提交 efa4a3d3f6
共修改 7 个文件,包含 411 行新增和 3 行删除
+59 -2
查看文件
@@ -2,6 +2,8 @@ import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { fetchDepartments } from "../api/departments";
import { fetchEmployees } from "../api/employees";
import {
createUser,
disableUser,
@@ -51,6 +53,8 @@ export function UserManagementPage() {
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [departmentId, setDepartmentId] = useState("");
const [directManagerId, setDirectManagerId] = useState("");
const [createRoles, setCreateRoles] = useState<Set<string>>(() => new Set(["employee"]));
const [roleDialogUser, setRoleDialogUser] = useState<UserSummary | null>(null);
@@ -61,6 +65,16 @@ export function UserManagementPage() {
queryFn: () => fetchUsers({ page, pageSize: PAGE_SIZE, status: status || undefined, q: q || undefined }),
});
const departmentsQuery = useQuery({
queryKey: ["departments", "active"],
queryFn: () => fetchDepartments({ includeArchived: false }),
});
const managersQuery = useQuery({
queryKey: ["employees", "active"],
queryFn: () => fetchEmployees({ page: 1, pageSize: 200, status: "active" }),
});
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
const createPayload = useMemo(() => {
@@ -70,9 +84,11 @@ export function UserManagementPage() {
phone: phone.trim(),
email: email.trim(),
password: password,
departmentId: departmentId || undefined,
directManagerId: directManagerId ? directManagerId : null,
roles: Array.from(createRoles),
};
}, [createRoles, email, employeeNo, name, password, phone]);
}, [createRoles, departmentId, directManagerId, email, employeeNo, name, password, phone]);
const createMutation = useMutation({
mutationFn: () => createUser(createPayload),
@@ -84,6 +100,8 @@ export function UserManagementPage() {
setPhone("");
setEmail("");
setPassword("");
setDepartmentId("");
setDirectManagerId("");
setCreateRoles(new Set(["employee"]));
setError("");
},
@@ -225,6 +243,46 @@ export function UserManagementPage() {
/>
</div>
<div className="grid gap-2">
<Label htmlFor="departmentId">部门(可选)</Label>
<select
id="departmentId"
data-testid="users-create-department"
className="h-11 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
value={departmentId}
onChange={(event) => setDepartmentId(event.target.value)}
>
<option value="">
{departmentsQuery.isLoading ? "加载中..." : "默认(与当前管理员一致)"}
</option>
{(departmentsQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="grid gap-2">
<Label htmlFor="directManagerId">直属上级(可选)</Label>
<select
id="directManagerId"
data-testid="users-create-direct-manager"
className="h-11 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
value={directManagerId}
onChange={(event) => setDirectManagerId(event.target.value)}
>
<option value="">
{managersQuery.isLoading ? "加载中..." : "无"}
</option>
{(managersQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="grid gap-2">
<Label>角色</Label>
<div className="flex flex-wrap gap-3 text-sm">
@@ -500,4 +558,3 @@ export function UserManagementPage() {
</div>
);
}