|
|
@@ -0,0 +1,716 @@
|
|
|
+import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
|
|
|
+import { userChangeStatus, userChangeStages, userChanges } from '@/data/mock'
|
|
|
+import {
|
|
|
+ addCreatedUserChange,
|
|
|
+ formatNow,
|
|
|
+ getCreatedUserChanges,
|
|
|
+ getCurrentUser,
|
|
|
+ getLoginUser,
|
|
|
+ getUserChangeIds,
|
|
|
+ getUserChangeStates,
|
|
|
+ rememberUserChangeId,
|
|
|
+ setUserChangeLocalState,
|
|
|
+ updateCreatedUserChange,
|
|
|
+} from '@/utils/storage'
|
|
|
+
|
|
|
+// ============ 模式判断 ============
|
|
|
+function isServerMode() {
|
|
|
+ return getServerConfig().mode === 'server'
|
|
|
+}
|
|
|
+
|
|
|
+function serverPath() {
|
|
|
+ const base = getApiBaseUrl()
|
|
|
+ if (!base) throw new Error('缺少服务器地址,请先在登录页右上角配置')
|
|
|
+ return base
|
|
|
+}
|
|
|
+
|
|
|
+function delay(ms = 200) {
|
|
|
+ return new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 工具 ============
|
|
|
+function compact(object = {}) {
|
|
|
+ return Object.keys(object).reduce((result, key) => {
|
|
|
+ const value = object[key]
|
|
|
+ if (value !== '' && value !== null && value !== undefined) result[key] = value
|
|
|
+ return result
|
|
|
+ }, {})
|
|
|
+}
|
|
|
+
|
|
|
+function dateOnly(value) {
|
|
|
+ if (!value) return ''
|
|
|
+ return String(value).slice(0, 10)
|
|
|
+}
|
|
|
+
|
|
|
+function labelOf(map, raw, fallback = '') {
|
|
|
+ const text = String(raw || '').trim()
|
|
|
+ if (!text) return fallback
|
|
|
+ return map[text] || map[text.toUpperCase()] || text
|
|
|
+}
|
|
|
+
|
|
|
+function asList(data) {
|
|
|
+ if (Array.isArray(data)) return data
|
|
|
+ if (Array.isArray(data?.list)) return data.list
|
|
|
+ if (Array.isArray(data?.records)) return data.records
|
|
|
+ if (data && typeof data === 'object' && data.id != null) return [data]
|
|
|
+ return []
|
|
|
+}
|
|
|
+
|
|
|
+function keepBigIntId(value) {
|
|
|
+ if (value === undefined || value === null || value === '') return undefined
|
|
|
+ const text = String(value).trim()
|
|
|
+ if (!/^-?\d+$/.test(text)) return value
|
|
|
+ if (!Number.isSafeInteger(Number(text))) return text
|
|
|
+ return Number(text)
|
|
|
+}
|
|
|
+
|
|
|
+function extractId(result) {
|
|
|
+ if (result == null) return null
|
|
|
+ if (typeof result === 'object') return result.id ?? result.changeId ?? null
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+function parseSnapshot(raw) {
|
|
|
+ if (!raw) return {}
|
|
|
+ if (typeof raw === 'object') return raw
|
|
|
+ try {
|
|
|
+ return JSON.parse(raw) || {}
|
|
|
+ } catch (error) {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function detailMap(details = []) {
|
|
|
+ const map = {}
|
|
|
+ const list = Array.isArray(details) ? details : []
|
|
|
+ list.forEach((item) => {
|
|
|
+ const field = String(item?.changeField || '').toUpperCase()
|
|
|
+ if (field) map[field] = item
|
|
|
+ })
|
|
|
+ return map
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 枚举(与 PC 端 src/api/hr/userChange.js 对齐)============
|
|
|
+
|
|
|
+/** 异动类型 */
|
|
|
+export const CHANGE_TYPE_LABELS = {
|
|
|
+ POSITION_TRANSFER: '调岗',
|
|
|
+ DEPARTMENT_TRANSFER: '调部门',
|
|
|
+ GRADE_CHANGE: '职级调整',
|
|
|
+ PROMOTION: '晋升',
|
|
|
+ DEMOTION: '降级',
|
|
|
+ SALARY_CHANGE: '调薪',
|
|
|
+ MANAGER_CHANGE: '调整上级',
|
|
|
+ WORK_LOCATION_CHANGE: '工作地点变更',
|
|
|
+ FACTORY_TRANSFER: '调工厂',
|
|
|
+ LEGAL_ENTITY_TRANSFER: '跨法人调动',
|
|
|
+ EMPLOYMENT_TYPE_CHANGE: '用工类型变更',
|
|
|
+ COMPOSITE_CHANGE: '综合异动',
|
|
|
+}
|
|
|
+
|
|
|
+/** 员工端可自助提交的异动类型,其余由 HR 在 PC 端办理 */
|
|
|
+export const ESS_CHANGE_TYPE_KEYS = [
|
|
|
+ 'POSITION_TRANSFER',
|
|
|
+ 'DEPARTMENT_TRANSFER',
|
|
|
+ 'PROMOTION',
|
|
|
+ 'GRADE_CHANGE',
|
|
|
+ 'COMPOSITE_CHANGE',
|
|
|
+]
|
|
|
+
|
|
|
+export const ESS_CHANGE_TYPE_OPTIONS = ESS_CHANGE_TYPE_KEYS.map((value) => ({
|
|
|
+ value,
|
|
|
+ label: CHANGE_TYPE_LABELS[value],
|
|
|
+}))
|
|
|
+
|
|
|
+export function changeTypeLabel(value) {
|
|
|
+ return labelOf(CHANGE_TYPE_LABELS, value, value || '调岗')
|
|
|
+}
|
|
|
+
|
|
|
+export const APPROVAL_STATUS_LABELS = {
|
|
|
+ DRAFT: '草稿',
|
|
|
+ PENDING: '审批中',
|
|
|
+ APPROVING: '审批中',
|
|
|
+ APPROVED: '已通过',
|
|
|
+ REJECTED: '已驳回',
|
|
|
+ WITHDRAWN: '已撤回',
|
|
|
+ CANCELLED: '已取消',
|
|
|
+}
|
|
|
+
|
|
|
+export const EFFECTIVE_STATUS_LABELS = {
|
|
|
+ PENDING: '待生效',
|
|
|
+ SCHEDULED: '已排期',
|
|
|
+ EFFECTIVE: '已生效',
|
|
|
+ CANCELLED: '已取消',
|
|
|
+}
|
|
|
+
|
|
|
+/** 页面展示态 → 标签 / 样式(枚举定义在 data/mock.js) */
|
|
|
+export const TRANSFER_DISPLAY_STATUS = userChangeStatus
|
|
|
+
|
|
|
+export const TRANSFER_STATUS_FILTERS = [
|
|
|
+ { value: 'all', label: '全部状态' },
|
|
|
+ { value: 'draft', label: '草稿' },
|
|
|
+ { value: 'pending', label: '审批中' },
|
|
|
+ { value: 'approved', label: '已通过' },
|
|
|
+ { value: 'rejected', label: '已驳回' },
|
|
|
+]
|
|
|
+
|
|
|
+// ============ 状态机 ============
|
|
|
+
|
|
|
+export function deriveDocStatus(raw = {}) {
|
|
|
+ const approval = String(raw.approvalStatus || '').toUpperCase()
|
|
|
+ const effective = String(raw.effectiveStatus || '').toUpperCase()
|
|
|
+ if (approval === 'CANCELLED' || effective === 'CANCELLED') return 'cancelled'
|
|
|
+ if (approval === 'APPROVED' || effective === 'EFFECTIVE') return 'approved'
|
|
|
+ if (approval === 'REJECTED') return 'rejected'
|
|
|
+ if (approval === 'PENDING' || approval === 'APPROVING') return 'pending'
|
|
|
+ if (approval === 'WITHDRAWN') return 'withdrawn'
|
|
|
+ return 'draft'
|
|
|
+}
|
|
|
+
|
|
|
+export function canEditUserChange(row = {}) {
|
|
|
+ const status = String(row.approvalStatus || '').toUpperCase()
|
|
|
+ return status === 'DRAFT' || status === 'REJECTED' || status === ''
|
|
|
+}
|
|
|
+
|
|
|
+export function canSubmitUserChange(row = {}) {
|
|
|
+ if (!row?.id) return false
|
|
|
+ return ['draft', 'rejected'].includes(row.displayStatus)
|
|
|
+}
|
|
|
+
|
|
|
+export function canWithdrawUserChange(row = {}) {
|
|
|
+ const status = String(row.approvalStatus || '').toUpperCase()
|
|
|
+ return status === 'PENDING' || status === 'APPROVING'
|
|
|
+}
|
|
|
+
|
|
|
+export function canCancelUserChange(row = {}) {
|
|
|
+ if (!row?.id) return false
|
|
|
+ if (String(row.approvalStatus || '').toUpperCase() === 'CANCELLED') return false
|
|
|
+ return String(row.effectiveStatus || '').toUpperCase() !== 'EFFECTIVE'
|
|
|
+}
|
|
|
+
|
|
|
+export function matchDisplayStatus(row = {}, filter = '') {
|
|
|
+ if (!filter || filter === 'all') return true
|
|
|
+ if (filter === 'rejected') return ['rejected', 'withdrawn', 'cancelled'].includes(row.displayStatus)
|
|
|
+ return row.displayStatus === filter
|
|
|
+}
|
|
|
+
|
|
|
+function stageKeyOf(displayStatus) {
|
|
|
+ if (displayStatus === 'approved') return 'effective'
|
|
|
+ if (displayStatus === 'pending') return 'approval'
|
|
|
+ return 'submit'
|
|
|
+}
|
|
|
+
|
|
|
+function progressOf(displayStatus, stageKey) {
|
|
|
+ if (displayStatus === 'approved') return 100
|
|
|
+ if (['rejected', 'withdrawn', 'cancelled'].includes(displayStatus)) return 30
|
|
|
+ const index = Math.max(
|
|
|
+ userChangeStages.findIndex((stage) => stage.key === stageKey),
|
|
|
+ 0,
|
|
|
+ )
|
|
|
+ return Math.round(((index + 1) / userChangeStages.length) * 100)
|
|
|
+}
|
|
|
+
|
|
|
+function progressTipOf(displayStatus) {
|
|
|
+ switch (displayStatus) {
|
|
|
+ case 'draft':
|
|
|
+ return '草稿尚未提交,提交后进入审批流程。'
|
|
|
+ case 'pending':
|
|
|
+ return '调岗审批中,请等待主管与 HR 的审批结果。'
|
|
|
+ case 'approved':
|
|
|
+ return '调岗审批已通过,将按生效日期更新岗位信息。'
|
|
|
+ case 'rejected':
|
|
|
+ return '审批已驳回,可修改后重新提交。'
|
|
|
+ case 'withdrawn':
|
|
|
+ return '本次调岗申请已撤回。'
|
|
|
+ case 'cancelled':
|
|
|
+ return '本次调岗申请已取消。'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============ VO / 演示记录 → 页面形状 ============
|
|
|
+export function adaptUserChange(raw = {}, extras = {}) {
|
|
|
+ const before = parseSnapshot(raw.beforeSnapshot)
|
|
|
+ const after = parseSnapshot(raw.afterSnapshot)
|
|
|
+ const details = Array.isArray(raw.details) ? raw.details : []
|
|
|
+ const map = detailMap(details)
|
|
|
+ const deptDetail = map.DEPT
|
|
|
+ const positionDetail = map.POSITION
|
|
|
+ const gradeDetail = map.GRADE
|
|
|
+ const locationDetail = map.WORK_LOCATION
|
|
|
+
|
|
|
+ const changeType = String(raw.changeType || '').toUpperCase()
|
|
|
+ const approvalStatus = String(raw.approvalStatus || '').toUpperCase()
|
|
|
+ const effectiveStatus = String(raw.effectiveStatus || '').toUpperCase()
|
|
|
+ const displayStatus = deriveDocStatus({ approvalStatus, effectiveStatus })
|
|
|
+ const statusMeta = TRANSFER_DISPLAY_STATUS[displayStatus] || TRANSFER_DISPLAY_STATUS.draft
|
|
|
+ const stageKey = stageKeyOf(displayStatus)
|
|
|
+ const name = extras.name || raw.name || raw.employeeName || ''
|
|
|
+
|
|
|
+ const dept =
|
|
|
+ extras.dept || raw.dept || raw.originDept || before.deptName || deptDetail?.beforeText || ''
|
|
|
+ const position =
|
|
|
+ extras.position ||
|
|
|
+ raw.position ||
|
|
|
+ raw.originPosition ||
|
|
|
+ before.positionName ||
|
|
|
+ positionDetail?.beforeText ||
|
|
|
+ ''
|
|
|
+ const targetDept = raw.targetDept || raw.targetDeptName || after.deptName || deptDetail?.afterText || ''
|
|
|
+ const targetPosition =
|
|
|
+ raw.targetPosition || raw.targetPositionName || after.positionName || positionDetail?.afterText || ''
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: raw.id,
|
|
|
+ changeNo: raw.changeNo || raw.contractNo || (raw.id == null ? '' : String(raw.id)),
|
|
|
+ source: extras.source || raw.source || '',
|
|
|
+ userId: raw.userId ?? extras.userId,
|
|
|
+ employeeId: raw.userId ?? extras.employeeId,
|
|
|
+ name,
|
|
|
+ avatar: raw.avatar || String(name).charAt(0),
|
|
|
+ employeeNo: extras.employeeNo || raw.employeeNo || raw.userNo || '',
|
|
|
+ dept,
|
|
|
+ position,
|
|
|
+ level: extras.level || raw.level || before.positionLevelName || before.grade || gradeDetail?.beforeText || '',
|
|
|
+ targetDept,
|
|
|
+ targetDeptId: raw.targetDeptId ?? deptDetail?.afterValue ?? null,
|
|
|
+ targetPosition,
|
|
|
+ targetPositionId: raw.targetPositionId ?? positionDetail?.afterValue ?? null,
|
|
|
+ targetLevel: raw.targetLevel || after.positionLevelName || after.grade || gradeDetail?.afterText || '',
|
|
|
+ workLocation: raw.workLocation || locationDetail?.afterText || '',
|
|
|
+ changeType,
|
|
|
+ changeTypeLabel: changeTypeLabel(changeType),
|
|
|
+ effectiveDate: dateOnly(raw.effectiveDate),
|
|
|
+ applyDate: dateOnly(raw.applyDate || raw.createTime || raw.submitDate),
|
|
|
+ reason: raw.reason || '',
|
|
|
+ remark: raw.remark || '',
|
|
|
+ approvalNode: raw.approvalNode || '',
|
|
|
+ approvalComment: raw.approvalComment || raw.hrComment || '',
|
|
|
+ approvalStatus,
|
|
|
+ approvalStatusLabel: labelOf(APPROVAL_STATUS_LABELS, approvalStatus, '草稿'),
|
|
|
+ effectiveStatus,
|
|
|
+ effectiveStatusLabel: labelOf(EFFECTIVE_STATUS_LABELS, effectiveStatus, '-'),
|
|
|
+ displayStatus,
|
|
|
+ statusLabel: statusMeta.label,
|
|
|
+ statusClass: statusMeta.class,
|
|
|
+ stageKey,
|
|
|
+ progress: progressOf(displayStatus, stageKey),
|
|
|
+ progressTip: progressTipOf(displayStatus),
|
|
|
+ details,
|
|
|
+ createTime: raw.createTime || '',
|
|
|
+ raw,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 演示模式本地实现 ============
|
|
|
+function mergeDemoRecords() {
|
|
|
+ const overrides = getUserChangeStates()
|
|
|
+ const created = getCreatedUserChanges()
|
|
|
+ const seen = new Set(created.map((item) => String(item.id)))
|
|
|
+ const base = [...created, ...userChanges.filter((item) => !seen.has(String(item.id)))]
|
|
|
+ return base.map((record) => {
|
|
|
+ const merged = { ...record, ...(overrides[String(record.id)] || {}) }
|
|
|
+ return adaptUserChange(merged, { source: 'demo' })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function updateDemoUserChange(id, patch) {
|
|
|
+ setUserChangeLocalState(id, patch)
|
|
|
+ updateCreatedUserChange(id, patch)
|
|
|
+ return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null
|
|
|
+}
|
|
|
+
|
|
|
+function persistDemoTransfer(id, form, submit) {
|
|
|
+ const user = getCurrentUser() || {}
|
|
|
+ const name = form.name || user.name || ''
|
|
|
+ const record = {
|
|
|
+ id: String(id),
|
|
|
+ userId: form.userId || form.employeeId || user.id || '',
|
|
|
+ name,
|
|
|
+ avatar: form.avatar || String(name).charAt(0),
|
|
|
+ employeeNo: form.employeeNo || user.id || '',
|
|
|
+ dept: form.dept || user.department || '',
|
|
|
+ position: form.position || user.position || '',
|
|
|
+ level: form.level || '',
|
|
|
+ targetDeptId: form.targetDeptId ?? null,
|
|
|
+ targetDept: form.targetDeptName || form.targetDept || '',
|
|
|
+ targetPositionId: form.targetPositionId ?? null,
|
|
|
+ targetPosition: form.targetPositionName || form.targetPosition || '',
|
|
|
+ targetLevel: form.targetLevel || '',
|
|
|
+ changeType: inferChangeType(form),
|
|
|
+ effectiveDate: dateOnly(form.effectiveDate),
|
|
|
+ applyDate: dateOnly(formatNow()),
|
|
|
+ reason: form.reason || '',
|
|
|
+ remark: form.remark || '',
|
|
|
+ approvalNode: submit ? '主管审批' : '',
|
|
|
+ approvalStatus: submit ? 'PENDING' : 'DRAFT',
|
|
|
+ effectiveStatus: 'PENDING',
|
|
|
+ source: 'demo',
|
|
|
+ createdByApp: true,
|
|
|
+ }
|
|
|
+ addCreatedUserChange(record)
|
|
|
+ return record
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 服务器接口(M02-人事异动)============
|
|
|
+const employeeCache = new Map()
|
|
|
+
|
|
|
+async function fetchEmployeeBrief(userId) {
|
|
|
+ if (userId === undefined || userId === null || userId === '') return null
|
|
|
+ const key = String(userId)
|
|
|
+ if (employeeCache.has(key)) return employeeCache.get(key)
|
|
|
+ try {
|
|
|
+ const res = await request({ url: `${serverPath()}/main/user/getById/${key}`, method: 'GET' })
|
|
|
+ const vo = res.data || {}
|
|
|
+ const brief = {
|
|
|
+ name: vo.name || vo.userName || '',
|
|
|
+ employeeNo: vo.jobNumber || '',
|
|
|
+ dept: vo.deptName || '',
|
|
|
+ position: vo.postName || vo.position || '',
|
|
|
+ level: vo.positionLevelName || vo.grade || '',
|
|
|
+ }
|
|
|
+ employeeCache.set(key, brief)
|
|
|
+ return brief
|
|
|
+ } catch (error) {
|
|
|
+ employeeCache.set(key, null)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function extrasOf(brief) {
|
|
|
+ if (!brief) return {}
|
|
|
+ return {
|
|
|
+ name: brief.name,
|
|
|
+ employeeNo: brief.employeeNo,
|
|
|
+ dept: brief.dept,
|
|
|
+ position: brief.position,
|
|
|
+ level: brief.level,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 当前登录人的服务端数字主键(登录响应里的 userId) */
|
|
|
+export function currentUserId() {
|
|
|
+ const login = getLoginUser() || {}
|
|
|
+ const raw = getCurrentUser()?.raw || {}
|
|
|
+ const candidates = [login.userId, login.id, raw.userId, raw.id]
|
|
|
+ return candidates.find((value) => value !== undefined && value !== null && value !== '')
|
|
|
+}
|
|
|
+
|
|
|
+function isMine(row = {}) {
|
|
|
+ const user = getCurrentUser() || {}
|
|
|
+ const login = getLoginUser() || {}
|
|
|
+ const mine = [user.id, user.name, login.userId, login.loginName, login.userName]
|
|
|
+ .filter((value) => value !== undefined && value !== null && value !== '')
|
|
|
+ .map(String)
|
|
|
+ if (!mine.length) return true
|
|
|
+ const rowIds = [row.userId, row.employeeNo, row.name]
|
|
|
+ .filter((value) => value !== undefined && value !== null && value !== '')
|
|
|
+ .map(String)
|
|
|
+ return rowIds.some((value) => mine.includes(value))
|
|
|
+}
|
|
|
+
|
|
|
+function transferText(row = {}) {
|
|
|
+ return `${row.name}${row.employeeNo}${row.changeNo}${row.dept}${row.position}${row.targetDept}${row.targetPosition}`
|
|
|
+}
|
|
|
+
|
|
|
+/** 条件查询异动单 GET /main/user-changes */
|
|
|
+export async function getUserChangePage(params = {}) {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes`,
|
|
|
+ method: 'GET',
|
|
|
+ data: compact({
|
|
|
+ pageNum: params.pageNum,
|
|
|
+ size: params.size,
|
|
|
+ userId: keepBigIntId(params.userId),
|
|
|
+ changeType: params.changeType,
|
|
|
+ approvalStatus: params.approvalStatus,
|
|
|
+ }),
|
|
|
+ })
|
|
|
+ const data = res.data || {}
|
|
|
+ return { list: asList(data), count: Number(data?.count ?? data?.total ?? 0) }
|
|
|
+}
|
|
|
+
|
|
|
+/** 某员工的异动记录 GET /main/user-changes/users/{userId} */
|
|
|
+export async function getUserChangesByUserId(userId) {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes/users/${userId}`,
|
|
|
+ method: 'GET',
|
|
|
+ })
|
|
|
+ return asList(res.data)
|
|
|
+}
|
|
|
+
|
|
|
+/** 异动单详情 GET /main/user-changes/{id} */
|
|
|
+export async function getUserChangeById(id) {
|
|
|
+ const res = await request({ url: `${serverPath()}/main/user-changes/${id}`, method: 'GET' })
|
|
|
+ return res.data
|
|
|
+}
|
|
|
+
|
|
|
+/** 新建异动单(草稿)POST /main/user-changes */
|
|
|
+export async function createUserChange(payload) {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes`,
|
|
|
+ method: 'POST',
|
|
|
+ data: payload,
|
|
|
+ })
|
|
|
+ const id = extractId(res.data)
|
|
|
+ if (id != null) rememberUserChangeId(id)
|
|
|
+ return id
|
|
|
+}
|
|
|
+
|
|
|
+/** 修改异动草稿 PUT /main/user-changes */
|
|
|
+export async function updateUserChange(payload) {
|
|
|
+ await request({ url: `${serverPath()}/main/user-changes`, method: 'PUT', data: payload })
|
|
|
+ if (payload?.id != null) rememberUserChangeId(payload.id)
|
|
|
+ return payload.id
|
|
|
+}
|
|
|
+
|
|
|
+/** 异动预校验 POST /main/user-changes/{id}/validate */
|
|
|
+export async function validateUserChange(id) {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes/${id}/validate`,
|
|
|
+ method: 'POST',
|
|
|
+ })
|
|
|
+ return res.data
|
|
|
+}
|
|
|
+
|
|
|
+/** 提交异动审批 POST /main/user-changes/{id}/submit */
|
|
|
+export async function submitUserChange(id) {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes/${id}/submit`,
|
|
|
+ method: 'POST',
|
|
|
+ })
|
|
|
+ return res.data
|
|
|
+}
|
|
|
+
|
|
|
+/** 撤回异动审批 POST /main/user-changes/{id}/withdraw */
|
|
|
+export async function withdrawUserChange(id, reason = '') {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes/${id}/withdraw`,
|
|
|
+ method: 'POST',
|
|
|
+ data: compact({ reason }),
|
|
|
+ })
|
|
|
+ return res.data
|
|
|
+}
|
|
|
+
|
|
|
+/** 取消异动单 POST /main/user-changes/{id}/cancel */
|
|
|
+export async function cancelUserChange(id, reason = '') {
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/main/user-changes/${id}/cancel`,
|
|
|
+ method: 'POST',
|
|
|
+ data: compact({ reason }),
|
|
|
+ })
|
|
|
+ return res.data
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 对外统一入口(页面只调用这些)============
|
|
|
+
|
|
|
+/**
|
|
|
+ * 调岗列表。
|
|
|
+ * @param {object} params
|
|
|
+ * - scope: 'mine' 只看本人;'all' 全部员工(HR 视图)
|
|
|
+ * - status: TRANSFER_STATUS_FILTERS 的 value
|
|
|
+ * - keyword: 姓名 / 工号 / 单号 / 岗位 模糊匹配
|
|
|
+ */
|
|
|
+export async function loadTransferList(params = {}) {
|
|
|
+ const { scope = 'mine', status = 'all', keyword = '', pageNum = 1, size = 50 } = params
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ const list = mergeDemoRecords()
|
|
|
+ .filter((row) => (scope === 'mine' ? isMine(row) : true))
|
|
|
+ .filter((row) => matchDisplayStatus(row, status))
|
|
|
+ .filter((row) => (keyword ? transferText(row).includes(keyword) : true))
|
|
|
+ .sort((a, b) =>
|
|
|
+ String(b.applyDate || b.createTime).localeCompare(String(a.applyDate || a.createTime)),
|
|
|
+ )
|
|
|
+ return { list, count: list.length, source: 'demo' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const userId = currentUserId()
|
|
|
+ let rows = []
|
|
|
+ if (scope === 'mine' && userId != null) {
|
|
|
+ try {
|
|
|
+ rows = await getUserChangesByUserId(userId)
|
|
|
+ } catch (error) {
|
|
|
+ rows = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!rows.length) {
|
|
|
+ try {
|
|
|
+ const page = await getUserChangePage({
|
|
|
+ pageNum,
|
|
|
+ size,
|
|
|
+ userId: scope === 'mine' ? userId : undefined,
|
|
|
+ })
|
|
|
+ rows = page.list
|
|
|
+ } catch (error) {
|
|
|
+ rows = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 集合接口不可用时,按本机记住的主键逐个拉详情兜底(与 PC 端策略一致)
|
|
|
+ if (!rows.length) {
|
|
|
+ const details = await Promise.all(
|
|
|
+ getUserChangeIds().map(async (id) => {
|
|
|
+ try {
|
|
|
+ return await getUserChangeById(id)
|
|
|
+ } catch (error) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ rows = details.filter(Boolean)
|
|
|
+ }
|
|
|
+
|
|
|
+ const list = []
|
|
|
+ for (const vo of rows) {
|
|
|
+ if (!vo || vo.id == null) continue
|
|
|
+ const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId))
|
|
|
+ const row = adaptUserChange(vo, extras)
|
|
|
+ rememberUserChangeId(row.id)
|
|
|
+ if (scope === 'mine' && userId != null && !isMine(row)) continue
|
|
|
+ if (!matchDisplayStatus(row, status)) continue
|
|
|
+ if (keyword && !transferText(row).includes(keyword)) continue
|
|
|
+ list.push(row)
|
|
|
+ }
|
|
|
+ return { list, count: list.length, source: 'api' }
|
|
|
+}
|
|
|
+
|
|
|
+/** 调岗单详情 */
|
|
|
+export async function loadTransferDetail(id) {
|
|
|
+ if (!id) return null
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ return mergeDemoRecords().find((item) => String(item.id) === String(id)) || null
|
|
|
+ }
|
|
|
+
|
|
|
+ const vo = await getUserChangeById(id)
|
|
|
+ if (!vo) return null
|
|
|
+ rememberUserChangeId(id)
|
|
|
+ const extras = vo.name ? {} : extrasOf(await fetchEmployeeBrief(vo.userId))
|
|
|
+ return adaptUserChange(vo, extras)
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 员工端申请表单 ============
|
|
|
+
|
|
|
+function inferChangeType(form = {}) {
|
|
|
+ if (form.changeType) return form.changeType
|
|
|
+ const hasDept = form.targetDeptId != null && form.targetDeptId !== ''
|
|
|
+ const hasPosition = form.targetPositionId != null && form.targetPositionId !== ''
|
|
|
+ if (hasDept && hasPosition) return 'COMPOSITE_CHANGE'
|
|
|
+ if (hasDept) return 'DEPARTMENT_TRANSFER'
|
|
|
+ return 'POSITION_TRANSFER'
|
|
|
+}
|
|
|
+
|
|
|
+function buildDetails(form = {}) {
|
|
|
+ if (Array.isArray(form.details) && form.details.length) return form.details
|
|
|
+ const details = []
|
|
|
+ if (form.targetDeptName) {
|
|
|
+ details.push({
|
|
|
+ changeField: 'DEPT',
|
|
|
+ beforeValue: form.originDeptId != null ? String(form.originDeptId) : undefined,
|
|
|
+ beforeText: form.dept || '',
|
|
|
+ afterValue: form.targetDeptId != null ? String(form.targetDeptId) : undefined,
|
|
|
+ afterText: form.targetDeptName,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (form.targetPositionName) {
|
|
|
+ details.push({
|
|
|
+ changeField: 'POSITION',
|
|
|
+ beforeValue: form.originPositionId != null ? String(form.originPositionId) : undefined,
|
|
|
+ beforeText: form.position || '',
|
|
|
+ afterValue: form.targetPositionId != null ? String(form.targetPositionId) : undefined,
|
|
|
+ afterText: form.targetPositionName,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 职级未变化时不产生 GRADE 变动,避免把岗位自带层级误判为职级调整
|
|
|
+ if (form.targetLevel && form.targetLevel !== form.level) {
|
|
|
+ details.push({
|
|
|
+ changeField: 'GRADE',
|
|
|
+ beforeValue: form.level || undefined,
|
|
|
+ beforeText: form.level || '',
|
|
|
+ afterValue: form.targetLevel,
|
|
|
+ afterText: form.targetLevel,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return details
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 表单字段 → /main/user-changes 请求参数(与 PC 端 buildUserChangePayload 对齐)。
|
|
|
+ * 申请人由服务端按当前任职确定,前端只送变动目标与事由。
|
|
|
+ */
|
|
|
+export function buildUserChangePayload(form = {}) {
|
|
|
+ return compact({
|
|
|
+ id: keepBigIntId(form.id),
|
|
|
+ userId: keepBigIntId(form.userId || form.employeeId),
|
|
|
+ changeType: inferChangeType(form),
|
|
|
+ effectiveDate: dateOnly(form.effectiveDate) || undefined,
|
|
|
+ reason: form.reason,
|
|
|
+ remark: form.remark,
|
|
|
+ targetDeptId: keepBigIntId(form.targetDeptId),
|
|
|
+ targetPositionId: keepBigIntId(form.targetPositionId),
|
|
|
+ targetPositionName: form.targetPositionName || form.targetPosition,
|
|
|
+ details: buildDetails(form),
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 提交(或存草稿)调岗申请。
|
|
|
+ * 服务器模式:建单 POST /main/user-changes → 提交 POST /{id}/submit
|
|
|
+ * 演示模式:本地落一条异动单,提交时状态置为审批中
|
|
|
+ *
|
|
|
+ * @returns {Promise<string|number>} 异动单主键
|
|
|
+ */
|
|
|
+export async function submitTransferApplication({ form, submit = true, existingId = null } = {}) {
|
|
|
+ if (!form) throw new Error('申请参数为空')
|
|
|
+ const id = existingId || form.id || null
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ await delay(300)
|
|
|
+ const localId = id || `DC${Date.now()}`
|
|
|
+ persistDemoTransfer(localId, form, submit)
|
|
|
+ return localId
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = buildUserChangePayload({ ...form, id })
|
|
|
+ const changeId = id || (await createUserChange(payload))
|
|
|
+ if (id) await updateUserChange({ ...payload, id: changeId })
|
|
|
+ if (submit && changeId != null) await submitUserChange(changeId)
|
|
|
+ return changeId
|
|
|
+}
|
|
|
+
|
|
|
+// —— 流程动作:服务器模式走接口,演示模式改本地态 ——
|
|
|
+
|
|
|
+export async function actSubmitTransfer(row) {
|
|
|
+ if (!row?.id) throw new Error('缺少异动单主键')
|
|
|
+ if (isServerMode()) {
|
|
|
+ // 与 PC 端一致:先预校验,再提交审批
|
|
|
+ const result = await validateUserChange(row.id)
|
|
|
+ if (result && result.valid === false) {
|
|
|
+ throw new Error((result.messages || []).join(';') || '异动预校验未通过')
|
|
|
+ }
|
|
|
+ return submitUserChange(row.id)
|
|
|
+ }
|
|
|
+ return updateDemoUserChange(row.id, { approvalStatus: 'PENDING', approvalNode: '主管审批' })
|
|
|
+}
|
|
|
+
|
|
|
+export async function actWithdrawTransfer(row, reason = '') {
|
|
|
+ if (!row?.id) throw new Error('缺少异动单主键')
|
|
|
+ if (isServerMode()) return withdrawUserChange(row.id, reason)
|
|
|
+ return updateDemoUserChange(row.id, {
|
|
|
+ approvalStatus: 'WITHDRAWN',
|
|
|
+ approvalComment: reason,
|
|
|
+ approvalNode: '',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export async function actCancelTransfer(row, reason = '') {
|
|
|
+ if (!row?.id) throw new Error('缺少异动单主键')
|
|
|
+ if (isServerMode()) return cancelUserChange(row.id, reason)
|
|
|
+ return updateDemoUserChange(row.id, {
|
|
|
+ approvalStatus: 'CANCELLED',
|
|
|
+ effectiveStatus: 'CANCELLED',
|
|
|
+ approvalComment: reason,
|
|
|
+ approvalNode: '',
|
|
|
+ })
|
|
|
+}
|