HRWD-0..9 实现人事生命周期企微通讯录同步

这个提交包含在:
2026-05-17 21:01:28 +08:00
父节点 778132760f
当前提交 9ae42dadc1
共修改 24 个文件,包含 1553 行新增和 123 行删除
+77 -1
查看文件
@@ -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();
}}
/>
</>
);
}