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.name}
{node.id}
{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); }} /> { if (!open) { setEditDepartment(null); setEditName(""); setEditParentId(""); } }} > 编辑部门 修改部门名称或上级部门,保存后列表和树形视图会同步刷新。
setEditName(event.target.value)} />
); }