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} */ 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} */ 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} */ 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 }