hiringApproval.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import request from "@/utils/request";
  2. function unwrap(response) {
  3. const result = response?.data || {};
  4. if (Number(result.code) === 0) return result.data;
  5. const error = new Error(result.message || "服务调用失败");
  6. if (response?.__toastShown) error.__toastShown = true;
  7. return Promise.reject(error);
  8. }
  9. function compact(object = {}) {
  10. return Object.keys(object).reduce((result, key) => {
  11. const value = object[key];
  12. if (value !== "" && value !== null && value !== undefined) result[key] = value;
  13. return result;
  14. }, {});
  15. }
  16. function dateOnly(value) {
  17. if (!value) return undefined;
  18. return String(value).slice(0, 10);
  19. }
  20. function toNumber(value) {
  21. if (value === null || value === undefined || value === "") return undefined;
  22. if (typeof value === "number" && Number.isFinite(value)) return value;
  23. const matched = String(value).replace(/,/g, "").match(/(\d+(?:\.\d+)?)/);
  24. return matched ? Number(matched[1]) : undefined;
  25. }
  26. function toProbationMonths(value) {
  27. if (value === null || value === undefined || value === "") return undefined;
  28. if (typeof value === "number" && Number.isFinite(value)) return value;
  29. const matched = String(value).match(/(\d+)/);
  30. return matched ? Number(matched[1]) : undefined;
  31. }
  32. /** 大整数主键保持字符串,避免雪花 ID 精度丢失 */
  33. function keepBigIntId(value) {
  34. if (value === undefined || value === null || value === "") return undefined;
  35. const text = String(value).trim();
  36. if (!/^-?\d+$/.test(text)) return value;
  37. if (!Number.isSafeInteger(Number(text))) return text;
  38. return Number(text);
  39. }
  40. /**
  41. * 录用审批状态(文档:PENDING / APPROVED / REJECTED)
  42. * 未发起流程、无状态码时前端按草稿展示
  43. */
  44. export const HIRING_STATUS_LABELS = {
  45. DRAFT: "草稿",
  46. PENDING: "审批中",
  47. APPROVED: "已通过",
  48. REJECTED: "已拒绝",
  49. };
  50. export const HIRING_STATUS_OPTIONS = Object.entries(HIRING_STATUS_LABELS).map(
  51. ([value, label]) => ({ value, label }),
  52. );
  53. /** 已通过:可发起 Offer */
  54. export const HIRING_APPROVED_STATUS = new Set(["APPROVED"]);
  55. /** BPM 流程标识,需与流程定义 code 一致;提交审核弹框按此拉取流程分类/发起流程 */
  56. export const HIRING_APPROVAL_BUSINESS_KEY = "hr_recruit_hiring_approval";
  57. export function normalizeHiringStatus(raw = {}) {
  58. const code = String(raw.approvalStatus || raw.status || "").trim().toUpperCase();
  59. if (code === "APPROVED" || code === "PASSED") return "APPROVED";
  60. if (code === "REJECTED" || code === "已拒绝" || code === "已驳回") return "REJECTED";
  61. if (
  62. code === "PENDING" ||
  63. code === "APPROVING" ||
  64. code === "PENDING_APPROVAL" ||
  65. code === "WAITING"
  66. ) {
  67. return "PENDING";
  68. }
  69. if (raw.processInstanceId) return "PENDING";
  70. if (raw.id) return "DRAFT";
  71. return "DRAFT";
  72. }
  73. /** 适配「录用审批视图」+ 前端展示扩展字段 */
  74. export function adaptHiringApproval(raw = {}, extras = {}) {
  75. const statusCode = normalizeHiringStatus(raw);
  76. const hireDate =
  77. dateOnly(raw.expectedEntryDate || extras.expectedEntryDate || raw.hireDate) ||
  78. "";
  79. const hiringDeptId =
  80. extras.hiringDeptId ??
  81. raw.hiringDeptId ??
  82. extras.departmentId ??
  83. raw.departmentId;
  84. const hiringDeptName =
  85. extras.hiringDeptName ||
  86. raw.hiringDeptName ||
  87. extras.departmentName ||
  88. raw.departmentName ||
  89. "";
  90. const deptId = extras.deptId ?? raw.deptId;
  91. const deptName = extras.deptName || raw.deptName || "";
  92. const phone =
  93. extras.phone ||
  94. extras.mobile ||
  95. raw.contactMobile ||
  96. raw.phone ||
  97. raw.mobile ||
  98. "";
  99. return {
  100. ...raw,
  101. id: raw.id,
  102. approvalNo:
  103. raw.approvalNo ||
  104. raw.documentCode ||
  105. (raw.id != null ? `HA${raw.id}` : ""),
  106. documentCode: raw.documentCode || "",
  107. applicationId: raw.applicationId ?? extras.applicationId,
  108. candidateId:
  109. raw.candidateId ??
  110. extras.candidateId ??
  111. raw.userId ??
  112. extras.userId,
  113. userId: raw.userId ?? extras.userId ?? raw.candidateId,
  114. candidateName:
  115. extras.candidateName || raw.candidateName || raw.name || "",
  116. positionId: raw.positionId ?? extras.positionId,
  117. positionName: raw.positionName || extras.positionName || "",
  118. hiringDeptId,
  119. hiringDeptName,
  120. departmentId: hiringDeptId,
  121. departmentName: hiringDeptName,
  122. deptId,
  123. deptName,
  124. gender: extras.gender || raw.gender || "",
  125. education: extras.education ?? raw.education ?? "",
  126. phone,
  127. mobile: phone,
  128. contactMobile: raw.contactMobile || phone,
  129. proposedSalary: raw.proposedSalary ?? extras.proposedSalary,
  130. workYears:
  131. extras.workYears ??
  132. raw.workYears ??
  133. extras.experienceYears ??
  134. raw.experienceYears,
  135. employmentType:
  136. extras.employmentType ||
  137. raw.employmentType ||
  138. raw.staffingType ||
  139. "",
  140. staffingType: extras.employmentType || raw.employmentType || raw.staffingType || "",
  141. probationPeriod: raw.probationPeriod ?? extras.probationPeriod,
  142. probation:
  143. (raw.probationPeriod ?? extras.probationPeriod) != null &&
  144. (raw.probationPeriod ?? extras.probationPeriod) !== ""
  145. ? `${raw.probationPeriod ?? extras.probationPeriod}个月`
  146. : extras.probation || raw.probation || "",
  147. hireDate,
  148. expectedEntryDate: hireDate,
  149. workLocation: extras.workLocation || raw.workLocation || "",
  150. specialTerms: extras.specialTerms || raw.specialTerms || raw.remark || "",
  151. remark: extras.specialTerms || raw.specialTerms || raw.remark || "",
  152. specialApprovalReason: raw.specialApprovalReason || "",
  153. processInstanceId: raw.processInstanceId || "",
  154. currentNode: extras.currentNode || raw.currentNode || raw.nodeName || "-",
  155. resumeAttachmentFileIds:
  156. raw.resumeAttachmentFileIds || extras.resumeAttachmentFileIds || "",
  157. statusCode,
  158. status: HIRING_STATUS_LABELS[statusCode] || raw.approvalStatus || "-",
  159. offerId: raw.offerId,
  160. offerSent: Boolean(raw.offerId || extras.offerSent),
  161. createTime: raw.createTime,
  162. createUserId: raw.createUserId,
  163. };
  164. }
  165. function toEducationValue(value) {
  166. if (value === null || value === undefined || value === "") return undefined;
  167. const text = String(value).trim();
  168. return text || undefined;
  169. }
  170. export function parseResumeAttachmentFileIds(value) {
  171. if (Array.isArray(value)) {
  172. return value
  173. .map((item) => String(item ?? "").trim())
  174. .filter(Boolean);
  175. }
  176. if (value == null || value === "") return [];
  177. return String(value)
  178. .split(",")
  179. .map((item) => item.trim())
  180. .filter(Boolean);
  181. }
  182. function joinResumeFileIds(form = {}) {
  183. const files = Array.isArray(form.attachmentFiles)
  184. ? form.attachmentFiles
  185. : Array.isArray(form.attachments)
  186. ? form.attachments
  187. : null;
  188. if (files) {
  189. const ids = files
  190. .map((item) => keepBigIntId(item.id || item.fileId))
  191. .filter((id) => id != null && id !== "");
  192. return ids.length ? ids.join(",") : undefined;
  193. }
  194. if (form.resumeAttachmentFileIds) {
  195. return (
  196. parseResumeAttachmentFileIds(form.resumeAttachmentFileIds).join(",") ||
  197. undefined
  198. );
  199. }
  200. return undefined;
  201. }
  202. /**
  203. * 保存草稿请求体 → 录用审批新增参数
  204. * 必填:expectedEntryDate、proposedSalary
  205. * 线下(无 applicationId):name、positionId 必填
  206. */
  207. export function serializeHiringApproval(form = {}) {
  208. const applicationId = keepBigIntId(form.applicationId);
  209. const name = String(form.name || form.candidateName || "").trim();
  210. const employmentType = keepBigIntId(form.employmentType ?? form.staffingType);
  211. const candidateId = keepBigIntId(
  212. form.candidateId ?? form.userId ?? form.personId,
  213. );
  214. const payload = compact({
  215. id: keepBigIntId(form.id),
  216. applicationId,
  217. candidateId,
  218. contactMobile: String(
  219. form.contactMobile || form.phone || form.mobile || "",
  220. ).trim() || undefined,
  221. demandId: keepBigIntId(form.demandId),
  222. education: toEducationValue(form.education),
  223. employmentType,
  224. expectedEntryDate: dateOnly(form.expectedEntryDate || form.hireDate),
  225. gender: form.gender || undefined,
  226. name: name || undefined,
  227. positionId: keepBigIntId(form.positionId),
  228. hiringDeptId: keepBigIntId(form.hiringDeptId ?? form.departmentId),
  229. hiringDeptName:
  230. String(form.hiringDeptName || form.departmentName || "").trim() ||
  231. undefined,
  232. deptId: keepBigIntId(form.deptId),
  233. deptName: String(form.deptName || "").trim() || undefined,
  234. probationPeriod: toProbationMonths(form.probationPeriod ?? form.probation),
  235. proposedLevel: form.proposedLevel || undefined,
  236. proposedSalary: (() => {
  237. const amount = toNumber(form.proposedSalary);
  238. return amount == null ? undefined : Math.round(amount);
  239. })(),
  240. resumeAttachmentFileIds: joinResumeFileIds(form),
  241. workYears: (() => {
  242. const years = toNumber(form.workYears);
  243. return years == null ? undefined : Math.round(years);
  244. })(),
  245. });
  246. if (payload.applicationId == null || payload.applicationId === "") {
  247. delete payload.applicationId;
  248. }
  249. payload.workLocation = String(form.workLocation || "").trim();
  250. payload.specialTerms = String(form.specialTerms || form.remark || "").trim();
  251. return payload;
  252. }
  253. /** 校验保存草稿必填(文档:expectedEntryDate、proposedSalary;线下须 name、positionId) */
  254. export function validateOfflineHiringDraft(form = {}) {
  255. const errors = [];
  256. if (!dateOnly(form.expectedEntryDate || form.hireDate)) {
  257. errors.push("请选择预计入职日期");
  258. }
  259. if (toNumber(form.proposedSalary) == null) {
  260. errors.push("请输入拟定薪资");
  261. }
  262. if (
  263. !keepBigIntId(form.applicationId) &&
  264. !String(form.name || form.candidateName || "").trim()
  265. ) {
  266. errors.push("请填写姓名");
  267. }
  268. if (!keepBigIntId(form.applicationId) && !keepBigIntId(form.positionId)) {
  269. errors.push("请选择应聘岗位");
  270. }
  271. return errors;
  272. }
  273. function toQueryInt(value) {
  274. if (value === null || value === undefined || value === "") return undefined;
  275. if (typeof value === "number" && Number.isFinite(value)) return value;
  276. const text = String(value).trim();
  277. if (!/^-?\d+$/.test(text)) return undefined;
  278. return keepBigIntId(text);
  279. }
  280. /**
  281. * 分页查询参数(GET /recruit/hiring-approvals)
  282. * approvalStatus / deptId / keyword / offset / orderBy / pageNum / size / sortName
  283. */
  284. export function serializeHiringApprovalQuery(params = {}) {
  285. return compact({
  286. approvalStatus: params.approvalStatus || undefined,
  287. deptId: toQueryInt(params.deptId),
  288. keyword: params.keyword || undefined,
  289. offset: toQueryInt(params.offset),
  290. orderBy: params.orderBy || undefined,
  291. pageNum: toQueryInt(params.pageNum),
  292. size: toQueryInt(params.size),
  293. sortName: params.sortName || undefined,
  294. });
  295. }
  296. // —— 人力资源-录用审批管理 ——
  297. /** 录用审批分页查询 GET /recruit/hiring-approvals */
  298. export const getHiringApprovalPage = async (params = {}) =>
  299. unwrap(
  300. await request.get("/recruit/hiring-approvals", {
  301. params: serializeHiringApprovalQuery(params),
  302. }),
  303. );
  304. /** 录用审批详情查询 GET /recruit/hiring-approvals/{id},id 为列表主键 */
  305. export const getHiringApprovalById = async (id) => {
  306. const hiringId = keepBigIntId(id);
  307. if (hiringId == null || hiringId === "") {
  308. return Promise.reject(new Error("缺少录用审批主键"));
  309. }
  310. return adaptHiringApproval(
  311. await unwrap(await request.get(`/recruit/hiring-approvals/${hiringId}`)),
  312. );
  313. };
  314. /**
  315. * 保存录用审批草稿 POST /recruit/hiring-approvals
  316. * 成功返回主键 id;前端发起时再调 BPM create
  317. */
  318. export const createHiringApproval = async (data) => {
  319. const payload = serializeHiringApproval(data || {});
  320. return unwrap(await request.post("/recruit/hiring-approvals", payload));
  321. };
  322. /** 与 create 同一接口(文档无独立修改接口) */
  323. export const updateHiringApproval = async (id, data) =>
  324. createHiringApproval({ ...(data || {}), id: id ?? data?.id });