regularization.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. // 员工转正(M02 /main/regularizations)——由 kd-aiot-frontend-hr-H5/src/api/regularization.ts 迁移,去掉演示模式分支
  2. import { get, postJ, putJ } from "@/utils/request";
  3. import Vue from "vue";
  4. import { regularizationStatus, regularizationStages } from "@/pages/hr/data/manage-status";
  5. import {
  6. getRegularizationIds,
  7. loadRegularizationBillMap,
  8. removeRegularizationBillId,
  9. rememberRegularizationId,
  10. saveRegularizationBillId,
  11. } from "@/utils/hr/storage";
  12. import { getOnlineTemplatePage, loadOnlineTemplateList } from "@/api/hr/onlineDocument";
  13. import { pickPrimaryPosition, resolveManagerName } from "@/api/hr/user";
  14. import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js";
  15. /** BPM 业务标识:与 PC 端 src/api/hr/index.js 保持一致 */
  16. export const REGULARIZATION_APPROVAL_BUSINESS_KEY = "main_regularization_approval";
  17. // ============ 工具 ============
  18. function ok(res) {
  19. return res && Number(res.code) === 0;
  20. }
  21. function compact(object = {}) {
  22. return Object.keys(object).reduce((result, key) => {
  23. const value = object[key];
  24. if (value !== "" && value !== null && value !== undefined) result[key] = value;
  25. return result;
  26. }, {});
  27. }
  28. function dateOnly(value) {
  29. if (!value) return "";
  30. return String(value).slice(0, 10);
  31. }
  32. function labelOf(map, raw, fallback = "") {
  33. const text = String(raw || "").trim();
  34. if (!text) return fallback;
  35. return map[text] || map[text.toUpperCase()] || text;
  36. }
  37. function asList(data) {
  38. if (Array.isArray(data)) return data;
  39. if (data && Array.isArray(data.list)) return data.list;
  40. if (data && Array.isArray(data.records)) return data.records;
  41. if (data && typeof data === "object" && data.id != null) return [data];
  42. return [];
  43. }
  44. function keepBigIntId(value) {
  45. if (value === undefined || value === null || value === "") return undefined;
  46. const text = String(value).trim();
  47. if (!/^-?\d+$/.test(text)) return value;
  48. if (!Number.isSafeInteger(Number(text))) return text;
  49. return Number(text);
  50. }
  51. function extractId(result) {
  52. if (result == null) return null;
  53. if (typeof result === "object") {
  54. return result.id != null ? result.id : result.regularizationId != null ? result.regularizationId : null;
  55. }
  56. return result;
  57. }
  58. function firstBigIntId(value) {
  59. if (Array.isArray(value)) {
  60. return keepBigIntId(value.find((item) => item !== undefined && item !== null && item !== ""));
  61. }
  62. if (value && typeof value === "object") {
  63. return keepBigIntId(value.id != null ? value.id : value.fileId != null ? value.fileId : value.value);
  64. }
  65. return keepBigIntId(value);
  66. }
  67. /**
  68. * 多附件主键 → 英文逗号拼接字符串(接口字段要求)
  69. * 兼容数组 / 单值 / 已拼接好的字符串
  70. */
  71. function joinFileIds(value) {
  72. if (Array.isArray(value)) {
  73. return value
  74. .map((id) => String(id == null ? "" : id).trim())
  75. .filter(Boolean)
  76. .join(",");
  77. }
  78. if (value && typeof value === "object") return joinFileIds(firstBigIntId(value));
  79. const text = String(value == null ? "" : value).trim();
  80. return text;
  81. }
  82. /**
  83. * 签署方式:显式 ONLINE / OFFLINE 优先,否则按线上签署开关推导
  84. * 移动端与 PC 一致:线上模板功能未开通,默认线下签署
  85. */
  86. function resolveSigningMethod(form = {}) {
  87. const method = String(form.signingMethod || form.signMethod || "")
  88. .trim()
  89. .toUpperCase();
  90. if (method === "ONLINE" || method === "OFFLINE") return method;
  91. return form.online ? "ONLINE" : "OFFLINE";
  92. }
  93. /** 取第一个非空主键(兼容大整数,统一转字符串) */
  94. function pickBig() {
  95. for (let i = 0; i < arguments.length; i += 1) {
  96. const value = arguments[i];
  97. if (value === undefined || value === null || value === "") continue;
  98. return String(value);
  99. }
  100. return "";
  101. }
  102. /**
  103. * 接口的 manager 字段有的环境存的是**主管 userId(雪花 id)**、有的存姓名。
  104. * 返回大整数就按 id 用,否则返回空串(当作姓名交给调用方)。
  105. */
  106. function managerIdInField(value) {
  107. const text = String(value == null ? "" : value).trim();
  108. return /^\d{10,}$/.test(text) ? text : "";
  109. }
  110. /**
  111. * 是否线上签署:接口 signingMethod(ONLINE 线上 / OFFLINE 线下)优先,
  112. * 老数据未回传该字段时再按线上单据字段推断
  113. */
  114. function resolveOnlineSigning(raw = {}, extras = {}) {
  115. const method = String(raw.signingMethod || raw.signMethod || "")
  116. .trim()
  117. .toUpperCase();
  118. if (method === "ONLINE") return true;
  119. if (method === "OFFLINE") return false;
  120. return Boolean(raw.onlineDocumentId || raw.onlineDocumentTemplateId || extras.onlineDocumentTemplateId);
  121. }
  122. // ============ 枚举 ============
  123. // 文案表保留 FAILED:历史单据里可能存在「不予转正」,展示时要能翻译出来
  124. export const REGULARIZATION_TYPE_LABELS = {
  125. NORMAL: "按期转正",
  126. EARLY: "提前转正",
  127. EXTENDED: "延期转正",
  128. FAILED: "不予转正",
  129. };
  130. /**
  131. * **可发起**的转正类型:与 PC 端新建表单一致,只有 3 种。
  132. * 「不予转正(FAILED)」是审批结论,不在申请页提供 —— 之前直接从 LABELS 派生,
  133. * 导致下拉里多出一条 PC 没有的「不予转正」。
  134. */
  135. export const REGULARIZATION_TYPE_KEYS = ["NORMAL", "EARLY", "EXTENDED"];
  136. export const REGULARIZATION_TYPE_OPTIONS = REGULARIZATION_TYPE_KEYS.map((value) => ({
  137. value,
  138. label: REGULARIZATION_TYPE_LABELS[value],
  139. }));
  140. export const APPROVAL_STATUS_LABELS = {
  141. DRAFT: "草稿",
  142. PENDING: "审批中",
  143. APPROVING: "审批中",
  144. APPROVED: "已通过",
  145. REJECTED: "已驳回",
  146. WITHDRAWN: "已撤回",
  147. CANCELLED: "已取消",
  148. };
  149. export const EFFECTIVE_STATUS_LABELS = {
  150. PENDING: "待生效",
  151. SCHEDULED: "已排期",
  152. EFFECTIVE: "已生效",
  153. CANCELLED: "已取消",
  154. };
  155. export const RESULT_LABELS = {
  156. NORMAL: "按期转正",
  157. EARLY: "提前转正",
  158. EXTENDED: "延期",
  159. FAILED: "不通过",
  160. };
  161. export const DISPLAY_STATUS_LABELS = {
  162. pending_apply: "待申请",
  163. draft: "草稿",
  164. reviewing: "审批中",
  165. done: "已转正",
  166. rejected: "已驳回",
  167. withdrawn: "已撤回",
  168. cancelled: "已取消",
  169. };
  170. export const REGULARIZATION_DISPLAY_STATUS = regularizationStatus;
  171. export const REGULARIZATION_STAGES = regularizationStages;
  172. export const REGULARIZATION_STATUS_FILTERS = [
  173. { value: "all", label: "全部" },
  174. { value: "pending_apply", label: "待申请" },
  175. { value: "draft", label: "草稿" },
  176. { value: "reviewing", label: "审批中" },
  177. { value: "done", label: "已转正" },
  178. { value: "rejected", label: "已驳回" },
  179. { value: "withdrawn", label: "已撤回" },
  180. ];
  181. // ============ 状态机 ============
  182. export function deriveRegularizationDisplayStatus(raw = {}) {
  183. if (!raw || (!raw.id && !raw.regularizationId && !raw.approvalStatus)) return "pending_apply";
  184. const approval = String(raw.approvalStatus || "").toUpperCase();
  185. const effective = String(raw.effectiveStatus || "").toUpperCase();
  186. const result = String(raw.result || "").toUpperCase();
  187. if (approval === "CANCELLED" || effective === "CANCELLED") return "cancelled";
  188. if (approval === "WITHDRAWN") return "withdrawn";
  189. if (approval === "REJECTED") return "rejected";
  190. if (effective === "EFFECTIVE" || (approval === "APPROVED" && (result === "NORMAL" || result === "EARLY" || !result))) {
  191. return "done";
  192. }
  193. if (approval === "PENDING" || approval === "APPROVING" || approval === "APPROVED") return "reviewing";
  194. if (approval === "DRAFT" || !approval) return "draft";
  195. return "pending_apply";
  196. }
  197. export const REGULARIZATION_APPLICABLE_STATUS = ["pending_apply", "draft", "rejected", "withdrawn"];
  198. export function isApplicableRegularizationStatus(status) {
  199. return REGULARIZATION_APPLICABLE_STATUS.indexOf(status) >= 0;
  200. }
  201. export function canApplyRegularization(row = {}) {
  202. return isApplicableRegularizationStatus(row.status);
  203. }
  204. export function canEditRegularization(row = {}) {
  205. return ["draft", "rejected", "withdrawn"].indexOf(row.status) >= 0;
  206. }
  207. export function canSubmitRegularization(row = {}) {
  208. if (!row || !row.regularizationId) return false;
  209. return ["draft", "rejected", "withdrawn"].indexOf(row.status) >= 0;
  210. }
  211. export function canWithdrawRegularization(row = {}) {
  212. return row && row.status === "reviewing" && !!row.regularizationId;
  213. }
  214. export function canCancelRegularization(row = {}) {
  215. if (!row || !row.regularizationId) return false;
  216. return ["draft", "pending_apply", "withdrawn", "rejected"].indexOf(row.status) >= 0;
  217. }
  218. export function matchDisplayStatus(row = {}, filter = "") {
  219. if (!filter || filter === "all") return true;
  220. return row.status === filter;
  221. }
  222. function stageKeyOf(displayStatus) {
  223. if (displayStatus === "done") return "effective";
  224. if (displayStatus === "reviewing") return "approval";
  225. return "submit";
  226. }
  227. function progressOf(displayStatus) {
  228. switch (displayStatus) {
  229. case "done":
  230. return 100;
  231. case "reviewing":
  232. return 60;
  233. case "rejected":
  234. return 40;
  235. case "withdrawn":
  236. case "cancelled":
  237. return 30;
  238. case "draft":
  239. return 20;
  240. default:
  241. return 10;
  242. }
  243. }
  244. function progressTipOf(displayStatus) {
  245. switch (displayStatus) {
  246. case "pending_apply":
  247. return "试用期员工尚未建立转正单,请发起转正申请。";
  248. case "draft":
  249. return "转正申请为草稿,提交后进入审批流程。";
  250. case "reviewing":
  251. return "转正审批中,请等待主管与 HR 的审批结果。";
  252. case "done":
  253. return "转正审批已通过,员工状态按生效日期更新。";
  254. case "rejected":
  255. return "审批已驳回,可修改后重新提交。";
  256. case "withdrawn":
  257. return "本次转正申请已撤回。";
  258. case "cancelled":
  259. return "本次转正申请已取消。";
  260. default:
  261. return "";
  262. }
  263. }
  264. // ============ VO → 页面形状 ============
  265. export function adaptRegularization(raw = {}, extras = {}) {
  266. if (!raw || typeof raw !== "object") return null;
  267. const displayStatus = deriveRegularizationDisplayStatus(raw);
  268. const statusMeta = REGULARIZATION_DISPLAY_STATUS[displayStatus] || REGULARIZATION_DISPLAY_STATUS.draft;
  269. const plannedRegularDate =
  270. dateOnly(raw.plannedRegularDate) ||
  271. dateOnly(extras.plannedRegularDate) ||
  272. dateOnly(extras.plannedDate) ||
  273. "";
  274. const effectiveDate = dateOnly(raw.effectiveDate) || dateOnly(extras.effectiveDate) || "";
  275. return Object.assign({}, raw, {
  276. id: raw.id,
  277. billId: raw.id,
  278. regularizationId: raw.id != null ? raw.id : extras.regularizationId,
  279. userId: raw.userId != null ? raw.userId : extras.userId,
  280. employeeNo: extras.employeeNo || raw.employeeNo || "",
  281. name: extras.name || raw.name || raw.employeeName || extras.employeeName || "",
  282. avatar: extras.avatar || raw.avatar || "",
  283. phone: extras.phone || raw.phone || "",
  284. company: extras.company || raw.organizationName || raw.groupName || raw.company || "",
  285. department: extras.department || raw.deptName || raw.department || "",
  286. position: extras.position || raw.postName || raw.positionName || raw.position || "",
  287. level: extras.level || "",
  288. // ===== 级联主键 / 名称快照(编辑回填、只读详情共用) =====
  289. groupId: pickBig(extras.groupId, raw.groupId, raw.legalId),
  290. deptId: pickBig(extras.deptId, raw.deptId),
  291. positionId: pickBig(raw.positionId, extras.positionId),
  292. positionSequenceId: pickBig(raw.positionSequenceId, extras.positionSequenceId),
  293. positionSequence:
  294. raw.positionSequence || raw.positionSequenceName || raw.sequenceName || extras.positionSequence || "",
  295. positionLevelId: pickBig(raw.positionLevelId, extras.positionLevelId),
  296. positionLevel: raw.positionLevel || raw.positionLevelName || extras.positionLevel || extras.level || "",
  297. // 岗位性质:档案值优先(页面下拉用字典 id),接口给的是编码快照
  298. positionType: pickBig(extras.positionType, raw.positionType),
  299. // employmentType 是岗位性质字典编码,不能当名称用(PC 同注释)
  300. positionTypeName: raw.positionTypeName || extras.positionTypeName || "",
  301. // 主管:manager 字段在部分环境里是 userId(雪花 id),不能当姓名用(同 adaptEmployeeBrief)
  302. manager:
  303. extras.manager ||
  304. raw.managerUserName ||
  305. raw.managerName ||
  306. (managerIdInField(raw.manager) ? "" : raw.manager) ||
  307. "",
  308. managerUserId: pickBig(raw.managerUserId, extras.managerUserId, managerIdInField(raw.manager)),
  309. managerUserName:
  310. raw.managerUserName || raw.managerName || extras.managerUserName || extras.managerName || "",
  311. hireDate: dateOnly(raw.joinDate || raw.hireDate || extras.hireDate) || "",
  312. plannedDate: plannedRegularDate,
  313. plannedRegularDate,
  314. regularDate: effectiveDate || plannedRegularDate,
  315. effectiveDate,
  316. regularizationType: raw.regularizationType || "NORMAL",
  317. regularizationTypeLabel: labelOf(REGULARIZATION_TYPE_LABELS, raw.regularizationType || "NORMAL"),
  318. extensionDays: raw.extensionDays,
  319. extensionReason: raw.extensionReason || "",
  320. newProbationEndDate: dateOnly(raw.newProbationEndDate) || "",
  321. userSummary: raw.userSummary || "",
  322. managerEvaluation: raw.managerEvaluation || "",
  323. hrComment: raw.hrComment || "",
  324. remark: raw.userSummary || extras.remark || "",
  325. approvalStatus: raw.approvalStatus || "",
  326. approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, raw.approvalStatus, "未提交"),
  327. approvalNode: raw.approvalNode || "",
  328. approvalComment: raw.hrComment || raw.managerEvaluation || extras.approvalComment || "",
  329. approvalInstanceId: raw.approvalInstanceId || "",
  330. // 流程详情弹窗按 processInstanceId → approvalInstanceId 依次解析
  331. processInstanceId: raw.processInstanceId || raw.approvalInstanceId || "",
  332. effectiveStatus: raw.effectiveStatus || "",
  333. effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, raw.effectiveStatus, "-"),
  334. result: raw.result || "",
  335. resultLabel: labelOf(RESULT_LABELS, raw.result),
  336. employmentPeriodId: raw.employmentPeriodId,
  337. onlineDocumentId: raw.onlineDocumentId,
  338. onlineDocumentTemplateId: raw.onlineDocumentTemplateId || extras.onlineDocumentTemplateId,
  339. onlineDocumentContent: raw.onlineDocumentContent || "",
  340. onlineDocumentVariableValuesJson: raw.onlineDocumentVariableValuesJson || "",
  341. approvalDocumentFileId: joinFileIds(raw.approvalDocumentFileId),
  342. signingMethod: resolveSigningMethod(raw),
  343. online: resolveOnlineSigning(raw, extras),
  344. status: displayStatus,
  345. statusLabel: statusMeta.label,
  346. statusClass: statusMeta.class,
  347. stageKey: stageKeyOf(displayStatus),
  348. progress: progressOf(displayStatus),
  349. progressTip: progressTipOf(displayStatus),
  350. applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate) || "",
  351. createTime: raw.createTime || "",
  352. source: extras.source || raw.source || "",
  353. raw,
  354. });
  355. }
  356. const EMPLOYEE_FIELDS = [
  357. "id",
  358. "employeeNo",
  359. "name",
  360. "avatar",
  361. "phone",
  362. "company",
  363. "department",
  364. "position",
  365. "level",
  366. "manager",
  367. "hireDate",
  368. "regularDate",
  369. "groupId",
  370. "deptId",
  371. // 级联回填所需的主键 / 名称快照
  372. "positionId",
  373. "positionSequenceId",
  374. "positionSequence",
  375. "positionLevelId",
  376. "positionLevel",
  377. "positionType",
  378. "positionTypeName",
  379. "managerUserId",
  380. "managerUserName",
  381. ];
  382. function pickEmployeeFields(employee = {}) {
  383. return EMPLOYEE_FIELDS.reduce((result, key) => {
  384. if (employee[key] !== undefined) result[key] = employee[key];
  385. return result;
  386. }, {});
  387. }
  388. /** 试用期员工 + 可选转正单 → 列表行 */
  389. export function mergeEmployeeRegularization(employee = {}, bill = null) {
  390. const base = Object.assign({}, pickEmployeeFields(employee), {
  391. userId: employee.id,
  392. id: employee.id,
  393. avatar: employee.avatar || String(employee.name || "").charAt(0),
  394. company: employee.company || employee.legalEntity || "",
  395. department: employee.department || employee.deptName || "",
  396. position: employee.position || employee.postName || "",
  397. manager: employee.manager || employee.managerUserName || "",
  398. hireDate: dateOnly(employee.hireDate),
  399. plannedDate: dateOnly(employee.regularDate),
  400. plannedRegularDate: dateOnly(employee.regularDate),
  401. regularDate: dateOnly(employee.regularDate),
  402. });
  403. if (!bill) {
  404. const statusMeta = REGULARIZATION_DISPLAY_STATUS.pending_apply;
  405. return Object.assign({}, base, {
  406. regularizationId: null,
  407. status: "pending_apply",
  408. statusLabel: statusMeta.label,
  409. statusClass: statusMeta.class,
  410. approvalStatus: "",
  411. approvalStatusLabel: "未提交",
  412. approvalComment: "",
  413. regularizationType: "NORMAL",
  414. regularizationTypeLabel: REGULARIZATION_TYPE_LABELS.NORMAL,
  415. effectiveDate: "",
  416. effectiveStatus: "",
  417. effectiveStatusLabel: "-",
  418. stageKey: "submit",
  419. progress: progressOf("pending_apply"),
  420. progressTip: progressTipOf("pending_apply"),
  421. online: true,
  422. onlineDocumentTemplateId: null,
  423. approvalDocumentFileId: null,
  424. remark: "",
  425. userSummary: "",
  426. managerEvaluation: "",
  427. hrComment: "",
  428. extensionDays: undefined,
  429. extensionReason: "",
  430. newProbationEndDate: "",
  431. });
  432. }
  433. return Object.assign({}, base, adaptRegularization(bill, base), {
  434. id: employee.id,
  435. userId: employee.id,
  436. regularizationId: bill.id,
  437. });
  438. }
  439. /** 表单字段 → /main/regularizations 请求参数(严格对齐 PC buildRegularizationPayload) */
  440. export function buildRegularizationPayload(form = {}) {
  441. const type = form.regularizationType || "NORMAL";
  442. return compact({
  443. id: keepBigIntId(form.regularizationId || form.billId || form.id),
  444. userId: keepBigIntId(form.userId),
  445. employmentPeriodId: keepBigIntId(form.employmentPeriodId),
  446. // 机构 / 部门 / 岗位主键(级联下拉,与录用审批提交字段保持一致)
  447. groupId: keepBigIntId(form.groupId),
  448. deptId: keepBigIntId(form.deptId),
  449. positionId: keepBigIntId(form.positionId),
  450. positionName: form.position || form.positionName || undefined,
  451. positionSequenceId: keepBigIntId(form.positionSequenceId),
  452. positionSequence: form.positionSequence || undefined,
  453. positionLevelId: keepBigIntId(form.positionLevelId),
  454. positionLevel: form.positionLevel || undefined,
  455. positionType: form.positionType || undefined,
  456. positionTypeName: form.positionTypeName || undefined,
  457. plannedRegularDate: dateOnly(form.plannedRegularDate || form.plannedDate || form.regularDate),
  458. effectiveDate: dateOnly(form.effectiveDate || form.regularDate),
  459. regularizationType: type,
  460. extensionDays: type === "EXTENDED" ? Number(form.extensionDays || 0) || undefined : undefined,
  461. extensionReason: type === "EXTENDED" ? form.extensionReason : undefined,
  462. newProbationEndDate: type === "EXTENDED" ? dateOnly(form.newProbationEndDate) : undefined,
  463. userSummary: form.userSummary || form.remark,
  464. managerEvaluation: form.managerEvaluation,
  465. hrComment: form.hrComment,
  466. managerUserId: keepBigIntId(form.managerUserId),
  467. managerUserName: form.managerUserName || form.manager || undefined,
  468. // 申请资料附件:多附件主键以英文逗号拼接
  469. approvalDocumentFileId: joinFileIds(form.approvalDocumentFileId) || undefined,
  470. // 签署方式(接口字段 signingMethod):ONLINE 线上 / OFFLINE 线下
  471. signingMethod: resolveSigningMethod(form),
  472. onlineDocumentTemplateId: form.online ? keepBigIntId(form.onlineDocumentTemplateId || form.templateId) : undefined,
  473. onlineDocumentContent: form.onlineDocumentContent,
  474. onlineDocumentVariableValuesJson: form.onlineDocumentVariableValuesJson,
  475. });
  476. }
  477. // ============ 服务端接口 ============
  478. export function adaptEmployeeBrief(raw = {}) {
  479. const name = raw.name || raw.userName || "";
  480. const primary = pickPrimaryPosition(raw) || {};
  481. const deptIds = Array.isArray(raw.deptIds) ? raw.deptIds.filter((id) => id != null && id !== "") : [];
  482. // 主管:有的接口把「主管」放在 manager 字段里且存的是 **userId(雪花 id)**,
  483. // 不能当姓名用 —— 否则「直属上级」会显示成 1933027632794832897。
  484. const managerName = raw.managerUserName || raw.managerName || "";
  485. const rawManager = String(raw.manager || "").trim();
  486. const managerIdFromField = managerIdInField(rawManager);
  487. return {
  488. id: raw.id,
  489. employeeNo: raw.jobNumber || raw.employeeNo || "",
  490. name,
  491. avatar: raw.avatar || String(name).charAt(0),
  492. phone: raw.phone || "",
  493. company: raw.groupName || raw.company || "",
  494. department: raw.deptName || raw.deptNames || "",
  495. position: raw.postName || raw.position || "",
  496. level: primary.positionLevelName || raw.positionLevelName || raw.level || "",
  497. manager: managerName || (managerIdFromField ? "" : rawManager),
  498. hireDate: dateOnly(raw.joinDate || raw.hireDate || raw.entryDate),
  499. regularDate: dateOnly(raw.positiveDate || raw.regularDate),
  500. // ===== 级联回填所需的组织 / 岗位快照 =====
  501. groupId: pickBig(raw.groupId, raw.legalId),
  502. companyName: raw.groupName || raw.company || "",
  503. deptId: pickBig(deptIds.length ? deptIds[deptIds.length - 1] : raw.deptId),
  504. positionId: pickBig(primary.positionId, raw.positionId),
  505. positionName: raw.postName || raw.position || "",
  506. positionSequenceId: pickBig(primary.positionSequenceId, raw.positionSequenceId),
  507. positionSequence: primary.positionSequenceName || primary.sequenceName || raw.positionSequenceName || "",
  508. positionLevelId: pickBig(primary.positionLevelId, raw.positionLevelId),
  509. positionLevel:
  510. primary.positionLevelName || primary.positionLevel || raw.positionLevelName || raw.positionLevel || "",
  511. // 岗位性质:编码优先取主岗位(positionList)里的字典编码;employmentType 是编码不是名称
  512. positionType: pickBig(primary.positionType, raw.positionType),
  513. positionTypeName: primary.positionTypeName || raw.positionTypeName || "",
  514. // 主管主键:managerUserId / managerId,以及「manager 字段存 id」的兜底
  515. managerUserId: pickBig(raw.managerUserId, raw.managerId, managerIdFromField),
  516. managerUserName: managerName,
  517. };
  518. }
  519. /** 试用期员工分页 GET /main/user/getUserPage */
  520. export async function getProbationUserPage(params = {}, loding = false) {
  521. const res = await get(
  522. Vue.prototype.apiUrl + "/main/user/getUserPage",
  523. compact({
  524. status: 5,
  525. isQueryLZ: 0,
  526. pageNum: params.pageNum,
  527. size: params.size,
  528. }),
  529. loding
  530. );
  531. const data = (res && res.data) || {};
  532. return {
  533. list: asList(data),
  534. count: Number(data.count != null ? data.count : data.total != null ? data.total : 0),
  535. };
  536. }
  537. export async function fetchProbationEmployees(options = {}, loding = false) {
  538. const pageSize = options.pageSize || 200;
  539. const maxPages = options.maxPages || 10;
  540. const list = [];
  541. let pageNum = 1;
  542. let total = Infinity;
  543. while (pageNum <= maxPages && list.length < total) {
  544. const data = await getProbationUserPage({ pageNum, size: pageSize }, loding);
  545. const chunk = Array.isArray(data.list) ? data.list : [];
  546. total = Number(data.count != null ? data.count : list.length + chunk.length);
  547. list.push(...chunk);
  548. if (!chunk.length || chunk.length < pageSize) break;
  549. pageNum += 1;
  550. }
  551. return {
  552. list: list.map(adaptEmployeeBrief),
  553. count: total === Infinity ? list.length : total,
  554. };
  555. }
  556. const employeeCache = {};
  557. /**
  558. * 员工档案补全:优先 /main/users/{id}/profile(含组织 / 岗位快照,可回填级联),
  559. * 失败时退回 /main/user/getById/{id}。
  560. */
  561. export async function fetchRegularizationEmployee(userId, loding = false) {
  562. if (userId === undefined || userId === null || userId === "") return null;
  563. const key = String(userId);
  564. if (key in employeeCache) return employeeCache[key];
  565. let brief = null;
  566. try {
  567. const res = await get(Vue.prototype.apiUrl + `/main/users/${key}/profile`, {}, loding);
  568. if (res && Number(res.code) === 0 && res.data) {
  569. const data = res.data;
  570. const merged = data.user && typeof data.user === "object" ? Object.assign({}, data, data.user) : data;
  571. // 只给了主管主键、没给姓名时,回查一次主管档案补上姓名
  572. // (否则「直属上级」只能空着或显示雪花 id)
  573. if (!merged.managerUserName && !merged.managerName && (merged.managerUserId || merged.managerId)) {
  574. try {
  575. await resolveManagerName({ user: merged });
  576. } catch (error) {
  577. // 主管姓名解析失败不阻塞
  578. }
  579. }
  580. brief = adaptEmployeeBrief(merged);
  581. }
  582. } catch (error) {
  583. brief = null;
  584. }
  585. if (!brief || !brief.name) {
  586. try {
  587. const res = await get(Vue.prototype.apiUrl + `/main/user/getById/${key}`, {}, false);
  588. const fallback = adaptEmployeeBrief((res && res.data) || {});
  589. brief = brief ? Object.assign({}, fallback, brief) : fallback;
  590. } catch (error) {
  591. // 员工档案不可用时保留已有结果
  592. }
  593. }
  594. employeeCache[key] = brief;
  595. return brief;
  596. }
  597. function fetchEmployeeBrief(userId) {
  598. return fetchRegularizationEmployee(userId, false);
  599. }
  600. /** 新建转正草稿 POST /main/regularizations */
  601. export async function createRegularization(payload, loding = true) {
  602. const res = await postJ(Vue.prototype.apiUrl + "/main/regularizations", payload, loding);
  603. if (!ok(res)) throw new Error(res.message || "创建转正单失败");
  604. const id = extractId(res.data);
  605. if (id != null) {
  606. rememberRegularizationId(id);
  607. if (payload.userId != null) saveRegularizationBillId(payload.userId, id);
  608. }
  609. return id;
  610. }
  611. /** 修改转正草稿 PUT /main/regularizations */
  612. export async function updateRegularization(payload, loding = true) {
  613. const res = await putJ(Vue.prototype.apiUrl + "/main/regularizations", payload, loding);
  614. if (!ok(res)) throw new Error(res.message || "更新转正单失败");
  615. const id = payload.id;
  616. if (id != null) {
  617. rememberRegularizationId(id);
  618. if (payload.userId != null) saveRegularizationBillId(payload.userId, id);
  619. }
  620. return id;
  621. }
  622. /** 查询转正单 GET /main/regularizations/{id} */
  623. export async function getRegularizationById(id, extras = {}, loding = true) {
  624. const res = await get(Vue.prototype.apiUrl + `/main/regularizations/${id}`, {}, loding);
  625. if (!ok(res)) return null;
  626. const adapted = adaptRegularization(res.data, extras);
  627. if (adapted && adapted.userId != null && adapted.id != null) {
  628. saveRegularizationBillId(adapted.userId, adapted.id);
  629. }
  630. return adapted;
  631. }
  632. /**
  633. * 提交审批。
  634. * 注意:转正模块后端没有 /submit endpoint(PC 端同样留空,走 BPM 公共流程),
  635. * 因此这里改为发起 BPM 流程;变量缺省时按转正单字段自动组装。
  636. */
  637. export async function submitRegularization(id, row = {}, loding = true) {
  638. if (id == null || id === "") throw new Error("缺少转正单主键");
  639. const source = Object.assign({}, row, { regularizationId: id });
  640. if (!source.regularizationType && id != null) {
  641. try {
  642. const bill = await getRegularizationById(id, {}, false);
  643. if (bill) Object.assign(source, bill);
  644. } catch (error) {
  645. // 详情取不到时按默认类型提交
  646. }
  647. }
  648. const variables = buildRegularizationVariables(source, id);
  649. return startRegularizationApproval(id, variables, loding);
  650. }
  651. export async function withdrawRegularization(id, reason = "", loding = true) {
  652. const res = await postJ(Vue.prototype.apiUrl + `/main/regularizations/${id}/withdraw`, compact({ reason }), loding);
  653. if (!ok(res)) throw new Error(res.message || "撤回失败");
  654. return res.data;
  655. }
  656. export async function cancelRegularization(id, reason = "", loding = true) {
  657. const res = await postJ(Vue.prototype.apiUrl + `/main/regularizations/${id}/cancel`, compact({ reason }), loding);
  658. if (!ok(res)) throw new Error(res.message || "取消失败");
  659. return res.data;
  660. }
  661. /** 任职周期 GET /main/users/{userId}/employment-periods */
  662. export async function getEmploymentPeriods(userId, loding = true) {
  663. const res = await get(Vue.prototype.apiUrl + `/main/users/${userId}/employment-periods`, {}, loding);
  664. if (!ok(res)) return [];
  665. return asList(res.data);
  666. }
  667. /** 从人事时间轴找回转正单 id */
  668. export async function resolveRegularizationIdFromHistory(userId, loding = false) {
  669. const mapped = loadRegularizationBillMap()[String(userId)];
  670. if (mapped) return mapped;
  671. try {
  672. const res = await get(Vue.prototype.apiUrl + `/main/users/${userId}/history`, {}, loding);
  673. const list = asList(res.data);
  674. const hit = list
  675. .slice()
  676. .reverse()
  677. .find((item) => {
  678. const text = `${(item && item.eventType) || ""} ${(item && item.sourceBusinessType) || ""}`.toUpperCase();
  679. return text.includes("REGULAR");
  680. });
  681. if (hit && hit.sourceBusinessId != null) {
  682. saveRegularizationBillId(userId, hit.sourceBusinessId);
  683. return hit.sourceBusinessId;
  684. }
  685. } catch (error) {
  686. // 时间轴不可用时不阻塞列表
  687. }
  688. return null;
  689. }
  690. /** 启用态线上 Word 模板分页 */
  691. export async function getRegularizationTemplatePage(params = {}, loding = true) {
  692. return getOnlineTemplatePage(Object.assign({ templateStatus: "ENABLED" }, params), loding);
  693. }
  694. // ============ 对外统一入口 ============
  695. async function mergeBillsIntoEmployees(list = []) {
  696. const billMap = loadRegularizationBillMap();
  697. return Promise.all(
  698. list.map(async (employee) => {
  699. const billId = billMap[String(employee.id)];
  700. if (!billId) return mergeEmployeeRegularization(employee, null);
  701. try {
  702. const bill = await getRegularizationById(billId, employee, false);
  703. return mergeEmployeeRegularization(employee, bill);
  704. } catch (error) {
  705. removeRegularizationBillId(employee.id);
  706. return mergeEmployeeRegularization(employee, null);
  707. }
  708. })
  709. );
  710. }
  711. function currentUserKeys() {
  712. const userInfo = uni.getStorageSync("userInfo") || {};
  713. const raw = userInfo.raw || {};
  714. return [userInfo.userId, userInfo.id, userInfo.userName, userInfo.name, raw.userId, raw.id, raw.loginName]
  715. .filter((value) => value !== undefined && value !== null && value !== "")
  716. .map(String);
  717. }
  718. function isMine(row = {}) {
  719. const mine = currentUserKeys();
  720. if (!mine.length) return true;
  721. return [row.userId, row.employeeNo, row.name]
  722. .filter((value) => value !== undefined && value !== null && value !== "")
  723. .map(String)
  724. .some((value) => mine.indexOf(value) >= 0);
  725. }
  726. function regularizationText(row = {}) {
  727. return `${row.name}${row.employeeNo}${row.department}${row.position}${row.regularizationTypeLabel}`;
  728. }
  729. /**
  730. * 转正办理列表。
  731. * scope: 'all' 全部员工(HR 视图,默认);'mine' 只看本人
  732. */
  733. export async function loadRegularizationList(params = {}) {
  734. const { scope = "all", status = "all", keyword = "" } = params;
  735. const match = (rows) =>
  736. rows
  737. .filter((row) => (scope === "mine" ? isMine(row) : true))
  738. .filter((row) => matchDisplayStatus(row, status))
  739. .filter((row) => (keyword ? regularizationText(row).includes(keyword) : true));
  740. let rows = [];
  741. try {
  742. const { list } = await fetchProbationEmployees();
  743. rows = await mergeBillsIntoEmployees(list);
  744. } catch (error) {
  745. rows = [];
  746. }
  747. const ids = getRegularizationIds();
  748. const attached = new Set(rows.filter((row) => row.regularizationId).map((row) => String(row.regularizationId)));
  749. const missing = ids.filter((id) => !attached.has(String(id)));
  750. if (missing.length) {
  751. const bills = (await Promise.all(missing.map((id) => getRegularizationById(id, {}, false).catch(() => null)))).filter(
  752. Boolean
  753. );
  754. bills.forEach((bill) => {
  755. const index = rows.findIndex((row) => String(row.userId) === String(bill.userId));
  756. if (index >= 0) {
  757. rows[index] = mergeEmployeeRegularization(rows[index], bill);
  758. } else {
  759. rows.push(
  760. Object.assign({}, bill, {
  761. id: bill.userId,
  762. userId: bill.userId,
  763. regularizationId: bill.regularizationId || bill.id,
  764. })
  765. );
  766. }
  767. });
  768. }
  769. return { list: match(rows), count: rows.length, source: "api" };
  770. }
  771. /**
  772. * 转正单详情;传列表行时优先用行内数据兜底。
  773. * 转正单详情接口只回 userId/deptId,姓名 / 岗位 / 组织快照用员工档案补齐,
  774. * 并回带机构 / 部门 / 岗位序列 / 岗位层级主键,供编辑态级联定位。
  775. */
  776. export async function loadRegularizationDetail(id, fallbackRow = null, loding = true) {
  777. if (!id) {
  778. if (!fallbackRow) return null;
  779. const brief = await fetchEmployeeBrief(fallbackRow.userId);
  780. return adaptRegularization(fallbackRow, Object.assign({}, fallbackRow, brief || {}));
  781. }
  782. const extras = fallbackRow
  783. ? {
  784. userId: fallbackRow.userId,
  785. employeeNo: fallbackRow.employeeNo,
  786. name: fallbackRow.name,
  787. avatar: fallbackRow.avatar,
  788. phone: fallbackRow.phone,
  789. company: fallbackRow.company,
  790. department: fallbackRow.department,
  791. position: fallbackRow.position,
  792. level: fallbackRow.level,
  793. manager: fallbackRow.manager,
  794. hireDate: fallbackRow.hireDate,
  795. groupId: fallbackRow.groupId,
  796. deptId: fallbackRow.deptId,
  797. positionId: fallbackRow.positionId,
  798. positionSequenceId: fallbackRow.positionSequenceId,
  799. positionSequence: fallbackRow.positionSequence,
  800. positionLevelId: fallbackRow.positionLevelId,
  801. positionLevel: fallbackRow.positionLevel,
  802. positionType: fallbackRow.positionType,
  803. positionTypeName: fallbackRow.positionTypeName,
  804. managerUserId: fallbackRow.managerUserId,
  805. managerUserName: fallbackRow.managerUserName,
  806. }
  807. : {};
  808. let bill = await getRegularizationById(id, extras, loding);
  809. if (!bill) return null;
  810. if (!bill.name || !bill.department || !bill.groupId) {
  811. const brief = await fetchEmployeeBrief(bill.userId);
  812. if (brief) {
  813. bill = adaptRegularization(
  814. Object.assign({}, bill.raw || {}, bill),
  815. Object.assign({}, brief, extras, {
  816. userId: bill.userId,
  817. name: bill.name || brief.name,
  818. avatar: bill.avatar || brief.avatar,
  819. employeeNo: bill.employeeNo || brief.employeeNo,
  820. phone: bill.phone || brief.phone,
  821. company: bill.company || brief.company || brief.companyName,
  822. department: bill.department || brief.department,
  823. position: bill.position || brief.position || brief.positionName,
  824. level: bill.level || brief.level || brief.positionLevel,
  825. manager: bill.manager || brief.manager || brief.managerUserName,
  826. hireDate: bill.hireDate || brief.hireDate,
  827. })
  828. );
  829. }
  830. }
  831. return fallbackRow
  832. ? Object.assign({}, fallbackRow, bill, {
  833. userId: bill.userId != null ? bill.userId : fallbackRow.userId,
  834. groupId: bill.groupId || fallbackRow.groupId,
  835. })
  836. : bill;
  837. }
  838. function toApplicableEmployeeOptions(rows = []) {
  839. return rows.filter((row) => isApplicableRegularizationStatus(row.status)).map((row) => ({
  840. id: row.userId,
  841. userId: row.userId,
  842. employeeNo: row.employeeNo || "",
  843. name: row.name || "",
  844. avatar: row.avatar || String(row.name || "").charAt(0),
  845. phone: row.phone || "",
  846. company: row.company || "",
  847. department: row.department || "",
  848. position: row.position || "",
  849. level: row.level || "",
  850. manager: row.manager || "",
  851. hireDate: row.hireDate || "",
  852. regularDate: dateOnly(row.plannedRegularDate || row.regularDate),
  853. // ===== 级联回填所需的组织 / 岗位快照 =====
  854. groupId: pickBig(row.groupId),
  855. deptId: pickBig(row.deptId),
  856. positionId: pickBig(row.positionId),
  857. positionSequenceId: pickBig(row.positionSequenceId),
  858. positionSequence: row.positionSequence || "",
  859. positionLevelId: pickBig(row.positionLevelId),
  860. positionLevel: row.positionLevel || "",
  861. positionType: pickBig(row.positionType),
  862. positionTypeName: row.positionTypeName || "",
  863. managerUserId: pickBig(row.managerUserId),
  864. managerUserName: row.managerUserName || row.manager || "",
  865. }));
  866. }
  867. /** 转正申请表单选项:可办理的试用期员工 + 启用态线上模板 */
  868. export async function loadRegularizationFormOptions() {
  869. const [employeePage, templates] = await Promise.all([
  870. fetchProbationEmployees().catch(() => ({ list: [] })),
  871. getRegularizationTemplatePage({}, false)
  872. .then((result) => result.list)
  873. .catch(() => []),
  874. ]);
  875. const source = Array.isArray(employeePage.list) ? employeePage.list : [];
  876. const rows = await mergeBillsIntoEmployees(source);
  877. let fallbackTemplates = [];
  878. if (!templates.length) {
  879. fallbackTemplates = await loadOnlineTemplateList({ templateStatus: "ENABLED" }, false)
  880. .then((page) => page.list || [])
  881. .catch(() => []);
  882. }
  883. return {
  884. employees: toApplicableEmployeeOptions(rows),
  885. templates: templates.length ? templates : fallbackTemplates,
  886. };
  887. }
  888. /** 某员工的任职周期,用于带出计划转正日与 employmentPeriodId */
  889. export async function loadEmploymentPeriod(userId, loding = false) {
  890. try {
  891. const list = await getEmploymentPeriods(userId, loding);
  892. return list.find((item) => String(item.periodStatus || "").toUpperCase() === "ACTIVE") || list[0] || null;
  893. } catch (error) {
  894. return null;
  895. }
  896. }
  897. /** 提交(或存草稿)转正申请 */
  898. export async function submitRegularizationApplication({ form, submit = true, existingId = null } = {}) {
  899. if (!form) throw new Error("申请参数为空");
  900. const id = existingId || form.regularizationId || form.id || null;
  901. if (id && form.status && !canEditRegularization({ status: form.status, regularizationId: id })) {
  902. throw new Error("当前单据状态不可编辑或提交");
  903. }
  904. if (!submit) {
  905. // 仅存草稿
  906. return saveRegularizationDraft(Object.assign({}, form, { regularizationId: id }), true);
  907. }
  908. const result = await submitRegularizationFlow({ form: Object.assign({}, form, { regularizationId: id }), existingId: id });
  909. if (!result.approvalStarted) throw new Error(result.approvalError || "审批发起失败");
  910. return result.id;
  911. }
  912. // —— 流程动作 ——
  913. export async function actSubmitRegularization(row) {
  914. if (!row || !row.regularizationId) throw new Error("缺少转正单主键");
  915. return submitRegularization(row.regularizationId, row);
  916. }
  917. export async function actWithdrawRegularization(row, reason = "") {
  918. if (!row || !row.regularizationId) throw new Error("缺少转正单主键");
  919. return withdrawRegularization(row.regularizationId, reason);
  920. }
  921. export async function actCancelRegularization(row, reason = "") {
  922. if (!row || !row.regularizationId) throw new Error("缺少转正单主键");
  923. const result = await cancelRegularization(row.regularizationId, reason);
  924. removeRegularizationBillId(row.userId);
  925. return result;
  926. }
  927. export function currentRegularizationUserId() {
  928. const userInfo = uni.getStorageSync("userInfo") || {};
  929. const raw = userInfo.raw || {};
  930. const candidates = [userInfo.userId, userInfo.id, raw.userId, raw.id];
  931. return candidates.find((value) => value !== undefined && value !== null && value !== "");
  932. }
  933. /** 解析某员工当前的转正单主键 */
  934. export async function resolveRegularizationId(userId) {
  935. if (userId === undefined || userId === null || userId === "") return null;
  936. const mapped = loadRegularizationBillMap()[String(userId)];
  937. if (mapped) return mapped;
  938. return resolveRegularizationIdFromHistory(userId);
  939. }
  940. // ============ PC 端新建页对齐:签署方式 / 直属上级 / 回填 / 审批发起 ============
  941. /** 签署方式:移动端页面不再提供切换,统一按 OFFLINE(线下签署)提交;此表仅用于回显旧数据 */
  942. export const SIGNING_METHOD_LABELS = {
  943. ONLINE: "线上签署",
  944. OFFLINE: "线下签署",
  945. };
  946. export function normalizeSigningMethod(raw = "") {
  947. const text = String(raw || "")
  948. .trim()
  949. .toUpperCase();
  950. return text === "ONLINE" ? "ONLINE" : "OFFLINE";
  951. }
  952. /**
  953. * 直属上级候选搜索(全员):GET /main/user/getUserPage
  954. * 与 PC 端 remote-method 的 searchManagers 行为一致,按姓名 / 工号模糊匹配。
  955. */
  956. export async function searchManagerCandidates(keyword = "", loding = false) {
  957. try {
  958. const res = await get(
  959. Vue.prototype.apiUrl + "/main/user/getUserPage",
  960. compact({
  961. pageNum: 1,
  962. size: 50,
  963. userName: String(keyword || "").trim() || undefined,
  964. isQueryLZ: 0,
  965. }),
  966. loding
  967. );
  968. const data = (res && res.data) || {};
  969. return asList(data).map((item) => ({
  970. id: item.id,
  971. userId: item.id,
  972. name: item.name || item.userName || "",
  973. employeeNo: item.jobNumber || item.employeeNo || "",
  974. label: `${item.name || item.userName || ""}(${item.jobNumber || item.employeeNo || "-"})`,
  975. }));
  976. } catch (error) {
  977. return [];
  978. }
  979. }
  980. /** 列表行 / 员工档案 → 可编辑表单(编辑回填、详情只读共用同一形状) */
  981. export function hydrateRegularizationForm(source = {}, extras = {}) {
  982. const raw = source || {};
  983. return Object.assign({}, raw, {
  984. regularizationId: raw.regularizationId != null ? raw.regularizationId : raw.billId != null ? raw.billId : raw.id,
  985. billId: raw.regularizationId != null ? raw.regularizationId : raw.billId != null ? raw.billId : raw.id,
  986. userId: raw.userId != null ? raw.userId : extras.userId,
  987. name: raw.name || extras.name || "",
  988. employeeNo: raw.employeeNo || extras.employeeNo || "",
  989. avatar: raw.avatar || extras.avatar || String(raw.name || extras.name || "").charAt(0),
  990. phone: raw.phone || extras.phone || "",
  991. company: raw.company || extras.company || "",
  992. groupId: raw.groupId || extras.groupId || "",
  993. deptId: raw.deptId || extras.deptId || "",
  994. department: raw.department || extras.department || "",
  995. positionId: raw.positionId || extras.positionId || "",
  996. position: raw.position || extras.position || "",
  997. positionSequenceId: raw.positionSequenceId || extras.positionSequenceId || "",
  998. positionSequence: raw.positionSequence || extras.positionSequence || "",
  999. positionLevelId: raw.positionLevelId || extras.positionLevelId || "",
  1000. positionLevel: raw.positionLevel || extras.positionLevel || "",
  1001. positionType: raw.positionType || extras.positionType || "",
  1002. positionTypeName: raw.positionTypeName || extras.positionTypeName || "",
  1003. managerUserId: raw.managerUserId || extras.managerUserId || "",
  1004. managerUserName: raw.managerUserName || raw.manager || extras.manager || "",
  1005. manager: raw.managerUserName || raw.manager || extras.manager || "",
  1006. hireDate: dateOnly(raw.hireDate || extras.hireDate) || "",
  1007. employmentPeriodId: raw.employmentPeriodId || extras.employmentPeriodId || "",
  1008. regularizationType: raw.regularizationType || "NORMAL",
  1009. plannedRegularDate: dateOnly(raw.plannedRegularDate || raw.plannedDate || raw.regularDate) || "",
  1010. effectiveDate: dateOnly(raw.effectiveDate) || "",
  1011. extensionDays: raw.extensionDays != null ? raw.extensionDays : "",
  1012. extensionReason: raw.extensionReason || "",
  1013. newProbationEndDate: dateOnly(raw.newProbationEndDate) || "",
  1014. userSummary: raw.userSummary || raw.remark || "",
  1015. signingMethod: normalizeSigningMethod(raw.signingMethod || (raw.online ? "ONLINE" : "OFFLINE")),
  1016. online: resolveOnlineSigning(raw, extras),
  1017. onlineDocumentTemplateId: raw.onlineDocumentTemplateId || extras.onlineDocumentTemplateId || "",
  1018. approvalDocumentFileId: joinFileIds(raw.approvalDocumentFileId),
  1019. });
  1020. }
  1021. /** 保存草稿:有主键走更新,否则新建;返回转正单主键 */
  1022. export async function saveRegularizationDraft(form = {}, loding = true) {
  1023. const existingId = form.regularizationId || form.billId || form.id || null;
  1024. const payload = buildRegularizationPayload(Object.assign({}, form, { id: existingId }));
  1025. let billId = existingId;
  1026. if (existingId != null && existingId !== "") {
  1027. await updateRegularization(Object.assign({}, payload, { id: billId }), loding);
  1028. } else {
  1029. billId = await createRegularization(payload, loding);
  1030. }
  1031. if (form.userId != null && billId != null) saveRegularizationBillId(form.userId, billId);
  1032. return billId;
  1033. }
  1034. /** 组装 BPM 发起审批所需变量(与 PC launchApproval 的 variables 一致) */
  1035. export function buildRegularizationVariables(form = {}, businessId) {
  1036. const type = form.regularizationType || "NORMAL";
  1037. const typeLabel = labelOf(REGULARIZATION_TYPE_LABELS, type, "转正");
  1038. // PC 端为 `${typeLabel}转正`,标签本身已含「转正」时会重复(如「按期转正转正」),这里去重
  1039. const businessType = /转正$/.test(typeLabel) ? typeLabel : `${typeLabel}转正`;
  1040. return compact({
  1041. businessCode: businessId != null && businessId !== "" ? String(businessId) : undefined,
  1042. businessName: form.name || form.employeeName || "",
  1043. businessType,
  1044. userId: form.userId != null ? form.userId : undefined,
  1045. regularizationType: type,
  1046. regularizationTypeLabel: typeLabel,
  1047. plannedRegularDate: dateOnly(form.plannedRegularDate || form.plannedDate) || undefined,
  1048. effectiveDate: dateOnly(form.effectiveDate) || undefined,
  1049. signingMethod: normalizeSigningMethod(form.signingMethod || (form.online ? "ONLINE" : "OFFLINE")),
  1050. });
  1051. }
  1052. /**
  1053. * 发起转正审批流程。
  1054. * businessKey 固定 main_regularization_approval,businessId 为转正单主键。
  1055. * 与 startAttendanceApproval / startUserChangeApproval 同构:
  1056. * 先用流程列表接口取 processDefinitionId,再调用 /bpm/process-instance/create。
  1057. */
  1058. export async function startRegularizationApproval(businessId, variables = {}, loding = false) {
  1059. if (businessId == null || businessId === "") throw new Error("缺少转正单主键,无法发起审批");
  1060. const page = await processInstancePage(
  1061. { pageNo: 1, pageSize: 1, reset: true, key: REGULARIZATION_APPROVAL_BUSINESS_KEY },
  1062. loding
  1063. );
  1064. const first = page && page.list && page.list[0];
  1065. const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : "";
  1066. if (!processDefinitionId) throw new Error("未匹配到转正审批流程,请联系管理员配置流程定义");
  1067. return processInstanceCreateAPI(
  1068. {
  1069. businessId,
  1070. businessKey: REGULARIZATION_APPROVAL_BUSINESS_KEY,
  1071. formCreateUserId: variables.userId || "",
  1072. processDefinitionId,
  1073. variables,
  1074. },
  1075. false
  1076. );
  1077. }
  1078. /**
  1079. * 提交转正申请:保存(或更新)转正单后直接发起审批,不再保留草稿态。
  1080. * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>}
  1081. */
  1082. export async function submitRegularizationFlow({ form, existingId = null, loding = true } = {}) {
  1083. if (!form) throw new Error("申请参数为空");
  1084. const id = await saveRegularizationDraft(Object.assign({}, form, { regularizationId: existingId || form.regularizationId }), loding);
  1085. const variables = buildRegularizationVariables(Object.assign({}, form, { regularizationId: id }), id);
  1086. let approvalStarted = false;
  1087. let approvalError = "";
  1088. try {
  1089. await startRegularizationApproval(id, variables);
  1090. approvalStarted = true;
  1091. } catch (error) {
  1092. approvalError = (error && error.message) || "审批发起失败";
  1093. }
  1094. return { id, approvalStarted, approvalError };
  1095. }