HRWD-0..9 实现人事生命周期企微通讯录同步
这个提交包含在:
@@ -7,6 +7,31 @@ export type EmployeeContractStatus = "active" | "expired" | "terminated";
|
||||
export type EmployeeTrainingType = "internal" | "external" | "online";
|
||||
export type EmployeeTrainingStatus = "planned" | "in_progress" | "completed" | "cancelled";
|
||||
export type EmployeeCertificateStatus = "valid" | "expired";
|
||||
export type WecomDirectorySyncOperation = "create" | "update" | "disable";
|
||||
export type WecomDirectorySyncStatus = "running" | "success" | "failed" | "skipped";
|
||||
export type WecomDirectoryStateStatus = "unbound" | "bound" | "synced" | "sync_success" | "sync_skipped" | "sync_failed";
|
||||
|
||||
export type WecomDirectorySyncRun = {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
externalUserId: string | null;
|
||||
operation: WecomDirectorySyncOperation;
|
||||
status: WecomDirectorySyncStatus;
|
||||
attempts: number;
|
||||
errorCode: string | null;
|
||||
errorMessage: string | null;
|
||||
responseSummary: Record<string, unknown> | null;
|
||||
triggeredBy: string | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type WecomDirectoryState = {
|
||||
status: WecomDirectoryStateStatus;
|
||||
externalUserId: string | null;
|
||||
bound: boolean;
|
||||
latestSync: WecomDirectorySyncRun | null;
|
||||
};
|
||||
|
||||
export type EmployeeDepartmentSummary = {
|
||||
id: string;
|
||||
@@ -31,6 +56,7 @@ export type EmployeeSummary = {
|
||||
roleCodes: string[];
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
wecomDirectory?: WecomDirectoryState;
|
||||
};
|
||||
|
||||
export type EmployeeListResponse = {
|
||||
@@ -175,6 +201,7 @@ export type EmployeeProfile = {
|
||||
roleCodes: string[];
|
||||
department: EmployeeDepartmentSummary | null;
|
||||
directManager: { id: string; employeeNo: string; name: string; avatarUrl: string | null } | null;
|
||||
wecomDirectory?: WecomDirectoryState;
|
||||
};
|
||||
lifecycle: {
|
||||
events: EmployeeLifecycleEvent[];
|
||||
@@ -403,6 +430,33 @@ export async function fetchEmployeeLifecycle(employeeId: string) {
|
||||
return apiRequestData<EmployeeProfile["lifecycle"]>(`/api/employees/${employeeId}/lifecycle`);
|
||||
}
|
||||
|
||||
export async function fetchEmployeeWecomDirectorySync(employeeId: string) {
|
||||
return apiRequestData<{
|
||||
employeeId: string;
|
||||
wecomDirectory: WecomDirectoryState;
|
||||
items: WecomDirectorySyncRun[];
|
||||
}>(`/api/employees/${employeeId}/wecom-directory-sync`);
|
||||
}
|
||||
|
||||
export async function retryEmployeeWecomDirectorySync(employeeId: string, operation?: WecomDirectorySyncOperation) {
|
||||
return apiRequestData<{
|
||||
employeeId: string;
|
||||
operation: WecomDirectorySyncOperation;
|
||||
result: {
|
||||
ok: boolean;
|
||||
skipped: boolean;
|
||||
status: WecomDirectorySyncStatus;
|
||||
code: string;
|
||||
message: string | null;
|
||||
run: WecomDirectorySyncRun | null;
|
||||
} | null;
|
||||
wecomDirectory: WecomDirectoryState;
|
||||
}>(`/api/employees/${employeeId}/wecom-directory-sync/retry`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(operation ? { operation } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchEmployeeOrganization() {
|
||||
return apiRequestData<{ items: OrganizationDepartmentSection[] }>("/api/employees/organization");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
patchEmployeeProfile,
|
||||
patchEmployeeTraining,
|
||||
regularizeEmployee,
|
||||
retryEmployeeWecomDirectorySync,
|
||||
skipEmployeeChecklistTask,
|
||||
startEmployeeOffboarding,
|
||||
transferEmployee,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
type EmployeeTraining,
|
||||
type EmployeeTrainingStatus,
|
||||
type EmployeeTrainingType,
|
||||
type WecomDirectoryState,
|
||||
} from "../api/employees";
|
||||
import { fetchEmployeePerformanceTimeline } from "../api/performance";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
@@ -151,6 +153,24 @@ function trainingTypeLabel(type: EmployeeTrainingType) {
|
||||
return TRAINING_TYPE_OPTIONS.find((item) => item.value === type)?.label ?? type;
|
||||
}
|
||||
|
||||
function wecomDirectoryStatusView(state?: WecomDirectoryState) {
|
||||
const errorCode = state?.latestSync?.errorCode ?? "";
|
||||
if (!state) return { label: "未同步", status: "not_triggered", hint: "无记录" };
|
||||
if (state.status === "synced" || state.status === "sync_success") {
|
||||
return { label: "同步成功", status: "succeeded", hint: state.externalUserId ?? "已绑定" };
|
||||
}
|
||||
if (state.status === "bound") return { label: "已绑定", status: "bound", hint: state.externalUserId ?? "已绑定" };
|
||||
if (state.status === "sync_failed") return { label: "同步失败", status: "failed", hint: state.latestSync?.errorCode ?? "待重试" };
|
||||
if (state.status === "sync_skipped") {
|
||||
return {
|
||||
label: errorCode === "wecom_contact_config_missing" || errorCode === "wecom_contact_sync_disabled" ? "配置缺失" : "已跳过",
|
||||
status: "skipped",
|
||||
hint: state.latestSync?.errorCode ?? "未执行",
|
||||
};
|
||||
}
|
||||
return { label: "未绑定", status: "not_triggered", hint: "待创建" };
|
||||
}
|
||||
|
||||
export function EmployeeProfilePage() {
|
||||
const { employeeId = "" } = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -181,6 +201,7 @@ export function EmployeeProfilePage() {
|
||||
const [certificateForm, setCertificateForm] = useState<CertificateFormState>(buildEmptyCertificateForm());
|
||||
const [deleteAvatarConfirmOpen, setDeleteAvatarConfirmOpen] = useState(false);
|
||||
const [deleteContractTarget, setDeleteContractTarget] = useState<EmployeeContract | null>(null);
|
||||
const [offboardingConfirmOpen, setOffboardingConfirmOpen] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
title: "",
|
||||
hireDate: "",
|
||||
@@ -362,12 +383,34 @@ export function EmployeeProfilePage() {
|
||||
const offboardingMutation = useMutation({
|
||||
mutationFn: () => startEmployeeOffboarding(employeeId),
|
||||
onSuccess: async () => {
|
||||
setOffboardingConfirmOpen(false);
|
||||
await queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] });
|
||||
toast.success("已发起离职");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "离职失败")),
|
||||
});
|
||||
|
||||
const wecomDirectoryRetryMutation = useMutation({
|
||||
mutationFn: () => retryEmployeeWecomDirectorySync(employeeId),
|
||||
onSuccess: async (result) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["employee-profile", employeeId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["employees"] }),
|
||||
]);
|
||||
const code = result.result?.code;
|
||||
if (result.result?.skipped) {
|
||||
toast.warning(code ? `企微同步已跳过:${code}` : "企微同步已跳过");
|
||||
return;
|
||||
}
|
||||
if (result.result?.status === "failed") {
|
||||
toast.error(code ? `企微同步失败:${code}` : "企微同步失败");
|
||||
return;
|
||||
}
|
||||
toast.success("企微同步已提交");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "企微同步重试失败")),
|
||||
});
|
||||
|
||||
const contractMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload = {
|
||||
@@ -575,6 +618,7 @@ export function EmployeeProfilePage() {
|
||||
const contracts = contractsQuery.data?.items ?? [];
|
||||
const trainings = trainingsQuery.data?.items ?? [];
|
||||
const certificates = certificatesQuery.data?.items ?? [];
|
||||
const wecomDirectoryView = wecomDirectoryStatusView(profile.employee.wecomDirectory);
|
||||
|
||||
function resetContractEditor() {
|
||||
setEditingContractId(null);
|
||||
@@ -673,7 +717,7 @@ export function EmployeeProfilePage() {
|
||||
<Button type="button" variant="secondary" onClick={() => transferMutation.mutate()} disabled={transferMutation.isPending}>
|
||||
记录调岗
|
||||
</Button>
|
||||
<Button type="button" onClick={() => offboardingMutation.mutate()} disabled={offboardingMutation.isPending}>
|
||||
<Button type="button" onClick={() => setOffboardingConfirmOpen(true)} disabled={offboardingMutation.isPending}>
|
||||
发起离职
|
||||
</Button>
|
||||
</div>
|
||||
@@ -760,6 +804,28 @@ export function EmployeeProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-zinc-500">企业微信通讯录</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<StatusBadge status={wecomDirectoryView.status} label={wecomDirectoryView.label} />
|
||||
<span className="text-sm text-zinc-500">{wecomDirectoryView.hint}</span>
|
||||
</div>
|
||||
</div>
|
||||
{canManageEmployeeProfile ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => wecomDirectoryRetryMutation.mutate()}
|
||||
disabled={wecomDirectoryRetryMutation.isPending}
|
||||
>
|
||||
{wecomDirectoryRetryMutation.isPending ? "同步中..." : "重试同步"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canEditExtendedFields ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
@@ -1459,6 +1525,16 @@ export function EmployeeProfilePage() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ActionConfirmDialog
|
||||
open={offboardingConfirmOpen}
|
||||
onOpenChange={setOffboardingConfirmOpen}
|
||||
kind="delete"
|
||||
targetName={profile.employee.name}
|
||||
description="将发起离职清单;清单完成后会禁用 OA 账号并尝试禁用企业微信成员。"
|
||||
onConfirm={async () => {
|
||||
offboardingMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+165
-64
@@ -9,9 +9,12 @@ import {
|
||||
fetchEmployees,
|
||||
fetchEmployeeOrganization,
|
||||
regularizeEmployee,
|
||||
retryEmployeeWecomDirectorySync,
|
||||
startEmployeeOffboarding,
|
||||
startEmployeeOnboarding,
|
||||
type EmployeeEmploymentStatus,
|
||||
type EmployeeSummary,
|
||||
type WecomDirectoryState,
|
||||
type OrganizationTreeNode,
|
||||
} from "../api/employees";
|
||||
import { exportEmployees } from "../api/exports";
|
||||
@@ -59,6 +62,34 @@ function employmentStatusLabel(status: EmployeeEmploymentStatus | null) {
|
||||
return EMPLOYMENT_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? "-";
|
||||
}
|
||||
|
||||
function wecomDirectoryStatusView(state?: WecomDirectoryState) {
|
||||
const errorCode = state?.latestSync?.errorCode ?? "";
|
||||
if (!state) {
|
||||
return { label: "未同步", status: "not_triggered", hint: "无记录" };
|
||||
}
|
||||
if (state.status === "synced" || state.status === "sync_success") {
|
||||
return { label: "同步成功", status: "succeeded", hint: state.externalUserId ?? "已绑定" };
|
||||
}
|
||||
if (state.status === "bound") {
|
||||
return { label: "已绑定", status: "bound", hint: state.externalUserId ?? "已绑定" };
|
||||
}
|
||||
if (state.status === "sync_failed") {
|
||||
return { label: "同步失败", status: "failed", hint: state.latestSync?.errorCode ?? "待重试" };
|
||||
}
|
||||
if (state.status === "sync_skipped") {
|
||||
return {
|
||||
label: errorCode === "wecom_contact_config_missing" || errorCode === "wecom_contact_sync_disabled" ? "配置缺失" : "已跳过",
|
||||
status: "skipped",
|
||||
hint: state.latestSync?.errorCode ?? "未执行",
|
||||
};
|
||||
}
|
||||
return { label: "未绑定", status: "not_triggered", hint: "待创建" };
|
||||
}
|
||||
|
||||
function shouldShowWecomRetry(state?: WecomDirectoryState) {
|
||||
return !state || ["unbound", "sync_failed", "sync_skipped"].includes(state.status);
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -93,6 +124,7 @@ export function PeoplePage() {
|
||||
const [managerDialogOpen, setManagerDialogOpen] = useState(false);
|
||||
const [enableConfirmOpen, setEnableConfirmOpen] = useState(false);
|
||||
const [disableConfirmOpen, setDisableConfirmOpen] = useState(false);
|
||||
const [offboardingTarget, setOffboardingTarget] = useState<EmployeeSummary | null>(null);
|
||||
const [targetDepartmentId, setTargetDepartmentId] = useState("");
|
||||
const [targetManagerId, setTargetManagerId] = useState("");
|
||||
|
||||
@@ -161,6 +193,7 @@ export function PeoplePage() {
|
||||
mutationFn: (employeeId: string) => startEmployeeOffboarding(employeeId),
|
||||
onSuccess: async () => {
|
||||
await invalidatePeopleQueries();
|
||||
setOffboardingTarget(null);
|
||||
toast.success("已发起离职流程");
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -168,6 +201,26 @@ export function PeoplePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const wecomDirectoryRetryMutation = useMutation({
|
||||
mutationFn: (employeeId: string) => retryEmployeeWecomDirectorySync(employeeId),
|
||||
onSuccess: async (result) => {
|
||||
await invalidatePeopleQueries();
|
||||
const code = result.result?.code;
|
||||
if (result.result?.skipped) {
|
||||
toast.warning(code ? `企微同步已跳过:${code}` : "企微同步已跳过");
|
||||
return;
|
||||
}
|
||||
if (result.result?.status === "failed") {
|
||||
toast.error(code ? `企微同步失败:${code}` : "企微同步失败");
|
||||
return;
|
||||
}
|
||||
toast.success("企微同步已提交");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(mapApiMessage(error, "企微同步重试失败"));
|
||||
},
|
||||
});
|
||||
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: (format: "csv" | "xlsx") =>
|
||||
exportEmployees({
|
||||
@@ -266,8 +319,9 @@ export function PeoplePage() {
|
||||
transferDepartmentMutation.isPending ||
|
||||
setManagerMutation.isPending ||
|
||||
batchEnableMutation.isPending ||
|
||||
batchDisableMutation.isPending;
|
||||
const tableColumnCount = canManageUsers ? 7 : 6;
|
||||
batchDisableMutation.isPending ||
|
||||
wecomDirectoryRetryMutation.isPending;
|
||||
const tableColumnCount = canManageUsers ? 8 : 7;
|
||||
|
||||
function toggleSelection(employeeId: string, checked: boolean) {
|
||||
setSelectedEmployeeIds((current) => {
|
||||
@@ -470,78 +524,105 @@ export function PeoplePage() {
|
||||
<TableHead>岗位 / 雇用状态</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>账号状态</TableHead>
|
||||
<TableHead>企业微信</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{canManageUsers ? (
|
||||
items.map((item) => {
|
||||
const wecomView = wecomDirectoryStatusView(item.wecomDirectory);
|
||||
return (
|
||||
<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>
|
||||
<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 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>
|
||||
</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}` : "-"}
|
||||
</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>
|
||||
</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 ?? "-"}
|
||||
</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>
|
||||
</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>
|
||||
))
|
||||
</TableCell>
|
||||
<TableCell>{item.roleCodes.join(" / ") || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<StatusBadge status={wecomView.status} label={wecomView.label} />
|
||||
<div className="max-w-40 truncate text-xs text-zinc-500">{wecomView.hint}</div>
|
||||
</div>
|
||||
</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>
|
||||
{shouldShowWecomRetry(item.wecomDirectory) ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={wecomDirectoryRetryMutation.isPending}
|
||||
onClick={() => wecomDirectoryRetryMutation.mutate(item.id)}
|
||||
>
|
||||
企微重试
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={offboardingMutation.isPending}
|
||||
onClick={() => setOffboardingTarget(item)}
|
||||
>
|
||||
发起离职
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-3 text-zinc-500" colSpan={tableColumnCount}>
|
||||
@@ -657,6 +738,26 @@ export function PeoplePage() {
|
||||
await batchDisableMutation.mutateAsync();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(offboardingTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setOffboardingTarget(null);
|
||||
}}
|
||||
title="发起离职流程"
|
||||
description={
|
||||
offboardingTarget
|
||||
? `将为 ${offboardingTarget.name} 发起离职清单;清单完成后会禁用 OA 账号并尝试禁用企业微信成员。`
|
||||
: ""
|
||||
}
|
||||
confirmLabel={offboardingMutation.isPending ? "处理中..." : "确认发起"}
|
||||
variant="destructive"
|
||||
onConfirm={async () => {
|
||||
if (offboardingTarget) {
|
||||
await offboardingMutation.mutateAsync(offboardingTarget.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户