import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
import { useMemo, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isApiError } from "../api/types";
import {
archiveDepartment,
createDepartment,
fetchDepartments,
updateDepartment,
type DepartmentItem,
type DepartmentTreeNode,
} from "../api/departments";
import { StatusBadge } from "../components/common/StatusBadge";
import { PageHeader } from "../components/common/PageHeader";
import { Button } from "../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { toast } from "sonner";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
function mapDepartmentErrorMessage(error: unknown, fallback: string) {
if (!isApiError(error)) {
return resolveUserErrorMessage(error, fallback);
}
if (error.code === "VALID_001") {
if (error.details?.parentId === "not_found") {
return "上级部门不存在。";
}
if (error.details?.parentId === "archived") {
return "上级部门已归档,无法继续使用。";
}
if (error.details?.parentId === "self_not_allowed") {
return "上级部门不能选择自身。";
}
if (error.details?.parentId === "cycle_not_allowed") {
return "上级部门不能形成循环层级。";
}
if (error.details?.status === "archived") {
return "已归档部门不可编辑。";
}
if (error.details?.department === "has_children") {
return "当前部门存在下级节点,无法归档。";
}
if (error.details?.department === "has_active_employees") {
return "当前部门仍有在职员工,无法归档。";
}
}
return resolveUserErrorMessage(error);
}
function flattenDepartmentTree(nodes: DepartmentTreeNode[], depth = 0): Array<{ id: string; label: string }> {
const items: Array<{ id: string; label: string }> = [];
for (const node of nodes) {
items.push({ id: node.id, label: `${" ".repeat(depth)}${node.name}` });
items.push(...flattenDepartmentTree(node.children, depth + 1));
}
return items;
}
function DepartmentTreeView({ nodes, depth = 0 }: { nodes: DepartmentTreeNode[]; depth?: number }) {
if (!nodes.length) {
return null;
}
return (
{nodes.map((node) => (
{node.archivedAt ?
: null}
{node.children.length ?
: null}
))}
);
}
export function DepartmentsPage() {
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const includeArchivedRaw = searchParams.get("includeArchived") ?? "0";
const includeArchived = includeArchivedRaw === "1" || includeArchivedRaw === "true";
const [name, setName] = useState("");
const [parentId, setParentId] = useState("");
const [error, setError] = useState("");
const [confirmArchive, setConfirmArchive] = useState<{ id: string; name: string } | null>(null);
const [editDepartment, setEditDepartment] = useState(null);
const [editName, setEditName] = useState("");
const [editParentId, setEditParentId] = useState("");
const listQuery = useQuery({
queryKey: ["departments", includeArchived],
queryFn: () => fetchDepartments({ includeArchived }),
});
const departments = listQuery.data?.items ?? [];
const departmentTree = listQuery.data?.tree ?? [];
const departmentById = useMemo(() => {
const map = new Map();
for (const dept of departments) {
map.set(dept.id, dept);
}
return map;
}, [departments]);
const departmentOptions = useMemo(
() => flattenDepartmentTree(departmentTree).filter((item) => !departmentById.get(item.id)?.archivedAt),
[departmentById, departmentTree],
);
const createMutation = useMutation({
mutationFn: () =>
createDepartment({
name: name.trim(),
parentId: parentId ? parentId : null,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["departments"] });
setName("");
setParentId("");
setError("");
toast.success("部门已创建");
},
onError: (err) => {
const msg = mapDepartmentErrorMessage(err, "创建部门失败");
setError(msg);
toast.error(msg);
},
});
const editMutation = useMutation({
mutationFn: () => {
if (!editDepartment) {
throw new Error("未选择待编辑部门");
}
return updateDepartment(editDepartment.id, {
name: editName.trim(),
parentId: editParentId ? editParentId : null,
});
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["departments"] });
setEditDepartment(null);
setEditName("");
setEditParentId("");
setError("");
toast.success("部门已更新");
},
onError: (err) => {
const msg = mapDepartmentErrorMessage(err, "更新部门失败");
setError(msg);
toast.error(msg);
},
});
const archiveMutation = useMutation({
mutationFn: (departmentId: string) => archiveDepartment(departmentId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["departments"] });
setError("");
toast.success("已归档");
},
onError: (err) => {
const msg = mapDepartmentErrorMessage(err, "归档部门失败");
setError(msg);
toast.error(msg);
},
});
return (
返回用户管理
}
/>
{error ? (
{error}
) : null}
新建部门
setName(event.target.value)}
/>
部门树形视图
{listQuery.isLoading ?
: null}
{!listQuery.isLoading && !listQuery.isError ? (
departmentTree.length ? (
) : (
暂无部门层级
)
) : null}
部门列表
{listQuery.isLoading ?
: null}
{listQuery.isError ? (
{resolveUserErrorMessage(listQuery.error, "加载部门列表失败")}
) : null}
{!listQuery.isLoading && !listQuery.isError ? (
名称
上级部门
归档时间
操作
{departments.length ? (
departments.map((item) => {
const parentName = item.parentId ? departmentById.get(item.parentId)?.name ?? "-" : "-";
return (
{item.name}
{item.id}
{parentName}
{item.archivedAt ? item.archivedAt.slice(0, 19).replace("T", " ") : "-"}
{!item.archivedAt ? (
) : (
已归档
)}
);
})
) : (
暂无部门
)}
) : null}
{
if (confirmArchive) archiveMutation.mutate(confirmArchive.id);
}}
onOpenChange={(open) => { if (!open) setConfirmArchive(null); }}
/>
);
}