// 人事异动(M02 /main/user-changes)——由 kd-aiot-frontend-hr-H5/src/api/userChange.ts 迁移,去掉演示模式分支 import { get, postJ, putJ } from "@/utils/request"; import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js"; import Vue from "vue"; import { userChangeStatus, userChangeStages } from "@/pages/hr/data/manage-status"; import { getUserChangeIds, rememberUserChangeId } from "@/utils/hr/storage"; import { getUserById } from "@/api/hr/organization"; // 异动类型文案只有一份来源(api/hr/user.js,与 PC 端 CHANGE_TYPE_LABELS 完全一致) import { CHANGE_TYPE_LABELS } from "@/api/hr/user"; /** 岗位调动(人事异动单)BPM 审批流程标识,需与流程定义 key 一致 */ export const USER_CHANGE_APPROVAL_BUSINESS_KEY = "main_user_change_approval"; // ============ 工具 ============ function ok(res) { return res && Number(res.code) === 0; } function compact(object = {}) { return Object.keys(object).reduce((result, key) => { const value = object[key]; if (value !== "" && value !== null && value !== undefined) result[key] = value; return result; }, {}); } function dateOnly(value) { if (!value) return ""; return String(value).slice(0, 10); } function labelOf(map, raw, fallback = "") { const text = String(raw || "").trim(); if (!text) return fallback; return map[text] || map[text.toUpperCase()] || text; } function asList(data) { if (Array.isArray(data)) return data; if (data && Array.isArray(data.list)) return data.list; if (data && Array.isArray(data.records)) return data.records; if (data && typeof data === "object" && data.id != null) return [data]; return []; } function keepBigIntId(value) { if (value === undefined || value === null || value === "") return undefined; const text = String(value).trim(); if (!/^-?\d+$/.test(text)) return value; if (!Number.isSafeInteger(Number(text))) return text; return Number(text); } function extractId(result) { if (result == null) return null; if (typeof result === "object") return result.id != null ? result.id : result.changeId != null ? result.changeId : null; return result; } function parseSnapshot(raw) { if (!raw) return {}; if (typeof raw === "object") return raw; try { return JSON.parse(String(raw)) || {}; } catch (error) { return {}; } } function detailMap(details) { const map = {}; (Array.isArray(details) ? details : []).forEach((item) => { const field = String((item && item.changeField) || "").toUpperCase(); if (field) map[field] = item; }); return map; } // ============ 枚举 ============ // 文案统一从 api/hr/user.js 再导出,避免两处各写一份导致漂移 // (原先这里写的是「调岗 / 调部门 / 职级调整…」,与 PC 的「岗位调动 / 部门调动 / 职级变更…」不一致) export { CHANGE_TYPE_LABELS }; /** 可发起的异动类型:与 PC 端表单一致,全部 12 种(原先 ESS 精简为 5 种,少 7 类) */ export const ESS_CHANGE_TYPE_KEYS = [ "DEPARTMENT_TRANSFER", "POSITION_TRANSFER", "GRADE_CHANGE", "PROMOTION", "DEMOTION", "SALARY_CHANGE", "MANAGER_CHANGE", "WORK_LOCATION_CHANGE", "FACTORY_TRANSFER", "LEGAL_ENTITY_TRANSFER", "EMPLOYMENT_TYPE_CHANGE", "COMPOSITE_CHANGE", ]; export const ESS_CHANGE_TYPE_OPTIONS = ESS_CHANGE_TYPE_KEYS.map((value) => ({ value, label: CHANGE_TYPE_LABELS[value], })); export function changeTypeLabel(value) { return labelOf(CHANGE_TYPE_LABELS, value || "", CHANGE_TYPE_LABELS.POSITION_TRANSFER); } export const APPROVAL_STATUS_LABELS = { DRAFT: "草稿", PENDING: "审批中", APPROVING: "审批中", APPROVED: "已通过", REJECTED: "已驳回", WITHDRAWN: "已撤回", CANCELLED: "已取消", }; export const EFFECTIVE_STATUS_LABELS = { PENDING: "待生效", SCHEDULED: "已排期", EFFECTIVE: "已生效", CANCELLED: "已取消", }; export const TRANSFER_DISPLAY_STATUS = userChangeStatus; export const TRANSFER_STATUS_FILTERS = [ { value: "all", label: "全部状态" }, { value: "draft", label: "草稿" }, { value: "pending", label: "审批中" }, { value: "approved", label: "已通过" }, { value: "rejected", label: "已驳回" }, ]; // ============ 状态机 ============ export function deriveDocStatus(raw = {}) { const approval = String(raw.approvalStatus || "").toUpperCase(); const effective = String(raw.effectiveStatus || "").toUpperCase(); if (approval === "CANCELLED" || effective === "CANCELLED") return "cancelled"; if (approval === "APPROVED" || effective === "EFFECTIVE") return "approved"; if (approval === "REJECTED") return "rejected"; if (approval === "PENDING" || approval === "APPROVING") return "pending"; if (approval === "WITHDRAWN") return "withdrawn"; return "draft"; } export function canEditUserChange(row = {}) { const status = String(row.approvalStatus || "").toUpperCase(); return status === "DRAFT" || status === "REJECTED" || status === ""; } export function canSubmitUserChange(row = {}) { if (!row || !row.id) return false; return ["draft", "rejected"].includes(row.displayStatus); } export function canWithdrawUserChange(row = {}) { const status = String(row.approvalStatus || "").toUpperCase(); return status === "PENDING" || status === "APPROVING"; } export function canCancelUserChange(row = {}) { if (!row || !row.id) return false; if (String(row.approvalStatus || "").toUpperCase() === "CANCELLED") return false; return String(row.effectiveStatus || "").toUpperCase() !== "EFFECTIVE"; } export function matchDisplayStatus(row = {}, filter = "") { if (!filter || filter === "all") return true; if (filter === "rejected") return ["rejected", "withdrawn", "cancelled"].includes(row.displayStatus); return row.displayStatus === filter; } function stageKeyOf(displayStatus) { if (displayStatus === "approved") return "effective"; if (displayStatus === "pending") return "approval"; return "submit"; } function progressOf(displayStatus, stageKey) { if (displayStatus === "approved") return 100; if (["rejected", "withdrawn", "cancelled"].includes(displayStatus)) return 30; const index = Math.max( userChangeStages.findIndex((stage) => stage.key === stageKey), 0 ); return Math.round(((index + 1) / userChangeStages.length) * 100); } function progressTipOf(displayStatus) { switch (displayStatus) { case "draft": return "草稿尚未提交,提交后进入审批流程。"; case "pending": return "调岗审批中,请等待主管与 HR 的审批结果。"; case "approved": return "调岗审批已通过,将按生效日期更新岗位信息。"; case "rejected": return "审批已驳回,可修改后重新提交。"; case "withdrawn": return "本次调岗申请已撤回。"; case "cancelled": return "本次调岗申请已取消。"; default: return ""; } } // ============ VO → 页面形状 ============ export function adaptUserChange(raw = {}, extras = {}) { const before = parseSnapshot(raw.beforeSnapshot); const after = parseSnapshot(raw.afterSnapshot); const details = Array.isArray(raw.details) ? raw.details : []; const map = detailMap(details); const deptDetail = map.DEPT; const positionDetail = map.POSITION; const gradeDetail = map.GRADE; const locationDetail = map.WORK_LOCATION; const changeType = String(raw.changeType || "").toUpperCase(); const approvalStatus = String(raw.approvalStatus || "").toUpperCase(); const effectiveStatus = String(raw.effectiveStatus || "").toUpperCase(); const displayStatus = deriveDocStatus({ approvalStatus, effectiveStatus }); const statusMeta = TRANSFER_DISPLAY_STATUS[displayStatus] || TRANSFER_DISPLAY_STATUS.draft; const stageKey = stageKeyOf(displayStatus); const name = extras.name || raw.name || raw.employeeName || ""; const dept = extras.dept || raw.dept || raw.originDept || before.deptName || (deptDetail && deptDetail.beforeText) || ""; const position = extras.position || raw.position || raw.originPosition || before.positionName || (positionDetail && positionDetail.beforeText) || ""; const targetDept = raw.targetDept || raw.targetDeptName || after.deptName || (deptDetail && deptDetail.afterText) || ""; const targetPosition = raw.targetPosition || raw.targetPositionName || after.positionName || (positionDetail && positionDetail.afterText) || ""; return { id: raw.id, changeNo: raw.changeNo || raw.contractNo || (raw.id == null ? "" : String(raw.id)), userId: raw.userId != null ? raw.userId : extras.userId, employeeId: raw.userId != null ? raw.userId : extras.employeeId, name, avatar: raw.avatar || String(name).charAt(0), employeeNo: extras.employeeNo || raw.employeeNo || raw.userNo || "", dept, position, level: extras.level || raw.level || before.positionLevelName || before.grade || (gradeDetail && gradeDetail.beforeText) || "", targetDept, targetDeptId: raw.targetDeptId != null ? raw.targetDeptId : deptDetail && deptDetail.afterValue != null ? deptDetail.afterValue : null, targetPosition, targetPositionId: raw.targetPositionId != null ? raw.targetPositionId : positionDetail && positionDetail.afterValue != null ? positionDetail.afterValue : null, targetLevel: raw.targetLevel || after.positionLevelName || after.grade || (gradeDetail && gradeDetail.afterText) || "", workLocation: raw.workLocation || (locationDetail && locationDetail.afterText) || "", changeType, changeTypeLabel: changeTypeLabel(changeType), effectiveDate: dateOnly(raw.effectiveDate), applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate), reason: raw.reason || "", remark: raw.remark || "", approvalNode: raw.approvalNode || "", approvalComment: raw.approvalComment || raw.hrComment || "", approvalStatus, approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, approvalStatus, "草稿"), effectiveStatus, effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, effectiveStatus, "-"), displayStatus, statusLabel: statusMeta.label, statusClass: statusMeta.class, stageKey, stages: userChangeStages, progress: progressOf(displayStatus, stageKey), progressTip: progressTipOf(displayStatus), details, createTime: raw.createTime || "", raw, }; } // ============ 员工信息补全 ============ const employeeCache = {}; export function currentUserId() { const userInfo = uni.getStorageSync("userInfo") || {}; const raw = userInfo.raw || {}; const candidates = [userInfo.userId, userInfo.id, raw.userId, raw.id]; return candidates.find((value) => value !== undefined && value !== null && value !== ""); } async function fetchEmployeeBrief(userId) { if (userId === undefined || userId === null || userId === "") return null; const key = String(userId); if (key in employeeCache) return employeeCache[key]; try { const vo = (await getUserById(key, false)) || {}; const brief = { name: vo.name || vo.userName || "", employeeNo: vo.jobNumber || "", dept: vo.deptName || "", position: vo.postName || vo.position || "", level: vo.positionLevelName || vo.grade || "", }; employeeCache[key] = brief; return brief; } catch (error) { employeeCache[key] = null; return null; } } function extrasOf(brief) { if (!brief) return {}; return { name: brief.name, employeeNo: brief.employeeNo, dept: brief.dept, position: brief.position, level: brief.level, }; } function isMine(row = {}) { const userInfo = uni.getStorageSync("userInfo") || {}; const mine = [userInfo.userId, userInfo.id, userInfo.userName, userInfo.name, userInfo.loginName] .filter((value) => value !== undefined && value !== null && value !== "") .map(String); if (!mine.length) return true; const rowIds = [row.userId, row.employeeNo, row.name] .filter((value) => value !== undefined && value !== null && value !== "") .map(String); return rowIds.some((value) => mine.includes(value)); } function transferText(row = {}) { return `${row.name}${row.employeeNo}${row.changeNo}${row.dept}${row.position}${row.targetDept}${row.targetPosition}`; } // ============ 服务端接口 ============ export async function getUserChangePage(params = {}, loding = true) { const res = await get( Vue.prototype.apiUrl + "/main/user-changes", compact({ pageNum: params.pageNum, size: params.size, userId: keepBigIntId(params.userId), changeType: params.changeType, approvalStatus: params.approvalStatus, }), loding ); if (!ok(res)) return { list: [], count: 0 }; const data = res.data || {}; return { list: asList(data), count: Number((data && (data.count != null ? data.count : data.total)) || 0), }; } export async function getUserChangesByUserId(userId, loding = true) { const res = await get(Vue.prototype.apiUrl + `/main/user-changes/users/${userId}`, {}, loding); if (!ok(res)) return []; return asList(res.data); } export async function getUserChangeById(id, loding = true) { const res = await get(Vue.prototype.apiUrl + `/main/user-changes/${id}`, {}, loding); if (!ok(res)) return null; return res.data; } export async function createUserChange(payload, loding = true) { const res = await postJ(Vue.prototype.apiUrl + "/main/user-changes", payload, loding); if (!ok(res)) throw new Error(res.message || "创建异动单失败"); const id = extractId(res.data); if (id != null) rememberUserChangeId(String(id)); return id; } export async function updateUserChange(payload, loding = true) { const res = await putJ(Vue.prototype.apiUrl + "/main/user-changes", payload, loding); if (!ok(res)) throw new Error(res.message || "更新异动单失败"); if (payload && payload.id != null) rememberUserChangeId(String(payload.id)); return payload && payload.id; } export async function validateUserChange(id, loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/validate`, {}, loding); if (!ok(res)) return null; return res.data; } export async function submitUserChange(id, loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/submit`, {}, loding); if (!ok(res)) throw new Error(res.message || "提交失败"); return res.data; } export async function withdrawUserChange(id, reason = "", loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/withdraw`, compact({ reason }), loding); if (!ok(res)) throw new Error(res.message || "撤回失败"); return res.data; } export async function cancelUserChange(id, reason = "", loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/cancel`, compact({ reason }), loding); if (!ok(res)) throw new Error(res.message || "取消失败"); return res.data; } // ============ 对外统一入口 ============ export async function loadTransferList(params = {}, loding = false) { const { scope = "mine", status = "all", keyword = "", pageNum = 1, size = 50 } = params; const userId = currentUserId(); let rows = []; if (scope === "mine" && userId != null) { try { rows = await getUserChangesByUserId(userId, loding); } catch (error) { rows = []; } } if (!rows.length) { try { const page = await getUserChangePage( { pageNum, size, userId: scope === "mine" ? userId : undefined }, loding ); rows = page.list; } catch (error) { rows = []; } } if (!rows.length) { const details = await Promise.all( getUserChangeIds().map(async (id) => { try { return await getUserChangeById(id, false); } catch (error) { return null; } }) ); rows = details.filter(Boolean); } const list = []; for (const vo of rows) { if (!vo || vo.id == null) continue; const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId)); const row = adaptUserChange(vo, extras); rememberUserChangeId(String(row.id)); if (scope === "mine" && userId != null && !isMine(row)) continue; if (!matchDisplayStatus(row, status)) continue; if (keyword && !transferText(row).includes(keyword)) continue; list.push(row); } return { list, count: list.length }; } export async function loadTransferDetail(id, loding = true) { if (!id) return null; const vo = await getUserChangeById(id, loding); if (!vo) return null; rememberUserChangeId(String(id)); const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId)); return adaptUserChange(vo, extras); } // ============ 员工端申请表单 ============ function inferChangeType(form = {}) { if (form.changeType) return form.changeType; const hasDept = form.targetDeptId != null && form.targetDeptId !== ""; const hasPosition = form.targetPositionId != null && form.targetPositionId !== ""; if (hasDept && hasPosition) return "COMPOSITE_CHANGE"; if (hasDept) return "DEPARTMENT_TRANSFER"; return "POSITION_TRANSFER"; } function buildDetails(form = {}) { if (Array.isArray(form.details) && form.details.length) return form.details; const details = []; if (form.targetDeptName || form.targetDeptId != null) { details.push({ changeField: "DEPT", beforeValue: form.originDeptId != null ? String(form.originDeptId) : undefined, beforeText: form.dept || "", afterValue: form.targetDeptId != null ? String(form.targetDeptId) : undefined, afterText: form.targetDeptName || form.targetDept || "", }); } if (form.targetPositionName || form.targetPositionId != null) { details.push({ changeField: "POSITION", beforeValue: form.originPositionId != null ? String(form.originPositionId) : undefined, beforeText: form.position || "", afterValue: form.targetPositionId != null ? String(form.targetPositionId) : undefined, afterText: form.targetPositionName || form.targetPosition || "", }); } if (form.positionLevel || form.targetLevel) { const level = form.positionLevel || form.targetLevel; details.push({ changeField: "GRADE", beforeValue: form.level || undefined, beforeText: form.level || "", afterValue: level, afterText: level, }); } if (form.targetManagerUserId != null && form.targetManagerUserId !== "") { details.push({ changeField: "MANAGER", beforeValue: form.originManagerUserId != null ? String(form.originManagerUserId) : undefined, beforeText: form.managerName || "", afterValue: String(form.targetManagerUserId), afterText: form.targetManagerName || "", }); } if (form.workLocation) { details.push({ changeField: "WORK_LOCATION", beforeValue: undefined, beforeText: form.originWorkLocation || "", afterValue: form.workLocation, afterText: form.workLocation, }); } return details; } export function buildUserChangePayload(form = {}) { return compact({ id: keepBigIntId(form.id || form.changeId), userId: keepBigIntId(form.userId || form.employeeId), employmentPeriodId: keepBigIntId(form.employmentPeriodId), changeType: inferChangeType(form), effectiveDate: dateOnly(form.effectiveDate) || undefined, reason: form.reason, remark: form.remark, // 手机号:PC 端 buildUserChangePayload 有传,移动端原先漏了(后端会拿不到联系方式) phone: form.phone, // —— 目标组织 —— targetLegalId: keepBigIntId(form.targetLegalId), targetDeptId: keepBigIntId(form.targetDeptId), targetFactoryId: keepBigIntId(form.targetFactoryId), targetTeamId: keepBigIntId(form.targetTeamId), targetManagerUserId: keepBigIntId(form.targetManagerUserId), // —— 目标岗位(含序列/层级/性质快照,接口标记为必填)—— targetPositionId: keepBigIntId(form.targetPositionId), targetPositionName: form.targetPositionName || form.targetPosition, targetEmploymentType: form.targetEmploymentType, positionSequenceId: keepBigIntId(form.positionSequenceId), positionSequence: form.positionSequence, positionLevelId: keepBigIntId(form.positionLevelId), positionLevel: form.positionLevel || form.targetLevel, positionType: form.positionType, positionTypeName: form.positionTypeName, // —— 签章附件 / 线上 Word 模板 —— approvalDocumentFileId: keepBigIntId(form.approvalDocumentFileId), onlineDocumentTemplateId: keepBigIntId(form.onlineDocumentTemplateId), details: buildDetails(form), }); } /** * 组装审批流程 variables(对齐 PC 端 processSubmitDialog 的入参形状)。 * 同时作为审批详情页的数据来源,便于审批人识别业务单据。 */ export function buildUserChangeVariables(form = {}, changeId = null) { const row = form || {}; const businessCode = row.changeNo || row.businessCode || (changeId != null && changeId !== "" ? String(changeId) : ""); return compact({ businessCode, businessName: row.name || row.employeeName || "", businessType: "岗位调动", userId: row.userId != null ? row.userId : row.employeeId, departmentName: row.targetDeptName || row.targetDept || row.dept || "", positionName: row.targetPositionName || row.targetPosition || row.position || "", changeType: row.changeType, changeTypeLabel: row.changeType ? changeTypeLabel(row.changeType) : undefined, effectiveDate: dateOnly(row.effectiveDate) || undefined, }); } /** * 发起人事异动(岗位调动)审批流程。 * businessKey 固定 main_user_change_approval,businessId 为异动单主键。 * 仿照 startAttendanceApproval:先用流程列表接口取 processDefinitionId, * 再调用 /bpm/process-instance/create。 */ export async function startUserChangeApproval(businessId, variables = {}, loding = false) { if (businessId == null || businessId === "") throw new Error("缺少异动单主键,无法发起审批"); const page = await processInstancePage( { pageNo: 1, pageSize: 1, reset: true, key: USER_CHANGE_APPROVAL_BUSINESS_KEY }, loding ); const first = page && page.list && page.list[0]; const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : ""; if (!processDefinitionId) throw new Error("未匹配到岗位调动审批流程,请联系管理员配置流程定义"); return processInstanceCreateAPI( { businessId, businessKey: USER_CHANGE_APPROVAL_BUSINESS_KEY, formCreateUserId: variables.userId || "", processDefinitionId, variables, }, false ); } /** * 提交调岗申请:创建(或更新)异动单后直接发起审批,不再保留草稿态。 * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>} */ export async function submitTransferApplication({ form, existingId = null } = {}) { if (!form) throw new Error("申请参数为空"); const id0 = existingId || form.id || null; const payload = buildUserChangePayload(Object.assign({}, form, { id: id0 })); let changeId = id0; if (id0 != null && id0 !== "") { await updateUserChange(Object.assign({}, payload, { id: changeId })); } else { changeId = await createUserChange(payload); } const variables = buildUserChangeVariables(Object.assign({}, form, { id: changeId }), changeId); let approvalStarted = false; let approvalError = ""; try { await startUserChangeApproval(changeId, variables); approvalStarted = true; } catch (error) { // 审批发起失败不阻断:异动单已落库,提示用户稍后重试,避免重复创建 approvalError = (error && error.message) || "审批流程发起失败"; } return { id: changeId, approvalStarted, approvalError }; } // —— 流程动作 —— export async function actSubmitTransfer(row) { if (!row || !row.id) throw new Error("缺少异动单主键"); const result = await validateUserChange(row.id, false); if (result && result.valid === false) { throw new Error((result.messages || []).join(";") || "异动预校验未通过"); } return startUserChangeApproval(row.id, buildUserChangeVariables(row, row.id)); } export async function actWithdrawTransfer(row, reason = "") { if (!row || !row.id) throw new Error("缺少异动单主键"); return withdrawUserChange(row.id, reason); } export async function actCancelTransfer(row, reason = "") { if (!row || !row.id) throw new Error("缺少异动单主键"); return cancelUserChange(row.id, reason); }