index.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290
  1. import request from "@/utils/request";
  2. import { downloadBlobResponse } from "@/utils/download";
  3. import {
  4. adaptContractFlowSignNodes,
  5. contractFlowSignStatusLabel,
  6. contractSigningStatusLabel,
  7. contractStatusLabel,
  8. pickFlowSignStatus,
  9. } from "./contractStatus";
  10. function unwrap(response) {
  11. const result = response?.data || {};
  12. if (Number(result.code) === 0) return result.data;
  13. const error = new Error(result.message || "服务调用失败");
  14. if (response?.__toastShown) error.__toastShown = true;
  15. return Promise.reject(error);
  16. }
  17. /**
  18. * 推荐培训(岗位层级配置 trainings):接口契约为 [{ trainingId, trainingName }],两者都必填。
  19. * - toTrainingIds:回显用,下拉 value 是字符串化的 id;
  20. * - toTrainingNameMap:id → 名称快照,提交时按接口要求带上 trainingName。
  21. */
  22. function toTrainingIds(trainings) {
  23. return (Array.isArray(trainings) ? trainings : [])
  24. .map((item) => (item && typeof item === "object" ? (item.trainingId ?? item.id) : item))
  25. .filter((value) => value !== null && value !== undefined && value !== "")
  26. .map((value) => String(value));
  27. }
  28. function toTrainingNameMap(trainings) {
  29. return (Array.isArray(trainings) ? trainings : []).reduce((map, item) => {
  30. const id = item?.trainingId ?? item?.id;
  31. const name = String(item?.trainingName ?? "").trim();
  32. if (id !== null && id !== undefined && id !== "" && name) {
  33. map[String(id)] = name;
  34. }
  35. return map;
  36. }, {});
  37. }
  38. function compact(object) {
  39. return Object.keys(object).reduce((result, key) => {
  40. const value = object[key];
  41. if (value !== "" && value !== null && value !== undefined)
  42. result[key] = value;
  43. return result;
  44. }, {});
  45. }
  46. /** 大整数主键保持字符串,避免 Number 精度丢失(如雪花 ID) */
  47. function keepBigIntId(value) {
  48. if (value === undefined || value === null || value === "") return undefined;
  49. const text = String(value).trim();
  50. if (!/^-?\d+$/.test(text)) return value;
  51. // 超过安全整数必须用字符串提交,否则 1457 → 1500
  52. if (!Number.isSafeInteger(Number(text))) return text;
  53. return Number(text);
  54. }
  55. /* 文件流下载统一走 utils/download 的 downloadBlobResponse:
  56. 文件名解码(filename= 里的 percent-encoding 不 decode 会乱码)+ JSON 业务错误拦截 */
  57. const STATUS_TO_API = { 1: 1, 2: 0, 3: 2 };
  58. const STATUS_FROM_API = { 0: 2, 1: 1, 2: 3 };
  59. const SEQUENCE_CODES = {
  60. 管理: "M",
  61. 管理序列: "M",
  62. 专业: "P",
  63. 专业序列: "P",
  64. 技术: "T",
  65. 技术序列: "T",
  66. 操作: "O",
  67. 操作序列: "O",
  68. 运营: "O",
  69. 运营序列: "O",
  70. 营销: "S",
  71. 营销序列: "S",
  72. };
  73. const SEQUENCE_NAMES = {
  74. M: "管理",
  75. P: "专业",
  76. T: "技术",
  77. O: "操作",
  78. S: "营销",
  79. };
  80. export function sequenceCode(value = "") {
  81. const text = String(value).trim();
  82. return (
  83. SEQUENCE_CODES[text] ||
  84. (/^[A-Za-z]/.test(text) ? text.charAt(0).toUpperCase() : text)
  85. );
  86. }
  87. export function sequenceName(value = "") {
  88. const code = sequenceCode(value);
  89. return SEQUENCE_NAMES[code] || String(value).replace(/序列$/, "") || code;
  90. }
  91. function levelNumber(value, sequence = "") {
  92. const text = String(value ?? "");
  93. const number = Number(text.match(/\d+/)?.[0]);
  94. if (number) return number;
  95. const chinese = {
  96. 一: 1,
  97. 二: 2,
  98. 三: 3,
  99. 四: 4,
  100. 五: 5,
  101. 六: 6,
  102. 七: 7,
  103. 八: 8,
  104. 九: 9,
  105. 十: 10,
  106. };
  107. const matched = Object.keys(chinese).find((key) => text.includes(key));
  108. return matched ? chinese[matched] : 1;
  109. }
  110. function apiLevel(value, sequence = "") {
  111. const code = sequenceCode(sequence);
  112. const text = String(value ?? "").trim();
  113. if (/^[A-Za-z]+\d+$/i.test(text)) return text.toUpperCase();
  114. return `${code}${levelNumber(text, code)}`;
  115. }
  116. /** 汇总岗位上的层级名字(positionLevels / positionLevel / levelConfigs.positionLevel) */
  117. function collectPositionLevelNames(raw = {}) {
  118. const names = [];
  119. const add = (value) => {
  120. const text = String(value || "").trim();
  121. if (text && !names.includes(text)) names.push(text);
  122. };
  123. if (Array.isArray(raw.positionLevels)) {
  124. raw.positionLevels.forEach(add);
  125. }
  126. add(raw.positionLevel);
  127. if (Array.isArray(raw.levelConfigs)) {
  128. raw.levelConfigs.forEach((cfg) => add(cfg?.positionLevel));
  129. }
  130. return names;
  131. }
  132. /**
  133. * 汇总岗位可选层级对象 [{id, name}]
  134. * id 从 levelConfigs[i].id / positionLevels[i].id 取,name 同 collectPositionLevelNames 规则
  135. * 用于岗位序列/层级联动选择,提交时把选中的 id 作为 positionLevelId 传给后端
  136. */
  137. function collectPositionLevels(raw = {}) {
  138. const result = [];
  139. const seen = new Set();
  140. const push = (id, name) => {
  141. const text = String(name || "").trim();
  142. if (!text) return;
  143. const idText = id != null && id !== "" ? String(id) : "";
  144. const key = `${idText}|${text}`;
  145. if (seen.has(key)) return;
  146. seen.add(key);
  147. result.push({ id: idText, name: text });
  148. };
  149. if (Array.isArray(raw.levelConfigs)) {
  150. raw.levelConfigs.forEach((cfg) => push(cfg?.id, cfg?.positionLevel));
  151. }
  152. if (Array.isArray(raw.positionLevels)) {
  153. raw.positionLevels.forEach((item) => {
  154. if (item && typeof item === "object") push(item.id, item.positionLevel || item.level || item.name);
  155. else push(null, item);
  156. });
  157. }
  158. if (raw.positionLevel) push(raw.levelConfigId ?? null, raw.positionLevel);
  159. return result;
  160. }
  161. /**
  162. * 职责文本 → 多条数组:按换行或分号拆分,**行内内容原样保留**。
  163. * ★ 2026-10-28 去掉原先「剥掉行首序号」的那条正则(匹配 "开头空白 + 数字 + 顿号/点" 并删除):
  164. * 它会把用户手写的「1、初级职责」剥成「初级职责」,回填到弹框再保存就永久丢序号
  165. * (提交和接口返回都是「1、初级职责」,页面却只剩「初级职责」)。
  166. * 前端不做内容清洗,序号属于用户原文。
  167. */
  168. function splitText(value) {
  169. if (Array.isArray(value)) return value.filter(Boolean);
  170. return String(value || "")
  171. .split(/\r?\n|[;;]/)
  172. .map((item) => item.trim())
  173. .filter(Boolean);
  174. }
  175. // 岗位序列层级配置 → 弹窗每层级字段
  176. function adaptLevelConfig(cfg = {}) {
  177. const salary = cfg.salary || {};
  178. // 后端 VO 的 promotions 既带 name 也带 id;前端 path 同时存 name 与 id,
  179. // 避免保存时再去 options 里按 name 二次解析(options 接口偶发漏项)
  180. const paths = (cfg.promotions || [])
  181. .map((item) => {
  182. const seqLevelName = (seq, lvl) =>
  183. [seq, lvl].filter(Boolean).join(" · ") || "";
  184. return {
  185. up: item.upwardPromotionPositionName || seqLevelName(item.upwardPromotionPositionSequence, item.upwardPromotionPositionLevel),
  186. upId: item.upwardPromotionPositionId ?? null,
  187. down: item.downwardDemotionPositionName || seqLevelName(item.downwardDemotionPositionSequence, item.downwardDemotionPositionLevel),
  188. downId: item.downwardDemotionPositionId ?? null,
  189. lateral: item.lateralRotationPositionName || seqLevelName(item.lateralRotationPositionSequence, item.lateralRotationPositionLevel),
  190. lateralId: item.lateralRotationPositionId ?? null,
  191. };
  192. })
  193. .filter((path) => path.up || path.down || path.lateral);
  194. const conditions = [
  195. ...new Set(
  196. (cfg.promotions || [])
  197. .flatMap((item) =>
  198. (item.conditions || []).map((condition) => condition.conditionName),
  199. )
  200. .filter(Boolean),
  201. ),
  202. ];
  203. const duties = splitText(salary.dutyContent);
  204. return {
  205. level: cfg.positionLevel || "",
  206. levelId: cfg.positionLevelId ?? null,
  207. salaryMin: Number(salary.salaryMin ?? 0),
  208. salaryMid: Number(salary.salaryMid ?? 0),
  209. salaryMax: Number(salary.salaryMax ?? 0),
  210. salaryTemplateId: salary.salaryTemplate || "",
  211. perfSchemeId: salary.performanceScheme || "",
  212. eduReq: salary.educationRequirement || "",
  213. genderReq: salary.genderRequirement || "不限",
  214. expReq: Number(salary.experienceYears ?? 0),
  215. ageRange: [Number(salary.ageMin ?? 18), Number(salary.ageMax ?? 60)],
  216. effectDate: salary.birthDateRequirement || "",
  217. workEnv: salary.workEnvironment || "",
  218. workShift: salary.workShift || "",
  219. skills: (cfg.skills || []).map((item) => ({
  220. ...item,
  221. name: item.skillName || "",
  222. level: item.skillLevel || "",
  223. required: String(item.essentialStatus) === "1",
  224. matchRate: Number(item.matchRate ?? 0),
  225. code: item.skillCode || "",
  226. skillId: item.skillId,
  227. certOrg: item.certOrg || "",
  228. certRequired: Number(item.certRequired || 0),
  229. certificateId: item.certificateId,
  230. })),
  231. certificates: (cfg.certificates || []).map((item) => ({
  232. ...item,
  233. name: item.certificateName || "",
  234. category: item.certificateType || "",
  235. certificateId: item.certificateId,
  236. })),
  237. // 推荐培训:接口按 trainingId/trainingName 回显(两者都必填),同 levelConfigs 口径
  238. trainings: toTrainingIds(cfg.trainings),
  239. trainingNameById: toTrainingNameMap(cfg.trainings),
  240. promote: {
  241. paths: paths.length ? paths : [{ up: "", down: "", lateral: "" }],
  242. conditions,
  243. },
  244. duties,
  245. /* 回填用原始 dutyContent(未经拆分/去分隔符),保证弹框里看到的和接口返回一字不差 */
  246. dutyText: salary.dutyContent || duties.join("\n"),
  247. };
  248. }
  249. export function adaptPosition(raw = {}) {
  250. const salary = raw.salary || {};
  251. const relation = raw.departmentPosition || {};
  252. const seq = sequenceCode(raw.positionSequence || raw.sequenceName);
  253. const promotions = raw.promotions || [];
  254. // 晋升路径改为多条:每条包含向上/向下/平级流动层级
  255. // 同时保留后端返回的 *PositionId,保存时优先复用避免 options 二次解析失败
  256. const paths = promotions
  257. .map((item) => {
  258. const seqLevelName = (seqField, lvlField, nameField) =>
  259. nameField || [seqField, lvlField].filter(Boolean).join(" · ") || "";
  260. return {
  261. up: seqLevelName(item.upwardPromotionPositionSequence, item.upwardPromotionPositionLevel, item.upwardPromotionPositionName),
  262. upId: item.upwardPromotionPositionId ?? null,
  263. down: seqLevelName(item.downwardDemotionPositionSequence, item.downwardDemotionPositionLevel, item.downwardDemotionPositionName),
  264. downId: item.downwardDemotionPositionId ?? null,
  265. lateral: seqLevelName(item.lateralRotationPositionSequence, item.lateralRotationPositionLevel, item.lateralRotationPositionName),
  266. lateralId: item.lateralRotationPositionId ?? null,
  267. };
  268. })
  269. .filter((path) => path.up || path.down || path.lateral);
  270. // 编辑时至少保留一条空路径(同时带 id 结构,避免 buildLevelConfig 缺字段)
  271. const pathsForForm = paths.length
  272. ? paths
  273. : [{ up: "", upId: null, down: "", downId: null, lateral: "", lateralId: null }];
  274. const up = paths.map((item) => item.up).filter(Boolean);
  275. const down = paths.map((item) => item.down).filter(Boolean);
  276. const lateral = paths.map((item) => item.lateral).filter(Boolean);
  277. // 公共条件:优先取顶层 conditions,其次兼容旧的 promotions[].conditions
  278. const conditions = [
  279. ...new Set(
  280. (Array.isArray(raw.conditions) ? raw.conditions : [])
  281. .map((condition) =>
  282. typeof condition === "string"
  283. ? condition
  284. : condition.conditionName,
  285. )
  286. .concat(
  287. promotions.flatMap((item) =>
  288. (item.conditions || []).map(
  289. (condition) => condition.conditionName,
  290. ),
  291. ),
  292. )
  293. .filter(Boolean),
  294. ),
  295. ];
  296. const version = raw.documentVersions?.[0]?.versionNo || 1;
  297. // 更新/删除主键必须用 page、getById 返回的 id,且始终保持字符串防精度丢失
  298. const primaryId =
  299. raw.id != null && raw.id !== "" ? raw.id : raw.positionId;
  300. const primaryIdText =
  301. primaryId == null || primaryId === "" ? primaryId : String(primaryId);
  302. return {
  303. ...raw,
  304. id: primaryIdText,
  305. positionId:
  306. raw.positionId != null && raw.positionId !== ""
  307. ? String(raw.positionId)
  308. : primaryIdText,
  309. code: raw.positionCode || "",
  310. positionCode: raw.positionCode || "",
  311. name: raw.positionName || "",
  312. legalId: raw.organizationId,
  313. legalName: raw.organizationName || "",
  314. deptId: raw.deptId,
  315. deptName: raw.deptName || "",
  316. seq,
  317. // 历史单层级字段;完整集合见 positionLevels(一岗可配置多层级,值为层级名字)
  318. level: apiLevel(raw.positionLevel, seq),
  319. positionLevel: raw.positionLevel || "",
  320. positionLevels: collectPositionLevelNames(raw),
  321. positionLevelOptions: collectPositionLevels(raw),
  322. positionSequence: raw.positionSequence || raw.sequenceName || "",
  323. sequenceId: raw.sequenceId || relation.sequenceId,
  324. positionSequenceId: raw.sequenceId || relation.sequenceId,
  325. type: raw.positionType || "",
  326. positionType: raw.positionType || "",
  327. staffing: Number(
  328. raw.establishmentCount ?? relation.establishmentCount ?? 0,
  329. ),
  330. actual: Number(raw.occupancyCount ?? relation.occupancyCount ?? 0),
  331. salaryMin: Number(salary.salaryMin ?? raw.salaryMin ?? 0),
  332. salaryMid: Number(salary.salaryMid ?? 0),
  333. salaryMax: Number(salary.salaryMax ?? raw.salaryMax ?? 0),
  334. salaryTemplateId: salary.salaryTemplate || "",
  335. perfSchemeId: salary.performanceScheme || "",
  336. status: STATUS_FROM_API[Number(raw.status)] || 2,
  337. revokeDate: raw.revokeDate || "",
  338. workShift: salary.workShift || "",
  339. workEnv: salary.workEnvironment || "",
  340. ageRange: [Number(salary.ageMin ?? 18), Number(salary.ageMax ?? 60)],
  341. genderReq: salary.genderRequirement || "不限",
  342. eduReq: salary.educationRequirement || "",
  343. expReq: Number(salary.experienceYears ?? 0),
  344. effectDate: salary.birthDateRequirement || "",
  345. version,
  346. duties: splitText(salary.dutyContent),
  347. /* 职责任务弹框回填直接用原始文本:splitText 会按「;」拆条,
  348. 拿 join("\n") 回填相当于擅自把用户的分号改成换行(2026-10-28) */
  349. dutyText: salary.dutyContent || "",
  350. skills: (raw.skills || []).map((item) => ({
  351. ...item,
  352. name: item.skillName || "",
  353. level: item.skillLevel || "",
  354. required: String(item.essentialStatus) === "1",
  355. matchRate: Number(item.matchRate ?? 0),
  356. code: item.skillCode || "",
  357. certOrg: item.certOrg || "",
  358. certRequired: Number(item.certRequired || 0),
  359. })),
  360. certificates: (raw.certificates || []).map((item) => ({
  361. ...item,
  362. name: item.certificateName || "",
  363. category: item.certificateType || "",
  364. })),
  365. // 推荐培训:接口按 trainingId/trainingName 返回(两者都必填),下拉 value 用 id,
  366. // 名称另存 trainingNameById,提交时按接口要求带 trainingName(名称快照)
  367. trainings: toTrainingIds(raw.trainings),
  368. trainingNameById: toTrainingNameMap(raw.trainings),
  369. promote: { paths: pathsForForm, up, down, lateral, conditions },
  370. promotions,
  371. employees: (raw.activeEmployees || []).map((item) => ({
  372. ...item,
  373. entryDate: item.joinDate || "",
  374. performance: item.performance || "-",
  375. skillRate: Number(item.skillRate ?? 0),
  376. })),
  377. history: (raw.changeHistory || []).map((item) => ({
  378. ...item,
  379. time: item.createTime || "",
  380. title: item.changeTitle || item.changeTypeName || "岗位变更",
  381. content: item.changeSummary || "",
  382. })),
  383. updatedAt: String(raw.lastChangeTime || raw.createTime || "").slice(0, 10),
  384. // 岗位序列下每层级的独立配置(接口 levelConfigs)
  385. levels: (Array.isArray(raw.levelConfigs) ? raw.levelConfigs : []).map(
  386. (cfg) => adaptLevelConfig(cfg),
  387. ),
  388. };
  389. }
  390. // 从层级配置或表单顶层字段解析岗位职责文本(dutyContent 必填,允许空串)
  391. function resolveDutyContent(level = {}, fallback = "") {
  392. const normalize = (text) =>
  393. String(text || "")
  394. .split("\n")
  395. .map((item) => item.trim())
  396. .filter(Boolean)
  397. .join("\n");
  398. const fromText = normalize(level.dutyText);
  399. if (fromText) return fromText;
  400. if (Array.isArray(level.duties) && level.duties.length) {
  401. const fromDuties = level.duties.filter(Boolean).join("\n");
  402. if (fromDuties) return fromDuties;
  403. }
  404. return normalize(fallback);
  405. }
  406. // 弹窗每层级字段 → 接口「岗位序列层级配置参数」
  407. // 文档:positionLevel、positionSequence、salary(含 dutyContent) 必填
  408. function buildLevelConfig(level = {}, positionList = [], positionSequence = "") {
  409. // ===== 晋升路径暂时下线(2026-09-29):不再向接口提交 promotions 参数,恢复时取消本段注释 =====
  410. // const targetId = (name) =>
  411. // keepBigIntId(positionTarget(positionList, name)?.id) || null;
  412. // const conditions = (level.promote?.conditions || [])
  413. // .map((conditionName) =>
  414. // typeof conditionName === "string"
  415. // ? conditionName
  416. // : conditionName?.conditionName,
  417. // )
  418. // .filter(Boolean)
  419. // .map((conditionName) => ({ conditionName }));
  420. // // 优先用 path 上已有的 id(adaptLevelConfig 从后端 VO 保留),
  421. // // 找不到再去 options 里按 name 二次解析(兼容手动编辑 / 旧数据)
  422. // const resolveTargetId = (pathId, pathName) =>
  423. // keepBigIntId(pathId) || keepBigIntId(positionTarget(positionList, pathName)?.id) || null;
  424. // let promotions = (level.promote?.paths || [])
  425. // .map((path) =>
  426. // compact({
  427. // upwardPromotionPositionId: resolveTargetId(path.upId, path.up),
  428. // upwardPromotionPositionName: path?.up,
  429. // downwardDemotionPositionId: resolveTargetId(path.downId, path.down),
  430. // downwardDemotionPositionName: path?.down,
  431. // lateralRotationPositionId: resolveTargetId(path.lateralId, path.lateral),
  432. // lateralRotationPositionName: path?.lateral,
  433. // conditions,
  434. // }),
  435. // )
  436. // .filter(
  437. // (item) =>
  438. // item.upwardPromotionPositionName ||
  439. // item.downwardDemotionPositionName ||
  440. // item.lateralRotationPositionName,
  441. // );
  442. // // 无路径但有条件时仍提交一条,避免晋升条件丢失
  443. // if (!promotions.length && conditions.length) {
  444. // promotions = [{ conditions }];
  445. // }
  446. // ===== 晋升路径暂时下线结束 =====
  447. const salary = compact({
  448. salaryMin: Number(level.salaryMin || 0),
  449. salaryMid: Number(level.salaryMid || 0),
  450. salaryMax: Number(level.salaryMax || 0),
  451. salaryTemplate: String(level.salaryTemplateId || ""),
  452. // 绩效方案:表单已临时隐藏,暂不提交该字段;恢复时取消下一行注释
  453. // performanceScheme: String(level.perfSchemeId || ""),
  454. ageMin: Number(level.ageRange?.[0] ?? 18),
  455. ageMax: Number(level.ageRange?.[1] ?? 60),
  456. birthDateRequirement: level.effectDate || undefined,
  457. educationRequirement: level.eduReq || "",
  458. experienceYears: Number(level.expReq || 0),
  459. genderRequirement: level.genderReq || "不限",
  460. workEnvironment: level.workEnv || "",
  461. workShift: level.workShift || "",
  462. industryExperience: level.industryExperience || "",
  463. professionalAbility: level.professionalAbility || "",
  464. otherRequirement: level.otherRequirement || "",
  465. });
  466. // dutyContent 为必填字段,即使空串也必须提交(不可被 compact 去掉)
  467. salary.dutyContent = resolveDutyContent(level, level.fallbackDutyContent);
  468. const skills = (level.skills || [])
  469. .filter((item) => item.name)
  470. .map((item, index) =>
  471. compact({
  472. skillId: item.skillId || undefined,
  473. // skillCode: item.code || item.skillCode || `SKILL-${index + 1}`,
  474. skillName: item.name,
  475. skillLevel: item.level || item.skillLevel || "",
  476. // 文档:1-必备、0-非必备、2-加分
  477. essentialStatus: item.required ? "1" : "0",
  478. certRequired: Number(item.certRequired || 0),
  479. certOrg: item.certOrg || "",
  480. certificateId: item.certificateId || undefined,
  481. coreSkill: item.coreSkill,
  482. skillWeight: item.skillWeight,
  483. affectsPostEligibility: item.affectsPostEligibility,
  484. minimumGuaranteeCount: item.minimumGuaranteeCount,
  485. }),
  486. );
  487. const certificates = (level.certificates || [])
  488. .filter((item) => item.name)
  489. .map((item, index) =>
  490. compact({
  491. certificateId: item.certificateId || undefined,
  492. certificateName: item.name,
  493. certificateType: item.category || item.certificateType || "",
  494. sortNo: index + 1,
  495. }),
  496. );
  497. /* 推荐培训:接口要求 trainingId + trainingName 同时必填(保存时固化名称快照)。
  498. 名称取表单里随下拉选项/回显数据维护的 trainingNameById,取不到就不提交名称(不编造,
  499. 由后端按缺必填项报错暴露问题)。 */
  500. const trainingNameById = level.trainingNameById || {};
  501. const trainings = (level.trainings || [])
  502. .map((item) =>
  503. item && typeof item === "object" ? (item.trainingId ?? item.id) : item,
  504. )
  505. .map((value) => keepBigIntId(value))
  506. .filter((value) => value !== undefined && value !== null && value !== "")
  507. .map((trainingId) => ({
  508. trainingId,
  509. trainingName:
  510. String(trainingNameById[String(trainingId)] ?? "").trim() || undefined,
  511. }));
  512. const sequenceText = String(
  513. positionSequence || level.positionSequence || "",
  514. ).trim();
  515. const config = {
  516. positionLevel: level.level || level.positionLevel || "",
  517. salary,
  518. };
  519. // 层级主键:关联 t_hr_position_sequence_level.id(文档「岗位序列层级配置」positionLevelId)
  520. const levelId = keepBigIntId(level.levelId);
  521. if (levelId) config.positionLevelId = levelId;
  522. // 层级配置同样需要岗位序列,否则后端会报 positionSequence is required
  523. if (sequenceText) config.positionSequence = sequenceText;
  524. if (skills.length) config.skills = skills;
  525. if (certificates.length) config.certificates = certificates;
  526. if (trainings.length) config.trainings = trainings;
  527. // 晋升路径暂时下线,不再提交 promotions;恢复时取消下一行注释
  528. // if (promotions.length) config.promotions = promotions;
  529. return config;
  530. }
  531. // 无 levels 时,用表单顶层字段兜底成一条层级配置(兼容旧数据)
  532. function fallbackLevelFromForm(form = {}) {
  533. return {
  534. level: form.positionLevel || apiLevel(form.level, form.seq) || "",
  535. positionLevel: form.positionLevel || apiLevel(form.level, form.seq) || "",
  536. salaryMin: form.salaryMin,
  537. salaryMid: form.salaryMid,
  538. salaryMax: form.salaryMax,
  539. salaryTemplateId: form.salaryTemplateId,
  540. perfSchemeId: form.perfSchemeId,
  541. eduReq: form.eduReq,
  542. genderReq: form.genderReq,
  543. expReq: form.expReq,
  544. ageRange: form.ageRange,
  545. effectDate: form.effectDate,
  546. workEnv: form.workEnv,
  547. workShift: form.workShift,
  548. skills: form.skills,
  549. certificates: form.certificates,
  550. trainings: form.trainings,
  551. /* 推荐培训名称快照(提交时接口要求 trainingId + trainingName 都必填) */
  552. trainingNameById: form.trainingNameById,
  553. promote: form.promote,
  554. duties: form.duties,
  555. dutyText:
  556. form.dutyText ||
  557. (Array.isArray(form.duties) ? form.duties.join("\n") : ""),
  558. };
  559. }
  560. /**
  561. * 序列化为「完整岗位档案新增/修改参数」
  562. * 文档约定:业务数据以 levelConfigs 为准;顶层 salary/skills/certificates/promotions 为历史兼容字段,新建修改请勿依赖
  563. */
  564. function serializePosition(form, positionList = []) {
  565. const levels =
  566. Array.isArray(form.levels) && form.levels.length
  567. ? form.levels
  568. : [fallbackLevelFromForm(form)];
  569. const plannedCount = Number(
  570. form.plannedRecruitmentCount ?? form.headcount ?? 0,
  571. );
  572. const rawId = form.id;
  573. const positionSequence = String(
  574. form.positionSequence || form.sequence || sequenceName(form.seq) || "",
  575. ).trim();
  576. const payload = compact({
  577. id: keepBigIntId(rawId),
  578. deptId: keepBigIntId(form.deptId),
  579. organizationId: keepBigIntId(form.legalId),
  580. positionCode: form.positionCode || form.code,
  581. positionName: form.name,
  582. positionType: form.type || "",
  583. status: STATUS_TO_API[form.status] ?? 0,
  584. // 待撤销(前端 status=3 → API=2) 必须带计划撤销日期
  585. revokeDate: form.status === 3 ? form.revokeDate || undefined : undefined,
  586. // 文档:plannedRecruitmentCount minimum 1
  587. plannedRecruitmentCount: plannedCount >= 1 ? plannedCount : undefined,
  588. });
  589. // 岗位序列为必填:不可被 compact 去掉空串后导致后端报 positionSequence is required
  590. payload.positionSequence = positionSequence;
  591. // 部门岗位关联仅含 establishmentCount;档案保存不写入 Main 编制表,仅作快照可选提交
  592. if (form.staffing !== undefined && form.staffing !== null && form.staffing !== "") {
  593. payload.departmentPosition = {
  594. establishmentCount: Number(form.staffing || 0),
  595. };
  596. }
  597. const dutyContent = resolveDutyContent({
  598. dutyText: form.dutyText,
  599. duties: form.duties,
  600. });
  601. const targets = [
  602. ...(positionList || []),
  603. ...(form.promoteTargetOptions || []),
  604. ];
  605. payload.levelConfigs = levels.map((level) =>
  606. buildLevelConfig(
  607. {
  608. ...level,
  609. dutyText: resolveDutyContent(level, dutyContent),
  610. duties:
  611. Array.isArray(level.duties) && level.duties.length
  612. ? level.duties
  613. : form.duties,
  614. fallbackDutyContent: dutyContent,
  615. },
  616. targets,
  617. positionSequence,
  618. ),
  619. );
  620. // 后端接收顶层 salary.dutyContent,同时与当前层级配置保持一致
  621. payload.salary = {
  622. ...(payload.levelConfigs[0]?.salary || {}),
  623. dutyContent:
  624. payload.levelConfigs[0]?.salary?.dutyContent || dutyContent,
  625. };
  626. return payload;
  627. }
  628. /** 晋升通道地图:List«晋升通道分组» */
  629. // 岗位管理
  630. export const getPositionDashboard = async () =>
  631. unwrap(await request.get("/hr/position/dashboard"));
  632. export const getPositionPage = async (data = {}) =>
  633. unwrap(await request.post("/hr/position/page", compact(data)));
  634. export const getPositionById = async (id) =>
  635. adaptPosition(
  636. await unwrap(
  637. await request.get(
  638. `/hr/position/getById/${encodeURIComponent(String(keepBigIntId(id) ?? id))}`,
  639. ),
  640. ),
  641. );
  642. // ========== 部门岗位关联表(/fm/tmaindepartmentposition)级联查询 ==========
  643. // deptId 入参均为入职部门 ID;响应统一 { code, data: [], message }
  644. /** 查询部门岗位序列(入职部门 → 岗位序列) */
  645. export const getDepartmentPositionSequences = async (deptId) =>
  646. unwrap(
  647. await request.get("/fm/tmaindepartmentposition/sequence/list", {
  648. params: compact({ deptId: keepBigIntId(deptId) }),
  649. }),
  650. );
  651. /** 查询序列下岗位(入职部门 + 岗位序列 → 岗位) */
  652. export const getDepartmentPositions = async ({ deptId, positionSequenceId }) =>
  653. unwrap(
  654. await request.get("/fm/tmaindepartmentposition/position/list", {
  655. params: compact({
  656. deptId: keepBigIntId(deptId),
  657. positionSequenceId: keepBigIntId(positionSequenceId),
  658. }),
  659. }),
  660. );
  661. /** 查询岗位性质(入职部门 + 岗位 + 岗位序列 → 岗位性质数组,调用方取第一条) */
  662. export const getDepartmentPositionType = async ({
  663. deptId,
  664. positionId,
  665. positionSequenceId,
  666. }) =>
  667. unwrap(
  668. await request.get("/fm/tmaindepartmentposition/positionType/list", {
  669. params: compact({
  670. deptId: keepBigIntId(deptId),
  671. positionId: keepBigIntId(positionId),
  672. positionSequenceId: keepBigIntId(positionSequenceId),
  673. }),
  674. }),
  675. );
  676. /** 查询岗位层级(入职部门 + 岗位 + 岗位序列 + 岗位性质编码 → 岗位层级) */
  677. export const getDepartmentPositionLevels = async ({
  678. deptId,
  679. positionId,
  680. positionSequenceId,
  681. positionType,
  682. }) =>
  683. unwrap(
  684. await request.get("/fm/tmaindepartmentposition/positionLevel/list", {
  685. params: compact({
  686. deptId: keepBigIntId(deptId),
  687. positionId: keepBigIntId(positionId),
  688. positionSequenceId: keepBigIntId(positionSequenceId),
  689. positionType,
  690. }),
  691. }),
  692. );
  693. export const savePosition = async (form, list) =>
  694. unwrap(
  695. await request.post("/hr/position/save", serializePosition(form, list)),
  696. );
  697. export const updatePosition = async (form, list) =>
  698. unwrap(
  699. await request.put("/hr/position/update", serializePosition(form, list)),
  700. );
  701. export const updatePositionStatus = async (data) =>
  702. unwrap(
  703. await request.put("/hr/position/updateStatus", {
  704. ...data,
  705. id: keepBigIntId(data?.id) ?? data?.id,
  706. }),
  707. );
  708. export const deletePosition = async (id) =>
  709. unwrap(
  710. await request.get(
  711. `/hr/position/delete/${encodeURIComponent(String(keepBigIntId(id) ?? id))}`,
  712. ),
  713. );
  714. export const synchronizePositionOccupancy = async () =>
  715. unwrap(await request.post("/hr/position/occupancy/synchronize"));
  716. export const getPositionChangeHistory = async (id) =>
  717. unwrap(
  718. await request.get(
  719. `/hr/position/${encodeURIComponent(String(keepBigIntId(id) ?? id))}/changeHistory`,
  720. ),
  721. );
  722. export const getPromotionTargets = async (type, params = {}) =>
  723. unwrap(
  724. await request.get(`/hr/positionSequence/promotion-levels/${type}`, {
  725. params: compact(params),
  726. }),
  727. );
  728. /** 流动岗位序列层级下拉 → 扁平选项(序列 · 层级) */
  729. export function adaptPromotionLevelOptions(list = []) {
  730. const options = [];
  731. (Array.isArray(list) ? list : []).forEach((seq) => {
  732. const sequenceName = seq.sequence || "";
  733. const sequenceId = seq.id;
  734. (seq.levelArrayList || []).forEach((level) => {
  735. const levelText = level.level || level.levelName || "";
  736. if (!levelText) return;
  737. if (level.status != null && Number(level.status) === 0) return;
  738. const name = sequenceName ? `${sequenceName} · ${levelText}` : levelText;
  739. options.push({
  740. id: level.id ?? `${sequenceId || "seq"}-${levelText}`,
  741. name,
  742. positionName: name,
  743. positionSequence: sequenceName,
  744. positionLevel: levelText,
  745. sequenceId,
  746. levelId: level.id,
  747. levelGrade: Number(level.levelGrade) || 0,
  748. });
  749. });
  750. });
  751. return options;
  752. }
  753. export const generatePositionDocument = async (id) =>
  754. unwrap(await request.post(`/hr/position/${id}/document`));
  755. export async function downloadPositionDocument(id) {
  756. downloadBlobResponse(
  757. await request.get(`/hr/position/document/${id}/download`, {
  758. responseType: "blob",
  759. }),
  760. "岗位说明书.pdf",
  761. );
  762. }
  763. export async function downloadPositionImportTemplate() {
  764. downloadBlobResponse(
  765. await request.get("/hr/position/import/template", { responseType: "blob" }),
  766. "岗位导入模板.xlsx",
  767. );
  768. }
  769. export async function importPositions(file) {
  770. const data = new FormData();
  771. data.append("file", file);
  772. return unwrap(
  773. await request.post("/hr/position/import", data, {
  774. headers: { "Content-Type": "multipart/form-data" },
  775. }),
  776. );
  777. }
  778. // 晋升(岗位晋升通道:地图 / 导出 / 申请分页;审批流由 BPM 负责,评审类接口已下线)
  779. export async function exportPromotionChannels() {
  780. downloadBlobResponse(
  781. await request.post(
  782. "/hr/promotion/channels/export",
  783. {},
  784. { responseType: "blob" },
  785. ),
  786. "晋升通道说明书.xlsx",
  787. );
  788. }
  789. // 岗位序列
  790. export const getPositionSequenceList = async (params = {}) => {
  791. // 查询岗位序列列表:支持按所属机构过滤(groupId)
  792. // 返回 List<岗位序列列表> { id, sequence, status, groupId, levelList:[{level,status}] }
  793. const list = unwrap(
  794. await request.get("/hr/positionSequence/getPositionSequenceList", {
  795. params: compact(params),
  796. }),
  797. );
  798. return (Array.isArray(list) ? list : []).map((item) => ({
  799. id: item.id,
  800. sequence: item.sequence || "",
  801. status: item.status,
  802. /* 序列归属机构:后端可能返回 groupId 或 legalId,供左侧按机构过滤 */
  803. groupId: item.groupId ?? item.legalId ?? null,
  804. levelList: (item.levelList || []).map((level) => {
  805. // 仅保留数字主键;勿用层级文案(P1/T4)冒充 id,否则人员档案无法提交 positionLevelId
  806. const rawId = level.id != null ? String(level.id).trim() : "";
  807. const id = /^-?\d+$/.test(rawId) ? rawId : "";
  808. return {
  809. id,
  810. label: level.level || level.levelName || "",
  811. // 岗位层级文本(如 P1/T4):岗位申请 positionLevel 需为该序列启用层级文本
  812. value: level.level || level.levelName || "",
  813. levelGrade: Number(level.levelGrade) || 0,
  814. status: level.status,
  815. };
  816. }),
  817. }));
  818. };
  819. export const getPositionSequenceLevels = async (sequence) =>
  820. unwrap(
  821. await request.get("/hr/positionSequence/levels", {
  822. params: { sequence },
  823. }),
  824. );
  825. export const getPositionSequencePage = async (data = {}) =>
  826. unwrap(await request.post("/hr/positionSequence/page", compact(data)));
  827. export const savePositionSequence = async (data) =>
  828. unwrap(await request.post("/hr/positionSequence/save", data));
  829. export const updatePositionSequence = async (data) =>
  830. unwrap(await request.put("/hr/positionSequence/update", data));
  831. export const isPositionSequenceInUse = async (id) =>
  832. unwrap(await request.get(`/hr/positionSequence/${id}/in-use`));
  833. export const savePositionSequenceLevels = async (data) =>
  834. unwrap(await request.post("/hr/positionSequence/levels/save", data));
  835. // 劳动合同模板
  836. export const getContractTemplatePage = async (data = {}) =>
  837. unwrap(await request.get("/hr/contracts/templates", { params: compact(data) }));
  838. export const getContractTemplateById = async (id) =>
  839. unwrap(await request.get(`/hr/contracts/templates/${id}`));
  840. export const saveContractTemplate = async (data) =>
  841. unwrap(await request.post("/hr/contracts/templates", data));
  842. export const updateContractTemplate = async (data) =>
  843. unwrap(await request.put(`/hr/contracts/templates/${data.id}`, data));
  844. export const deleteContractTemplate = async (id) =>
  845. unwrap(await request.get(`/hr/contracts/templates/delete/${id}`));
  846. export const disableContractTemplate = async (id) =>
  847. unwrap(await request.post(`/hr/contracts/templates/${id}/disable`));
  848. export const enableContractTemplate = async (id) =>
  849. unwrap(await request.post(`/hr/contracts/templates/${id}/enable`));
  850. export const submitContractTemplate = async (id) =>
  851. unwrap(await request.post(`/hr/contracts/templates/${id}/submit`));
  852. export const getContractTemplateVersions = async (id) =>
  853. unwrap(await request.get(`/hr/contracts/templates/${id}/versions`));
  854. export const createContractTemplateVersion = async (id, data) =>
  855. unwrap(await request.post(`/hr/contracts/templates/${id}/versions`, data));
  856. export {
  857. CONTRACT_FLOW_SIGN_STATUS_LABELS,
  858. CONTRACT_FLOW_SIGN_STATUS_OPTIONS,
  859. CONTRACT_SIGNING_STATUS_LABELS,
  860. CONTRACT_SIGNING_STATUS_OPTIONS,
  861. CONTRACT_STATUS_LABELS,
  862. CONTRACT_STATUS_OPTIONS,
  863. adaptContractFlowSignNodes,
  864. contractFlowSignStatusLabel,
  865. contractSigningStatusLabel,
  866. contractStatusLabel,
  867. pickFlowSignStatus,
  868. } from "./contractStatus";
  869. // 劳动合同管理
  870. export const getContractPage = async (data = {}) =>
  871. unwrap(await request.get("/hr/contracts/page", { params: compact(data) }));
  872. /**
  873. * 导出劳动合同台账 POST /hr/contracts/export
  874. * ★ 复用分页查询的组织范围与**全部业务筛选条件**;pageNum / size 不传(文档:不限制导出范围)。
  875. * 接口直接返回 .xlsx 文件流,故 responseType=blob + 放宽 timeout;
  876. * downloadBlob 会拦截「HTTP 200 + JSON 业务错误」,避免把错误 JSON 存成 xlsx。
  877. * @param {{keyword?:string, contractType?:string, contractStatus?:string, departmentId?:*, legalEntityId?:*,
  878. * positionId?:*, signingMethod?:string, signingStatus?:string, startDate?:string, endDate?:string,
  879. * expiringOnly?:boolean}} params
  880. */
  881. export const exportContractLedger = async (params = {}) => {
  882. const payload = compact({
  883. keyword: params.keyword || undefined,
  884. contractType: params.contractType || undefined,
  885. contractStatus: params.contractStatus || undefined,
  886. departmentId: keepBigIntId(params.departmentId),
  887. legalEntityId: keepBigIntId(params.legalEntityId),
  888. positionId: keepBigIntId(params.positionId),
  889. signingMethod: params.signingMethod || undefined,
  890. signingStatus: params.signingStatus || undefined,
  891. startDate: params.startDate ? String(params.startDate).slice(0, 10) : undefined,
  892. endDate: params.endDate ? String(params.endDate).slice(0, 10) : undefined,
  893. // 仅勾选「即将到期」时才传 true(不传 = 不限制)
  894. expiringOnly: params.expiringOnly === true ? true : undefined,
  895. });
  896. await downloadBlobResponse(
  897. await request.post("/hr/contracts/export", payload, {
  898. responseType: "blob",
  899. timeout: 120000,
  900. }),
  901. `劳动合同台账_${new Date().toISOString().slice(0, 10)}.xlsx`,
  902. );
  903. };
  904. /** 新建劳动合同 POST /hr/contracts/save */
  905. export const CONTRACT_APPROVAL_BUSINESS_KEY = "hr_contract_approval";
  906. /** 劳动合同「解除/终止」审批的业务标识(与合同审批区分开) */
  907. export const CONTRACT_TERMINATION_APPROVAL_BUSINESS_KEY =
  908. "hr_contract_termination_approval";
  909. /**
  910. * 劳动合同「变更」审批的业务标识(2026-10-17 后端确认):
  911. * 流程为 —— 先 POST /hr/contracts/{id}/changes 保存变更草稿(返回变更单主键),
  912. * 再以该业务标识 + **变更单主键** 调 /bpm/process-instance/create 发起审批;
  913. * 审批终态通过后才把变更字段应用到合同上。
  914. */
  915. export const CONTRACT_CHANGE_APPROVAL_BUSINESS_KEY = "hr_contract_change_approval";
  916. export const REGULARIZATION_APPROVAL_BUSINESS_KEY = "main_regularization_approval";
  917. export const saveContract = async (data) =>
  918. unwrap(await request.post("/hr/contracts/save", data));
  919. /** @deprecated 使用 saveContract,接口已改为 /hr/contracts/save */
  920. /** 修改劳动合同草稿 PUT /hr/contracts/{id},参数同新建 + id */
  921. export const updateContract = async (id, data) =>
  922. unwrap(await request.put(`/hr/contracts/${id}`, compact({ ...data, id })));
  923. export const getContractById = async (id) =>
  924. unwrap(await request.get(`/hr/contracts/${id}`));
  925. export const matchContractTemplate = async (data) =>
  926. unwrap(await request.post("/hr/contracts/template-match", data));
  927. export const changeContract = async (id, data) =>
  928. unwrap(await request.post(`/hr/contracts/${id}/changes`, data));
  929. /** 查询合同变更历史 GET /hr/contracts/{id}/changes(含 DRAFT 草稿,afterSnapshot 可回显变更后值) */
  930. export const getContractChanges = async (id) =>
  931. unwrap(await request.get(`/hr/contracts/${id}/changes`));
  932. export const renewContract = async (id, data) =>
  933. unwrap(await request.post(`/hr/contracts/${id}/renew`, data));
  934. export const terminateContract = async (id, data) =>
  935. unwrap(await request.post(`/hr/contracts/${id}/termination`, data));
  936. export const archiveContractOffline = async (id, data) =>
  937. unwrap(await request.post(`/hr/contracts/${id}/offline/archive`, data));
  938. export const voidContract = async (id, data) =>
  939. unwrap(await request.post(`/hr/contracts/${id}/void`, data));
  940. /* 作废三类「未提交审批」的单据草稿(2026-10-20 契约确认):
  941. 变更草稿 / 续签草稿 / 解除终止草稿各自挂在原合同下,id 是原合同主键,第二个路径参数是草稿主键。
  942. ★ 变更、解除的作废接口只有路径参数、无请求体;续签作废收「劳动合同作废参数」{ reason }。
  943. ★ 不要再用 /hr/contracts/{id}/void 传草稿主键顶替合同主键。 */
  944. export const voidContractChange = async (id, changeId) =>
  945. unwrap(await request.post(`/hr/contracts/${id}/changes/${changeId}/void`));
  946. export const voidContractRenewal = async (id, renewalContractId, data) =>
  947. unwrap(await request.post(`/hr/contracts/${id}/renewals/${renewalContractId}/void`, data));
  948. export const voidContractTermination = async (id, terminationId) =>
  949. unwrap(await request.post(`/hr/contracts/${id}/terminations/${terminationId}/void`));
  950. export const getContractUnrenewedReport = async (data = {}) =>
  951. unwrap(await request.get("/hr/contracts/reports/unrenewed", { params: compact(data) }));
  952. /**
  953. * 查询员工合同流程状态 GET /hr/contracts/employee/{employeeId}/status
  954. * 返回当前员工合同所处流程节点:
  955. * - 无合同:返回 null / 空
  956. * - 草稿(已保存未提交):{ contractId, statusCode: 'DRAFT', ... }
  957. * - 审批中 / 待签署等:{ contractId, statusCode: 'PENDING_APPROVAL', flowNode, ... }
  958. */
  959. export const getEmployeeContractStatus = async (employeeId) =>
  960. unwrap(await request.get(`/hr/contracts/employee/${employeeId}/status`));
  961. /** 合同类型 code → 中文标签 */
  962. const CONTRACT_TYPE_LABELS = {
  963. FIXED: "固定期限劳动合同",
  964. NON_FIXED: "无固定期限劳动合同",
  965. TASK: "以完成一定任务为期限",
  966. DISPATCH: "劳务派遣合同",
  967. INTERN: "实习协议",
  968. CONFIDENTIAL: "保密协议",
  969. NON_COMPETE: "竞业限制协议",
  970. APPRENTICE: "学徒合同",
  971. OTHER: "其他",
  972. };
  973. export function contractTypeLabel(code) {
  974. return CONTRACT_TYPE_LABELS[String(code || "").toUpperCase()] || String(code || "");
  975. }
  976. /* 签署方式不再本地翻译:接口已返回 signingMethodName,直接展示 */
  977. function fmtDate(v) {
  978. if (!v) return "";
  979. const d = new Date(v);
  980. if (isNaN(d.getTime())) return String(v);
  981. const pad = (n) => String(n).padStart(2, "0");
  982. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  983. }
  984. function fmtDateTime(v) {
  985. if (!v) return "";
  986. const d = new Date(v);
  987. if (isNaN(d.getTime())) return String(v);
  988. const pad = (n) => String(n).padStart(2, "0");
  989. return `${fmtDate(v)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  990. }
  991. /** 合同附件文件主键 */
  992. function collectAttachmentFileIds(item = {}) {
  993. const fromIds = String(item.attachmentIds || "").split(",").map((s) => s.trim()).filter(Boolean);
  994. if (fromIds.length) return fromIds;
  995. const files = Array.isArray(item.files) ? item.files : [];
  996. const attach = files.filter((f) => ["ATTACHMENT", "SCAN", "ARCHIVE"].includes(f.filePurpose));
  997. const source = attach.length ? attach : files;
  998. const fromFiles = source.map((f) => f.fileId || f.id).filter((id) => id != null && id !== "").map(String);
  999. if (fromFiles.length) return fromFiles;
  1000. const extra = [item.offlineFileId, item.fileId].filter((id) => id != null && id !== "");
  1001. return extra.map(String);
  1002. }
  1003. /**
  1004. * 试用期展示文案:合同详情回显字段是 probationMonths(月数)
  1005. * 0 / 空 → "无试用期";大于 0 → "N个月"(如 3个月)
  1006. */
  1007. export function probationMonthsText(value) {
  1008. const months = Number(value);
  1009. if (!Number.isFinite(months) || months <= 0) return "无试用期";
  1010. return `${months}个月`;
  1011. }
  1012. /**
  1013. * 合同后端 DTO → 前端展示对象
  1014. * 合同管理列表、合同详情、入职签署合同节点共用
  1015. */
  1016. export function adaptContract(item = {}) {
  1017. const startDate = fmtDate(item.startDate);
  1018. const endDate = fmtDate(item.endDate);
  1019. const termText =
  1020. item.contractTerm ||
  1021. (item.contractTermMonths != null && item.contractTermMonths !== ""
  1022. ? `${item.contractTermMonths}个月`
  1023. : "");
  1024. const processInstanceId =
  1025. item.processInstanceId ||
  1026. item.approvalInstanceId ||
  1027. item.bpmProcessInstanceId ||
  1028. item.processInstance ||
  1029. item.processInstanceIdStr ||
  1030. "";
  1031. const approvalInstanceId = item.approvalInstanceId || item.processInstanceId || "";
  1032. return {
  1033. ...item,
  1034. employeeNo: item.employeeCode || item.employeeNo || "",
  1035. employeeCode: item.employeeCode || item.employeeNo || "",
  1036. legalEntity: item.legalEntityName || "",
  1037. department: item.departmentName || "",
  1038. contractTypeCode: item.contractType || "",
  1039. /* ★ 接口已补中文字段(contractTypeName / contractStatusName / signingStatusName /
  1040. signingMethodName),列表与详情直接展示接口值,不再本地枚举翻译 */
  1041. contractType: item.contractTypeName || "",
  1042. signMethod: item.signingMethodName || "",
  1043. signingStatus: item.signingStatus || "",
  1044. signingStatusLabel: item.signingStatusName || "",
  1045. flowSignStatus: pickFlowSignStatus(item),
  1046. flowSignStatusLabel: contractFlowSignStatusLabel(pickFlowSignStatus(item)),
  1047. flowSignNodes: adaptContractFlowSignNodes(item),
  1048. status: item.contractStatusName || "",
  1049. statusCode: item.contractStatus || "",
  1050. processInstanceId,
  1051. approvalInstanceId,
  1052. salary: item.salaryStandard,
  1053. signDate: fmtDate(item.signingDate),
  1054. startDate,
  1055. endDate,
  1056. contractTerm: termText,
  1057. remainingDays:
  1058. item.remainingDays === null || item.remainingDays === undefined || item.remainingDays === ""
  1059. ? null
  1060. : Number(item.remainingDays),
  1061. probationMonths: item.probationMonths,
  1062. /* 详情展示用文案:0/空 = 无试用期,>0 = N个月 */
  1063. probationText: probationMonthsText(item.probationMonths),
  1064. createdAt: fmtDateTime(item.createTime),
  1065. probationEnd: fmtDate(item.probationEndDate),
  1066. attachmentFileIds: collectAttachmentFileIds(item),
  1067. level: item.positionLevel || item.level || "",
  1068. positionSequence: item.positionSequence || item.sequenceName || "",
  1069. positionSequenceId: item.positionSequenceId || item.sequenceId || "",
  1070. positionLevel: item.positionLevel || item.level || "",
  1071. positionLevelId: item.positionLevelId || "",
  1072. positionType: item.positionType || "",
  1073. /* 名称只取真正的名称字段:employmentType 是岗位性质字典编码,当名称用会显示成 "1" */
  1074. positionTypeName: item.positionTypeName || "",
  1075. employmentType: item.employmentType || "",
  1076. manager: item.managerName || item.manager || "",
  1077. /* 合同变更状态:字段是 changeApprovalStatus / changeApprovalStatusName(2026-10-17 抓包确认)。
  1078. ★ 只有发生过变更的合同才返回该字段,没变更的合同没有 —— 字段缺失 = 未发生变更(用户口径),
  1079. 不是接口异常。给了中文名就用中文名,只给编码时原样展示编码(不编造文案)。 */
  1080. changeStatusCode: item.changeApprovalStatus || "",
  1081. changeStatusLabel: item.changeApprovalStatusName || "",
  1082. /* 变更单主键:发起变更审批(hr_contract_change_approval)时作为 businessId */
  1083. changeId: item.changeId || "",
  1084. /* 变更审批流程实例:变更审批中的行查看「变更审批记录」用 */
  1085. changeProcessInstanceId: item.changeProcessInstanceId || "",
  1086. /* 最近一次续签状态:DRAFT/PENDING_APPROVAL/PENDING_SIGN/SIGNED_WAIT_EFFECTIVE/
  1087. EFFECTIVE/EXPIRING/EXPIRED/RENEWED/CHANGED/CANCELLED;从未发起时为空(2026-10-17 抓包) */
  1088. renewalStatusCode: item.renewalStatus || "",
  1089. renewalStatusLabel: item.renewalStatusName || "",
  1090. /* 最近一次解除终止审批状态:DRAFT/PENDING_APPROVAL/SIGNING/EFFECTIVE/REJECTED/CANCELLED;
  1091. 从未发起时为空 */
  1092. terminationStatusCode: item.terminationApprovalStatus || "",
  1093. terminationStatusLabel: item.terminationApprovalStatusName || "",
  1094. /* 续签生成的新合同主键(若列表行返回):续签审批的 businessId;
  1095. 字段名以后端实际返回为准,没返回时前端明确提示,不拿原合同 id 顶替 */
  1096. renewalContractId: item.renewalContractId || "",
  1097. /* 续签审批流程实例:续签审批中的行查看「审批记录」用(列表未返回时回落到新合同详情) */
  1098. renewalProcessInstanceId: item.renewalProcessInstanceId || "",
  1099. /* 解除终止单主键(2026-10-19 契约确认):解除审批(hr_contract_termination_approval)
  1100. 的 businessId;编辑解除草稿时也用它原地修改 */
  1101. terminationId: item.terminationId || "",
  1102. /* 解除审批流程实例:解除审批中的行查看「审批记录」用 */
  1103. terminationProcessInstanceId: item.terminationProcessInstanceId || "",
  1104. };
  1105. }
  1106. // 招聘需求
  1107. export const getRecruitDemandPage = async (data = {}) =>
  1108. unwrap(await request.get("/recruit/demands", { params: compact(data) }));
  1109. export const submitRecruitDemand = async (data) =>
  1110. unwrap(await request.post("/recruit/demands", compact(data)));
  1111. export const getRecruitDemandById = async (id) =>
  1112. unwrap(await request.get(`/recruit/demands/${id}`));
  1113. // 岗位申请
  1114. export const createPositionApplication = async (data) =>
  1115. unwrap(await request.post("/hr/position/apply", data));
  1116. export const getPositionApplicationPage = async (data = {}) =>
  1117. unwrap(await request.post("/hr/position/apply/page", compact(data)));
  1118. export const getPositionApplicationById = async (id) =>
  1119. unwrap(await request.get(`/hr/position/apply/${id}`));
  1120. export const updatePositionApplication = async (id, data) =>
  1121. unwrap(await request.put(`/hr/position/apply/${id}`, data));
  1122. export const deletePositionApplication = async (id) =>
  1123. unwrap(await request.delete(`/hr/position/apply/${id}`));
  1124. export const submitPositionApplication = async (id) =>
  1125. unwrap(await request.post(`/hr/position/apply/${id}/submit`));
  1126. // 岗位申请列表项:后端列表字段 → 页面字段(列表不含职责/薪资/技能,由详情接口补齐)
  1127. export function adaptPositionApplication(raw = {}) {
  1128. return {
  1129. ...raw,
  1130. id: raw.id,
  1131. code: raw.positionCode || "",
  1132. documentCode: raw.documentCode || "",
  1133. positionName: raw.positionName || "",
  1134. organizationId: raw.organizationId,
  1135. organizationName: raw.organizationName || "",
  1136. deptId: raw.deptId,
  1137. deptName: raw.deptName || "",
  1138. // 岗位层级为序列层级文本(如 P1/T4),保持字符串展示,字段与后端 positionLevel 对齐
  1139. positionLevel: raw.positionLevel || "",
  1140. positionSequence: raw.positionSequence || "",
  1141. type: raw.positionType || "",
  1142. headcount: Number(raw.plannedRecruitmentCount ?? 0),
  1143. applicant: raw.applicantName || "",
  1144. status: raw.status || "DRAFT",
  1145. updatedAt: raw.updateTime || raw.createTime || "",
  1146. reviewer: raw.reviewerName || "",
  1147. reviewTime: raw.reviewTime || "",
  1148. processInstanceId: raw.processInstanceId || raw.approvalInstanceId || "",
  1149. approvalInstanceId: raw.approvalInstanceId || raw.processInstanceId || "",
  1150. effectivePositionId: raw.effectivePositionId,
  1151. };
  1152. }
  1153. // 岗位申请详情:在列表字段基础上补职责、薪资区间、技能要求与审批记录
  1154. export function adaptPositionApplicationDetail(raw = {}) {
  1155. const salary = raw.salary || {};
  1156. const duties = splitText(salary.dutyContent);
  1157. return {
  1158. ...adaptPositionApplication(raw),
  1159. duties,
  1160. /* 展示用原始 dutyContent,避免 join("\n") 把用户写的「;」改成换行 */
  1161. dutiesText: salary.dutyContent || duties.join("\n"),
  1162. salaryMin: Number(salary.salaryMin ?? 0),
  1163. salaryMid: Number(salary.salaryMid ?? 0),
  1164. salaryMax: Number(salary.salaryMax ?? 0),
  1165. expYears: Number(salary.experienceYears ?? 0),
  1166. education: salary.educationRequirement || "",
  1167. industryExp: salary.industryExperience || "",
  1168. professionalAbility: salary.professionalAbility || "",
  1169. otherRequirement: salary.otherRequirement || "",
  1170. skills: (raw.skills || []).map((item) => ({
  1171. ...item,
  1172. name: item.skillName || "",
  1173. level: item.skillLevel || "",
  1174. required: String(item.essentialStatus) === "1",
  1175. })),
  1176. certificates: (raw.certificates || []).map((item) => ({
  1177. ...item,
  1178. name: item.certificateName || "",
  1179. category: item.certificateType || "",
  1180. })),
  1181. reviewOpinion: raw.reviewOpinion || "",
  1182. operationLogs: raw.operationLogs || [],
  1183. };
  1184. }
  1185. // 岗位申请表单 → 提交参数:基本信息 + 定薪定责 + 技能/证书要求
  1186. export function serializePositionApplication(form) {
  1187. const duties = (form.dutiesText || "")
  1188. .split("\n")
  1189. .map((item) => item.trim())
  1190. .filter(Boolean);
  1191. const salary = compact({
  1192. dutyContent: duties.join("\n"),
  1193. // 薪资字段统一数字类型
  1194. salaryMin: Number(form.salaryMin || 0),
  1195. salaryMid: Number(form.salaryMid || 0),
  1196. salaryMax: Number(form.salaryMax || 0),
  1197. experienceYears: Number(form.expYears || 0),
  1198. educationRequirement: form.education || "",
  1199. industryExperience: form.industryExp || "",
  1200. professionalAbility: form.professionalAbility || "",
  1201. otherRequirement: form.otherRequirement || "",
  1202. });
  1203. const payload = {
  1204. organizationId: form.organizationId,
  1205. deptId: form.deptId,
  1206. positionName: form.positionName,
  1207. positionLevel: form.positionLevel ? String(form.positionLevel) : "",
  1208. positionSequence: form.positionSequence || "",
  1209. positionType: form.type || "",
  1210. plannedRecruitmentCount: Number(form.headcount || 0),
  1211. };
  1212. if (Object.keys(salary).length) payload.salary = salary;
  1213. const skills = (form.skills || [])
  1214. .filter((item) => item.name)
  1215. .map((item) => ({
  1216. skillName: item.name,
  1217. skillLevel: item.level,
  1218. essentialStatus: item.required ? "1" : "2",
  1219. }));
  1220. if (skills.length) payload.skills = skills;
  1221. const certificates = (form.certificates || [])
  1222. .filter((item) => item.name)
  1223. .map((item, index) => ({
  1224. certificateName: item.name,
  1225. certificateType: item.category,
  1226. sortNo: index + 1,
  1227. }));
  1228. if (certificates.length) payload.certificates = certificates;
  1229. if (form.id) payload.id = form.id;
  1230. return compact(payload);
  1231. }