|
@@ -0,0 +1,271 @@
|
|
|
|
|
+import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
|
|
|
|
|
+import { defaultOnboardingInvitations, onboardStatus } from '@/data/mock'
|
|
|
|
|
+
|
|
|
|
|
+// ============ 模式判断 ============
|
|
|
|
|
+function isServerMode() {
|
|
|
|
|
+ return getServerConfig().mode === 'server'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function serverPath() {
|
|
|
|
|
+ const base = getApiBaseUrl()
|
|
|
|
|
+ if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
|
|
|
|
|
+ return base
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============ 枚举 ============
|
|
|
|
|
+
|
|
|
|
|
+// 用工类型枚举值 → 中文标签(与后端约定一致)
|
|
|
|
|
+export const EMPLOYMENT_TYPE_LABEL = {
|
|
|
|
|
+ FULL_TIME: '全职',
|
|
|
|
|
+ INTERN: '实习',
|
|
|
|
|
+ PART_TIME: '兼职',
|
|
|
|
|
+ OTHER: '其他',
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============ VO → 前端形状映射 ============
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 把后端入职记录 VO 归一为前端可用形状;base 用于演示版合并 mock 字段
|
|
|
|
|
+ * 后端字段映射约定:
|
|
|
|
|
+ * - onboardingId / id
|
|
|
|
|
+ * - employeeName → name
|
|
|
|
|
+ * - deptName → department、deptId
|
|
|
|
|
+ * - positionName → position、positionId
|
|
|
|
|
+ * - companyName → company、legalId
|
|
|
|
|
+ * - plannedJoinDate → hireDate(取前 10 位)
|
|
|
|
|
+ * - employmentType / probationMonths / validDays / createMode / idNumber / gender / age / phone / email
|
|
|
|
|
+ * - onboardingStatus / approvalStatus → status (pending|processing|done)
|
|
|
|
|
+ * - offerStatus → offer、contractStatus → contract、assetsStatus → assets
|
|
|
|
|
+ * - publicPath / publicToken / invitationStatus
|
|
|
|
|
+ */
|
|
|
|
|
+export function adaptOnboardingRecord(vo, base = {}) {
|
|
|
|
|
+ if (!vo || typeof vo !== 'object') return null
|
|
|
|
|
+ const id = String(vo.onboardingId ?? vo.id ?? base.id ?? '')
|
|
|
|
|
+ const onboardingStatus = vo.onboardingStatus ?? base.onboardingStatus ?? ''
|
|
|
|
|
+ const approvalStatus = vo.approvalStatus ?? base.approvalStatus ?? ''
|
|
|
|
|
+ const status =
|
|
|
|
|
+ onboardingStatus === 'COMPLETED' || approvalStatus === 'APPROVED'
|
|
|
|
|
+ ? 'done'
|
|
|
|
|
+ : onboardingStatus === 'SUBMITTED' || approvalStatus === 'APPROVING'
|
|
|
|
|
+ ? 'processing'
|
|
|
|
|
+ : base.status ?? 'pending'
|
|
|
|
|
+ return {
|
|
|
|
|
+ id,
|
|
|
|
|
+ onboardingId: id,
|
|
|
|
|
+ invitationId: vo.invitationId ?? base.invitationId ?? '',
|
|
|
|
|
+ invitationNo: vo.invitationNo ?? base.invitationNo ?? '',
|
|
|
|
|
+ employeeNo: vo.employeeNo ?? base.employeeNo ?? id,
|
|
|
|
|
+ name: vo.employeeName ?? base.name ?? '',
|
|
|
|
|
+ department: vo.deptName ?? base.department ?? '',
|
|
|
|
|
+ deptId: vo.deptId ?? base.deptId ?? null,
|
|
|
|
|
+ position: vo.positionName ?? base.position ?? '',
|
|
|
|
|
+ positionId: vo.positionId ?? base.positionId ?? null,
|
|
|
|
|
+ level: vo.level ?? base.level ?? '',
|
|
|
|
|
+ hireDate: (vo.plannedJoinDate || base.hireDate || base.expectedEntryDate || '').slice(0, 10),
|
|
|
|
|
+ company: vo.companyName ?? base.company ?? '',
|
|
|
|
|
+ legalId: vo.legalId ?? base.legalId ?? null,
|
|
|
|
|
+ phone: vo.phone ?? base.phone ?? '',
|
|
|
|
|
+ email: vo.email ?? base.email ?? '',
|
|
|
|
|
+ idNumber: vo.idNumber ?? base.idNumber ?? '',
|
|
|
|
|
+ gender: vo.gender ?? base.gender ?? '',
|
|
|
|
|
+ age: vo.age ?? base.age ?? '',
|
|
|
|
|
+ employmentType: vo.employmentType ?? base.employmentType ?? 'FULL_TIME',
|
|
|
|
|
+ probationFlag: vo.probationFlag ?? base.probationFlag ?? 1,
|
|
|
|
|
+ probationMonths: vo.probationMonths ?? base.probationMonths ?? 3,
|
|
|
|
|
+ validDays: vo.validDays ?? base.validDays ?? 7,
|
|
|
|
|
+ createMode: vo.createMode ?? base.createMode ?? 'manual',
|
|
|
|
|
+ status,
|
|
|
|
|
+ flow: onboardingStatus === 'SUBMITTED' ? '档案已完善' : base.flow ?? '待补充档案',
|
|
|
|
|
+ offer: vo.offerStatus ?? base.offer ?? '未发起',
|
|
|
|
|
+ contract: vo.contractStatus ?? base.contract ?? '未发起',
|
|
|
|
|
+ assets: vo.assetsStatus ?? base.assets ?? '未领用',
|
|
|
|
|
+ remark: vo.remark ?? base.remark ?? '',
|
|
|
|
|
+ publicPath: vo.publicPath ?? base.publicPath ?? '',
|
|
|
|
|
+ publicToken: vo.publicToken ?? base.publicToken ?? '',
|
|
|
|
|
+ invitationStatus: vo.invitationStatus ?? base.invitationStatus ?? '',
|
|
|
|
|
+ sourceType: vo.sourceType ?? 'MOCK',
|
|
|
|
|
+ rowType: 'record',
|
|
|
|
|
+ materials: base.materials ?? {},
|
|
|
|
|
+ socialSecurity: base.socialSecurity ?? { status: '未办理', city: '北京', deadline: '' },
|
|
|
|
|
+ training: base.training ?? { status: '未开始', name: '新员工入职培训', duration: '2天', deadline: '' },
|
|
|
|
|
+ workstation: base.workstation ?? { status: '待分配', location: '' },
|
|
|
|
|
+ itAccount: base.itAccount ?? { status: '未开通', items: [] },
|
|
|
|
|
+ stage: base.stage ?? 'offer',
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 把入职记录(mock 或 API VO)映射成列表卡片渲染形状
|
|
|
|
|
+ * 两种输入形态:
|
|
|
|
|
+ * - adaptOnboardingRecord(vo)(服务器模式):id, name, department, position, hireDate, status(key), employeeNo
|
|
|
|
|
+ * - mock / storage 中的入职记录(演示模式):id, name, avatar, department, position,
|
|
|
|
|
+ * entryDate, expectedEntryDate, materials, status(key), employeeNo
|
|
|
|
|
+ * 输出:avatar / name / sub / status(中文 label) / statusClass / label1 / value1 / label2 / value2 / id
|
|
|
|
|
+ */
|
|
|
|
|
+export function mapOnboardingForList(record) {
|
|
|
|
|
+ if (!record || typeof record !== 'object') return null
|
|
|
|
|
+ const id = record.id || record.onboardingId || ''
|
|
|
|
|
+ const name = record.name || record.employeeName || ''
|
|
|
|
|
+ const department = record.department || record.deptName || ''
|
|
|
|
|
+ const position = record.position || record.positionName || ''
|
|
|
|
|
+ const statusKey = record.status || 'pending'
|
|
|
|
|
+ const statusInfo = onboardStatus[statusKey] || onboardStatus.pending
|
|
|
|
|
+ const hireDate = record.hireDate || record.entryDate || record.expectedEntryDate || ''
|
|
|
|
|
+ const employeeNo = record.employeeNo || id
|
|
|
|
|
+ const isEntered = Boolean(record.entryDate)
|
|
|
|
|
+ const materials = record.materials
|
|
|
|
|
+ let materialsLabel = ''
|
|
|
|
|
+ if (materials && typeof materials === 'object') {
|
|
|
|
|
+ const total = Object.keys(materials).length
|
|
|
|
|
+ if (total) {
|
|
|
|
|
+ const done = Object.values(materials).filter(Boolean).length
|
|
|
|
|
+ materialsLabel = `${done}/${total}`
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ id,
|
|
|
|
|
+ avatar: record.avatar || name.charAt(0) || '',
|
|
|
|
|
+ name,
|
|
|
|
|
+ sub: [department, position].filter(Boolean).join(' · '),
|
|
|
|
|
+ status: statusInfo.label,
|
|
|
|
|
+ statusClass: statusInfo.class,
|
|
|
|
|
+ label1: isEntered ? '入职日期' : '预计入职',
|
|
|
|
|
+ value1: isEntered ? record.entryDate : hireDate,
|
|
|
|
|
+ label2: isEntered ? '员工编号' : (materialsLabel ? '资料进度' : '员工编号'),
|
|
|
|
|
+ value2: isEntered ? employeeNo : (materialsLabel || employeeNo),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============ 对外 API ============
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 把前端表单(name/hireDate/...)映射为后端 HR 移动端待入职创建参数。
|
|
|
|
|
+ * 后端要求字段:employeeName / plannedJoinDate / legalId / deptId / positionId / positionName
|
|
|
|
|
+ * / phone / email / idNumber / employmentType / probationFlag / probationMonths
|
|
|
|
|
+ * / remark / factoryId / managerUserId / teamId
|
|
|
|
|
+ * 长整型字段在 number 字符串间互转时保留为 number,避免后端 @RequestParam 严格校验失败
|
|
|
|
|
+ */
|
|
|
|
|
+export function serializeOnboardingManual(form = {}) {
|
|
|
|
|
+ const toId = (v) => {
|
|
|
|
|
+ if (v == null || v === '') return ""
|
|
|
|
|
+ const n = String(v)
|
|
|
|
|
+ return n ? n : ""
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ employeeName: String(form.name || form.employeeName || '').trim(),
|
|
|
|
|
+ plannedJoinDate: String(form.hireDate || form.plannedJoinDate || '').trim(),
|
|
|
|
|
+ legalId: toId(form.legalId),
|
|
|
|
|
+ deptId: toId(form.deptId),
|
|
|
|
|
+ positionId: toId(form.positionId),
|
|
|
|
|
+ positionName: String(form.positionName || form.position || '').trim(),
|
|
|
|
|
+ phone: String(form.phone || '').trim(),
|
|
|
|
|
+ email: String(form.email || '').trim(),
|
|
|
|
|
+ idNumber: String(form.idNumber || '').trim(),
|
|
|
|
|
+ employmentType: String(form.employmentType || 'FULL_TIME').trim(),
|
|
|
|
|
+ probationFlag: Number(form.probationFlag) === 1 ? 1 : 0,
|
|
|
|
|
+ probationMonths: Number(form.probationMonths) || 0,
|
|
|
|
|
+ remark: String(form.remark || '').trim(),
|
|
|
|
|
+ factoryId: toId(form.factoryId),
|
|
|
|
|
+ managerUserId: toId(form.managerUserId),
|
|
|
|
|
+ teamId: toId(form.teamId),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 手动创建入职单
|
|
|
|
|
+ * POST /hr/ess/admin/onboardings/manual
|
|
|
|
|
+ *
|
|
|
|
|
+ * 后端必填字段:plannedJoinDate / legalId / deptId / probationFlag
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {object} payload 入职表单字段(前端语义:name / hireDate / ...)
|
|
|
|
|
+ * @returns {Promise<{ onboardingId: string|number, id: string|number, employeeNo: string }>}
|
|
|
|
|
+ */
|
|
|
|
|
+export async function createOnboardingManual(payload) {
|
|
|
|
|
+ const body = serializeOnboardingManual(payload)
|
|
|
|
|
+ if (!isServerMode()) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 250))
|
|
|
|
|
+ const id = `ZY${Date.now()}${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`
|
|
|
|
|
+ return { onboardingId: id, id, employeeNo: id }
|
|
|
|
|
+ }
|
|
|
|
|
+ const path = `${serverPath()}/hr/ess/admin/onboardings/manual`
|
|
|
|
|
+ const res = await request({ url: path, method: 'POST', data: body })
|
|
|
|
|
+ return res.data || {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 邀请方式创建入职单(生成公开链接)
|
|
|
|
|
+ * POST /hr/ess/admin/onboardings/invitations?validDays=N
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {object} payload 入职邀请字段
|
|
|
|
|
+ * @param {number} validDays 链接有效天数
|
|
|
|
|
+ * @returns {Promise<{ invitationId: string, publicToken: string, publicPath: string, onboardingId: string|null }>}
|
|
|
|
|
+ */
|
|
|
|
|
+export async function createOnboardingInvitation(payload, validDays) {
|
|
|
|
|
+ // 邀请模式下姓名/手机/邮箱/身份证允许为空,后端字段照常映射即可
|
|
|
|
|
+ const body = serializeOnboardingManual(payload)
|
|
|
|
|
+ if (!isServerMode()) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 250))
|
|
|
|
|
+ const token = `TKN${Date.now().toString(36)}`
|
|
|
|
|
+ return {
|
|
|
|
|
+ invitationId: `INV${Date.now()}`,
|
|
|
|
|
+ publicToken: token,
|
|
|
|
|
+ publicPath: `/onboarding/public/${token}`,
|
|
|
|
|
+ onboardingId: null,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // validDays 走 query string(与后端约定一致),不放 body
|
|
|
|
|
+ const qs = validDays != null && validDays !== '' ? `?validDays=${encodeURIComponent(Number(validDays))}` : ''
|
|
|
|
|
+ const path = `${serverPath()}/hr/ess/admin/onboardings/invitations${qs}`
|
|
|
|
|
+ const res = await request({ url: path, method: 'POST', data: body })
|
|
|
|
|
+ return res.data || {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 入职记录列表
|
|
|
|
|
+ * GET /hr/ess/admin/onboardings/records
|
|
|
|
|
+ *
|
|
|
|
|
+ * @returns {Promise<Array>}
|
|
|
|
|
+ */
|
|
|
|
|
+export async function getOnboardingRecords() {
|
|
|
|
|
+ if (!isServerMode()) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 200))
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+ const path = `${serverPath()}/hr/ess/admin/onboardings/records`
|
|
|
|
|
+ const res = await request({ url: path, method: 'GET' })
|
|
|
|
|
+ return Array.isArray(res.data) ? res.data : []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 入职邀请记录列表(演示版回退到 mock 默认邀请)
|
|
|
|
|
+ * GET /hr/ess/admin/onboardings/invitations
|
|
|
|
|
+ *
|
|
|
|
|
+ * @returns {Promise<Array>}
|
|
|
|
|
+ */
|
|
|
|
|
+export async function getOnboardingInvitationsList() {
|
|
|
|
|
+ if (!isServerMode()) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 150))
|
|
|
|
|
+ return defaultOnboardingInvitations
|
|
|
|
|
+ }
|
|
|
|
|
+ const path = `${serverPath()}/hr/ess/admin/onboardings/invitations`
|
|
|
|
|
+ const res = await request({ url: path, method: 'GET' })
|
|
|
|
|
+ return Array.isArray(res.data) ? res.data : []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 提交入职审批
|
|
|
|
|
+ * POST /hr/ess/admin/onboardings/records/{id}/submit
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string} onboardingId
|
|
|
|
|
+ * @returns {Promise<object>}
|
|
|
|
|
+ */
|
|
|
|
|
+export async function submitOnboardingRecord(onboardingId) {
|
|
|
|
|
+ if (!onboardingId) throw new Error('缺少入职单主键')
|
|
|
|
|
+ if (!isServerMode()) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 200))
|
|
|
|
|
+ return { code: 0, message: '演示:已提交审批' }
|
|
|
|
|
+ }
|
|
|
|
|
+ const path = `${serverPath()}/hr/ess/admin/onboardings/records/${encodeURIComponent(onboardingId)}/submit`
|
|
|
|
|
+ const res = await request({ url: path, method: 'POST' })
|
|
|
|
|
+ return res
|
|
|
|
|
+}
|