userChange.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. // 人事异动(M02 /main/user-changes)——由 kd-aiot-frontend-hr-H5/src/api/userChange.ts 迁移,去掉演示模式分支
  2. import { get, postJ, putJ } from "@/utils/request";
  3. import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js";
  4. import Vue from "vue";
  5. import { userChangeStatus, userChangeStages } from "@/pages/hr/data/manage-status";
  6. import { getUserChangeIds, rememberUserChangeId } from "@/utils/hr/storage";
  7. import { getUserById } from "@/api/hr/organization";
  8. // 异动类型文案只有一份来源(api/hr/user.js,与 PC 端 CHANGE_TYPE_LABELS 完全一致)
  9. import { CHANGE_TYPE_LABELS } from "@/api/hr/user";
  10. /** 岗位调动(人事异动单)BPM 审批流程标识,需与流程定义 key 一致 */
  11. export const USER_CHANGE_APPROVAL_BUSINESS_KEY = "main_user_change_approval";
  12. // ============ 工具 ============
  13. function ok(res) {
  14. return res && Number(res.code) === 0;
  15. }
  16. function compact(object = {}) {
  17. return Object.keys(object).reduce((result, key) => {
  18. const value = object[key];
  19. if (value !== "" && value !== null && value !== undefined) result[key] = value;
  20. return result;
  21. }, {});
  22. }
  23. function dateOnly(value) {
  24. if (!value) return "";
  25. return String(value).slice(0, 10);
  26. }
  27. function labelOf(map, raw, fallback = "") {
  28. const text = String(raw || "").trim();
  29. if (!text) return fallback;
  30. return map[text] || map[text.toUpperCase()] || text;
  31. }
  32. function asList(data) {
  33. if (Array.isArray(data)) return data;
  34. if (data && Array.isArray(data.list)) return data.list;
  35. if (data && Array.isArray(data.records)) return data.records;
  36. if (data && typeof data === "object" && data.id != null) return [data];
  37. return [];
  38. }
  39. function keepBigIntId(value) {
  40. if (value === undefined || value === null || value === "") return undefined;
  41. const text = String(value).trim();
  42. if (!/^-?\d+$/.test(text)) return value;
  43. if (!Number.isSafeInteger(Number(text))) return text;
  44. return Number(text);
  45. }
  46. function extractId(result) {
  47. if (result == null) return null;
  48. if (typeof result === "object") return result.id != null ? result.id : result.changeId != null ? result.changeId : null;
  49. return result;
  50. }
  51. function parseSnapshot(raw) {
  52. if (!raw) return {};
  53. if (typeof raw === "object") return raw;
  54. try {
  55. return JSON.parse(String(raw)) || {};
  56. } catch (error) {
  57. return {};
  58. }
  59. }
  60. function detailMap(details) {
  61. const map = {};
  62. (Array.isArray(details) ? details : []).forEach((item) => {
  63. const field = String((item && item.changeField) || "").toUpperCase();
  64. if (field) map[field] = item;
  65. });
  66. return map;
  67. }
  68. // ============ 枚举 ============
  69. // 文案统一从 api/hr/user.js 再导出,避免两处各写一份导致漂移
  70. // (原先这里写的是「调岗 / 调部门 / 职级调整…」,与 PC 的「岗位调动 / 部门调动 / 职级变更…」不一致)
  71. export { CHANGE_TYPE_LABELS };
  72. /** 可发起的异动类型:与 PC 端表单一致,全部 12 种(原先 ESS 精简为 5 种,少 7 类) */
  73. export const ESS_CHANGE_TYPE_KEYS = [
  74. "DEPARTMENT_TRANSFER",
  75. "POSITION_TRANSFER",
  76. "GRADE_CHANGE",
  77. "PROMOTION",
  78. "DEMOTION",
  79. "SALARY_CHANGE",
  80. "MANAGER_CHANGE",
  81. "WORK_LOCATION_CHANGE",
  82. "FACTORY_TRANSFER",
  83. "LEGAL_ENTITY_TRANSFER",
  84. "EMPLOYMENT_TYPE_CHANGE",
  85. "COMPOSITE_CHANGE",
  86. ];
  87. export const ESS_CHANGE_TYPE_OPTIONS = ESS_CHANGE_TYPE_KEYS.map((value) => ({
  88. value,
  89. label: CHANGE_TYPE_LABELS[value],
  90. }));
  91. export function changeTypeLabel(value) {
  92. return labelOf(CHANGE_TYPE_LABELS, value || "", CHANGE_TYPE_LABELS.POSITION_TRANSFER);
  93. }
  94. export const APPROVAL_STATUS_LABELS = {
  95. DRAFT: "草稿",
  96. PENDING: "审批中",
  97. APPROVING: "审批中",
  98. APPROVED: "已通过",
  99. REJECTED: "已驳回",
  100. WITHDRAWN: "已撤回",
  101. CANCELLED: "已取消",
  102. };
  103. export const EFFECTIVE_STATUS_LABELS = {
  104. PENDING: "待生效",
  105. SCHEDULED: "已排期",
  106. EFFECTIVE: "已生效",
  107. CANCELLED: "已取消",
  108. };
  109. export const TRANSFER_DISPLAY_STATUS = userChangeStatus;
  110. export const TRANSFER_STATUS_FILTERS = [
  111. { value: "all", label: "全部状态" },
  112. { value: "draft", label: "草稿" },
  113. { value: "pending", label: "审批中" },
  114. { value: "approved", label: "已通过" },
  115. { value: "rejected", label: "已驳回" },
  116. ];
  117. // ============ 状态机 ============
  118. export function deriveDocStatus(raw = {}) {
  119. const approval = String(raw.approvalStatus || "").toUpperCase();
  120. const effective = String(raw.effectiveStatus || "").toUpperCase();
  121. if (approval === "CANCELLED" || effective === "CANCELLED") return "cancelled";
  122. if (approval === "APPROVED" || effective === "EFFECTIVE") return "approved";
  123. if (approval === "REJECTED") return "rejected";
  124. if (approval === "PENDING" || approval === "APPROVING") return "pending";
  125. if (approval === "WITHDRAWN") return "withdrawn";
  126. return "draft";
  127. }
  128. export function canEditUserChange(row = {}) {
  129. const status = String(row.approvalStatus || "").toUpperCase();
  130. return status === "DRAFT" || status === "REJECTED" || status === "";
  131. }
  132. export function canSubmitUserChange(row = {}) {
  133. if (!row || !row.id) return false;
  134. return ["draft", "rejected"].includes(row.displayStatus);
  135. }
  136. export function canWithdrawUserChange(row = {}) {
  137. const status = String(row.approvalStatus || "").toUpperCase();
  138. return status === "PENDING" || status === "APPROVING";
  139. }
  140. export function canCancelUserChange(row = {}) {
  141. if (!row || !row.id) return false;
  142. if (String(row.approvalStatus || "").toUpperCase() === "CANCELLED") return false;
  143. return String(row.effectiveStatus || "").toUpperCase() !== "EFFECTIVE";
  144. }
  145. export function matchDisplayStatus(row = {}, filter = "") {
  146. if (!filter || filter === "all") return true;
  147. if (filter === "rejected") return ["rejected", "withdrawn", "cancelled"].includes(row.displayStatus);
  148. return row.displayStatus === filter;
  149. }
  150. function stageKeyOf(displayStatus) {
  151. if (displayStatus === "approved") return "effective";
  152. if (displayStatus === "pending") return "approval";
  153. return "submit";
  154. }
  155. function progressOf(displayStatus, stageKey) {
  156. if (displayStatus === "approved") return 100;
  157. if (["rejected", "withdrawn", "cancelled"].includes(displayStatus)) return 30;
  158. const index = Math.max(
  159. userChangeStages.findIndex((stage) => stage.key === stageKey),
  160. 0
  161. );
  162. return Math.round(((index + 1) / userChangeStages.length) * 100);
  163. }
  164. function progressTipOf(displayStatus) {
  165. switch (displayStatus) {
  166. case "draft":
  167. return "草稿尚未提交,提交后进入审批流程。";
  168. case "pending":
  169. return "调岗审批中,请等待主管与 HR 的审批结果。";
  170. case "approved":
  171. return "调岗审批已通过,将按生效日期更新岗位信息。";
  172. case "rejected":
  173. return "审批已驳回,可修改后重新提交。";
  174. case "withdrawn":
  175. return "本次调岗申请已撤回。";
  176. case "cancelled":
  177. return "本次调岗申请已取消。";
  178. default:
  179. return "";
  180. }
  181. }
  182. // ============ VO → 页面形状 ============
  183. export function adaptUserChange(raw = {}, extras = {}) {
  184. const before = parseSnapshot(raw.beforeSnapshot);
  185. const after = parseSnapshot(raw.afterSnapshot);
  186. const details = Array.isArray(raw.details) ? raw.details : [];
  187. const map = detailMap(details);
  188. const deptDetail = map.DEPT;
  189. const positionDetail = map.POSITION;
  190. const gradeDetail = map.GRADE;
  191. const locationDetail = map.WORK_LOCATION;
  192. const changeType = String(raw.changeType || "").toUpperCase();
  193. const approvalStatus = String(raw.approvalStatus || "").toUpperCase();
  194. const effectiveStatus = String(raw.effectiveStatus || "").toUpperCase();
  195. const displayStatus = deriveDocStatus({ approvalStatus, effectiveStatus });
  196. const statusMeta = TRANSFER_DISPLAY_STATUS[displayStatus] || TRANSFER_DISPLAY_STATUS.draft;
  197. const stageKey = stageKeyOf(displayStatus);
  198. const name = extras.name || raw.name || raw.employeeName || "";
  199. const dept = extras.dept || raw.dept || raw.originDept || before.deptName || (deptDetail && deptDetail.beforeText) || "";
  200. const position =
  201. extras.position ||
  202. raw.position ||
  203. raw.originPosition ||
  204. before.positionName ||
  205. (positionDetail && positionDetail.beforeText) ||
  206. "";
  207. const targetDept = raw.targetDept || raw.targetDeptName || after.deptName || (deptDetail && deptDetail.afterText) || "";
  208. const targetPosition =
  209. raw.targetPosition || raw.targetPositionName || after.positionName || (positionDetail && positionDetail.afterText) || "";
  210. return {
  211. id: raw.id,
  212. changeNo: raw.changeNo || raw.contractNo || (raw.id == null ? "" : String(raw.id)),
  213. userId: raw.userId != null ? raw.userId : extras.userId,
  214. employeeId: raw.userId != null ? raw.userId : extras.employeeId,
  215. name,
  216. avatar: raw.avatar || String(name).charAt(0),
  217. employeeNo: extras.employeeNo || raw.employeeNo || raw.userNo || "",
  218. dept,
  219. position,
  220. level:
  221. extras.level ||
  222. raw.level ||
  223. before.positionLevelName ||
  224. before.grade ||
  225. (gradeDetail && gradeDetail.beforeText) ||
  226. "",
  227. targetDept,
  228. targetDeptId: raw.targetDeptId != null ? raw.targetDeptId : deptDetail && deptDetail.afterValue != null ? deptDetail.afterValue : null,
  229. targetPosition,
  230. targetPositionId:
  231. raw.targetPositionId != null
  232. ? raw.targetPositionId
  233. : positionDetail && positionDetail.afterValue != null
  234. ? positionDetail.afterValue
  235. : null,
  236. targetLevel: raw.targetLevel || after.positionLevelName || after.grade || (gradeDetail && gradeDetail.afterText) || "",
  237. workLocation: raw.workLocation || (locationDetail && locationDetail.afterText) || "",
  238. changeType,
  239. changeTypeLabel: changeTypeLabel(changeType),
  240. effectiveDate: dateOnly(raw.effectiveDate),
  241. applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate),
  242. reason: raw.reason || "",
  243. remark: raw.remark || "",
  244. approvalNode: raw.approvalNode || "",
  245. approvalComment: raw.approvalComment || raw.hrComment || "",
  246. approvalStatus,
  247. approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, approvalStatus, "草稿"),
  248. effectiveStatus,
  249. effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, effectiveStatus, "-"),
  250. displayStatus,
  251. statusLabel: statusMeta.label,
  252. statusClass: statusMeta.class,
  253. stageKey,
  254. stages: userChangeStages,
  255. progress: progressOf(displayStatus, stageKey),
  256. progressTip: progressTipOf(displayStatus),
  257. details,
  258. createTime: raw.createTime || "",
  259. raw,
  260. };
  261. }
  262. // ============ 员工信息补全 ============
  263. const employeeCache = {};
  264. export function currentUserId() {
  265. const userInfo = uni.getStorageSync("userInfo") || {};
  266. const raw = userInfo.raw || {};
  267. const candidates = [userInfo.userId, userInfo.id, raw.userId, raw.id];
  268. return candidates.find((value) => value !== undefined && value !== null && value !== "");
  269. }
  270. async function fetchEmployeeBrief(userId) {
  271. if (userId === undefined || userId === null || userId === "") return null;
  272. const key = String(userId);
  273. if (key in employeeCache) return employeeCache[key];
  274. try {
  275. const vo = (await getUserById(key, false)) || {};
  276. const brief = {
  277. name: vo.name || vo.userName || "",
  278. employeeNo: vo.jobNumber || "",
  279. dept: vo.deptName || "",
  280. position: vo.postName || vo.position || "",
  281. level: vo.positionLevelName || vo.grade || "",
  282. };
  283. employeeCache[key] = brief;
  284. return brief;
  285. } catch (error) {
  286. employeeCache[key] = null;
  287. return null;
  288. }
  289. }
  290. function extrasOf(brief) {
  291. if (!brief) return {};
  292. return {
  293. name: brief.name,
  294. employeeNo: brief.employeeNo,
  295. dept: brief.dept,
  296. position: brief.position,
  297. level: brief.level,
  298. };
  299. }
  300. function isMine(row = {}) {
  301. const userInfo = uni.getStorageSync("userInfo") || {};
  302. const mine = [userInfo.userId, userInfo.id, userInfo.userName, userInfo.name, userInfo.loginName]
  303. .filter((value) => value !== undefined && value !== null && value !== "")
  304. .map(String);
  305. if (!mine.length) return true;
  306. const rowIds = [row.userId, row.employeeNo, row.name]
  307. .filter((value) => value !== undefined && value !== null && value !== "")
  308. .map(String);
  309. return rowIds.some((value) => mine.includes(value));
  310. }
  311. function transferText(row = {}) {
  312. return `${row.name}${row.employeeNo}${row.changeNo}${row.dept}${row.position}${row.targetDept}${row.targetPosition}`;
  313. }
  314. // ============ 服务端接口 ============
  315. export async function getUserChangePage(params = {}, loding = true) {
  316. const res = await get(
  317. Vue.prototype.apiUrl + "/main/user-changes",
  318. compact({
  319. pageNum: params.pageNum,
  320. size: params.size,
  321. userId: keepBigIntId(params.userId),
  322. changeType: params.changeType,
  323. approvalStatus: params.approvalStatus,
  324. }),
  325. loding
  326. );
  327. if (!ok(res)) return { list: [], count: 0 };
  328. const data = res.data || {};
  329. return {
  330. list: asList(data),
  331. count: Number((data && (data.count != null ? data.count : data.total)) || 0),
  332. };
  333. }
  334. export async function getUserChangesByUserId(userId, loding = true) {
  335. const res = await get(Vue.prototype.apiUrl + `/main/user-changes/users/${userId}`, {}, loding);
  336. if (!ok(res)) return [];
  337. return asList(res.data);
  338. }
  339. export async function getUserChangeById(id, loding = true) {
  340. const res = await get(Vue.prototype.apiUrl + `/main/user-changes/${id}`, {}, loding);
  341. if (!ok(res)) return null;
  342. return res.data;
  343. }
  344. export async function createUserChange(payload, loding = true) {
  345. const res = await postJ(Vue.prototype.apiUrl + "/main/user-changes", payload, loding);
  346. if (!ok(res)) throw new Error(res.message || "创建异动单失败");
  347. const id = extractId(res.data);
  348. if (id != null) rememberUserChangeId(String(id));
  349. return id;
  350. }
  351. export async function updateUserChange(payload, loding = true) {
  352. const res = await putJ(Vue.prototype.apiUrl + "/main/user-changes", payload, loding);
  353. if (!ok(res)) throw new Error(res.message || "更新异动单失败");
  354. if (payload && payload.id != null) rememberUserChangeId(String(payload.id));
  355. return payload && payload.id;
  356. }
  357. export async function validateUserChange(id, loding = true) {
  358. const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/validate`, {}, loding);
  359. if (!ok(res)) return null;
  360. return res.data;
  361. }
  362. export async function submitUserChange(id, loding = true) {
  363. const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/submit`, {}, loding);
  364. if (!ok(res)) throw new Error(res.message || "提交失败");
  365. return res.data;
  366. }
  367. export async function withdrawUserChange(id, reason = "", loding = true) {
  368. const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/withdraw`, compact({ reason }), loding);
  369. if (!ok(res)) throw new Error(res.message || "撤回失败");
  370. return res.data;
  371. }
  372. export async function cancelUserChange(id, reason = "", loding = true) {
  373. const res = await postJ(Vue.prototype.apiUrl + `/main/user-changes/${id}/cancel`, compact({ reason }), loding);
  374. if (!ok(res)) throw new Error(res.message || "取消失败");
  375. return res.data;
  376. }
  377. // ============ 对外统一入口 ============
  378. export async function loadTransferList(params = {}, loding = false) {
  379. const { scope = "mine", status = "all", keyword = "", pageNum = 1, size = 50 } = params;
  380. const userId = currentUserId();
  381. let rows = [];
  382. if (scope === "mine" && userId != null) {
  383. try {
  384. rows = await getUserChangesByUserId(userId, loding);
  385. } catch (error) {
  386. rows = [];
  387. }
  388. }
  389. if (!rows.length) {
  390. try {
  391. const page = await getUserChangePage(
  392. { pageNum, size, userId: scope === "mine" ? userId : undefined },
  393. loding
  394. );
  395. rows = page.list;
  396. } catch (error) {
  397. rows = [];
  398. }
  399. }
  400. if (!rows.length) {
  401. const details = await Promise.all(
  402. getUserChangeIds().map(async (id) => {
  403. try {
  404. return await getUserChangeById(id, false);
  405. } catch (error) {
  406. return null;
  407. }
  408. })
  409. );
  410. rows = details.filter(Boolean);
  411. }
  412. const list = [];
  413. for (const vo of rows) {
  414. if (!vo || vo.id == null) continue;
  415. const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId));
  416. const row = adaptUserChange(vo, extras);
  417. rememberUserChangeId(String(row.id));
  418. if (scope === "mine" && userId != null && !isMine(row)) continue;
  419. if (!matchDisplayStatus(row, status)) continue;
  420. if (keyword && !transferText(row).includes(keyword)) continue;
  421. list.push(row);
  422. }
  423. return { list, count: list.length };
  424. }
  425. export async function loadTransferDetail(id, loding = true) {
  426. if (!id) return null;
  427. const vo = await getUserChangeById(id, loding);
  428. if (!vo) return null;
  429. rememberUserChangeId(String(id));
  430. const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId));
  431. return adaptUserChange(vo, extras);
  432. }
  433. // ============ 员工端申请表单 ============
  434. function inferChangeType(form = {}) {
  435. if (form.changeType) return form.changeType;
  436. const hasDept = form.targetDeptId != null && form.targetDeptId !== "";
  437. const hasPosition = form.targetPositionId != null && form.targetPositionId !== "";
  438. if (hasDept && hasPosition) return "COMPOSITE_CHANGE";
  439. if (hasDept) return "DEPARTMENT_TRANSFER";
  440. return "POSITION_TRANSFER";
  441. }
  442. function buildDetails(form = {}) {
  443. if (Array.isArray(form.details) && form.details.length) return form.details;
  444. const details = [];
  445. if (form.targetDeptName || form.targetDeptId != null) {
  446. details.push({
  447. changeField: "DEPT",
  448. beforeValue: form.originDeptId != null ? String(form.originDeptId) : undefined,
  449. beforeText: form.dept || "",
  450. afterValue: form.targetDeptId != null ? String(form.targetDeptId) : undefined,
  451. afterText: form.targetDeptName || form.targetDept || "",
  452. });
  453. }
  454. if (form.targetPositionName || form.targetPositionId != null) {
  455. details.push({
  456. changeField: "POSITION",
  457. beforeValue: form.originPositionId != null ? String(form.originPositionId) : undefined,
  458. beforeText: form.position || "",
  459. afterValue: form.targetPositionId != null ? String(form.targetPositionId) : undefined,
  460. afterText: form.targetPositionName || form.targetPosition || "",
  461. });
  462. }
  463. if (form.positionLevel || form.targetLevel) {
  464. const level = form.positionLevel || form.targetLevel;
  465. details.push({
  466. changeField: "GRADE",
  467. beforeValue: form.level || undefined,
  468. beforeText: form.level || "",
  469. afterValue: level,
  470. afterText: level,
  471. });
  472. }
  473. if (form.targetManagerUserId != null && form.targetManagerUserId !== "") {
  474. details.push({
  475. changeField: "MANAGER",
  476. beforeValue: form.originManagerUserId != null ? String(form.originManagerUserId) : undefined,
  477. beforeText: form.managerName || "",
  478. afterValue: String(form.targetManagerUserId),
  479. afterText: form.targetManagerName || "",
  480. });
  481. }
  482. if (form.workLocation) {
  483. details.push({
  484. changeField: "WORK_LOCATION",
  485. beforeValue: undefined,
  486. beforeText: form.originWorkLocation || "",
  487. afterValue: form.workLocation,
  488. afterText: form.workLocation,
  489. });
  490. }
  491. return details;
  492. }
  493. export function buildUserChangePayload(form = {}) {
  494. return compact({
  495. id: keepBigIntId(form.id || form.changeId),
  496. userId: keepBigIntId(form.userId || form.employeeId),
  497. employmentPeriodId: keepBigIntId(form.employmentPeriodId),
  498. changeType: inferChangeType(form),
  499. effectiveDate: dateOnly(form.effectiveDate) || undefined,
  500. reason: form.reason,
  501. remark: form.remark,
  502. // 手机号:PC 端 buildUserChangePayload 有传,移动端原先漏了(后端会拿不到联系方式)
  503. phone: form.phone,
  504. // —— 目标组织 ——
  505. targetLegalId: keepBigIntId(form.targetLegalId),
  506. targetDeptId: keepBigIntId(form.targetDeptId),
  507. targetFactoryId: keepBigIntId(form.targetFactoryId),
  508. targetTeamId: keepBigIntId(form.targetTeamId),
  509. targetManagerUserId: keepBigIntId(form.targetManagerUserId),
  510. // —— 目标岗位(含序列/层级/性质快照,接口标记为必填)——
  511. targetPositionId: keepBigIntId(form.targetPositionId),
  512. targetPositionName: form.targetPositionName || form.targetPosition,
  513. targetEmploymentType: form.targetEmploymentType,
  514. positionSequenceId: keepBigIntId(form.positionSequenceId),
  515. positionSequence: form.positionSequence,
  516. positionLevelId: keepBigIntId(form.positionLevelId),
  517. positionLevel: form.positionLevel || form.targetLevel,
  518. positionType: form.positionType,
  519. positionTypeName: form.positionTypeName,
  520. // —— 签章附件 / 线上 Word 模板 ——
  521. approvalDocumentFileId: keepBigIntId(form.approvalDocumentFileId),
  522. onlineDocumentTemplateId: keepBigIntId(form.onlineDocumentTemplateId),
  523. details: buildDetails(form),
  524. });
  525. }
  526. /**
  527. * 组装审批流程 variables(对齐 PC 端 processSubmitDialog 的入参形状)。
  528. * 同时作为审批详情页的数据来源,便于审批人识别业务单据。
  529. */
  530. export function buildUserChangeVariables(form = {}, changeId = null) {
  531. const row = form || {};
  532. const businessCode =
  533. row.changeNo ||
  534. row.businessCode ||
  535. (changeId != null && changeId !== "" ? String(changeId) : "");
  536. return compact({
  537. businessCode,
  538. businessName: row.name || row.employeeName || "",
  539. businessType: "岗位调动",
  540. userId: row.userId != null ? row.userId : row.employeeId,
  541. departmentName: row.targetDeptName || row.targetDept || row.dept || "",
  542. positionName: row.targetPositionName || row.targetPosition || row.position || "",
  543. changeType: row.changeType,
  544. changeTypeLabel: row.changeType ? changeTypeLabel(row.changeType) : undefined,
  545. effectiveDate: dateOnly(row.effectiveDate) || undefined,
  546. });
  547. }
  548. /**
  549. * 发起人事异动(岗位调动)审批流程。
  550. * businessKey 固定 main_user_change_approval,businessId 为异动单主键。
  551. * 仿照 startAttendanceApproval:先用流程列表接口取 processDefinitionId,
  552. * 再调用 /bpm/process-instance/create。
  553. */
  554. export async function startUserChangeApproval(businessId, variables = {}, loding = false) {
  555. if (businessId == null || businessId === "") throw new Error("缺少异动单主键,无法发起审批");
  556. const page = await processInstancePage(
  557. { pageNo: 1, pageSize: 1, reset: true, key: USER_CHANGE_APPROVAL_BUSINESS_KEY },
  558. loding
  559. );
  560. const first = page && page.list && page.list[0];
  561. const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : "";
  562. if (!processDefinitionId) throw new Error("未匹配到岗位调动审批流程,请联系管理员配置流程定义");
  563. return processInstanceCreateAPI(
  564. {
  565. businessId,
  566. businessKey: USER_CHANGE_APPROVAL_BUSINESS_KEY,
  567. formCreateUserId: variables.userId || "",
  568. processDefinitionId,
  569. variables,
  570. },
  571. false
  572. );
  573. }
  574. /**
  575. * 提交调岗申请:创建(或更新)异动单后直接发起审批,不再保留草稿态。
  576. * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>}
  577. */
  578. export async function submitTransferApplication({ form, existingId = null } = {}) {
  579. if (!form) throw new Error("申请参数为空");
  580. const id0 = existingId || form.id || null;
  581. const payload = buildUserChangePayload(Object.assign({}, form, { id: id0 }));
  582. let changeId = id0;
  583. if (id0 != null && id0 !== "") {
  584. await updateUserChange(Object.assign({}, payload, { id: changeId }));
  585. } else {
  586. changeId = await createUserChange(payload);
  587. }
  588. const variables = buildUserChangeVariables(Object.assign({}, form, { id: changeId }), changeId);
  589. let approvalStarted = false;
  590. let approvalError = "";
  591. try {
  592. await startUserChangeApproval(changeId, variables);
  593. approvalStarted = true;
  594. } catch (error) {
  595. // 审批发起失败不阻断:异动单已落库,提示用户稍后重试,避免重复创建
  596. approvalError = (error && error.message) || "审批流程发起失败";
  597. }
  598. return { id: changeId, approvalStarted, approvalError };
  599. }
  600. // —— 流程动作 ——
  601. export async function actSubmitTransfer(row) {
  602. if (!row || !row.id) throw new Error("缺少异动单主键");
  603. const result = await validateUserChange(row.id, false);
  604. if (result && result.valid === false) {
  605. throw new Error((result.messages || []).join(";") || "异动预校验未通过");
  606. }
  607. return startUserChangeApproval(row.id, buildUserChangeVariables(row, row.id));
  608. }
  609. export async function actWithdrawTransfer(row, reason = "") {
  610. if (!row || !row.id) throw new Error("缺少异动单主键");
  611. return withdrawUserChange(row.id, reason);
  612. }
  613. export async function actCancelTransfer(row, reason = "") {
  614. if (!row || !row.id) throw new Error("缺少异动单主键");
  615. return cancelUserChange(row.id, reason);
  616. }