essApplication.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /**
  2. * M09 员工自助「统一申请」接口层
  3. *
  4. * 员工端:GET /hr/ess/applications (只返回本人申请)
  5. * GET /hr/ess/applications/{id}
  6. * 管理端:GET /hr/ess/admin/applications (当前公司组织树内全部申请)
  7. * GET /hr/ess/admin/applications/{id}
  8. *
  9. * 说明:该视图只返回「来源引用 + 脱敏摘要 + 状态 + 当前节点」,
  10. * 业务明细需按 businessType 走各业务来源接口(本文件的 resolveSourceRoute 负责分发)。
  11. */
  12. import { get } from "@/utils/request";
  13. import Vue from "vue";
  14. // ============ 业务类型 → 提交页 / 展示元数据 ============
  15. /**
  16. * 每种业务类型对应的「提交页」。type 为该页接受的表单类型参数。
  17. * path 与 pages.json 注册路径保持一致。
  18. */
  19. export const ESS_BUSINESS_META = {
  20. ATTENDANCE_LEAVE: {
  21. type: "leave",
  22. path: "/pages/hr/apply/form",
  23. title: "请假申请",
  24. icon: "假",
  25. color: "#157a2c",
  26. },
  27. ATTENDANCE_OVERTIME: {
  28. type: "overtime",
  29. path: "/pages/hr/apply/form",
  30. title: "加班申请",
  31. icon: "班",
  32. color: "#61cdbb",
  33. },
  34. ATTENDANCE_OUTING: {
  35. type: "field",
  36. path: "/pages/hr/apply/form",
  37. title: "外勤申请",
  38. icon: "外",
  39. color: "#4b7902",
  40. },
  41. ATTENDANCE_TRIP: {
  42. type: "trip",
  43. path: "/pages/hr/apply/form",
  44. title: "出差申请",
  45. icon: "差",
  46. color: "#f6bd16",
  47. },
  48. ATTENDANCE_CARD_REPAIR: {
  49. type: "check",
  50. path: "/pages/hr/apply/form",
  51. title: "补卡申请",
  52. icon: "卡",
  53. color: "#07a44d",
  54. },
  55. RESIGNATION: {
  56. type: "resign",
  57. path: "/pages/hr/apply/form",
  58. title: "离职申请",
  59. icon: "离",
  60. color: "#ee6666",
  61. },
  62. MAIN_USER_CHANGE: {
  63. type: "transfer",
  64. path: "/pages/hr/manage/transfer-create",
  65. title: "调岗申请",
  66. icon: "岗",
  67. color: "#9270ca",
  68. },
  69. REGULARIZATION: {
  70. type: "regular",
  71. path: "/pages/hr/manage/regular-create",
  72. title: "转正申请",
  73. icon: "正",
  74. color: "#269a99",
  75. },
  76. };
  77. // 历史/别名编码 → 标准编码(后端枚举命名不完全统一时兜底)
  78. const BUSINESS_TYPE_ALIAS = {
  79. RESIGN: "RESIGNATION",
  80. EMPLOYEE_RESIGNATION: "RESIGNATION",
  81. USER_CHANGE: "MAIN_USER_CHANGE",
  82. USERCHANGE: "MAIN_USER_CHANGE",
  83. TRANSFER: "MAIN_USER_CHANGE",
  84. POSITION_TRANSFER: "MAIN_USER_CHANGE",
  85. REGULAR: "REGULARIZATION",
  86. PROBATION_REGULARIZATION: "REGULARIZATION",
  87. ATTENDANCE_REPAIR: "ATTENDANCE_CARD_REPAIR",
  88. ATTENDANCE_FIELD: "ATTENDANCE_OUTING",
  89. };
  90. /** 业务类型编码归一化 */
  91. export function normalizeBusinessType(value) {
  92. const code = String(value || "").trim().toUpperCase();
  93. if (!code) return "";
  94. return BUSINESS_TYPE_ALIAS[code] || code;
  95. }
  96. /** 取业务类型元数据;未知编码返回 null(页面可提示「暂不支持查看」) */
  97. export function businessMetaOf(businessType) {
  98. return ESS_BUSINESS_META[normalizeBusinessType(businessType)] || null;
  99. }
  100. // ============ 状态字典 ============
  101. /** 业务状态 businessStatus */
  102. export const BUSINESS_STATUS_LABELS = {
  103. DRAFT: "草稿",
  104. SUBMITTED: "已提交",
  105. APPROVING: "审批中",
  106. APPROVED: "已通过",
  107. REJECTED: "已驳回",
  108. WITHDRAWN: "已撤回",
  109. CANCELLED: "已取消",
  110. COMPLETED: "已完成",
  111. };
  112. /** 审批状态 approvalStatus */
  113. export const APPROVAL_STATUS_LABELS = {
  114. PENDING: "审批中",
  115. SUBMITTED: "已提交",
  116. APPROVING: "审批中",
  117. APPROVED: "已通过",
  118. REJECTED: "已驳回",
  119. WITHDRAWN: "已撤回",
  120. CANCELLED: "已取消",
  121. };
  122. /** 状态标签配色键:pending 审批中 / approved 已通过 / rejected 已驳回 / cancelled 已取消 / draft 草稿 */
  123. export const ESS_STATUS_CLASS = {
  124. DRAFT: "draft",
  125. SUBMITTED: "pending",
  126. APPROVING: "pending",
  127. APPROVED: "approved",
  128. REJECTED: "rejected",
  129. WITHDRAWN: "cancelled",
  130. CANCELLED: "cancelled",
  131. PENDING: "pending",
  132. COMPLETED: "approved",
  133. };
  134. function labelOf(map, code, fallback = "") {
  135. const key = String(code || "").trim().toUpperCase();
  136. if (!key) return fallback;
  137. return map[key] || key;
  138. }
  139. // ============ 请求封装 ============
  140. // 去掉空串 / null / undefined,避免后端把空值当有效筛选条件
  141. function compact(object = {}) {
  142. return Object.keys(object).reduce((result, key) => {
  143. const value = object[key];
  144. if (value !== "" && value !== null && value !== undefined) result[key] = value;
  145. return result;
  146. }, {});
  147. }
  148. function asList(data) {
  149. if (Array.isArray(data)) return data;
  150. if (data && Array.isArray(data.list)) return data.list;
  151. if (data && Array.isArray(data.records)) return data.records;
  152. return [];
  153. }
  154. /** 大整数主键保持字符串,避免 JS 精度丢失(后端 Long 序列化可能是数字) */
  155. export function essIdOf(value) {
  156. if (value === undefined || value === null || value === "") return "";
  157. return String(value);
  158. }
  159. function normalizePage(res) {
  160. const data = res.data || {};
  161. const list = asList(data);
  162. const total = Number(
  163. data.count !== undefined ? data.count : data.total !== undefined ? data.total : list.length
  164. );
  165. return { list, total };
  166. }
  167. function pageParams(params = {}) {
  168. return compact({
  169. pageNum: params.pageNum || 1,
  170. size: params.size || 20,
  171. businessType: normalizeBusinessType(params.businessType) || undefined,
  172. businessStatus: params.businessStatus,
  173. approvalStatus: params.approvalStatus,
  174. keyword: params.keyword,
  175. // 管理端专用
  176. applicantUserId: params.applicantUserId,
  177. organizationId: params.organizationId,
  178. });
  179. }
  180. /**
  181. * 我的申请分页
  182. * GET /hr/ess/applications
  183. */
  184. export async function listMyApplications(params = {}, loding = true) {
  185. const res = await get(
  186. Vue.prototype.apiUrl + "/hr/ess/applications",
  187. pageParams(params),
  188. loding
  189. );
  190. if (res.code == 0) return normalizePage(res);
  191. return Promise.reject(new Error(res.message || "查询我的申请失败"));
  192. }
  193. /**
  194. * 我的申请详情
  195. * GET /hr/ess/applications/{id}
  196. */
  197. export async function getMyApplicationDetail(id, loding = true) {
  198. if (!essIdOf(id)) return null;
  199. const res = await get(
  200. Vue.prototype.apiUrl + `/hr/ess/applications/${encodeURIComponent(essIdOf(id))}`,
  201. {},
  202. loding
  203. );
  204. if (res.code == 0) return res.data || null;
  205. return Promise.reject(new Error(res.message || "查询申请详情失败"));
  206. }
  207. /**
  208. * 管理端:公司组织树内统一申请分页
  209. * GET /hr/ess/admin/applications
  210. */
  211. export async function listAdminApplications(params = {}, loding = true) {
  212. const res = await get(
  213. Vue.prototype.apiUrl + "/hr/ess/admin/applications",
  214. pageParams(params),
  215. loding
  216. );
  217. if (res.code == 0) return normalizePage(res);
  218. return Promise.reject(new Error(res.message || "查询申请记录失败"));
  219. }
  220. /**
  221. * 管理端:统一申请详情
  222. * GET /hr/ess/admin/applications/{id}
  223. */
  224. export async function getAdminApplicationDetail(id, loding = true) {
  225. if (!essIdOf(id)) return null;
  226. const res = await get(
  227. Vue.prototype.apiUrl +
  228. `/hr/ess/admin/applications/${encodeURIComponent(essIdOf(id))}`,
  229. {},
  230. loding
  231. );
  232. if (res.code == 0) return res.data || null;
  233. return Promise.reject(new Error(res.message || "查询申请详情失败"));
  234. }
  235. // ============ 适配与判定 ============
  236. function formatTime(value) {
  237. if (!value) return "";
  238. const text = String(value).replace("T", " ");
  239. return text.length >= 16 ? text.slice(0, 16) : text;
  240. }
  241. /**
  242. * 统一申请 VO → 列表/详情展示结构
  243. */
  244. export function adaptEssApplication(vo = {}) {
  245. const businessType = normalizeBusinessType(vo.businessType);
  246. const meta = ESS_BUSINESS_META[businessType] || null;
  247. const businessStatus = String(vo.businessStatus || "").toUpperCase();
  248. const approvalStatus = String(vo.approvalStatus || "").toUpperCase();
  249. // 状态优先取业务状态;业务状态缺失时退回审批状态
  250. const statusCode = businessStatus || approvalStatus;
  251. const statusLabel =
  252. labelOf(BUSINESS_STATUS_LABELS, businessStatus) ||
  253. labelOf(APPROVAL_STATUS_LABELS, approvalStatus);
  254. return {
  255. id: essIdOf(vo.id),
  256. applicationNo: vo.applicationNo || "",
  257. title: vo.title || (meta && meta.title) || vo.businessTypeName || "申请",
  258. businessType,
  259. businessTypeName: vo.businessTypeName || (meta && meta.title) || businessType || "",
  260. businessId: essIdOf(vo.businessId),
  261. businessStatus,
  262. approvalStatus,
  263. statusCode,
  264. statusLabel: statusLabel || "—",
  265. statusClass: ESS_STATUS_CLASS[statusCode] || "pending",
  266. currentNode: vo.currentNode || "",
  267. maskedSummary: vo.maskedSummary || "",
  268. applicantName: vo.applicantName || "",
  269. applicantJobNumber: vo.applicantJobNumber || "",
  270. applicantPic: vo.applicantPic || "",
  271. applicantUserId: essIdOf(vo.applicantUserId),
  272. organizationId: essIdOf(vo.organizationId),
  273. organizationName: vo.organizationName || "",
  274. processInstanceId: vo.processInstanceId || "",
  275. sourceTerminal: vo.sourceTerminal || "",
  276. createTime: vo.createTime || "",
  277. createdAt: formatTime(vo.createTime),
  278. icon: (meta && meta.icon) || "申",
  279. color: (meta && meta.color) || "#157a2c",
  280. supported: Boolean(meta),
  281. raw: vo,
  282. };
  283. }
  284. /**
  285. * 是否允许「编辑后重新提交」:仅已驳回(含审批状态为已驳回)可编辑。
  286. * @param {object} row adaptEssApplication 的结果,或原始 VO
  287. * @param {boolean} admin 管理端查看时恒为只读
  288. */
  289. export function canEditEssApplication(row = {}, admin = false) {
  290. if (admin) return false;
  291. const businessStatus = String(row.businessStatus || "").toUpperCase();
  292. const approvalStatus = String(row.approvalStatus || "").toUpperCase();
  293. return businessStatus === "REJECTED" || approvalStatus === "REJECTED";
  294. }
  295. function buildQuery(params = {}) {
  296. return Object.keys(params)
  297. .filter((key) => params[key] !== undefined && params[key] !== null && params[key] !== "")
  298. .map((key) => `${key}=${encodeURIComponent(String(params[key]))}`)
  299. .join("&");
  300. }
  301. /**
  302. * 列表项 → 目标提交页路由(详情模式)。
  303. * @param {object} row 原始 VO 或 adaptEssApplication 结果
  304. * @param {{admin?: boolean}} options
  305. * @returns {string} 可直接 uni.navigateTo 的 url;不支持的业务类型返回 ""
  306. */
  307. export function resolveEssRoute(row = {}, options = {}) {
  308. const meta = businessMetaOf(row.businessType);
  309. if (!meta) return "";
  310. const query = buildQuery({
  311. mode: "detail",
  312. id: essIdOf(row.id),
  313. businessId: essIdOf(row.businessId),
  314. businessType: normalizeBusinessType(row.businessType),
  315. processInstanceId: row.processInstanceId || "",
  316. type: meta.type,
  317. admin: options.admin ? 1 : "",
  318. });
  319. return query ? `${meta.path}?${query}` : meta.path;
  320. }
  321. /**
  322. * 移动端没有详情页的业务类型 → 提示文案。
  323. * 后端还有一类业务(如「劳动合同申请」)没有移动端详情页,原来只提示一句
  324. * 「不支持移动端查看」,用户不知道去哪看;这里改成明确指出去 PC 端。
  325. */
  326. const PC_ONLY_HINT = {
  327. // 劳动合同:只有 PC 端 HR 模块能看详情
  328. LABOR_CONTRACT: "劳动合同查看详情请在 PC 端 HR 查看",
  329. HR_CONTRACT: "劳动合同查看详情请在 PC 端 HR 查看",
  330. CONTRACT_APPLY: "劳动合同查看详情请在 PC 端 HR 查看",
  331. };
  332. /** 按业务编码 / 展示名称给出「去 PC 端看」的提示文案 */
  333. export function pcOnlyHintOf(row = {}) {
  334. const code = String(row.businessType || "").toUpperCase();
  335. const name = String(row.businessTypeName || row.title || row.businessType || "该类型").trim();
  336. if (PC_ONLY_HINT[code]) return PC_ONLY_HINT[code];
  337. // 后端业务编码未统一时,按名称兜底识别劳动合同(该业务只有 PC 端有页面)
  338. if (/劳动合同/.test(name)) return "劳动合同查看详情请在 PC 端 HR 查看";
  339. return `${name}查看详情请在 PC 端查看`;
  340. }
  341. /**
  342. * 打开申请详情;不支持的类型给出提示
  343. * @returns {boolean} 是否已跳转
  344. */
  345. export function openEssApplication(row = {}, options = {}) {
  346. const url = resolveEssRoute(row, options);
  347. if (!url) {
  348. uni.showToast({ title: pcOnlyHintOf(row), icon: "none" });
  349. return false;
  350. }
  351. uni.navigateTo({ url });
  352. return true;
  353. }
  354. /**
  355. * businessType → apply/form 的表单类型(apply/form 用 type 参数决定渲染哪套字段)
  356. */
  357. export function formTypeOf(businessType) {
  358. const meta = businessMetaOf(businessType);
  359. return meta ? meta.type : "";
  360. }
  361. /**
  362. * 审批状态筛选选项(列表页 tab 用)
  363. */
  364. export const APPROVAL_STATUS_FILTERS = [
  365. { label: "全部", value: "" },
  366. { label: "审批中", value: "APPROVING" },
  367. { label: "已通过", value: "APPROVED" },
  368. { label: "已驳回", value: "REJECTED" },
  369. ];