665 行
25 KiB
TypeScript
665 行
25 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
batchDisableUsers,
|
|
batchEnableUsers,
|
|
batchSetEmployeesManager,
|
|
batchTransferEmployeesDepartment,
|
|
fetchEmployees,
|
|
fetchEmployeeOrganization,
|
|
regularizeEmployee,
|
|
startEmployeeOffboarding,
|
|
startEmployeeOnboarding,
|
|
type EmployeeEmploymentStatus,
|
|
type OrganizationTreeNode,
|
|
} from "../api/employees";
|
|
import { exportEmployees } from "../api/exports";
|
|
import { fetchDepartments } from "../api/departments";
|
|
import { isApiError } from "../api/types";
|
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
import { hasPermission } from "../auth/permissions";
|
|
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
|
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
|
import { PageHeader } from "../components/common/PageHeader";
|
|
import { Button } from "../components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "../components/ui/dialog";
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
|
import { Input } from "../components/ui/input";
|
|
import { StatusBadge } from "../components/common/StatusBadge";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
|
import { downloadBlob } from "../lib/download";
|
|
import { toast } from "sonner";
|
|
|
|
const EMPLOYMENT_STATUS_OPTIONS: Array<{ value: EmployeeEmploymentStatus; label: string }> = [
|
|
{ value: "onboarding", label: "待入职" },
|
|
{ value: "probation", label: "试用期" },
|
|
{ value: "active", label: "在职" },
|
|
{ value: "offboarding", label: "离职办理中" },
|
|
{ value: "inactive", label: "已离职" },
|
|
];
|
|
|
|
function mapApiMessage(error: unknown, fallback: string) {
|
|
return isApiError(error)
|
|
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
|
: error instanceof Error
|
|
? error.message
|
|
: fallback;
|
|
}
|
|
|
|
function employmentStatusLabel(status: EmployeeEmploymentStatus | null) {
|
|
return EMPLOYMENT_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? "-";
|
|
}
|
|
|
|
function collectOrganizationEmployees(nodes: OrganizationTreeNode[], target: Map<string, { id: string; name: string; employeeNo: string; departmentName: string }>) {
|
|
for (const node of nodes) {
|
|
if (!target.has(node.id)) {
|
|
target.set(node.id, {
|
|
id: node.id,
|
|
name: node.name,
|
|
employeeNo: node.employeeNo,
|
|
departmentName: node.department?.name ?? "未分配部门",
|
|
});
|
|
}
|
|
collectOrganizationEmployees(node.children, target);
|
|
}
|
|
}
|
|
|
|
export function PeoplePage() {
|
|
const { permissions } = useAuth();
|
|
const queryClient = useQueryClient();
|
|
const canManageUsers = hasPermission(permissions, "system.users");
|
|
const canViewStats = canManageUsers || hasPermission(permissions, "report.dashboard.view");
|
|
|
|
const [q, setQ] = useState("");
|
|
const [status, setStatus] = useState<"" | "active" | "archived">("");
|
|
const [departmentId, setDepartmentId] = useState("");
|
|
const [title, setTitle] = useState("");
|
|
const [managerId, setManagerId] = useState("");
|
|
const [hireDateFrom, setHireDateFrom] = useState("");
|
|
const [hireDateTo, setHireDateTo] = useState("");
|
|
const [employmentStatus, setEmploymentStatus] = useState<"" | EmployeeEmploymentStatus>("");
|
|
const [exportError, setExportError] = useState("");
|
|
const [selectedEmployeeIds, setSelectedEmployeeIds] = useState<string[]>([]);
|
|
const [transferDialogOpen, setTransferDialogOpen] = useState(false);
|
|
const [managerDialogOpen, setManagerDialogOpen] = useState(false);
|
|
const [enableConfirmOpen, setEnableConfirmOpen] = useState(false);
|
|
const [disableConfirmOpen, setDisableConfirmOpen] = useState(false);
|
|
const [targetDepartmentId, setTargetDepartmentId] = useState("");
|
|
const [targetManagerId, setTargetManagerId] = useState("");
|
|
|
|
const departmentsQuery = useQuery({
|
|
queryKey: ["departments", "people-page", "active"],
|
|
queryFn: () => fetchDepartments({ includeArchived: false }),
|
|
});
|
|
|
|
const organizationQuery = useQuery({
|
|
queryKey: ["employee-organization", "people-page"],
|
|
queryFn: fetchEmployeeOrganization,
|
|
});
|
|
|
|
const employeesQuery = useQuery({
|
|
queryKey: ["employees", "people-page", q, status, departmentId, title, managerId, hireDateFrom, hireDateTo, employmentStatus],
|
|
queryFn: () =>
|
|
fetchEmployees({
|
|
page: 1,
|
|
pageSize: 100,
|
|
q: q || undefined,
|
|
status: status || undefined,
|
|
departmentId: departmentId || undefined,
|
|
title: title || undefined,
|
|
managerId: managerId || undefined,
|
|
hireDateFrom: hireDateFrom || undefined,
|
|
hireDateTo: hireDateTo || undefined,
|
|
employmentStatus: employmentStatus || undefined,
|
|
}),
|
|
});
|
|
|
|
useEffect(() => {
|
|
setSelectedEmployeeIds([]);
|
|
}, [q, status, departmentId, title, managerId, hireDateFrom, hireDateTo, employmentStatus]);
|
|
|
|
async function invalidatePeopleQueries() {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ["employees"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["employee-organization"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["employee-stats"] }),
|
|
]);
|
|
}
|
|
|
|
const onboardingMutation = useMutation({
|
|
mutationFn: (employeeId: string) => startEmployeeOnboarding(employeeId),
|
|
onSuccess: async () => {
|
|
await invalidatePeopleQueries();
|
|
toast.success("已发起入职流程");
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "发起入职失败"));
|
|
},
|
|
});
|
|
|
|
const regularizeMutation = useMutation({
|
|
mutationFn: (employeeId: string) => regularizeEmployee(employeeId),
|
|
onSuccess: async () => {
|
|
await invalidatePeopleQueries();
|
|
toast.success("已完成转正");
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "转正失败"));
|
|
},
|
|
});
|
|
|
|
const offboardingMutation = useMutation({
|
|
mutationFn: (employeeId: string) => startEmployeeOffboarding(employeeId),
|
|
onSuccess: async () => {
|
|
await invalidatePeopleQueries();
|
|
toast.success("已发起离职流程");
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "发起离职失败"));
|
|
},
|
|
});
|
|
|
|
const exportMutation = useMutation({
|
|
mutationFn: (format: "csv" | "xlsx") =>
|
|
exportEmployees({
|
|
format,
|
|
q: q || undefined,
|
|
status: status || undefined,
|
|
departmentId: departmentId || undefined,
|
|
title: title || undefined,
|
|
managerId: managerId || undefined,
|
|
hireDateFrom: hireDateFrom || undefined,
|
|
hireDateTo: hireDateTo || undefined,
|
|
employmentStatus: employmentStatus || undefined,
|
|
}),
|
|
onSuccess: (response, format) => {
|
|
downloadBlob(response.blob, response.filename ?? `employees.${format}`);
|
|
setExportError("");
|
|
toast.success("导出完成");
|
|
},
|
|
onError: (error) => {
|
|
const message = mapApiMessage(error, "导出失败");
|
|
setExportError(message);
|
|
toast.error(message);
|
|
},
|
|
});
|
|
|
|
const transferDepartmentMutation = useMutation({
|
|
mutationFn: () =>
|
|
batchTransferEmployeesDepartment({
|
|
employeeIds: selectedEmployeeIds,
|
|
departmentId: targetDepartmentId,
|
|
}),
|
|
onSuccess: async (result) => {
|
|
await invalidatePeopleQueries();
|
|
setSelectedEmployeeIds([]);
|
|
setTransferDialogOpen(false);
|
|
setTargetDepartmentId("");
|
|
toast.success(`已批量调部门 ${result.summary.updated} 人`);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "批量调部门失败"));
|
|
},
|
|
});
|
|
|
|
const setManagerMutation = useMutation({
|
|
mutationFn: () =>
|
|
batchSetEmployeesManager({
|
|
employeeIds: selectedEmployeeIds,
|
|
managerId: targetManagerId,
|
|
}),
|
|
onSuccess: async (result) => {
|
|
await invalidatePeopleQueries();
|
|
setSelectedEmployeeIds([]);
|
|
setManagerDialogOpen(false);
|
|
setTargetManagerId("");
|
|
toast.success(`已批量设置上级 ${result.summary.updated} 人`);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "批量设置上级失败"));
|
|
},
|
|
});
|
|
|
|
const batchEnableMutation = useMutation({
|
|
mutationFn: () => batchEnableUsers({ employeeIds: selectedEmployeeIds }),
|
|
onSuccess: async (result) => {
|
|
await invalidatePeopleQueries();
|
|
setSelectedEmployeeIds([]);
|
|
toast.success(`已批量启用 ${result.summary.updated} 人`);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "批量启用失败"));
|
|
},
|
|
});
|
|
|
|
const batchDisableMutation = useMutation({
|
|
mutationFn: () => batchDisableUsers({ employeeIds: selectedEmployeeIds }),
|
|
onSuccess: async (result) => {
|
|
await invalidatePeopleQueries();
|
|
setSelectedEmployeeIds([]);
|
|
toast.success(`已批量禁用 ${result.summary.updated} 人`);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(mapApiMessage(error, "批量禁用失败"));
|
|
},
|
|
});
|
|
|
|
const items = employeesQuery.data?.items ?? [];
|
|
const departments = departmentsQuery.data?.items ?? [];
|
|
const managerMap = new Map<string, { id: string; name: string; employeeNo: string; departmentName: string }>();
|
|
for (const section of organizationQuery.data?.items ?? []) {
|
|
collectOrganizationEmployees(section.roots, managerMap);
|
|
}
|
|
const managerOptions = Array.from(managerMap.values()).sort(
|
|
(left, right) => left.departmentName.localeCompare(right.departmentName) || left.employeeNo.localeCompare(right.employeeNo),
|
|
);
|
|
const batchPending =
|
|
transferDepartmentMutation.isPending ||
|
|
setManagerMutation.isPending ||
|
|
batchEnableMutation.isPending ||
|
|
batchDisableMutation.isPending;
|
|
const tableColumnCount = canManageUsers ? 7 : 6;
|
|
|
|
function toggleSelection(employeeId: string, checked: boolean) {
|
|
setSelectedEmployeeIds((current) => {
|
|
if (!checked) {
|
|
return current.filter((item) => item !== employeeId);
|
|
}
|
|
if (current.includes(employeeId)) {
|
|
return current;
|
|
}
|
|
if (current.length >= 50) {
|
|
toast.error("单次最多选择 50 名员工");
|
|
return current;
|
|
}
|
|
return [...current, employeeId];
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="人员管理"
|
|
subtitle="按当前员工档案与账号状态做筛选、批量处理和统计查看。"
|
|
actions={
|
|
<div className="flex flex-wrap gap-2">
|
|
{canManageUsers ? (
|
|
<Button asChild variant="secondary" type="button">
|
|
<Link to="/payroll">薪资管理</Link>
|
|
</Button>
|
|
) : null}
|
|
{canManageUsers ? (
|
|
<Button asChild variant="secondary" type="button">
|
|
<Link to="/performance">绩效考核</Link>
|
|
</Button>
|
|
) : null}
|
|
{canViewStats ? (
|
|
<Button asChild variant="secondary" type="button">
|
|
<Link to="/people/stats">员工统计</Link>
|
|
</Button>
|
|
) : null}
|
|
{canManageUsers ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="secondary" type="button" disabled={exportMutation.isPending}>
|
|
{exportMutation.isPending ? "导出中..." : "导出"}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onSelect={() => exportMutation.mutate("csv")}>
|
|
导出 CSV
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onSelect={() => exportMutation.mutate("xlsx")}>
|
|
导出 Excel
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
{canManageUsers ? (
|
|
<Button asChild variant="secondary" type="button">
|
|
<Link to="/system/users">先新建账号</Link>
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<div className="card-std space-y-4 p-4">
|
|
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
|
<Input
|
|
placeholder="搜索姓名、工号、手机号、邮箱"
|
|
value={q}
|
|
onChange={(event) => setQ(event.target.value)}
|
|
/>
|
|
<select
|
|
className="select-std w-full"
|
|
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-full"
|
|
value={status}
|
|
onChange={(event) => setStatus(event.target.value as "" | "active" | "archived")}
|
|
>
|
|
<option value="">全部账号状态</option>
|
|
<option value="active">生效中</option>
|
|
<option value="archived">已归档</option>
|
|
</select>
|
|
<Input
|
|
placeholder="按职位模糊筛选"
|
|
value={title}
|
|
onChange={(event) => setTitle(event.target.value)}
|
|
/>
|
|
<select
|
|
className="select-std w-full"
|
|
value={managerId}
|
|
onChange={(event) => setManagerId(event.target.value)}
|
|
>
|
|
<option value="">全部直属上级</option>
|
|
{managerOptions.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.name} · {item.employeeNo} · {item.departmentName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
className="select-std w-full"
|
|
value={employmentStatus}
|
|
onChange={(event) => setEmploymentStatus(event.target.value as "" | EmployeeEmploymentStatus)}
|
|
>
|
|
<option value="">全部雇用状态</option>
|
|
{EMPLOYMENT_STATUS_OPTIONS.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Input
|
|
type="date"
|
|
value={hireDateFrom}
|
|
onChange={(event) => setHireDateFrom(event.target.value)}
|
|
/>
|
|
<Input
|
|
type="date"
|
|
value={hireDateTo}
|
|
onChange={(event) => setHireDateTo(event.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2 text-xs text-zinc-500">
|
|
<span>入职日期从</span>
|
|
<span>{hireDateFrom || "-"}</span>
|
|
<span>到</span>
|
|
<span>{hireDateTo || "-"}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{canManageUsers && selectedEmployeeIds.length ? (
|
|
<div className="card-std flex flex-wrap items-center justify-between gap-3 p-4">
|
|
<div className="text-sm text-zinc-700">
|
|
已选 <span className="font-semibold text-zinc-900">{selectedEmployeeIds.length}</span> 名员工
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={batchPending}
|
|
onClick={() => {
|
|
setTargetDepartmentId("");
|
|
setTransferDialogOpen(true);
|
|
}}
|
|
>
|
|
批量调部门
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={batchPending}
|
|
onClick={() => {
|
|
setTargetManagerId("");
|
|
setManagerDialogOpen(true);
|
|
}}
|
|
>
|
|
批量设置上级
|
|
</Button>
|
|
<Button type="button" variant="secondary" disabled={batchPending} onClick={() => setEnableConfirmOpen(true)}>
|
|
批量启用
|
|
</Button>
|
|
<Button type="button" variant="secondary" disabled={batchPending} onClick={() => setDisableConfirmOpen(true)}>
|
|
批量禁用
|
|
</Button>
|
|
<Button type="button" variant="secondary" disabled={batchPending} onClick={() => setSelectedEmployeeIds([])}>
|
|
清空选择
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="card-std p-6">
|
|
{exportError ? (
|
|
<p className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
|
{exportError}
|
|
</p>
|
|
) : null}
|
|
{employeesQuery.isError ? (
|
|
<p className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
|
{mapApiMessage(employeesQuery.error, "加载人员列表失败")}
|
|
</p>
|
|
) : null}
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{canManageUsers ? <TableHead>选择</TableHead> : null}
|
|
<TableHead>员工</TableHead>
|
|
<TableHead>部门 / 上级</TableHead>
|
|
<TableHead>岗位 / 雇用状态</TableHead>
|
|
<TableHead>角色</TableHead>
|
|
<TableHead>账号状态</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.length ? (
|
|
items.map((item) => (
|
|
<TableRow key={item.id}>
|
|
{canManageUsers ? (
|
|
<TableCell>
|
|
<input
|
|
aria-label={`选择员工 ${item.name}`}
|
|
checked={selectedEmployeeIds.includes(item.id)}
|
|
className="size-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-400"
|
|
disabled={batchPending}
|
|
type="checkbox"
|
|
onChange={(event) => toggleSelection(item.id, event.target.checked)}
|
|
/>
|
|
</TableCell>
|
|
) : null}
|
|
<TableCell>
|
|
<div className="flex items-center gap-3">
|
|
<EmployeeAvatar name={item.name} src={item.avatarUrl} />
|
|
<div>
|
|
<div className="font-medium text-zinc-900">{item.name}</div>
|
|
<div className="text-xs text-zinc-500">
|
|
{item.employeeNo} · {item.email}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1 text-sm">
|
|
<div className="text-zinc-900">{item.department?.name ?? "-"}</div>
|
|
<div className="text-xs text-zinc-500">
|
|
上级:{item.directManager ? `${item.directManager.name} · ${item.directManager.employeeNo}` : "-"}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1 text-sm">
|
|
<div className="text-zinc-900">{item.title || "-"}</div>
|
|
<div className="text-xs text-zinc-500">
|
|
{employmentStatusLabel(item.employmentStatus)} · 入职 {item.hireDate ?? "-"}
|
|
</div>
|
|
</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>
|
|
{canManageUsers ? (
|
|
<>
|
|
<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>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell className="py-3 text-zinc-500" colSpan={tableColumnCount}>
|
|
暂无匹配员工
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<Dialog open={transferDialogOpen} onOpenChange={setTransferDialogOpen}>
|
|
<DialogContent className="max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>批量调部门</DialogTitle>
|
|
<DialogDescription>
|
|
将对已选 {selectedEmployeeIds.length} 名员工统一调部门,并写入 transfer 生命周期事件。
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-zinc-700" htmlFor="people-batch-department">
|
|
目标部门
|
|
</label>
|
|
<select
|
|
id="people-batch-department"
|
|
className="select-std w-full"
|
|
value={targetDepartmentId}
|
|
onChange={(event) => setTargetDepartmentId(event.target.value)}
|
|
>
|
|
<option value="">请选择目标部门</option>
|
|
{departments.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={() => setTransferDialogOpen(false)}>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={!targetDepartmentId || transferDepartmentMutation.isPending}
|
|
onClick={() => transferDepartmentMutation.mutate()}
|
|
>
|
|
{transferDepartmentMutation.isPending ? "处理中..." : "确认调部门"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={managerDialogOpen} onOpenChange={setManagerDialogOpen}>
|
|
<DialogContent className="max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>批量设置直属上级</DialogTitle>
|
|
<DialogDescription>
|
|
将对已选 {selectedEmployeeIds.length} 名员工统一设置直属上级,并写入 transfer 生命周期事件。
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-zinc-700" htmlFor="people-batch-manager">
|
|
目标上级
|
|
</label>
|
|
<select
|
|
id="people-batch-manager"
|
|
className="select-std w-full"
|
|
value={targetManagerId}
|
|
onChange={(event) => setTargetManagerId(event.target.value)}
|
|
>
|
|
<option value="">请选择直属上级</option>
|
|
{managerOptions.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.name} · {item.employeeNo} · {item.departmentName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={() => setManagerDialogOpen(false)}>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={!targetManagerId || setManagerMutation.isPending}
|
|
onClick={() => setManagerMutation.mutate()}
|
|
>
|
|
{setManagerMutation.isPending ? "处理中..." : "确认设置"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<ConfirmDialog
|
|
open={enableConfirmOpen}
|
|
onOpenChange={setEnableConfirmOpen}
|
|
title="批量启用账号"
|
|
description={`将统一启用已选 ${selectedEmployeeIds.length} 名员工账号,并吊销当前登录会话。`}
|
|
confirmLabel={batchEnableMutation.isPending ? "处理中..." : "确认启用"}
|
|
onConfirm={async () => {
|
|
await batchEnableMutation.mutateAsync();
|
|
}}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={disableConfirmOpen}
|
|
onOpenChange={setDisableConfirmOpen}
|
|
title="批量禁用账号"
|
|
description={`将统一禁用已选 ${selectedEmployeeIds.length} 名员工账号,并立即吊销当前登录会话。`}
|
|
confirmLabel={batchDisableMutation.isPending ? "处理中..." : "确认禁用"}
|
|
variant="destructive"
|
|
onConfirm={async () => {
|
|
await batchDisableMutation.mutateAsync();
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default PeoplePage;
|