onboarding.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
  2. import { defaultOnboardingInvitations, onboardStatus } from '@/data/mock'
  3. // ============ 模式判断 ============
  4. function isServerMode() {
  5. return getServerConfig().mode === 'server'
  6. }
  7. function serverPath() {
  8. const base = getApiBaseUrl()
  9. if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
  10. return base
  11. }
  12. // ============ 枚举 ============
  13. // 用工类型枚举值 → 中文标签(与后端约定一致)
  14. export const EMPLOYMENT_TYPE_LABEL = {
  15. FULL_TIME: '全职',
  16. INTERN: '实习',
  17. PART_TIME: '兼职',
  18. OTHER: '其他',
  19. }
  20. // ============ VO → 前端形状映射 ============
  21. /**
  22. * 把后端入职记录 VO 归一为前端可用形状;base 用于演示版合并 mock 字段
  23. * 后端字段映射约定:
  24. * - onboardingId / id
  25. * - employeeName → name
  26. * - deptName → department、deptId
  27. * - positionName → position、positionId
  28. * - companyName → company、legalId
  29. * - plannedJoinDate → hireDate(取前 10 位)
  30. * - employmentType / probationMonths / validDays / createMode / idNumber / gender / age / phone / email
  31. * - onboardingStatus / approvalStatus → status (pending|processing|done)
  32. * - offerStatus → offer、contractStatus → contract、assetsStatus → assets
  33. * - publicPath / publicToken / invitationStatus
  34. */
  35. export function adaptOnboardingRecord(vo, base = {}) {
  36. if (!vo || typeof vo !== 'object') return null
  37. const id = String(vo.onboardingId ?? vo.id ?? base.id ?? '')
  38. const onboardingStatus = vo.onboardingStatus ?? base.onboardingStatus ?? ''
  39. const approvalStatus = vo.approvalStatus ?? base.approvalStatus ?? ''
  40. const status =
  41. onboardingStatus === 'COMPLETED' || approvalStatus === 'APPROVED'
  42. ? 'done'
  43. : onboardingStatus === 'SUBMITTED' || approvalStatus === 'APPROVING'
  44. ? 'processing'
  45. : base.status ?? 'pending'
  46. return {
  47. id,
  48. onboardingId: id,
  49. invitationId: vo.invitationId ?? base.invitationId ?? '',
  50. invitationNo: vo.invitationNo ?? base.invitationNo ?? '',
  51. employeeNo: vo.employeeNo ?? base.employeeNo ?? id,
  52. name: vo.employeeName ?? base.name ?? '',
  53. department: vo.deptName ?? base.department ?? '',
  54. deptId: vo.deptId ?? base.deptId ?? null,
  55. position: vo.positionName ?? base.position ?? '',
  56. positionId: vo.positionId ?? base.positionId ?? null,
  57. level: vo.level ?? base.level ?? '',
  58. hireDate: (vo.plannedJoinDate || base.hireDate || base.expectedEntryDate || '').slice(0, 10),
  59. company: vo.companyName ?? base.company ?? '',
  60. legalId: vo.legalId ?? base.legalId ?? null,
  61. phone: vo.phone ?? base.phone ?? '',
  62. email: vo.email ?? base.email ?? '',
  63. idNumber: vo.idNumber ?? base.idNumber ?? '',
  64. gender: vo.gender ?? base.gender ?? '',
  65. age: vo.age ?? base.age ?? '',
  66. employmentType: vo.employmentType ?? base.employmentType ?? 'FULL_TIME',
  67. probationFlag: vo.probationFlag ?? base.probationFlag ?? 1,
  68. probationMonths: vo.probationMonths ?? base.probationMonths ?? 3,
  69. validDays: vo.validDays ?? base.validDays ?? 7,
  70. createMode: vo.createMode ?? base.createMode ?? 'manual',
  71. status,
  72. flow: onboardingStatus === 'SUBMITTED' ? '档案已完善' : base.flow ?? '待补充档案',
  73. offer: vo.offerStatus ?? base.offer ?? '未发起',
  74. contract: vo.contractStatus ?? base.contract ?? '未发起',
  75. assets: vo.assetsStatus ?? base.assets ?? '未领用',
  76. remark: vo.remark ?? base.remark ?? '',
  77. publicPath: vo.publicPath ?? base.publicPath ?? '',
  78. publicToken: vo.publicToken ?? base.publicToken ?? '',
  79. invitationStatus: vo.invitationStatus ?? base.invitationStatus ?? '',
  80. sourceType: vo.sourceType ?? 'MOCK',
  81. rowType: 'record',
  82. materials: base.materials ?? {},
  83. socialSecurity: base.socialSecurity ?? { status: '未办理', city: '北京', deadline: '' },
  84. training: base.training ?? { status: '未开始', name: '新员工入职培训', duration: '2天', deadline: '' },
  85. workstation: base.workstation ?? { status: '待分配', location: '' },
  86. itAccount: base.itAccount ?? { status: '未开通', items: [] },
  87. stage: base.stage ?? 'offer',
  88. }
  89. }
  90. /**
  91. * 把入职记录(mock 或 API VO)映射成列表卡片渲染形状
  92. * 两种输入形态:
  93. * - adaptOnboardingRecord(vo)(服务器模式):id, name, department, position, hireDate, status(key), employeeNo
  94. * - mock / storage 中的入职记录(演示模式):id, name, avatar, department, position,
  95. * entryDate, expectedEntryDate, materials, status(key), employeeNo
  96. * 输出:avatar / name / sub / status(中文 label) / statusClass / label1 / value1 / label2 / value2 / id
  97. */
  98. export function mapOnboardingForList(record) {
  99. if (!record || typeof record !== 'object') return null
  100. const id = record.id || record.onboardingId || ''
  101. const name = record.name || record.employeeName || ''
  102. const department = record.department || record.deptName || ''
  103. const position = record.position || record.positionName || ''
  104. const statusKey = record.status || 'pending'
  105. const statusInfo = onboardStatus[statusKey] || onboardStatus.pending
  106. const hireDate = record.hireDate || record.entryDate || record.expectedEntryDate || ''
  107. const employeeNo = record.employeeNo || id
  108. const isEntered = Boolean(record.entryDate)
  109. const materials = record.materials
  110. let materialsLabel = ''
  111. if (materials && typeof materials === 'object') {
  112. const total = Object.keys(materials).length
  113. if (total) {
  114. const done = Object.values(materials).filter(Boolean).length
  115. materialsLabel = `${done}/${total}`
  116. }
  117. }
  118. return {
  119. id,
  120. avatar: record.avatar || name.charAt(0) || '',
  121. name,
  122. sub: [department, position].filter(Boolean).join(' · '),
  123. status: statusInfo.label,
  124. statusClass: statusInfo.class,
  125. label1: isEntered ? '入职日期' : '预计入职',
  126. value1: isEntered ? record.entryDate : hireDate,
  127. label2: isEntered ? '员工编号' : (materialsLabel ? '资料进度' : '员工编号'),
  128. value2: isEntered ? employeeNo : (materialsLabel || employeeNo),
  129. }
  130. }
  131. // ============ 对外 API ============
  132. /**
  133. * 把前端表单(name/hireDate/...)映射为后端 HR 移动端待入职创建参数。
  134. * 后端要求字段:employeeName / plannedJoinDate / legalId / deptId / positionId / positionName
  135. * / phone / email / idNumber / employmentType / probationFlag / probationMonths
  136. * / remark / factoryId / managerUserId / teamId
  137. * 长整型字段在 number 字符串间互转时保留为 number,避免后端 @RequestParam 严格校验失败
  138. */
  139. export function serializeOnboardingManual(form = {}) {
  140. const toId = (v) => {
  141. if (v == null || v === '') return ""
  142. const n = String(v)
  143. return n ? n : ""
  144. }
  145. return {
  146. employeeName: String(form.name || form.employeeName || '').trim(),
  147. plannedJoinDate: String(form.hireDate || form.plannedJoinDate || '').trim(),
  148. legalId: toId(form.legalId),
  149. deptId: toId(form.deptId),
  150. positionId: toId(form.positionId),
  151. positionName: String(form.positionName || form.position || '').trim(),
  152. phone: String(form.phone || '').trim(),
  153. email: String(form.email || '').trim(),
  154. idNumber: String(form.idNumber || '').trim(),
  155. employmentType: String(form.employmentType || 'FULL_TIME').trim(),
  156. probationFlag: Number(form.probationFlag) === 1 ? 1 : 0,
  157. probationMonths: Number(form.probationMonths) || 0,
  158. remark: String(form.remark || '').trim(),
  159. factoryId: toId(form.factoryId),
  160. managerUserId: toId(form.managerUserId),
  161. teamId: toId(form.teamId),
  162. }
  163. }
  164. /**
  165. * 手动创建入职单
  166. * POST /hr/ess/admin/onboardings/manual
  167. *
  168. * 后端必填字段:plannedJoinDate / legalId / deptId / probationFlag
  169. *
  170. * @param {object} payload 入职表单字段(前端语义:name / hireDate / ...)
  171. * @returns {Promise<{ onboardingId: string|number, id: string|number, employeeNo: string }>}
  172. */
  173. export async function createOnboardingManual(payload) {
  174. const body = serializeOnboardingManual(payload)
  175. if (!isServerMode()) {
  176. await new Promise((r) => setTimeout(r, 250))
  177. const id = `ZY${Date.now()}${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`
  178. return { onboardingId: id, id, employeeNo: id }
  179. }
  180. const path = `${serverPath()}/hr/ess/admin/onboardings/manual`
  181. const res = await request({ url: path, method: 'POST', data: body })
  182. return res.data || {}
  183. }
  184. /**
  185. * 邀请方式创建入职单(生成公开链接)
  186. * POST /hr/ess/admin/onboardings/invitations?validDays=N
  187. *
  188. * @param {object} payload 入职邀请字段
  189. * @param {number} validDays 链接有效天数
  190. * @returns {Promise<{ invitationId: string, publicToken: string, publicPath: string, onboardingId: string|null }>}
  191. */
  192. export async function createOnboardingInvitation(payload, validDays) {
  193. // 邀请模式下姓名/手机/邮箱/身份证允许为空,后端字段照常映射即可
  194. const body = serializeOnboardingManual(payload)
  195. if (!isServerMode()) {
  196. await new Promise((r) => setTimeout(r, 250))
  197. const token = `TKN${Date.now().toString(36)}`
  198. return {
  199. invitationId: `INV${Date.now()}`,
  200. publicToken: token,
  201. publicPath: `/onboarding/public/${token}`,
  202. onboardingId: null,
  203. }
  204. }
  205. // validDays 走 query string(与后端约定一致),不放 body
  206. const qs = validDays != null && validDays !== '' ? `?validDays=${encodeURIComponent(Number(validDays))}` : ''
  207. const path = `${serverPath()}/hr/ess/admin/onboardings/invitations${qs}`
  208. const res = await request({ url: path, method: 'POST', data: body })
  209. return res.data || {}
  210. }
  211. /**
  212. * 入职记录列表
  213. * GET /hr/ess/admin/onboardings/records
  214. *
  215. * @returns {Promise<Array>}
  216. */
  217. export async function getOnboardingRecords() {
  218. if (!isServerMode()) {
  219. await new Promise((r) => setTimeout(r, 200))
  220. return []
  221. }
  222. const path = `${serverPath()}/hr/ess/admin/onboardings/records`
  223. const res = await request({ url: path, method: 'GET' })
  224. return Array.isArray(res.data) ? res.data : []
  225. }
  226. /**
  227. * 入职邀请记录列表(演示版回退到 mock 默认邀请)
  228. * GET /hr/ess/admin/onboardings/invitations
  229. *
  230. * @returns {Promise<Array>}
  231. */
  232. export async function getOnboardingInvitationsList() {
  233. if (!isServerMode()) {
  234. await new Promise((r) => setTimeout(r, 150))
  235. return defaultOnboardingInvitations
  236. }
  237. const path = `${serverPath()}/hr/ess/admin/onboardings/invitations`
  238. const res = await request({ url: path, method: 'GET' })
  239. return Array.isArray(res.data) ? res.data : []
  240. }
  241. /**
  242. * 提交入职审批
  243. * POST /hr/ess/admin/onboardings/records/{id}/submit
  244. *
  245. * @param {string} onboardingId
  246. * @returns {Promise<object>}
  247. */
  248. export async function submitOnboardingRecord(onboardingId) {
  249. if (!onboardingId) throw new Error('缺少入职单主键')
  250. if (!isServerMode()) {
  251. await new Promise((r) => setTimeout(r, 200))
  252. return { code: 0, message: '演示:已提交审批' }
  253. }
  254. const path = `${serverPath()}/hr/ess/admin/onboardings/records/${encodeURIComponent(onboardingId)}/submit`
  255. const res = await request({ url: path, method: 'POST' })
  256. return res
  257. }