HRWD-0..9 实现人事生命周期企微通讯录同步
这个提交包含在:
@@ -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();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户