resign.js 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319
  1. import { deleteApi, get, postJ, putJ } from "@/utils/request";
  2. import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js";
  3. import { dateOnly, getMyResignIds, rememberResignId } from "@/utils/hr/storage";
  4. import Vue from "vue";
  5. // ============ 枚举 ============
  6. // M02 后端枚举 → 中文
  7. export const RESIGNATION_TYPE_LABELS = {
  8. VOLUNTARY: "主动离职",
  9. NEGOTIATED: "协商解除",
  10. PROBATION_FAILED: "试用不合格",
  11. PERFORMANCE_FAILED: "绩效不合格",
  12. TERMINATED: "辞退",
  13. DISMISSED: "辞退",
  14. CONTRACT_EXPIRED: "合同到期",
  15. RETIRE: "退休",
  16. RETIREMENT: "退休",
  17. OTHER: "其他",
  18. };
  19. export const RESIGN_APPROVAL_STATUS_LABELS = {
  20. DRAFT: "草稿",
  21. PENDING: "审批中",
  22. APPROVING: "审批中",
  23. APPROVED: "已通过",
  24. REJECTED: "已驳回",
  25. WITHDRAWN: "已撤回",
  26. CANCELLED: "已取消",
  27. };
  28. export const HANDOVER_STATUS_LABELS = {
  29. NOT_STARTED: "未开始",
  30. PENDING: "待交接",
  31. IN_PROGRESS: "交接中",
  32. PROCESSING: "交接中",
  33. COMPLETED: "已完成",
  34. FORCE_COMPLETED: "强制办结",
  35. };
  36. export const RESIGN_EFFECTIVE_STATUS_LABELS = {
  37. PENDING: "待生效",
  38. SCHEDULED: "已排期",
  39. EFFECTIVE: "已生效",
  40. CANCELLED: "已取消",
  41. };
  42. export const ARCHIVE_STATUS_LABELS = {
  43. NOT_ARCHIVED: "未归档",
  44. PENDING: "待归档",
  45. ARCHIVED: "已归档",
  46. };
  47. export const HANDOVER_CATEGORY_LABELS = {
  48. WORK: "工作交接",
  49. ASSET: "资产交接",
  50. FINANCE: "财务交接",
  51. HR: "HR交接",
  52. };
  53. /** 交接方式:ONLINE-线上文档、OFFLINE-线下附件(对齐 PC 端 resignation.js) */
  54. export const HANDOVER_TYPE_LABELS = {
  55. ONLINE: "线上文档",
  56. OFFLINE: "线下附件",
  57. };
  58. export const HANDOVER_TYPE_OPTIONS = [
  59. { value: "ONLINE", label: "线上文档", desc: "由服务端创建线上交接文档,无需上传附件" },
  60. { value: "OFFLINE", label: "线下附件", desc: "必须上传交接附件(可多选)" },
  61. ];
  62. /** 交接事项分类选项 */
  63. export const HANDOVER_ITEM_CATEGORY_OPTIONS = [
  64. { value: "WORK", label: "工作交接" },
  65. { value: "ASSET", label: "资产交接" },
  66. { value: "FINANCE", label: "财务交接" },
  67. { value: "HR", label: "HR交接" },
  68. ];
  69. /** 交接事项状态 */
  70. export const HANDOVER_ITEM_STATUS_LABELS = {
  71. PENDING: "待交接",
  72. PROCESSING: "交接中",
  73. IN_PROGRESS: "交接中",
  74. COMPLETED: "已完成",
  75. DONE: "已完成",
  76. };
  77. export const RESIGN_DISPLAY_STATUS = {
  78. draft: { label: "草稿", class: "status-pending" },
  79. pending: { label: "审批中", class: "status-pending" },
  80. rejected: { label: "已驳回", class: "status-rejected" },
  81. withdrawn: { label: "已撤回", class: "status-rejected" },
  82. cancelled: { label: "已取消", class: "status-rejected" },
  83. overdue: { label: "已逾期", class: "status-rejected" },
  84. approved: { label: "已通过", class: "status-approved" },
  85. handover: { label: "交接中", class: "status-processing" },
  86. completed: { label: "已完成", class: "status-approved" },
  87. };
  88. export const RESIGN_FLOW_STAGES = [
  89. { key: "submit", label: "申请提交", desc: "员工提交离职申请并填写预计离职日" },
  90. { key: "approval", label: "离职审批", desc: "直属主管、部门负责人与 HRBP 审批" },
  91. { key: "handover", label: "工作交接", desc: "交接人确认工作、资产与文档移交" },
  92. { key: "archive", label: "归档生效", desc: "交接完成后归档,劳动关系解除" },
  93. ];
  94. export const RESIGN_STATUS_FILTERS = [
  95. { value: "all", label: "全部状态" },
  96. { value: "pending", label: "审批中" },
  97. { value: "handover", label: "交接中" },
  98. { value: "completed", label: "已完成" },
  99. { value: "rejected", label: "已驳回" },
  100. { value: "withdrawn", label: "已撤回" },
  101. ];
  102. /**
  103. * 创建态默认事项:与 PC 端 HandoverGenerateDialog 的 defaultItems() 完全一致 ——
  104. * 按四个交接分类各预置一行,**只自动填充「交接分类」,事项名称 / 说明 / 接收人一律留空**,
  105. * 由申请人自行填写(PC 提示「共 4 项;分类与名称必填」)。
  106. */
  107. export const DEFAULT_HANDOVER_ITEMS = HANDOVER_ITEM_CATEGORY_OPTIONS.map((option) => ({
  108. category: option.value,
  109. name: "",
  110. detail: "",
  111. }));
  112. // ============ 工具函数 ============
  113. function labelOf(map, raw, fallback = "") {
  114. const text = String(raw || "").trim();
  115. if (!text) return fallback;
  116. return map[text] || map[text.toUpperCase()] || text;
  117. }
  118. function asList(data) {
  119. if (Array.isArray(data)) return data;
  120. if (data && Array.isArray(data.list)) return data.list;
  121. if (data && Array.isArray(data.records)) return data.records;
  122. if (data && typeof data === "object" && data.id != null) return [data];
  123. return [];
  124. }
  125. function isDoneItemStatus(status) {
  126. return ["COMPLETED", "DONE", "FINISHED"].indexOf(String(status || "").toUpperCase()) >= 0;
  127. }
  128. function approvalCode(raw = {}) {
  129. return String(raw.approvalStatus || raw.statusCode || "").toUpperCase();
  130. }
  131. // ============ 状态推导与操作可用性 ============
  132. export function deriveResignationDisplayStatus(raw = {}) {
  133. const archive = String(raw.archiveStatus || "").toUpperCase();
  134. const effective = String(raw.effectiveStatus || "").toUpperCase();
  135. const handover = String(raw.handoverStatus || "").toUpperCase();
  136. const approval = String(raw.approvalStatus || "").toUpperCase();
  137. if (approval === "CANCELLED") return "cancelled";
  138. if (archive === "ARCHIVED" || effective === "EFFECTIVE") return "completed";
  139. if (approval === "APPROVED") {
  140. if (!handover || ["IN_PROGRESS", "PROCESSING", "PENDING", "NOT_STARTED"].indexOf(handover) >= 0) {
  141. return "handover";
  142. }
  143. return "approved";
  144. }
  145. if (approval === "REJECTED") return "rejected";
  146. if (approval === "PENDING" || approval === "APPROVING") return "pending";
  147. if (approval === "WITHDRAWN") return "withdrawn";
  148. return "draft";
  149. }
  150. export function canSubmitResignation(row = {}) {
  151. if (!row.id) return false;
  152. const code = approvalCode(row);
  153. return code === "DRAFT" || code === "REJECTED" || code === "";
  154. }
  155. export function canWithdrawResignation(row = {}) {
  156. const code = approvalCode(row);
  157. return code === "PENDING" || code === "APPROVING" || row.displayStatus === "pending";
  158. }
  159. export function canCancelResignation(row = {}) {
  160. if (!row.id) return false;
  161. if (approvalCode(row) === "CANCELLED") return false;
  162. return row.displayStatus !== "completed";
  163. }
  164. export function canGenerateHandover(row = {}) {
  165. if (!row.id || row.handover) return false;
  166. return approvalCode(row) === "APPROVED";
  167. }
  168. export function canArchiveResignation(row = {}) {
  169. if (!row.id) return false;
  170. if (!row.handover || row.handover.status !== "completed") return false;
  171. return String(row.archiveStatus || "").toUpperCase() !== "ARCHIVED";
  172. }
  173. export function canForceCompleteResignation(row = {}) {
  174. if (!row.id) return false;
  175. return String(row.archiveStatus || "").toUpperCase() !== "ARCHIVED" && row.displayStatus !== "completed";
  176. }
  177. export function matchDisplayStatus(row = {}, filter = "") {
  178. if (!filter || filter === "all") return true;
  179. if (filter === "rejected") return ["rejected", "cancelled"].indexOf(row.displayStatus) >= 0;
  180. if (filter === "withdrawn") return ["withdrawn", "cancelled"].indexOf(row.displayStatus) >= 0;
  181. return row.displayStatus === filter;
  182. }
  183. // ============ VO → 页面形状映射 ============
  184. export function adaptHandoverItem(raw = {}) {
  185. const done = isDoneItemStatus(raw.itemStatus || raw.status);
  186. const category = raw.itemCategory || raw.category || "WORK";
  187. const statusCode = String(raw.itemStatus || raw.status || "").toUpperCase();
  188. return {
  189. id: raw.id,
  190. name: raw.itemName || raw.name || "",
  191. itemName: raw.itemName || raw.name || "",
  192. detail: raw.itemContent || raw.detail || raw.completionRemark || "",
  193. itemContent: raw.itemContent || raw.detail || "",
  194. category,
  195. itemCategory: category,
  196. categoryLabel: labelOf(HANDOVER_CATEGORY_LABELS, category),
  197. receiverUserId: raw.receiverUserId,
  198. receiverName: raw.receiverName || raw.receiverUserName || "",
  199. status: raw.itemStatus || raw.status || "",
  200. itemStatus: raw.itemStatus || raw.status || "",
  201. // 待交接 / 交接中 / 已完成
  202. itemStatusLabel: labelOf(HANDOVER_ITEM_STATUS_LABELS, statusCode, done ? "已完成" : "待交接"),
  203. completionRemark: raw.completionRemark || "",
  204. done,
  205. doneAt: dateOnly(raw.completeTime || raw.doneAt),
  206. };
  207. }
  208. /** 资产归还清单行(详情 / 编辑态展示用) */
  209. export function adaptAssetReturn(raw = {}) {
  210. const statusCode = String(raw.returnStatus || "").toUpperCase();
  211. return {
  212. id: raw.id,
  213. handoverId: raw.handoverId,
  214. itemName: raw.itemName || raw.assetName || raw.returnName || "",
  215. assetCode: raw.assetCode || "",
  216. returnName: raw.returnName || raw.itemName || raw.assetName || "",
  217. returnStatus: statusCode || "PENDING",
  218. returnStatusLabel: statusCode === "RETURNED" ? "已归还" : "未归还",
  219. returned: statusCode === "RETURNED",
  220. returnRemark: raw.returnRemark || "",
  221. returnedTime: raw.returnedTime || "",
  222. };
  223. }
  224. export function adaptHandover(raw = {}) {
  225. if (!raw || typeof raw !== "object") return null;
  226. const items = ((raw.items || [])).map(adaptHandoverItem);
  227. const assetReturns = ((raw.assetReturns || raw.assetReturnList || [])).map(adaptAssetReturn);
  228. const doneCount = items.filter((item) => item.done).length;
  229. const itemsProgress = items.length ? Math.round((doneCount / items.length) * 100) : 0;
  230. const code = String(raw.handoverStatus || "").toUpperCase();
  231. const completed = ["COMPLETED", "FORCE_COMPLETED"].indexOf(code) >= 0 || (items.length > 0 && doneCount === items.length);
  232. const progress = completed ? 100 : itemsProgress;
  233. const receiverItem = items.find((item) => item.receiverName);
  234. // 附件:后端返回逗号分隔字符串
  235. const attachmentIdText = raw.attachmentIds ? String(raw.attachmentIds) : "";
  236. const attachmentIdList = attachmentIdText
  237. ? attachmentIdText.split(",").map((text) => text.trim()).filter(Boolean)
  238. : [];
  239. return {
  240. id: raw.id,
  241. resignationId: raw.resignationId,
  242. resignationNo: raw.resignationNo || "",
  243. documentCode: raw.documentCode || "",
  244. userId: raw.userId,
  245. employee: raw.employeeName || raw.userName || "",
  246. employeeNo: raw.employeeNo || "",
  247. department: raw.departmentName || raw.deptName || "",
  248. deptId: raw.deptId,
  249. deptName: raw.deptName || raw.departmentName || "",
  250. position: raw.positionName || "",
  251. person: raw.handoverPerson || raw.handoverUserName || (receiverItem ? receiverItem.receiverName : "") || "",
  252. handoverUserId: raw.handoverUserId,
  253. handoverUserName: raw.handoverUserName || "",
  254. handoverType: String(raw.handoverType || "").toUpperCase(),
  255. handoverTypeLabel: labelOf(HANDOVER_TYPE_LABELS, raw.handoverType),
  256. attachmentIds: attachmentIdText,
  257. attachmentIdList,
  258. handoverDate: dateOnly(raw.handoverDate),
  259. deadline: dateOnly(raw.deadline || raw.handoverDate),
  260. remark: raw.remark || "",
  261. status: completed ? "completed" : progress > 0 ? "processing" : "not_started",
  262. statusLabel: completed ? "已完成" : progress > 0 ? "交接中" : "待交接",
  263. statusCode: raw.handoverStatus || "",
  264. statusCodeLabel: labelOf(HANDOVER_STATUS_LABELS, raw.handoverStatus),
  265. approvalStatus: raw.approvalStatus || "",
  266. approvalStatusLabel: labelOf(RESIGN_APPROVAL_STATUS_LABELS, raw.approvalStatus),
  267. processInstanceId: raw.processInstanceId || raw.approvalInstanceId || "",
  268. progress,
  269. items,
  270. assetReturns,
  271. createTime: raw.createTime || "",
  272. updated: dateOnly(raw.handoverDate || raw.createTime),
  273. };
  274. }
  275. /**
  276. * 交接单是否可编辑(对齐 PC 端 resignation.js 的 canEditHandover):
  277. * 仅「审批草稿(DRAFT / 空)+ 交接未办结」可修改;
  278. * 审批中 / 已通过 / 已驳回 / 已撤回 / 已取消,或已完成 / 强制办结均不可改。
  279. */
  280. export function canEditHandover(row = {}) {
  281. if (!row || row.id === undefined || row.id === null) return false;
  282. const approval = String(row.approvalStatus || "").toUpperCase();
  283. if (approval && approval !== "DRAFT") return false;
  284. const code = String(row.statusCode || "").toUpperCase();
  285. return ["COMPLETED", "FORCE_COMPLETED"].indexOf(code) < 0;
  286. }
  287. function stageProgressOf(stageKey, stages, handover) {
  288. const index = stages.findIndex((stage) => stage.key === stageKey);
  289. if (index < 0) return 0;
  290. if (stageKey === "handover" && handover) {
  291. return Math.min(99, 50 + Math.round((handover.progress || 0) / 2));
  292. }
  293. return Math.round(((index + 1) / stages.length) * 100);
  294. }
  295. function progressTipOf(displayStatus, handover) {
  296. switch (displayStatus) {
  297. case "draft":
  298. return "草稿未提交,请提交审批后进入离职流程。";
  299. case "pending":
  300. return "离职审批中,请等待审批结果。";
  301. case "rejected":
  302. return "审批已驳回,可修改后重新提交。";
  303. case "withdrawn":
  304. return "本次离职申请已撤回。";
  305. case "cancelled":
  306. return "本次离职申请已取消。";
  307. case "approved":
  308. return "审批已通过,待生成离职交接单。";
  309. case "handover":
  310. return handover ? `交接进行中,事项已完成 ${handover.progress}%。` : "审批已通过,待生成离职交接单。";
  311. case "completed":
  312. return "离职流程已全部完成。";
  313. case "overdue":
  314. return "已超过最后工作日,请尽快推进交接与结算。";
  315. default:
  316. return "";
  317. }
  318. }
  319. export function adaptResignation(vo = {}, extras = {}) {
  320. if (!vo || typeof vo !== "object") return null;
  321. const displayStatus = deriveResignationDisplayStatus(vo);
  322. const handover = vo.handover ? adaptHandover(vo.handover) : null;
  323. const statusMeta = RESIGN_DISPLAY_STATUS[displayStatus] || RESIGN_DISPLAY_STATUS.pending;
  324. const stages = RESIGN_FLOW_STAGES;
  325. const stageKey =
  326. displayStatus === "completed"
  327. ? "archive"
  328. : displayStatus === "handover" || displayStatus === "approved"
  329. ? "handover"
  330. : displayStatus === "pending"
  331. ? "approval"
  332. : "submit";
  333. const name = extras.name || vo.employeeName || vo.userName || "";
  334. return {
  335. id: vo.id,
  336. source: "server",
  337. resignationNo: vo.resignationNo || "",
  338. userId: vo.userId,
  339. name,
  340. avatar: String(name).charAt(0),
  341. employeeNo: extras.employeeNo || vo.employeeNo || "",
  342. department: extras.department || vo.departmentName || vo.deptName || "",
  343. position: extras.position || vo.positionName || "",
  344. entryDate: dateOnly(extras.hireDate || vo.hireDate),
  345. expectedLeaveDate: dateOnly(vo.plannedLastWorkDate || vo.effectiveDate),
  346. lastWorkDate: dateOnly(vo.effectiveDate),
  347. applyDate: dateOnly(vo.applyDate || vo.createTime),
  348. effectiveDate: dateOnly(vo.effectiveDate),
  349. type: labelOf(RESIGNATION_TYPE_LABELS, vo.resignationType),
  350. typeCode: vo.resignationType || "",
  351. reason: vo.reason || "",
  352. userComment: vo.userComment || "",
  353. hrComment: vo.hrComment || "",
  354. approvalStatus: vo.approvalStatus || "",
  355. approvalStatusLabel: labelOf(RESIGN_APPROVAL_STATUS_LABELS, vo.approvalStatus),
  356. handoverStatus: vo.handoverStatus || "",
  357. handoverStatusLabel: labelOf(HANDOVER_STATUS_LABELS, vo.handoverStatus),
  358. effectiveStatus: vo.effectiveStatus || "",
  359. effectiveStatusLabel: labelOf(RESIGN_EFFECTIVE_STATUS_LABELS, vo.effectiveStatus),
  360. archiveStatus: vo.archiveStatus || "",
  361. archiveStatusLabel: labelOf(ARCHIVE_STATUS_LABELS, vo.archiveStatus),
  362. displayStatus,
  363. statusLabel: statusMeta.label,
  364. statusClass: statusMeta.class,
  365. stageKey,
  366. stages,
  367. progress: displayStatus === "completed" ? 100 : stageProgressOf(stageKey, stages, handover),
  368. progressTip: progressTipOf(displayStatus, handover),
  369. handover,
  370. onlineSign: Boolean(vo.approvalDocumentFileId || vo.onlineDocumentId),
  371. createTime: vo.createTime || "",
  372. // ===== PC「新建离职申请」表单回填所需的组织 / 岗位快照字段 =====
  373. phone: extras.phone || vo.phone || vo.mobile || "",
  374. groupId: pickBig(vo.groupId, vo.legalId, extras.groupId, extras.legalId),
  375. company: extras.company || vo.company || vo.groupName || vo.legalName || "",
  376. deptId: pickBig(vo.deptId, extras.deptId),
  377. deptName: extras.deptName || vo.deptName || vo.departmentName || "",
  378. positionId: pickBig(vo.positionId, extras.positionId),
  379. positionName: extras.position || vo.positionName || "",
  380. positionSequenceId: pickBig(vo.positionSequenceId, extras.positionSequenceId),
  381. positionSequence: extras.positionSequence || vo.positionSequence || "",
  382. positionLevelId: pickBig(vo.positionLevelId, extras.positionLevelId),
  383. positionLevel: extras.positionLevel || vo.positionLevel || "",
  384. positionType: extras.positionType || vo.positionType || "",
  385. positionTypeName: extras.positionTypeName || vo.positionTypeName || "",
  386. employmentPeriodId: pickBig(vo.employmentPeriodId, extras.employmentPeriodId),
  387. handlingMode:
  388. String(vo.handlingMode || vo.resignHandlingMode || extras.handlingMode || "")
  389. .trim()
  390. .toUpperCase() === "ONLINE"
  391. ? "ONLINE"
  392. : normalizeResignHandlingMode(vo),
  393. approvalDocumentFileId: firstFileIdOf([
  394. vo.approvalDocumentFileId,
  395. vo.onlineDocumentId,
  396. extras.approvalDocumentFileId,
  397. ]),
  398. raw: vo,
  399. };
  400. }
  401. /** 取第一个非空 ID(兼容大整数,统一转字符串) */
  402. function pickBig() {
  403. for (let i = 0; i < arguments.length; i += 1) {
  404. const value = arguments[i];
  405. if (value === undefined || value === null || value === "") continue;
  406. return String(value);
  407. }
  408. return "";
  409. }
  410. // ============ 员工信息补全 ============
  411. const employeeCache = {};
  412. async function fetchEmployeeBrief(userId) {
  413. if (userId === undefined || userId === null || userId === "") return null;
  414. const key = String(userId);
  415. if (key in employeeCache) return employeeCache[key];
  416. try {
  417. const res = await get(Vue.prototype.apiUrl + `/main/user/getById/${key}`, {}, false);
  418. const vo = (res && res.data) || {};
  419. const brief = {
  420. name: vo.name || vo.userName || "",
  421. employeeNo: vo.jobNumber || "",
  422. department: vo.deptName || "",
  423. position: vo.postName || vo.position || "",
  424. hireDate: dateOnly(vo.joinDate || vo.entryDate),
  425. };
  426. employeeCache[key] = brief;
  427. return brief;
  428. } catch (error) {
  429. employeeCache[key] = null;
  430. return null;
  431. }
  432. }
  433. function extrasOf(brief) {
  434. if (!brief) return {};
  435. return {
  436. name: brief.name,
  437. employeeNo: brief.employeeNo,
  438. department: brief.department,
  439. position: brief.position,
  440. hireDate: brief.hireDate,
  441. };
  442. }
  443. /**
  444. * 离职单详情
  445. * GET /main/resignations/{id}
  446. */
  447. export async function getResignationById(id, loding = true) {
  448. if (!id) return null;
  449. const res = await get(Vue.prototype.apiUrl + `/main/resignations/${id}`, {}, loding);
  450. if (Number(res.code) !== 0) return null;
  451. return res.data || null;
  452. }
  453. /**
  454. * 离职单详情(归一化后,失败返回 null,不阻断上层)
  455. */
  456. export async function loadResignationDetail(id) {
  457. if (!id) return null;
  458. try {
  459. const vo = await getResignationById(id, false);
  460. if (!vo) return null;
  461. rememberResignId(String(id));
  462. const brief = await fetchEmployeeBrief(vo.userId);
  463. return adaptResignation(vo, extrasOf(brief));
  464. } catch (error) {
  465. return null;
  466. }
  467. }
  468. /**
  469. * 离职单列表(归一化后)
  470. * POST /main/resignations/page
  471. */
  472. export async function loadResignationList(params = {}, loding = false) {
  473. const { keyword = "", status = "all", pageNum = 1, size = 50 } = params;
  474. let rows = [];
  475. try {
  476. const res = await postJ(
  477. Vue.prototype.apiUrl + "/main/resignations/page",
  478. { pageNum, size, resignationNo: keyword || undefined },
  479. loding
  480. );
  481. rows = res && Number(res.code) === 0 ? asList(res.data) : [];
  482. } catch (error) {
  483. rows = [];
  484. }
  485. if (!rows.length) {
  486. const details = await Promise.all(
  487. getMyResignIds().map(async (id) => {
  488. try {
  489. return await getResignationById(id, false);
  490. } catch (error) {
  491. return null;
  492. }
  493. })
  494. );
  495. rows = details.filter(Boolean);
  496. }
  497. const list = [];
  498. for (const vo of rows) {
  499. if (!vo || vo.id == null) continue;
  500. const brief = await fetchEmployeeBrief(vo.userId);
  501. const row = adaptResignation(vo, extrasOf(brief));
  502. if (!row) continue;
  503. rememberResignId(String(row.id));
  504. if (!matchDisplayStatus(row, status)) continue;
  505. if (keyword && !`${row.name}${row.employeeNo}${row.resignationNo}${row.department}`.includes(keyword)) continue;
  506. list.push(row);
  507. }
  508. return { list, count: list.length, source: "api" };
  509. }
  510. /** 交接单列表 POST /main/handovers/page */
  511. export async function loadHandoverList(params = {}) {
  512. const { keyword = "", status = "all" } = params;
  513. const page = await getHandoverPage({ keyword }, false);
  514. const list = page.list
  515. .map((item) => adaptHandover(item))
  516. .filter(Boolean)
  517. .filter((item) => (status === "all" ? true : item.status === status))
  518. .filter((item) => (keyword ? String(item.resignationNo || "").includes(keyword) : true));
  519. return { list, count: page.count || list.length, source: "api" };
  520. }
  521. export async function loadHandoverDetail(id) {
  522. if (!id) return null;
  523. try {
  524. const vo = await getHandoverDetail(id, false);
  525. return adaptHandover(vo);
  526. } catch (error) {
  527. return null;
  528. }
  529. }
  530. /**
  531. * 查询「我的离职交接单」。
  532. *
  533. * 员工自助场景下一个人只有一张交接单,所以这里按登录人过滤后取最新一条:
  534. * POST /main/handovers/page(userId 由当前登录用户传入,服务端仍会做归属校验)
  535. * @returns {Promise<object|null>} adaptHandover 适配后的交接单,未创建过返回 null
  536. */
  537. export async function loadMyHandover(userId, loding = false) {
  538. const params = { pageNum: 1, size: 20, orderBy: "desc", sortName: "createTime" };
  539. if (userId !== undefined && userId !== null && userId !== "") params.userId = userId;
  540. try {
  541. const res = await postJ(Vue.prototype.apiUrl + "/main/handovers/page", params, loding);
  542. if (!res || Number(res.code) !== 0) return null;
  543. const rows = asList(res.data).map((item) => adaptHandover(item)).filter(Boolean);
  544. if (!rows.length) return null;
  545. // 列表 VO 不含事项明细,拿详情补齐(失败时退回列表行)
  546. const detail = await loadHandoverDetail(rows[0].id);
  547. return detail || rows[0];
  548. } catch (error) {
  549. return null;
  550. }
  551. }
  552. /* ---------------- 交接单发起 / 编辑 ---------------- */
  553. function compactHandover(object = {}) {
  554. return Object.keys(object).reduce((acc, key) => {
  555. const value = object[key];
  556. if (value !== "" && value !== null && value !== undefined) acc[key] = value;
  557. return acc;
  558. }, {});
  559. }
  560. /**
  561. * 组装新建 / 修改离职交接单参数,严格对齐后端 PO(与 PC 端 serializeHandover 一致):
  562. * - handoverType 必填;ONLINE 不得传 attachmentIds,OFFLINE 必须传(英文逗号分隔的文件 id)
  563. * - items 必填:创建时每项不得带 id,修改时回传已有事项 id(未回传的事项会被删除)
  564. * - assetReturns(入职领用物品归还清单)移动端暂不提交
  565. */
  566. export function buildHandoverPayload(form = {}) {
  567. const handoverType = form.handoverType ? String(form.handoverType).toUpperCase() : "ONLINE";
  568. const isOffline = handoverType === "OFFLINE";
  569. let attachmentIds = form.attachmentIds;
  570. if (Array.isArray(attachmentIds)) attachmentIds = attachmentIds.filter(Boolean).join(",");
  571. attachmentIds = attachmentIds ? String(attachmentIds) : undefined;
  572. if (!isOffline) attachmentIds = undefined;
  573. const items = (Array.isArray(form.items) ? form.items : [])
  574. .filter((item) => item && String(item.itemName || "").trim())
  575. .map((item) =>
  576. compactHandover({
  577. id: item.id,
  578. itemCategory: item.itemCategory || item.category || "WORK",
  579. itemName: String(item.itemName || item.name).trim(),
  580. itemContent: item.itemContent ? String(item.itemContent).trim() : undefined,
  581. receiverUserId: item.receiverUserId,
  582. })
  583. );
  584. // 资产归还清单:创建时按「归还名称 + 状态」新增(不得带主键);
  585. // 编辑时仅回传已进入清单(有主键)的行,修改接口不接受新增明细(与 PC 一致)
  586. const assetReturns = (Array.isArray(form.assetReturns) ? form.assetReturns : [])
  587. .map((row) => {
  588. const name = String(row.returnName || row.itemName || "").trim();
  589. if (!name) return null;
  590. const status = String(row.returnStatus || "PENDING").toUpperCase() === "RETURNED" ? "RETURNED" : "PENDING";
  591. return compactHandover({
  592. id: row.id,
  593. returnName: name,
  594. returnStatus: status,
  595. returnRemark: row.returnRemark ? String(row.returnRemark).trim() : undefined,
  596. });
  597. })
  598. .filter(Boolean);
  599. return compactHandover({
  600. handoverType,
  601. attachmentIds,
  602. handoverDate: dateOnly(form.handoverDate) || undefined,
  603. handoverUserId: form.handoverUserId,
  604. items: items.length ? items : undefined,
  605. assetReturns: assetReturns.length ? assetReturns : undefined,
  606. remark: form.remark ? String(form.remark).trim() : undefined,
  607. resignationId: form.resignationId,
  608. });
  609. }
  610. /** 创建离职交接单 POST /main/handovers(data 为新建交接单主键) */
  611. export async function createHandover(form, loding = true) {
  612. const payload = buildHandoverPayload(form);
  613. if (!payload.resignationId) return Promise.reject(new Error("缺少关联的离职申请"));
  614. if (!payload.handoverDate) return Promise.reject(new Error("请选择交接日期"));
  615. if (!payload.handoverUserId) return Promise.reject(new Error("请选择交接人"));
  616. if (payload.handoverType === "OFFLINE" && !payload.attachmentIds) {
  617. return Promise.reject(new Error("线下交接必须上传交接附件"));
  618. }
  619. if (!payload.items || !payload.items.length) {
  620. return Promise.reject(new Error("请至少填写一条交接事项"));
  621. }
  622. const res = await postJ(Vue.prototype.apiUrl + "/main/handovers", payload, loding);
  623. if (res && res.code == 0) return res.data;
  624. return Promise.reject(new Error((res && res.message) || "交接单创建失败"));
  625. }
  626. /** 修改离职交接单 PUT /main/handovers/{id}(items 采用全量回传,未回传的事项会被删除) */
  627. export async function updateHandover(id, form, loding = true) {
  628. if (!id) return Promise.reject(new Error("缺少交接单主键"));
  629. const payload = buildHandoverPayload(form);
  630. if (!payload.handoverDate) return Promise.reject(new Error("请选择交接日期"));
  631. if (!payload.handoverUserId) return Promise.reject(new Error("请选择交接人"));
  632. if (payload.handoverType === "OFFLINE" && !payload.attachmentIds) {
  633. return Promise.reject(new Error("线下交接必须上传交接附件"));
  634. }
  635. if (!payload.items || !payload.items.length) {
  636. return Promise.reject(new Error("请至少填写一条交接事项"));
  637. }
  638. const res = await putJ(Vue.prototype.apiUrl + `/main/handovers/${id}`, payload, loding);
  639. if (res && res.code == 0) return true;
  640. return Promise.reject(new Error((res && res.message) || "交接单保存失败"));
  641. }
  642. // —— 流程动作 ——
  643. export async function actSubmitResignation(row) {
  644. if (!row || !row.id) throw new Error("缺少离职单主键");
  645. return submitResignation(row.id);
  646. }
  647. export async function actWithdrawResignation(row, reason = "") {
  648. if (!row || !row.id) throw new Error("缺少离职单主键");
  649. return withdrawResignation(row.id, reason);
  650. }
  651. export async function actCancelResignation(row, reason = "") {
  652. if (!row || !row.id) throw new Error("缺少离职单主键");
  653. return cancelResignation(row.id, reason);
  654. }
  655. export async function actGenerateHandover(row) {
  656. if (!row || !row.id) throw new Error("缺少离职单主键");
  657. return generateResignationHandover(row.id);
  658. }
  659. export async function actCompleteHandoverItem(resignationId, itemId, remark = "") {
  660. return completeResignationHandoverItem(resignationId, itemId, remark);
  661. }
  662. export async function actArchiveResignation(row) {
  663. if (!row || !row.id) throw new Error("缺少离职单主键");
  664. return archiveResignation(row.id);
  665. }
  666. export async function actForceCompleteResignation(row, reason) {
  667. if (!row || !row.id) throw new Error("缺少离职单主键");
  668. return forceCompleteResignation(row.id, reason);
  669. }
  670. /** 提交离职单 POST /main/resignations/{id}/submit */
  671. export async function submitResignation(id, loding = true) {
  672. const res = await postJ(Vue.prototype.apiUrl + `/main/resignations/${id}/submit`, {}, loding);
  673. if (res.code == 0) return res.data;
  674. return Promise.reject(new Error(res.message));
  675. }
  676. /** 撤回离职单 POST /main/resignations/{id}/withdraw */
  677. export async function withdrawResignation(id, reason = "", loding = true) {
  678. const res = await postJ(
  679. Vue.prototype.apiUrl + `/main/resignations/${id}/withdraw`,
  680. { reason },
  681. loding
  682. );
  683. if (res.code == 0) return res.data;
  684. return Promise.reject(new Error(res.message));
  685. }
  686. /** 撤销离职单 POST /main/resignations/cancel/{id} */
  687. export async function cancelResignation(id, reason = "", loding = true) {
  688. const res = await postJ(
  689. Vue.prototype.apiUrl + `/main/resignations/cancel/${id}`,
  690. { reason },
  691. loding
  692. );
  693. if (res.code == 0) return res.data;
  694. return Promise.reject(new Error(res.message));
  695. }
  696. /** 归档离职单 POST /main/resignations/{id}/archive */
  697. export async function archiveResignation(id, loding = true) {
  698. const res = await postJ(Vue.prototype.apiUrl + `/main/resignations/${id}/archive`, {}, loding);
  699. if (res.code == 0) return res.data;
  700. return Promise.reject(new Error(res.message));
  701. }
  702. /** 强制完成离职 POST /main/resignations/{id}/force-complete */
  703. export async function forceCompleteResignation(id, reason = "", loding = true) {
  704. const res = await postJ(
  705. Vue.prototype.apiUrl + `/main/resignations/${id}/force-complete`,
  706. { reason },
  707. loding
  708. );
  709. if (res.code == 0) return res.data;
  710. return Promise.reject(new Error(res.message));
  711. }
  712. /** 生成交接单 POST /main/resignations/{id}/generate-handover */
  713. export async function generateResignationHandover(id, loding = true) {
  714. const res = await postJ(
  715. Vue.prototype.apiUrl + `/main/resignations/${id}/generate-handover`,
  716. {},
  717. loding
  718. );
  719. if (res.code == 0) return res.data;
  720. return Promise.reject(new Error(res.message));
  721. }
  722. /** 完成交接项 PUT /main/resignations/{resignationId}/handover/items/{itemId}/complete */
  723. export async function completeResignationHandoverItem(resignationId, itemId, loding = true) {
  724. const res = await putJ(
  725. Vue.prototype.apiUrl + `/main/resignations/${resignationId}/handover/items/${itemId}/complete`,
  726. {},
  727. loding
  728. );
  729. if (res.code == 0) return res.data;
  730. return Promise.reject(new Error(res.message));
  731. }
  732. /** 回退交接项 PUT /main/handovers/{handoverId}/items/{itemId}/rollback */
  733. export async function rollbackResignationHandoverItem(handoverId, itemId, reason = "", loding = true) {
  734. if (!handoverId || !itemId) throw new Error("缺少交接单或事项主键");
  735. const res = await putJ(
  736. Vue.prototype.apiUrl + `/main/handovers/${handoverId}/items/${itemId}/rollback`,
  737. { rollbackReason: reason },
  738. loding
  739. );
  740. if (res.code == 0) return res.data;
  741. return Promise.reject(new Error(res.message));
  742. }
  743. /** 回退交接项(流程动作封装,handoverId 取自离职详情 handover.id) */
  744. export async function actRollbackHandoverItem(handoverId, itemId, reason = "") {
  745. return rollbackResignationHandoverItem(handoverId, itemId, reason);
  746. }
  747. /** 删除交接单 DELETE /main/handovers/{id} */
  748. export async function deleteHandover(id, loding = true) {
  749. const res = await deleteApi(Vue.prototype.apiUrl + `/main/handovers/${id}`, {}, loding);
  750. if (res.code == 0) return res.data;
  751. return Promise.reject(new Error(res.message));
  752. }
  753. /** 删除交接单(流程动作封装) */
  754. export async function actDeleteHandover(id) {
  755. if (!id) throw new Error("缺少交接单主键");
  756. return deleteHandover(id);
  757. }
  758. /** 交接单分页 POST /main/handovers/page */
  759. export async function getHandoverPage(params = {}, loding = true) {
  760. const res = await postJ(Vue.prototype.apiUrl + "/main/handovers/page", params, loding);
  761. if (res.code == 0) return res.data;
  762. return Promise.reject(new Error(res.message));
  763. }
  764. /** 交接单详情 GET /main/handovers/{id} */
  765. export async function getHandoverDetail(id, loding = true) {
  766. const res = await get(Vue.prototype.apiUrl + `/main/handovers/${id}`, {}, loding);
  767. if (res.code == 0) return res.data;
  768. return Promise.reject(new Error(res.message));
  769. }
  770. // ============ 员工端离职申请(对齐 PC 端 /main/resignations)============
  771. /** 离职申请 BPM 流程标识(与 PC 端 RESIGNATION_APPLY_BUSINESS_KEY 一致) */
  772. export const RESIGNATION_APPROVAL_BUSINESS_KEY = "main_resignation_approval";
  773. /** 离职交接单 BPM 流程标识(与 PC 端 HANDOVER_APPROVAL_BUSINESS_KEY 一致) */
  774. export const HANDOVER_APPROVAL_BUSINESS_KEY = "main_handover_approval";
  775. /**
  776. * 办理方式:ONLINE 线上模板 / OFFLINE 线下附件。
  777. * 移动端页面不再提供切换,统一按 OFFLINE(线下附件)提交;此表仅用于回显旧数据。
  778. */
  779. export const RESIGN_HANDLING_MODE_LABELS = {
  780. ONLINE: "线上模板",
  781. OFFLINE: "线下附件",
  782. };
  783. /** 离职申请表单 → 后端 resignationType 枚举(表单直接存码,此处仅做兜底映射) */
  784. export const RESIGN_TYPE_CODE_LABELS = {
  785. VOLUNTARY: "主动离职",
  786. NEGOTIATED: "协商解除",
  787. PROBATION_FAILED: "试用不合格",
  788. PERFORMANCE_FAILED: "绩效不合格",
  789. TERMINATED: "辞退",
  790. DISMISSED: "辞退",
  791. CONTRACT_EXPIRED: "合同到期",
  792. RETIRE: "退休",
  793. RETIREMENT: "退休",
  794. OTHER: "其他",
  795. };
  796. /** 需要展示在下拉里的离职类型(对齐 PC 端 RESIGNATION_TYPE_OPTIONS,去掉重复的辞退/退休别名) */
  797. export const RESIGN_TYPE_OPTIONS = Object.keys(RESIGNATION_TYPE_LABELS)
  798. .filter((code) => code !== "TERMINATED" && code !== "RETIRE")
  799. .map((code) => ({ value: code, label: RESIGNATION_TYPE_LABELS[code] }));
  800. function ok(res) {
  801. return res && Number(res.code) === 0;
  802. }
  803. function compact(object = {}) {
  804. return Object.keys(object).reduce((result, key) => {
  805. const value = object[key];
  806. if (value !== "" && value !== null && value !== undefined) result[key] = value;
  807. return result;
  808. }, {});
  809. }
  810. function keepBigIntId(value) {
  811. if (value === undefined || value === null || value === "") return undefined;
  812. const text = String(value).trim();
  813. if (!/^-?\d+$/.test(text)) return value;
  814. if (!Number.isSafeInteger(Number(text))) return text;
  815. return Number(text);
  816. }
  817. function firstFileIdOf(value) {
  818. if (value == null || value === "") return undefined;
  819. if (Array.isArray(value)) return firstFileIdOf(value.find((item) => item !== undefined && item !== null && item !== ""));
  820. if (typeof value === "object") return value.id != null ? value.id : value.fileId;
  821. return value;
  822. }
  823. function todayIso() {
  824. const d = new Date();
  825. const pad = (n) => String(n).padStart(2, "0");
  826. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  827. }
  828. function extractId(result) {
  829. if (result == null) return null;
  830. if (typeof result === "object") {
  831. return result.id != null ? result.id : result.resignationId != null ? result.resignationId : null;
  832. }
  833. return result;
  834. }
  835. /** 办理方式规范化:老数据无该字段时按线上单据字段推断,默认线下 */
  836. export function normalizeResignHandlingMode(raw = {}) {
  837. const mode = String(raw.handlingMode || raw.handlingType || "").trim().toUpperCase();
  838. if (mode === "ONLINE" || mode === "OFFLINE") return mode;
  839. return raw.onlineDocumentTemplateId != null && raw.onlineDocumentTemplateId !== ""
  840. ? "ONLINE"
  841. : raw.onlineDocumentId
  842. ? "ONLINE"
  843. : "OFFLINE";
  844. }
  845. /**
  846. * 组装新建 / 修改离职单参数,严格对齐 PC 端 buildResignationPayload(POST /main/resignations PO):
  847. * - 机构 / 部门 / 岗位序列 / 岗位名称 / 岗位层级 主键 + 名称快照
  848. * - 办理方式:ONLINE 传模板主键;OFFLINE 传附件主键(取表单文件数组首个)
  849. */
  850. export function buildResignationPayload(form = {}) {
  851. const source = form || {};
  852. const handlingMode = normalizeResignHandlingMode(source);
  853. const online = handlingMode === "ONLINE";
  854. return compact({
  855. id: keepBigIntId(source.resignationId || source.id),
  856. userId: keepBigIntId(source.userId || source.employeeId),
  857. employmentPeriodId: keepBigIntId(source.employmentPeriodId),
  858. // 机构 / 部门 / 岗位主键
  859. groupId: keepBigIntId(source.groupId),
  860. deptId: keepBigIntId(source.deptId),
  861. positionId: keepBigIntId(source.positionId),
  862. positionName: source.position || source.positionName || undefined,
  863. positionSequenceId: keepBigIntId(source.positionSequenceId),
  864. positionSequence: source.positionSequence || undefined,
  865. positionLevelId: keepBigIntId(source.positionLevelId),
  866. positionLevel: source.positionLevel || undefined,
  867. positionType: source.positionType || undefined,
  868. positionTypeName: source.positionTypeName || undefined,
  869. phone: source.phone || undefined,
  870. applyDate: dateOnly(source.applyDate) || todayIso(),
  871. plannedLastWorkDate: dateOnly(source.plannedLastWorkDate || source.expectedLeaveDate),
  872. effectiveDate: dateOnly(source.effectiveDate),
  873. reason: source.reason,
  874. resignationType: source.resignationType || "VOLUNTARY",
  875. userComment: source.userComment || undefined,
  876. hrComment: source.hrComment || undefined,
  877. // 办理方式:ONLINE 线上模板 / OFFLINE 线下附件
  878. handlingMode,
  879. // 线下附件:接口字段为单个 fileId,取表单文件数组首个
  880. approvalDocumentFileId: online
  881. ? undefined
  882. : keepBigIntId(source.approvalDocumentFileId || firstFileIdOf(source.attachmentIds)),
  883. // 线上模板:仅线上办理时传模板主键
  884. onlineDocumentTemplateId: online ? keepBigIntId(source.onlineDocumentTemplateId) : undefined,
  885. onlineDocumentContent: source.onlineDocumentContent,
  886. onlineDocumentVariableValuesJson: source.onlineDocumentVariableValuesJson,
  887. });
  888. }
  889. /** 新建离职草稿 POST /main/resignations(返回离职单主键) */
  890. export async function createResignation(payload, loding = true) {
  891. const res = await postJ(Vue.prototype.apiUrl + "/main/resignations", payload || {}, loding);
  892. if (!ok(res)) throw new Error(res.message || "离职申请创建失败");
  893. const id = extractId(res.data);
  894. if (id != null) rememberResignId(String(id));
  895. return id;
  896. }
  897. /** 修改离职草稿 PUT /main/resignations */
  898. export async function updateResignation(payload, loding = true) {
  899. const id = payload && (payload.id || payload.resignationId);
  900. if (id === undefined || id === null || id === "") throw new Error("缺少离职单主键");
  901. const res = await putJ(Vue.prototype.apiUrl + "/main/resignations", payload || {}, loding);
  902. if (!ok(res)) throw new Error(res.message || "离职申请修改失败");
  903. rememberResignId(String(id));
  904. return extractId(res.data) != null ? extractId(res.data) : id;
  905. }
  906. /**
  907. * 保存离职单(新建 / 修改统一入口)。
  908. * @returns {Promise<number|string>} 离职单主键
  909. */
  910. export async function saveResignationDraft(form, loding = true) {
  911. const payload = buildResignationPayload(form);
  912. if (!payload.userId) throw new Error("请选择离职人员");
  913. if (!payload.resignationType) throw new Error("请选择离职类型");
  914. if (!payload.plannedLastWorkDate) throw new Error("请选择计划最后工作日");
  915. if (!payload.reason) throw new Error("请填写离职原因");
  916. if (payload.handlingMode === "OFFLINE" && !payload.approvalDocumentFileId) {
  917. throw new Error("请上传离职申请附件");
  918. }
  919. if (payload.id != null && payload.id !== "") {
  920. await updateResignation(payload, loding);
  921. return payload.id;
  922. }
  923. return createResignation(payload, loding);
  924. }
  925. /** 审批流程 variables(与 PC 端 processSubmitDialog 的入参形状一致) */
  926. export function buildResignationVariables(form = {}, resignationId = null) {
  927. const row = form || {};
  928. const id = resignationId != null && resignationId !== "" ? resignationId : row.resignationId || row.id;
  929. return compact({
  930. businessCode: row.resignationNo || row.businessCode || (id != null ? String(id) : ""),
  931. businessName: row.name || row.employeeName || row.userName || "",
  932. businessType: "离职申请",
  933. userId: row.userId != null ? row.userId : row.employeeId,
  934. groupId: row.groupId || "",
  935. groupName: row.company || "",
  936. deptId: row.deptId || "",
  937. departmentName: row.department || "",
  938. positionId: row.positionId || "",
  939. positionName: row.position || "",
  940. adjustHeadcount: true,
  941. });
  942. }
  943. /**
  944. * 发起离职申请审批流程。
  945. * businessKey 固定 main_resignation_approval,businessId 为离职单主键。
  946. */
  947. export async function startResignationApproval(businessId, variables = {}, loding = false) {
  948. if (businessId == null || businessId === "") throw new Error("缺少离职单主键,无法发起审批");
  949. const page = await processInstancePage(
  950. { pageNo: 1, pageSize: 1, reset: true, key: RESIGNATION_APPROVAL_BUSINESS_KEY },
  951. loding
  952. );
  953. const first = page && page.list && page.list[0];
  954. const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : "";
  955. if (!processDefinitionId) throw new Error("未匹配到离职审批流程,请联系管理员配置流程定义");
  956. return processInstanceCreateAPI(
  957. {
  958. businessId,
  959. businessKey: RESIGNATION_APPROVAL_BUSINESS_KEY,
  960. formCreateUserId: variables.userId || "",
  961. processDefinitionId,
  962. variables,
  963. },
  964. false
  965. );
  966. }
  967. /**
  968. * 提交离职申请:创建(或更新)离职单后直接发起审批。
  969. * 审批发起失败不阻断主流程(单据已落库,避免重复创建)。
  970. * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>}
  971. */
  972. export async function submitResignationFlow({ form, existingId = null, loding = true } = {}) {
  973. if (!form) throw new Error("申请参数为空");
  974. const id0 = existingId != null && existingId !== "" ? existingId : form.resignationId || form.id || null;
  975. const payload = buildResignationPayload(Object.assign({}, form, { id: id0 }));
  976. let resignationId = id0;
  977. if (id0 != null && id0 !== "") {
  978. await updateResignation(Object.assign({}, payload, { id: resignationId }), loding);
  979. } else {
  980. resignationId = await createResignation(payload, loding);
  981. if (resignationId == null) throw new Error("离职申请已提交但未返回主键,请联系管理员");
  982. }
  983. const variables = buildResignationVariables(Object.assign({}, form, { id: resignationId }), resignationId);
  984. let approvalStarted = false;
  985. let approvalError = "";
  986. try {
  987. await startResignationApproval(resignationId, variables);
  988. approvalStarted = true;
  989. } catch (error) {
  990. approvalError = (error && error.message) || "审批流程发起失败";
  991. }
  992. return { id: resignationId, approvalStarted, approvalError };
  993. }
  994. /**
  995. * 离职单 → 离职申请表单回填(只读详情 / 编辑态共用)
  996. * @param {object} raw GET /main/resignations/{id} 的原始 VO
  997. * @param {object} extras 列表行 / 员工档案补出的姓名、工号等展示字段
  998. */
  999. export function hydrateResignationForm(raw = {}, extras = {}) {
  1000. const vo = raw || {};
  1001. const ext = extras || {};
  1002. const handlingMode = normalizeResignHandlingMode(vo);
  1003. const attachmentId = vo.approvalDocumentFileId;
  1004. return {
  1005. resignationId: vo.id != null ? vo.id : null,
  1006. userId: vo.userId != null ? String(vo.userId) : ext.userId != null ? String(ext.userId) : "",
  1007. name: ext.name || ext.employeeName || vo.employeeName || vo.userName || "",
  1008. employeeNo: ext.employeeNo || vo.employeeNo || "",
  1009. phone: vo.phone || ext.phone || "",
  1010. hireDate: dateOnly(vo.hireDate || vo.joinDate || ext.hireDate),
  1011. // —— 员工信息(级联字段)——
  1012. groupId: vo.groupId != null ? String(vo.groupId) : "",
  1013. company: vo.groupName || vo.companyName || vo.legalEntityName || ext.company || "",
  1014. deptId: vo.deptId != null ? String(vo.deptId) : "",
  1015. department: vo.departmentName || vo.deptName || ext.department || "",
  1016. positionId: vo.positionId != null ? String(vo.positionId) : "",
  1017. position: vo.positionName || ext.position || "",
  1018. positionSequenceId: vo.positionSequenceId != null ? String(vo.positionSequenceId) : "",
  1019. positionSequence: vo.positionSequence || vo.positionSequenceName || "",
  1020. positionLevelId: vo.positionLevelId != null ? String(vo.positionLevelId) : "",
  1021. positionLevel: vo.positionLevel || "",
  1022. positionType: vo.positionType != null ? String(vo.positionType) : "",
  1023. positionTypeName: vo.positionTypeName || "",
  1024. employmentPeriodId: vo.employmentPeriodId != null ? vo.employmentPeriodId : null,
  1025. // —— 离职信息 ——
  1026. resignationType: String(vo.resignationType || "VOLUNTARY").toUpperCase(),
  1027. applyDate: dateOnly(vo.applyDate) || dateOnly(vo.createTime) || todayIso(),
  1028. plannedLastWorkDate: dateOnly(vo.plannedLastWorkDate) || dateOnly(vo.leaveDate),
  1029. effectiveDate: dateOnly(vo.effectiveDate),
  1030. handlingMode,
  1031. onlineDocumentTemplateId: vo.onlineDocumentTemplateId != null ? vo.onlineDocumentTemplateId : null,
  1032. // 附件主键可能是逗号拼接的多附件字符串
  1033. attachmentIds:
  1034. attachmentId != null && attachmentId !== ""
  1035. ? String(attachmentId)
  1036. .split(",")
  1037. .map((id) => id.trim())
  1038. .filter(Boolean)
  1039. : [],
  1040. reason: vo.reason || "",
  1041. userComment: vo.userComment || "",
  1042. hrComment: vo.hrComment || "",
  1043. // —— 状态(只读详情展示用)——
  1044. resignationNo: vo.resignationNo || "",
  1045. approvalStatus: vo.approvalStatus || "",
  1046. createTime: vo.createTime || "",
  1047. };
  1048. }
  1049. /* ---------------- 离职交接单:物品归还清单 / 审批 ---------------- */
  1050. /** 领用单状态 */
  1051. export const RECEIPT_STATUS_LABELS = {
  1052. DRAFT: "草稿",
  1053. APPROVING: "审批中",
  1054. COMPLETED: "已完成",
  1055. };
  1056. /** 入职领用明细 → 交接单可编辑行(默认全部按「已归还」预选) */
  1057. export function decorateReceiptItems(receipt) {
  1058. const details = Array.isArray(receipt && receipt.details) ? receipt.details : [];
  1059. return details.map((item) => ({
  1060. id: item.id,
  1061. itemName: item.itemName || "",
  1062. itemId: item.itemId != null ? String(item.itemId) : "",
  1063. itemTypeText: item.itemTypeText || item.itemType || "",
  1064. model: item.model || "",
  1065. specification: item.specification || "",
  1066. quantity: item.quantity != null ? item.quantity : "",
  1067. // 逐行标记的归还状态会随交接单一并提交为物品归还清单
  1068. returnStatus: "RETURNED",
  1069. returnRemark: item.remark || "",
  1070. substanceId: item.substanceId,
  1071. }));
  1072. }
  1073. /**
  1074. * 按员工取最新入职物品领用单 GET /main/onboarding-item-receipts/latest/{employeeId}
  1075. * 后端可能返回 null / {}(该员工无领用单),此时视为「无记录」。
  1076. */
  1077. export async function getLatestItemReceipt(employeeId, loding = false) {
  1078. if (employeeId === undefined || employeeId === null || employeeId === "") return null;
  1079. try {
  1080. const res = await get(
  1081. Vue.prototype.apiUrl + `/main/onboarding-item-receipts/latest/${encodeURIComponent(String(employeeId))}`,
  1082. {},
  1083. loding
  1084. );
  1085. if (!ok(res)) return null;
  1086. const data = res.data;
  1087. if (!data || typeof data !== "object") return null;
  1088. const hasId = data.id != null && data.id !== "";
  1089. const details = Array.isArray(data.details) ? data.details : [];
  1090. if (!hasId && !details.length) return null;
  1091. return {
  1092. id: data.id != null ? data.id : "",
  1093. documentCode: data.documentCode || "",
  1094. receiptStatus: String(data.receiptStatus || "DRAFT").toUpperCase(),
  1095. employeeId: data.employeeId,
  1096. details: details.map((item) => ({
  1097. id: item.id,
  1098. itemName: item.itemName || "",
  1099. itemId: item.itemId != null ? String(item.itemId) : "",
  1100. itemTypeText: item.itemType || item.categoryName || "",
  1101. model: item.itemModel || item.model || "",
  1102. specification: item.specifications || item.specification || "",
  1103. quantity: item.quantity != null ? Number(item.quantity) : 1,
  1104. remark: item.remark || "",
  1105. substanceId: item.substanceId != null && item.substanceId !== "" ? item.substanceId : undefined,
  1106. })),
  1107. };
  1108. } catch (error) {
  1109. return null;
  1110. }
  1111. }
  1112. /**
  1113. * 可发起交接单的离职申请:审批通过 + 未归档 + 尚未生成交接单
  1114. * POST /main/resignations/page
  1115. */
  1116. export async function loadApprovedResignations(params = {}, loding = false) {
  1117. const res = await postJ(
  1118. Vue.prototype.apiUrl + "/main/resignations/page",
  1119. compact({
  1120. pageNum: params.pageNum || 1,
  1121. size: params.size || 100,
  1122. approvalStatus: "APPROVED",
  1123. archiveStatus: "NOT_ARCHIVED",
  1124. }),
  1125. loding
  1126. );
  1127. if (!ok(res)) return [];
  1128. const seen = new Set();
  1129. const rows = [];
  1130. // /main/resignations/page 的 VO 常不带 employeeName / departmentName / 计划最后工作日,
  1131. // 直接 adapt 会得到「离职单号 · · 」这种空白选项(PC 端用 withEmployeeInfo 补全,这里对齐)
  1132. for (const vo of asList(res.data)) {
  1133. const brief = await fetchEmployeeBrief(vo && vo.userId);
  1134. const row = adaptResignation(vo, extrasOf(brief));
  1135. if (row) rows.push(row);
  1136. }
  1137. return rows
  1138. .filter((row) => {
  1139. const code = String(row.handoverStatus || "").toUpperCase();
  1140. const created = row.handover && row.handover.id ? true : Boolean(code) && code !== "NOT_CREATED";
  1141. return !created;
  1142. })
  1143. .filter((row) => {
  1144. const key = String(row.resignationNo || row.id || "");
  1145. if (!key) return true;
  1146. if (seen.has(key)) return false;
  1147. seen.add(key);
  1148. return true;
  1149. });
  1150. }
  1151. /** 交接单审批流程 variables(对齐 PC 端 launchApproval 入参) */
  1152. export function buildHandoverVariables(form = {}, handoverId = null) {
  1153. const row = form || {};
  1154. const id = handoverId != null && handoverId !== "" ? handoverId : row.handoverId || row.id;
  1155. return compact({
  1156. businessCode: row.documentCode || row.resignationNo || (id != null ? String(id) : ""),
  1157. businessName: row.person || row.handoverUserName || row.employee || "",
  1158. businessType: "离职交接单",
  1159. userId: row.userId,
  1160. groupId: row.groupId || row.deptId || "",
  1161. groupName: row.deptName || row.department || "",
  1162. });
  1163. }
  1164. /** 发起离职交接单审批流程(businessKey 固定 main_handover_approval) */
  1165. export async function startHandoverApproval(businessId, variables = {}, loding = false) {
  1166. if (businessId == null || businessId === "") throw new Error("缺少交接单主键,无法发起审批");
  1167. const page = await processInstancePage(
  1168. { pageNo: 1, pageSize: 1, reset: true, key: HANDOVER_APPROVAL_BUSINESS_KEY },
  1169. loding
  1170. );
  1171. const first = page && page.list && page.list[0];
  1172. const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : "";
  1173. if (!processDefinitionId) throw new Error("未匹配到离职交接单审批流程,请联系管理员配置流程定义");
  1174. return processInstanceCreateAPI(
  1175. {
  1176. businessId,
  1177. businessKey: HANDOVER_APPROVAL_BUSINESS_KEY,
  1178. formCreateUserId: variables.userId || "",
  1179. processDefinitionId,
  1180. variables,
  1181. },
  1182. false
  1183. );
  1184. }
  1185. /**
  1186. * 提交离职交接单:创建(或更新)交接单后直接发起审批。
  1187. * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>}
  1188. */
  1189. export async function submitHandoverFlow({ form, existingId = null } = {}) {
  1190. if (!form) throw new Error("交接单参数为空");
  1191. const id0 = existingId != null && existingId !== "" ? existingId : form.handoverId || null;
  1192. let handoverId = id0;
  1193. if (id0 != null && id0 !== "") {
  1194. await updateHandover(id0, form, true);
  1195. } else {
  1196. handoverId = await createHandover(form, true);
  1197. if (handoverId == null) throw new Error("交接单已创建但未返回主键,请联系管理员");
  1198. }
  1199. const variables = buildHandoverVariables(
  1200. Object.assign({}, form, { handoverId, documentCode: form.documentCode, resignationNo: form.resignationNo }),
  1201. handoverId
  1202. );
  1203. let approvalStarted = false;
  1204. let approvalError = "";
  1205. try {
  1206. await startHandoverApproval(handoverId, variables);
  1207. approvalStarted = true;
  1208. } catch (error) {
  1209. approvalError = (error && error.message) || "审批流程发起失败";
  1210. }
  1211. return { id: handoverId, approvalStarted, approvalError };
  1212. }