userChange.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
  2. import { userChangeStatus, userChangeStages, userChanges } from '@/data/mock'
  3. import {
  4. addCreatedUserChange,
  5. formatNow,
  6. getCreatedUserChanges,
  7. getCurrentUser,
  8. getLoginUser,
  9. getUserChangeIds,
  10. getUserChangeStates,
  11. rememberUserChangeId,
  12. setUserChangeLocalState,
  13. updateCreatedUserChange,
  14. } from '@/utils/storage'
  15. // ============ 模式判断 ============
  16. function isServerMode() {
  17. return getServerConfig().mode === 'server'
  18. }
  19. function serverPath() {
  20. const base = getApiBaseUrl()
  21. if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
  22. return base
  23. }
  24. function delay(ms = 200) {
  25. return new Promise((resolve) => setTimeout(resolve, ms))
  26. }
  27. // ============ 工具 ============
  28. function compact(object = {}) {
  29. return Object.keys(object).reduce((result, key) => {
  30. const value = object[key]
  31. if (value !== '' && value !== null && value !== undefined) result[key] = value
  32. return result
  33. }, {})
  34. }
  35. function dateOnly(value) {
  36. if (!value) return ''
  37. return String(value).slice(0, 10)
  38. }
  39. function labelOf(map, raw, fallback = '') {
  40. const text = String(raw || '').trim()
  41. if (!text) return fallback
  42. return map[text] || map[text.toUpperCase()] || text
  43. }
  44. function asList(data) {
  45. if (Array.isArray(data)) return data
  46. if (Array.isArray(data?.list)) return data.list
  47. if (Array.isArray(data?.records)) return data.records
  48. if (data && typeof data === 'object' && data.id != null) return [data]
  49. return []
  50. }
  51. function keepBigIntId(value) {
  52. if (value === undefined || value === null || value === '') return undefined
  53. const text = String(value).trim()
  54. if (!/^-?\d+$/.test(text)) return value
  55. if (!Number.isSafeInteger(Number(text))) return text
  56. return Number(text)
  57. }
  58. function extractId(result) {
  59. if (result == null) return null
  60. if (typeof result === 'object') return result.id ?? result.changeId ?? null
  61. return result
  62. }
  63. function parseSnapshot(raw) {
  64. if (!raw) return {}
  65. if (typeof raw === 'object') return raw
  66. try {
  67. return JSON.parse(raw) || {}
  68. } catch (error) {
  69. return {}
  70. }
  71. }
  72. function detailMap(details = []) {
  73. const map = {}
  74. const list = Array.isArray(details) ? details : []
  75. list.forEach((item) => {
  76. const field = String(item?.changeField || '').toUpperCase()
  77. if (field) map[field] = item
  78. })
  79. return map
  80. }
  81. // ============ 枚举(与 PC 端 src/api/hr/userChange.js 对齐)============
  82. /** 异动类型 */
  83. export const CHANGE_TYPE_LABELS = {
  84. POSITION_TRANSFER: '调岗',
  85. DEPARTMENT_TRANSFER: '调部门',
  86. GRADE_CHANGE: '职级调整',
  87. PROMOTION: '晋升',
  88. DEMOTION: '降级',
  89. SALARY_CHANGE: '调薪',
  90. MANAGER_CHANGE: '调整上级',
  91. WORK_LOCATION_CHANGE: '工作地点变更',
  92. FACTORY_TRANSFER: '调工厂',
  93. LEGAL_ENTITY_TRANSFER: '跨法人调动',
  94. EMPLOYMENT_TYPE_CHANGE: '用工类型变更',
  95. COMPOSITE_CHANGE: '综合异动',
  96. }
  97. /** 员工端可自助提交的异动类型,其余由 HR 在 PC 端办理 */
  98. export const ESS_CHANGE_TYPE_KEYS = [
  99. 'POSITION_TRANSFER',
  100. 'DEPARTMENT_TRANSFER',
  101. 'PROMOTION',
  102. 'GRADE_CHANGE',
  103. 'COMPOSITE_CHANGE',
  104. ]
  105. export const ESS_CHANGE_TYPE_OPTIONS = ESS_CHANGE_TYPE_KEYS.map((value) => ({
  106. value,
  107. label: CHANGE_TYPE_LABELS[value],
  108. }))
  109. export function changeTypeLabel(value) {
  110. return labelOf(CHANGE_TYPE_LABELS, value, value || '调岗')
  111. }
  112. export const APPROVAL_STATUS_LABELS = {
  113. DRAFT: '草稿',
  114. PENDING: '审批中',
  115. APPROVING: '审批中',
  116. APPROVED: '已通过',
  117. REJECTED: '已驳回',
  118. WITHDRAWN: '已撤回',
  119. CANCELLED: '已取消',
  120. }
  121. export const EFFECTIVE_STATUS_LABELS = {
  122. PENDING: '待生效',
  123. SCHEDULED: '已排期',
  124. EFFECTIVE: '已生效',
  125. CANCELLED: '已取消',
  126. }
  127. /** 页面展示态 → 标签 / 样式(枚举定义在 data/mock.js) */
  128. export const TRANSFER_DISPLAY_STATUS = userChangeStatus
  129. export const TRANSFER_STATUS_FILTERS = [
  130. { value: 'all', label: '全部状态' },
  131. { value: 'draft', label: '草稿' },
  132. { value: 'pending', label: '审批中' },
  133. { value: 'approved', label: '已通过' },
  134. { value: 'rejected', label: '已驳回' },
  135. ]
  136. // ============ 状态机 ============
  137. export function deriveDocStatus(raw = {}) {
  138. const approval = String(raw.approvalStatus || '').toUpperCase()
  139. const effective = String(raw.effectiveStatus || '').toUpperCase()
  140. if (approval === 'CANCELLED' || effective === 'CANCELLED') return 'cancelled'
  141. if (approval === 'APPROVED' || effective === 'EFFECTIVE') return 'approved'
  142. if (approval === 'REJECTED') return 'rejected'
  143. if (approval === 'PENDING' || approval === 'APPROVING') return 'pending'
  144. if (approval === 'WITHDRAWN') return 'withdrawn'
  145. return 'draft'
  146. }
  147. export function canEditUserChange(row = {}) {
  148. const status = String(row.approvalStatus || '').toUpperCase()
  149. return status === 'DRAFT' || status === 'REJECTED' || status === ''
  150. }
  151. export function canSubmitUserChange(row = {}) {
  152. if (!row?.id) return false
  153. return ['draft', 'rejected'].includes(row.displayStatus)
  154. }
  155. export function canWithdrawUserChange(row = {}) {
  156. const status = String(row.approvalStatus || '').toUpperCase()
  157. return status === 'PENDING' || status === 'APPROVING'
  158. }
  159. export function canCancelUserChange(row = {}) {
  160. if (!row?.id) return false
  161. if (String(row.approvalStatus || '').toUpperCase() === 'CANCELLED') return false
  162. return String(row.effectiveStatus || '').toUpperCase() !== 'EFFECTIVE'
  163. }
  164. export function matchDisplayStatus(row = {}, filter = '') {
  165. if (!filter || filter === 'all') return true
  166. if (filter === 'rejected') return ['rejected', 'withdrawn', 'cancelled'].includes(row.displayStatus)
  167. return row.displayStatus === filter
  168. }
  169. function stageKeyOf(displayStatus) {
  170. if (displayStatus === 'approved') return 'effective'
  171. if (displayStatus === 'pending') return 'approval'
  172. return 'submit'
  173. }
  174. function progressOf(displayStatus, stageKey) {
  175. if (displayStatus === 'approved') return 100
  176. if (['rejected', 'withdrawn', 'cancelled'].includes(displayStatus)) return 30
  177. const index = Math.max(
  178. userChangeStages.findIndex((stage) => stage.key === stageKey),
  179. 0,
  180. )
  181. return Math.round(((index + 1) / userChangeStages.length) * 100)
  182. }
  183. function progressTipOf(displayStatus) {
  184. switch (displayStatus) {
  185. case 'draft':
  186. return '草稿尚未提交,提交后进入审批流程。'
  187. case 'pending':
  188. return '调岗审批中,请等待主管与 HR 的审批结果。'
  189. case 'approved':
  190. return '调岗审批已通过,将按生效日期更新岗位信息。'
  191. case 'rejected':
  192. return '审批已驳回,可修改后重新提交。'
  193. case 'withdrawn':
  194. return '本次调岗申请已撤回。'
  195. case 'cancelled':
  196. return '本次调岗申请已取消。'
  197. default:
  198. return ''
  199. }
  200. }
  201. // ============ VO / 演示记录 → 页面形状 ============
  202. export function adaptUserChange(raw = {}, extras = {}) {
  203. const before = parseSnapshot(raw.beforeSnapshot)
  204. const after = parseSnapshot(raw.afterSnapshot)
  205. const details = Array.isArray(raw.details) ? raw.details : []
  206. const map = detailMap(details)
  207. const deptDetail = map.DEPT
  208. const positionDetail = map.POSITION
  209. const gradeDetail = map.GRADE
  210. const locationDetail = map.WORK_LOCATION
  211. const changeType = String(raw.changeType || '').toUpperCase()
  212. const approvalStatus = String(raw.approvalStatus || '').toUpperCase()
  213. const effectiveStatus = String(raw.effectiveStatus || '').toUpperCase()
  214. const displayStatus = deriveDocStatus({ approvalStatus, effectiveStatus })
  215. const statusMeta = TRANSFER_DISPLAY_STATUS[displayStatus] || TRANSFER_DISPLAY_STATUS.draft
  216. const stageKey = stageKeyOf(displayStatus)
  217. const name = extras.name || raw.name || raw.employeeName || ''
  218. const dept =
  219. extras.dept || raw.dept || raw.originDept || before.deptName || deptDetail?.beforeText || ''
  220. const position =
  221. extras.position ||
  222. raw.position ||
  223. raw.originPosition ||
  224. before.positionName ||
  225. positionDetail?.beforeText ||
  226. ''
  227. const targetDept = raw.targetDept || raw.targetDeptName || after.deptName || deptDetail?.afterText || ''
  228. const targetPosition =
  229. raw.targetPosition || raw.targetPositionName || after.positionName || positionDetail?.afterText || ''
  230. return {
  231. id: raw.id,
  232. changeNo: raw.changeNo || raw.contractNo || (raw.id == null ? '' : String(raw.id)),
  233. source: extras.source || raw.source || '',
  234. userId: raw.userId ?? extras.userId,
  235. employeeId: raw.userId ?? extras.employeeId,
  236. name,
  237. avatar: raw.avatar || String(name).charAt(0),
  238. employeeNo: extras.employeeNo || raw.employeeNo || raw.userNo || '',
  239. dept,
  240. position,
  241. level: extras.level || raw.level || before.positionLevelName || before.grade || gradeDetail?.beforeText || '',
  242. targetDept,
  243. targetDeptId: raw.targetDeptId ?? deptDetail?.afterValue ?? null,
  244. targetPosition,
  245. targetPositionId: raw.targetPositionId ?? positionDetail?.afterValue ?? null,
  246. targetLevel: raw.targetLevel || after.positionLevelName || after.grade || gradeDetail?.afterText || '',
  247. workLocation: raw.workLocation || locationDetail?.afterText || '',
  248. changeType,
  249. changeTypeLabel: changeTypeLabel(changeType),
  250. effectiveDate: dateOnly(raw.effectiveDate),
  251. applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate),
  252. reason: raw.reason || '',
  253. remark: raw.remark || '',
  254. approvalNode: raw.approvalNode || '',
  255. approvalComment: raw.approvalComment || raw.hrComment || '',
  256. approvalStatus,
  257. approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, approvalStatus, '草稿'),
  258. effectiveStatus,
  259. effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, effectiveStatus, '-'),
  260. displayStatus,
  261. statusLabel: statusMeta.label,
  262. statusClass: statusMeta.class,
  263. stageKey,
  264. progress: progressOf(displayStatus, stageKey),
  265. progressTip: progressTipOf(displayStatus),
  266. details,
  267. createTime: raw.createTime || '',
  268. raw,
  269. }
  270. }
  271. // ============ 演示模式本地实现 ============
  272. function mergeDemoRecords() {
  273. const overrides = getUserChangeStates()
  274. const created = getCreatedUserChanges()
  275. const seen = new Set(created.map((item) => String(item.id)))
  276. const base = [...created, ...userChanges.filter((item) => !seen.has(String(item.id)))]
  277. return base.map((record) => {
  278. const merged = { ...record, ...(overrides[String(record.id)] || {}) }
  279. return adaptUserChange(merged, { source: 'demo' })
  280. })
  281. }
  282. function updateDemoUserChange(id, patch) {
  283. setUserChangeLocalState(id, patch)
  284. updateCreatedUserChange(id, patch)
  285. return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null
  286. }
  287. function persistDemoTransfer(id, form, submit) {
  288. const user = getCurrentUser() || {}
  289. const name = form.name || user.name || ''
  290. const record = {
  291. id: String(id),
  292. userId: form.userId || form.employeeId || user.id || '',
  293. name,
  294. avatar: form.avatar || String(name).charAt(0),
  295. employeeNo: form.employeeNo || user.id || '',
  296. dept: form.dept || user.department || '',
  297. position: form.position || user.position || '',
  298. level: form.level || '',
  299. targetDeptId: form.targetDeptId ?? null,
  300. targetDept: form.targetDeptName || form.targetDept || '',
  301. targetPositionId: form.targetPositionId ?? null,
  302. targetPosition: form.targetPositionName || form.targetPosition || '',
  303. targetLevel: form.targetLevel || '',
  304. changeType: inferChangeType(form),
  305. effectiveDate: dateOnly(form.effectiveDate),
  306. applyDate: dateOnly(formatNow()),
  307. reason: form.reason || '',
  308. remark: form.remark || '',
  309. approvalNode: submit ? '主管审批' : '',
  310. approvalStatus: submit ? 'PENDING' : 'DRAFT',
  311. effectiveStatus: 'PENDING',
  312. source: 'demo',
  313. createdByApp: true,
  314. }
  315. addCreatedUserChange(record)
  316. return record
  317. }
  318. // ============ 服务器接口(M02-人事异动)============
  319. const employeeCache = new Map()
  320. async function fetchEmployeeBrief(userId) {
  321. if (userId === undefined || userId === null || userId === '') return null
  322. const key = String(userId)
  323. if (employeeCache.has(key)) return employeeCache.get(key)
  324. try {
  325. const res = await request({ url: `${serverPath()}/main/user/getById/${key}`, method: 'GET' })
  326. const vo = res.data || {}
  327. const brief = {
  328. name: vo.name || vo.userName || '',
  329. employeeNo: vo.jobNumber || '',
  330. dept: vo.deptName || '',
  331. position: vo.postName || vo.position || '',
  332. level: vo.positionLevelName || vo.grade || '',
  333. }
  334. employeeCache.set(key, brief)
  335. return brief
  336. } catch (error) {
  337. employeeCache.set(key, null)
  338. return null
  339. }
  340. }
  341. function extrasOf(brief) {
  342. if (!brief) return {}
  343. return {
  344. name: brief.name,
  345. employeeNo: brief.employeeNo,
  346. dept: brief.dept,
  347. position: brief.position,
  348. level: brief.level,
  349. }
  350. }
  351. /** 当前登录人的服务端数字主键(登录响应里的 userId) */
  352. export function currentUserId() {
  353. const login = getLoginUser() || {}
  354. const raw = getCurrentUser()?.raw || {}
  355. const candidates = [login.userId, login.id, raw.userId, raw.id]
  356. return candidates.find((value) => value !== undefined && value !== null && value !== '')
  357. }
  358. function isMine(row = {}) {
  359. const user = getCurrentUser() || {}
  360. const login = getLoginUser() || {}
  361. const mine = [user.id, user.name, login.userId, login.loginName, login.userName]
  362. .filter((value) => value !== undefined && value !== null && value !== '')
  363. .map(String)
  364. if (!mine.length) return true
  365. const rowIds = [row.userId, row.employeeNo, row.name]
  366. .filter((value) => value !== undefined && value !== null && value !== '')
  367. .map(String)
  368. return rowIds.some((value) => mine.includes(value))
  369. }
  370. function transferText(row = {}) {
  371. return `${row.name}${row.employeeNo}${row.changeNo}${row.dept}${row.position}${row.targetDept}${row.targetPosition}`
  372. }
  373. /** 条件查询异动单 GET /main/user-changes */
  374. export async function getUserChangePage(params = {}) {
  375. const res = await request({
  376. url: `${serverPath()}/main/user-changes`,
  377. method: 'GET',
  378. data: compact({
  379. pageNum: params.pageNum,
  380. size: params.size,
  381. userId: keepBigIntId(params.userId),
  382. changeType: params.changeType,
  383. approvalStatus: params.approvalStatus,
  384. }),
  385. })
  386. const data = res.data || {}
  387. return { list: asList(data), count: Number(data?.count ?? data?.total ?? 0) }
  388. }
  389. /** 某员工的异动记录 GET /main/user-changes/users/{userId} */
  390. export async function getUserChangesByUserId(userId) {
  391. const res = await request({
  392. url: `${serverPath()}/main/user-changes/users/${userId}`,
  393. method: 'GET',
  394. })
  395. return asList(res.data)
  396. }
  397. /** 异动单详情 GET /main/user-changes/{id} */
  398. export async function getUserChangeById(id) {
  399. const res = await request({ url: `${serverPath()}/main/user-changes/${id}`, method: 'GET' })
  400. return res.data
  401. }
  402. /** 新建异动单(草稿)POST /main/user-changes */
  403. export async function createUserChange(payload) {
  404. const res = await request({
  405. url: `${serverPath()}/main/user-changes`,
  406. method: 'POST',
  407. data: payload,
  408. })
  409. const id = extractId(res.data)
  410. if (id != null) rememberUserChangeId(id)
  411. return id
  412. }
  413. /** 修改异动草稿 PUT /main/user-changes */
  414. export async function updateUserChange(payload) {
  415. await request({ url: `${serverPath()}/main/user-changes`, method: 'PUT', data: payload })
  416. if (payload?.id != null) rememberUserChangeId(payload.id)
  417. return payload.id
  418. }
  419. /** 异动预校验 POST /main/user-changes/{id}/validate */
  420. export async function validateUserChange(id) {
  421. const res = await request({
  422. url: `${serverPath()}/main/user-changes/${id}/validate`,
  423. method: 'POST',
  424. })
  425. return res.data
  426. }
  427. /** 提交异动审批 POST /main/user-changes/{id}/submit */
  428. export async function submitUserChange(id) {
  429. const res = await request({
  430. url: `${serverPath()}/main/user-changes/${id}/submit`,
  431. method: 'POST',
  432. })
  433. return res.data
  434. }
  435. /** 撤回异动审批 POST /main/user-changes/{id}/withdraw */
  436. export async function withdrawUserChange(id, reason = '') {
  437. const res = await request({
  438. url: `${serverPath()}/main/user-changes/${id}/withdraw`,
  439. method: 'POST',
  440. data: compact({ reason }),
  441. })
  442. return res.data
  443. }
  444. /** 取消异动单 POST /main/user-changes/{id}/cancel */
  445. export async function cancelUserChange(id, reason = '') {
  446. const res = await request({
  447. url: `${serverPath()}/main/user-changes/${id}/cancel`,
  448. method: 'POST',
  449. data: compact({ reason }),
  450. })
  451. return res.data
  452. }
  453. // ============ 对外统一入口(页面只调用这些)============
  454. /**
  455. * 调岗列表。
  456. * @param {object} params
  457. * - scope: 'mine' 只看本人;'all' 全部员工(HR 视图)
  458. * - status: TRANSFER_STATUS_FILTERS 的 value
  459. * - keyword: 姓名 / 工号 / 单号 / 岗位 模糊匹配
  460. */
  461. export async function loadTransferList(params = {}) {
  462. const { scope = 'mine', status = 'all', keyword = '', pageNum = 1, size = 50 } = params
  463. if (!isServerMode()) {
  464. const list = mergeDemoRecords()
  465. .filter((row) => (scope === 'mine' ? isMine(row) : true))
  466. .filter((row) => matchDisplayStatus(row, status))
  467. .filter((row) => (keyword ? transferText(row).includes(keyword) : true))
  468. .sort((a, b) =>
  469. String(b.applyDate || b.createTime).localeCompare(String(a.applyDate || a.createTime)),
  470. )
  471. return { list, count: list.length, source: 'demo' }
  472. }
  473. const userId = currentUserId()
  474. let rows = []
  475. if (scope === 'mine' && userId != null) {
  476. try {
  477. rows = await getUserChangesByUserId(userId)
  478. } catch (error) {
  479. rows = []
  480. }
  481. }
  482. if (!rows.length) {
  483. try {
  484. const page = await getUserChangePage({
  485. pageNum,
  486. size,
  487. userId: scope === 'mine' ? userId : undefined,
  488. })
  489. rows = page.list
  490. } catch (error) {
  491. rows = []
  492. }
  493. }
  494. // 集合接口不可用时,按本机记住的主键逐个拉详情兜底(与 PC 端策略一致)
  495. if (!rows.length) {
  496. const details = await Promise.all(
  497. getUserChangeIds().map(async (id) => {
  498. try {
  499. return await getUserChangeById(id)
  500. } catch (error) {
  501. return null
  502. }
  503. }),
  504. )
  505. rows = details.filter(Boolean)
  506. }
  507. const list = []
  508. for (const vo of rows) {
  509. if (!vo || vo.id == null) continue
  510. const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId))
  511. const row = adaptUserChange(vo, extras)
  512. rememberUserChangeId(row.id)
  513. if (scope === 'mine' && userId != null && !isMine(row)) continue
  514. if (!matchDisplayStatus(row, status)) continue
  515. if (keyword && !transferText(row).includes(keyword)) continue
  516. list.push(row)
  517. }
  518. return { list, count: list.length, source: 'api' }
  519. }
  520. /** 调岗单详情 */
  521. export async function loadTransferDetail(id) {
  522. if (!id) return null
  523. if (!isServerMode()) {
  524. return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null
  525. }
  526. const vo = await getUserChangeById(id)
  527. if (!vo) return null
  528. rememberUserChangeId(id)
  529. const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId))
  530. return adaptUserChange(vo, extras)
  531. }
  532. // ============ 员工端申请表单 ============
  533. function inferChangeType(form = {}) {
  534. if (form.changeType) return form.changeType
  535. const hasDept = form.targetDeptId != null && form.targetDeptId !== ''
  536. const hasPosition = form.targetPositionId != null && form.targetPositionId !== ''
  537. if (hasDept && hasPosition) return 'COMPOSITE_CHANGE'
  538. if (hasDept) return 'DEPARTMENT_TRANSFER'
  539. return 'POSITION_TRANSFER'
  540. }
  541. function buildDetails(form = {}) {
  542. if (Array.isArray(form.details) && form.details.length) return form.details
  543. const details = []
  544. if (form.targetDeptName) {
  545. details.push({
  546. changeField: 'DEPT',
  547. beforeValue: form.originDeptId != null ? String(form.originDeptId) : undefined,
  548. beforeText: form.dept || '',
  549. afterValue: form.targetDeptId != null ? String(form.targetDeptId) : undefined,
  550. afterText: form.targetDeptName,
  551. })
  552. }
  553. if (form.targetPositionName) {
  554. details.push({
  555. changeField: 'POSITION',
  556. beforeValue: form.originPositionId != null ? String(form.originPositionId) : undefined,
  557. beforeText: form.position || '',
  558. afterValue: form.targetPositionId != null ? String(form.targetPositionId) : undefined,
  559. afterText: form.targetPositionName,
  560. })
  561. }
  562. // 职级未变化时不产生 GRADE 变动,避免把岗位自带层级误判为职级调整
  563. if (form.targetLevel && form.targetLevel !== form.level) {
  564. details.push({
  565. changeField: 'GRADE',
  566. beforeValue: form.level || undefined,
  567. beforeText: form.level || '',
  568. afterValue: form.targetLevel,
  569. afterText: form.targetLevel,
  570. })
  571. }
  572. return details
  573. }
  574. /**
  575. * 表单字段 → /main/user-changes 请求参数(与 PC 端 buildUserChangePayload 对齐)。
  576. * 申请人由服务端按当前任职确定,前端只送变动目标与事由。
  577. */
  578. export function buildUserChangePayload(form = {}) {
  579. return compact({
  580. id: keepBigIntId(form.id),
  581. userId: keepBigIntId(form.userId || form.employeeId),
  582. changeType: inferChangeType(form),
  583. effectiveDate: dateOnly(form.effectiveDate) || undefined,
  584. reason: form.reason,
  585. remark: form.remark,
  586. targetDeptId: keepBigIntId(form.targetDeptId),
  587. targetPositionId: keepBigIntId(form.targetPositionId),
  588. targetPositionName: form.targetPositionName || form.targetPosition,
  589. details: buildDetails(form),
  590. })
  591. }
  592. /**
  593. * 提交(或存草稿)调岗申请。
  594. * 服务器模式:建单 POST /main/user-changes → 提交 POST /{id}/submit
  595. * 演示模式:本地落一条异动单,提交时状态置为审批中
  596. *
  597. * @returns {Promise<string|number>} 异动单主键
  598. */
  599. export async function submitTransferApplication({ form, submit = true, existingId = null } = {}) {
  600. if (!form) throw new Error('申请参数为空')
  601. const id = existingId || form.id || null
  602. if (!isServerMode()) {
  603. await delay(300)
  604. const localId = id || `DC${Date.now()}`
  605. persistDemoTransfer(localId, form, submit)
  606. return localId
  607. }
  608. const payload = buildUserChangePayload({ ...form, id })
  609. const changeId = id || (await createUserChange(payload))
  610. if (id) await updateUserChange({ ...payload, id: changeId })
  611. if (submit && changeId != null) await submitUserChange(changeId)
  612. return changeId
  613. }
  614. // —— 流程动作:服务器模式走接口,演示模式改本地态 ——
  615. export async function actSubmitTransfer(row) {
  616. if (!row?.id) throw new Error('缺少异动单主键')
  617. if (isServerMode()) {
  618. // 与 PC 端一致:先预校验,再提交审批
  619. const result = await validateUserChange(row.id)
  620. if (result && result.valid === false) {
  621. throw new Error((result.messages || []).join(';') || '异动预校验未通过')
  622. }
  623. return submitUserChange(row.id)
  624. }
  625. return updateDemoUserChange(row.id, { approvalStatus: 'PENDING', approvalNode: '主管审批' })
  626. }
  627. export async function actWithdrawTransfer(row, reason = '') {
  628. if (!row?.id) throw new Error('缺少异动单主键')
  629. if (isServerMode()) return withdrawUserChange(row.id, reason)
  630. return updateDemoUserChange(row.id, {
  631. approvalStatus: 'WITHDRAWN',
  632. approvalComment: reason,
  633. approvalNode: '',
  634. })
  635. }
  636. export async function actCancelTransfer(row, reason = '') {
  637. if (!row?.id) throw new Error('缺少异动单主键')
  638. if (isServerMode()) return cancelUserChange(row.id, reason)
  639. return updateDemoUserChange(row.id, {
  640. approvalStatus: 'CANCELLED',
  641. effectiveStatus: 'CANCELLED',
  642. approvalComment: reason,
  643. approvalNode: '',
  644. })
  645. }