import { deleteApi, get, postJ, putJ } from "@/utils/request"; import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js"; import { dateOnly, getMyResignIds, rememberResignId } from "@/utils/hr/storage"; import Vue from "vue"; // ============ 枚举 ============ // M02 后端枚举 → 中文 export const RESIGNATION_TYPE_LABELS = { VOLUNTARY: "主动离职", NEGOTIATED: "协商解除", PROBATION_FAILED: "试用不合格", PERFORMANCE_FAILED: "绩效不合格", TERMINATED: "辞退", DISMISSED: "辞退", CONTRACT_EXPIRED: "合同到期", RETIRE: "退休", RETIREMENT: "退休", OTHER: "其他", }; export const RESIGN_APPROVAL_STATUS_LABELS = { DRAFT: "草稿", PENDING: "审批中", APPROVING: "审批中", APPROVED: "已通过", REJECTED: "已驳回", WITHDRAWN: "已撤回", CANCELLED: "已取消", }; export const HANDOVER_STATUS_LABELS = { NOT_STARTED: "未开始", PENDING: "待交接", IN_PROGRESS: "交接中", PROCESSING: "交接中", COMPLETED: "已完成", FORCE_COMPLETED: "强制办结", }; export const RESIGN_EFFECTIVE_STATUS_LABELS = { PENDING: "待生效", SCHEDULED: "已排期", EFFECTIVE: "已生效", CANCELLED: "已取消", }; export const ARCHIVE_STATUS_LABELS = { NOT_ARCHIVED: "未归档", PENDING: "待归档", ARCHIVED: "已归档", }; export const HANDOVER_CATEGORY_LABELS = { WORK: "工作交接", ASSET: "资产交接", FINANCE: "财务交接", HR: "HR交接", }; /** 交接方式:ONLINE-线上文档、OFFLINE-线下附件(对齐 PC 端 resignation.js) */ export const HANDOVER_TYPE_LABELS = { ONLINE: "线上文档", OFFLINE: "线下附件", }; export const HANDOVER_TYPE_OPTIONS = [ { value: "ONLINE", label: "线上文档", desc: "由服务端创建线上交接文档,无需上传附件" }, { value: "OFFLINE", label: "线下附件", desc: "必须上传交接附件(可多选)" }, ]; /** 交接事项分类选项 */ export const HANDOVER_ITEM_CATEGORY_OPTIONS = [ { value: "WORK", label: "工作交接" }, { value: "ASSET", label: "资产交接" }, { value: "FINANCE", label: "财务交接" }, { value: "HR", label: "HR交接" }, ]; /** 交接事项状态 */ export const HANDOVER_ITEM_STATUS_LABELS = { PENDING: "待交接", PROCESSING: "交接中", IN_PROGRESS: "交接中", COMPLETED: "已完成", DONE: "已完成", }; export const RESIGN_DISPLAY_STATUS = { draft: { label: "草稿", class: "status-pending" }, pending: { label: "审批中", class: "status-pending" }, rejected: { label: "已驳回", class: "status-rejected" }, withdrawn: { label: "已撤回", class: "status-rejected" }, cancelled: { label: "已取消", class: "status-rejected" }, overdue: { label: "已逾期", class: "status-rejected" }, approved: { label: "已通过", class: "status-approved" }, handover: { label: "交接中", class: "status-processing" }, completed: { label: "已完成", class: "status-approved" }, }; export const RESIGN_FLOW_STAGES = [ { key: "submit", label: "申请提交", desc: "员工提交离职申请并填写预计离职日" }, { key: "approval", label: "离职审批", desc: "直属主管、部门负责人与 HRBP 审批" }, { key: "handover", label: "工作交接", desc: "交接人确认工作、资产与文档移交" }, { key: "archive", label: "归档生效", desc: "交接完成后归档,劳动关系解除" }, ]; export const RESIGN_STATUS_FILTERS = [ { value: "all", label: "全部状态" }, { value: "pending", label: "审批中" }, { value: "handover", label: "交接中" }, { value: "completed", label: "已完成" }, { value: "rejected", label: "已驳回" }, { value: "withdrawn", label: "已撤回" }, ]; /** * 创建态默认事项:与 PC 端 HandoverGenerateDialog 的 defaultItems() 完全一致 —— * 按四个交接分类各预置一行,**只自动填充「交接分类」,事项名称 / 说明 / 接收人一律留空**, * 由申请人自行填写(PC 提示「共 4 项;分类与名称必填」)。 */ export const DEFAULT_HANDOVER_ITEMS = HANDOVER_ITEM_CATEGORY_OPTIONS.map((option) => ({ category: option.value, name: "", detail: "", })); // ============ 工具函数 ============ 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 (data && Array.isArray(data.list)) return data.list; if (data && Array.isArray(data.records)) return data.records; if (data && typeof data === "object" && data.id != null) return [data]; return []; } function isDoneItemStatus(status) { return ["COMPLETED", "DONE", "FINISHED"].indexOf(String(status || "").toUpperCase()) >= 0; } function approvalCode(raw = {}) { return String(raw.approvalStatus || raw.statusCode || "").toUpperCase(); } // ============ 状态推导与操作可用性 ============ export function deriveResignationDisplayStatus(raw = {}) { const archive = String(raw.archiveStatus || "").toUpperCase(); const effective = String(raw.effectiveStatus || "").toUpperCase(); const handover = String(raw.handoverStatus || "").toUpperCase(); const approval = String(raw.approvalStatus || "").toUpperCase(); if (approval === "CANCELLED") return "cancelled"; if (archive === "ARCHIVED" || effective === "EFFECTIVE") return "completed"; if (approval === "APPROVED") { if (!handover || ["IN_PROGRESS", "PROCESSING", "PENDING", "NOT_STARTED"].indexOf(handover) >= 0) { return "handover"; } return "approved"; } if (approval === "REJECTED") return "rejected"; if (approval === "PENDING" || approval === "APPROVING") return "pending"; if (approval === "WITHDRAWN") return "withdrawn"; return "draft"; } export function canSubmitResignation(row = {}) { if (!row.id) return false; const code = approvalCode(row); return code === "DRAFT" || code === "REJECTED" || code === ""; } export function canWithdrawResignation(row = {}) { const code = approvalCode(row); return code === "PENDING" || code === "APPROVING" || row.displayStatus === "pending"; } export function canCancelResignation(row = {}) { if (!row.id) return false; if (approvalCode(row) === "CANCELLED") return false; return row.displayStatus !== "completed"; } export function canGenerateHandover(row = {}) { if (!row.id || row.handover) return false; return approvalCode(row) === "APPROVED"; } export function canArchiveResignation(row = {}) { if (!row.id) return false; if (!row.handover || row.handover.status !== "completed") return false; return String(row.archiveStatus || "").toUpperCase() !== "ARCHIVED"; } export function canForceCompleteResignation(row = {}) { if (!row.id) return false; return String(row.archiveStatus || "").toUpperCase() !== "ARCHIVED" && row.displayStatus !== "completed"; } export function matchDisplayStatus(row = {}, filter = "") { if (!filter || filter === "all") return true; if (filter === "rejected") return ["rejected", "cancelled"].indexOf(row.displayStatus) >= 0; if (filter === "withdrawn") return ["withdrawn", "cancelled"].indexOf(row.displayStatus) >= 0; return row.displayStatus === filter; } // ============ VO → 页面形状映射 ============ export function adaptHandoverItem(raw = {}) { const done = isDoneItemStatus(raw.itemStatus || raw.status); const category = raw.itemCategory || raw.category || "WORK"; const statusCode = String(raw.itemStatus || raw.status || "").toUpperCase(); return { id: raw.id, name: raw.itemName || raw.name || "", itemName: raw.itemName || raw.name || "", detail: raw.itemContent || raw.detail || raw.completionRemark || "", itemContent: raw.itemContent || raw.detail || "", category, itemCategory: category, categoryLabel: labelOf(HANDOVER_CATEGORY_LABELS, category), receiverUserId: raw.receiverUserId, receiverName: raw.receiverName || raw.receiverUserName || "", status: raw.itemStatus || raw.status || "", itemStatus: raw.itemStatus || raw.status || "", // 待交接 / 交接中 / 已完成 itemStatusLabel: labelOf(HANDOVER_ITEM_STATUS_LABELS, statusCode, done ? "已完成" : "待交接"), completionRemark: raw.completionRemark || "", done, doneAt: dateOnly(raw.completeTime || raw.doneAt), }; } /** 资产归还清单行(详情 / 编辑态展示用) */ export function adaptAssetReturn(raw = {}) { const statusCode = String(raw.returnStatus || "").toUpperCase(); return { id: raw.id, handoverId: raw.handoverId, itemName: raw.itemName || raw.assetName || raw.returnName || "", assetCode: raw.assetCode || "", returnName: raw.returnName || raw.itemName || raw.assetName || "", returnStatus: statusCode || "PENDING", returnStatusLabel: statusCode === "RETURNED" ? "已归还" : "未归还", returned: statusCode === "RETURNED", returnRemark: raw.returnRemark || "", returnedTime: raw.returnedTime || "", }; } export function adaptHandover(raw = {}) { if (!raw || typeof raw !== "object") return null; const items = ((raw.items || [])).map(adaptHandoverItem); const assetReturns = ((raw.assetReturns || raw.assetReturnList || [])).map(adaptAssetReturn); const doneCount = items.filter((item) => item.done).length; const itemsProgress = items.length ? Math.round((doneCount / items.length) * 100) : 0; const code = String(raw.handoverStatus || "").toUpperCase(); const completed = ["COMPLETED", "FORCE_COMPLETED"].indexOf(code) >= 0 || (items.length > 0 && doneCount === items.length); const progress = completed ? 100 : itemsProgress; const receiverItem = items.find((item) => item.receiverName); // 附件:后端返回逗号分隔字符串 const attachmentIdText = raw.attachmentIds ? String(raw.attachmentIds) : ""; const attachmentIdList = attachmentIdText ? attachmentIdText.split(",").map((text) => text.trim()).filter(Boolean) : []; return { id: raw.id, resignationId: raw.resignationId, resignationNo: raw.resignationNo || "", documentCode: raw.documentCode || "", userId: raw.userId, employee: raw.employeeName || raw.userName || "", employeeNo: raw.employeeNo || "", department: raw.departmentName || raw.deptName || "", deptId: raw.deptId, deptName: raw.deptName || raw.departmentName || "", position: raw.positionName || "", person: raw.handoverPerson || raw.handoverUserName || (receiverItem ? receiverItem.receiverName : "") || "", handoverUserId: raw.handoverUserId, handoverUserName: raw.handoverUserName || "", handoverType: String(raw.handoverType || "").toUpperCase(), handoverTypeLabel: labelOf(HANDOVER_TYPE_LABELS, raw.handoverType), attachmentIds: attachmentIdText, attachmentIdList, handoverDate: dateOnly(raw.handoverDate), deadline: dateOnly(raw.deadline || raw.handoverDate), remark: raw.remark || "", status: completed ? "completed" : progress > 0 ? "processing" : "not_started", statusLabel: completed ? "已完成" : progress > 0 ? "交接中" : "待交接", statusCode: raw.handoverStatus || "", statusCodeLabel: labelOf(HANDOVER_STATUS_LABELS, raw.handoverStatus), approvalStatus: raw.approvalStatus || "", approvalStatusLabel: labelOf(RESIGN_APPROVAL_STATUS_LABELS, raw.approvalStatus), processInstanceId: raw.processInstanceId || raw.approvalInstanceId || "", progress, items, assetReturns, createTime: raw.createTime || "", updated: dateOnly(raw.handoverDate || raw.createTime), }; } /** * 交接单是否可编辑(对齐 PC 端 resignation.js 的 canEditHandover): * 仅「审批草稿(DRAFT / 空)+ 交接未办结」可修改; * 审批中 / 已通过 / 已驳回 / 已撤回 / 已取消,或已完成 / 强制办结均不可改。 */ export function canEditHandover(row = {}) { if (!row || row.id === undefined || row.id === null) return false; const approval = String(row.approvalStatus || "").toUpperCase(); if (approval && approval !== "DRAFT") return false; const code = String(row.statusCode || "").toUpperCase(); return ["COMPLETED", "FORCE_COMPLETED"].indexOf(code) < 0; } function stageProgressOf(stageKey, stages, handover) { const index = stages.findIndex((stage) => stage.key === stageKey); if (index < 0) return 0; if (stageKey === "handover" && handover) { return Math.min(99, 50 + Math.round((handover.progress || 0) / 2)); } return Math.round(((index + 1) / stages.length) * 100); } function progressTipOf(displayStatus, handover) { switch (displayStatus) { case "draft": return "草稿未提交,请提交审批后进入离职流程。"; case "pending": return "离职审批中,请等待审批结果。"; case "rejected": return "审批已驳回,可修改后重新提交。"; case "withdrawn": return "本次离职申请已撤回。"; case "cancelled": return "本次离职申请已取消。"; case "approved": return "审批已通过,待生成离职交接单。"; case "handover": return handover ? `交接进行中,事项已完成 ${handover.progress}%。` : "审批已通过,待生成离职交接单。"; case "completed": return "离职流程已全部完成。"; case "overdue": return "已超过最后工作日,请尽快推进交接与结算。"; default: return ""; } } export function adaptResignation(vo = {}, extras = {}) { if (!vo || typeof vo !== "object") return null; const displayStatus = deriveResignationDisplayStatus(vo); const handover = vo.handover ? adaptHandover(vo.handover) : null; const statusMeta = RESIGN_DISPLAY_STATUS[displayStatus] || RESIGN_DISPLAY_STATUS.pending; const stages = RESIGN_FLOW_STAGES; const stageKey = displayStatus === "completed" ? "archive" : displayStatus === "handover" || displayStatus === "approved" ? "handover" : displayStatus === "pending" ? "approval" : "submit"; const name = extras.name || vo.employeeName || vo.userName || ""; return { id: vo.id, source: "server", resignationNo: vo.resignationNo || "", userId: vo.userId, name, avatar: String(name).charAt(0), employeeNo: extras.employeeNo || vo.employeeNo || "", department: extras.department || vo.departmentName || vo.deptName || "", position: extras.position || vo.positionName || "", entryDate: dateOnly(extras.hireDate || vo.hireDate), expectedLeaveDate: dateOnly(vo.plannedLastWorkDate || vo.effectiveDate), lastWorkDate: dateOnly(vo.effectiveDate), applyDate: dateOnly(vo.applyDate || vo.createTime), effectiveDate: dateOnly(vo.effectiveDate), type: labelOf(RESIGNATION_TYPE_LABELS, vo.resignationType), typeCode: vo.resignationType || "", reason: vo.reason || "", userComment: vo.userComment || "", hrComment: vo.hrComment || "", approvalStatus: vo.approvalStatus || "", approvalStatusLabel: labelOf(RESIGN_APPROVAL_STATUS_LABELS, vo.approvalStatus), handoverStatus: vo.handoverStatus || "", handoverStatusLabel: labelOf(HANDOVER_STATUS_LABELS, vo.handoverStatus), effectiveStatus: vo.effectiveStatus || "", effectiveStatusLabel: labelOf(RESIGN_EFFECTIVE_STATUS_LABELS, vo.effectiveStatus), archiveStatus: vo.archiveStatus || "", archiveStatusLabel: labelOf(ARCHIVE_STATUS_LABELS, vo.archiveStatus), displayStatus, statusLabel: statusMeta.label, statusClass: statusMeta.class, stageKey, stages, progress: displayStatus === "completed" ? 100 : stageProgressOf(stageKey, stages, handover), progressTip: progressTipOf(displayStatus, handover), handover, onlineSign: Boolean(vo.approvalDocumentFileId || vo.onlineDocumentId), createTime: vo.createTime || "", // ===== PC「新建离职申请」表单回填所需的组织 / 岗位快照字段 ===== phone: extras.phone || vo.phone || vo.mobile || "", groupId: pickBig(vo.groupId, vo.legalId, extras.groupId, extras.legalId), company: extras.company || vo.company || vo.groupName || vo.legalName || "", deptId: pickBig(vo.deptId, extras.deptId), deptName: extras.deptName || vo.deptName || vo.departmentName || "", positionId: pickBig(vo.positionId, extras.positionId), positionName: extras.position || vo.positionName || "", positionSequenceId: pickBig(vo.positionSequenceId, extras.positionSequenceId), positionSequence: extras.positionSequence || vo.positionSequence || "", positionLevelId: pickBig(vo.positionLevelId, extras.positionLevelId), positionLevel: extras.positionLevel || vo.positionLevel || "", positionType: extras.positionType || vo.positionType || "", positionTypeName: extras.positionTypeName || vo.positionTypeName || "", employmentPeriodId: pickBig(vo.employmentPeriodId, extras.employmentPeriodId), handlingMode: String(vo.handlingMode || vo.resignHandlingMode || extras.handlingMode || "") .trim() .toUpperCase() === "ONLINE" ? "ONLINE" : normalizeResignHandlingMode(vo), approvalDocumentFileId: firstFileIdOf([ vo.approvalDocumentFileId, vo.onlineDocumentId, extras.approvalDocumentFileId, ]), raw: vo, }; } /** 取第一个非空 ID(兼容大整数,统一转字符串) */ function pickBig() { for (let i = 0; i < arguments.length; i += 1) { const value = arguments[i]; if (value === undefined || value === null || value === "") continue; return String(value); } return ""; } // ============ 员工信息补全 ============ const employeeCache = {}; async function fetchEmployeeBrief(userId) { if (userId === undefined || userId === null || userId === "") return null; const key = String(userId); if (key in employeeCache) return employeeCache[key]; try { const res = await get(Vue.prototype.apiUrl + `/main/user/getById/${key}`, {}, false); const vo = (res && res.data) || {}; const brief = { name: vo.name || vo.userName || "", employeeNo: vo.jobNumber || "", department: vo.deptName || "", position: vo.postName || vo.position || "", hireDate: dateOnly(vo.joinDate || vo.entryDate), }; employeeCache[key] = brief; return brief; } catch (error) { employeeCache[key] = null; return null; } } function extrasOf(brief) { if (!brief) return {}; return { name: brief.name, employeeNo: brief.employeeNo, department: brief.department, position: brief.position, hireDate: brief.hireDate, }; } /** * 离职单详情 * GET /main/resignations/{id} */ export async function getResignationById(id, loding = true) { if (!id) return null; const res = await get(Vue.prototype.apiUrl + `/main/resignations/${id}`, {}, loding); if (Number(res.code) !== 0) return null; return res.data || null; } /** * 离职单详情(归一化后,失败返回 null,不阻断上层) */ export async function loadResignationDetail(id) { if (!id) return null; try { const vo = await getResignationById(id, false); if (!vo) return null; rememberResignId(String(id)); const brief = await fetchEmployeeBrief(vo.userId); return adaptResignation(vo, extrasOf(brief)); } catch (error) { return null; } } /** * 离职单列表(归一化后) * POST /main/resignations/page */ export async function loadResignationList(params = {}, loding = false) { const { keyword = "", status = "all", pageNum = 1, size = 50 } = params; let rows = []; try { const res = await postJ( Vue.prototype.apiUrl + "/main/resignations/page", { pageNum, size, resignationNo: keyword || undefined }, loding ); rows = res && Number(res.code) === 0 ? asList(res.data) : []; } catch (error) { rows = []; } if (!rows.length) { const details = await Promise.all( getMyResignIds().map(async (id) => { try { return await getResignationById(id, false); } catch (error) { return null; } }) ); rows = details.filter(Boolean); } const list = []; for (const vo of rows) { if (!vo || vo.id == null) continue; const brief = await fetchEmployeeBrief(vo.userId); const row = adaptResignation(vo, extrasOf(brief)); if (!row) continue; rememberResignId(String(row.id)); if (!matchDisplayStatus(row, status)) continue; if (keyword && !`${row.name}${row.employeeNo}${row.resignationNo}${row.department}`.includes(keyword)) continue; list.push(row); } return { list, count: list.length, source: "api" }; } /** 交接单列表 POST /main/handovers/page */ export async function loadHandoverList(params = {}) { const { keyword = "", status = "all" } = params; const page = await getHandoverPage({ keyword }, false); const list = page.list .map((item) => adaptHandover(item)) .filter(Boolean) .filter((item) => (status === "all" ? true : item.status === status)) .filter((item) => (keyword ? String(item.resignationNo || "").includes(keyword) : true)); return { list, count: page.count || list.length, source: "api" }; } export async function loadHandoverDetail(id) { if (!id) return null; try { const vo = await getHandoverDetail(id, false); return adaptHandover(vo); } catch (error) { return null; } } /** * 查询「我的离职交接单」。 * * 员工自助场景下一个人只有一张交接单,所以这里按登录人过滤后取最新一条: * POST /main/handovers/page(userId 由当前登录用户传入,服务端仍会做归属校验) * @returns {Promise} adaptHandover 适配后的交接单,未创建过返回 null */ export async function loadMyHandover(userId, loding = false) { const params = { pageNum: 1, size: 20, orderBy: "desc", sortName: "createTime" }; if (userId !== undefined && userId !== null && userId !== "") params.userId = userId; try { const res = await postJ(Vue.prototype.apiUrl + "/main/handovers/page", params, loding); if (!res || Number(res.code) !== 0) return null; const rows = asList(res.data).map((item) => adaptHandover(item)).filter(Boolean); if (!rows.length) return null; // 列表 VO 不含事项明细,拿详情补齐(失败时退回列表行) const detail = await loadHandoverDetail(rows[0].id); return detail || rows[0]; } catch (error) { return null; } } /* ---------------- 交接单发起 / 编辑 ---------------- */ function compactHandover(object = {}) { return Object.keys(object).reduce((acc, key) => { const value = object[key]; if (value !== "" && value !== null && value !== undefined) acc[key] = value; return acc; }, {}); } /** * 组装新建 / 修改离职交接单参数,严格对齐后端 PO(与 PC 端 serializeHandover 一致): * - handoverType 必填;ONLINE 不得传 attachmentIds,OFFLINE 必须传(英文逗号分隔的文件 id) * - items 必填:创建时每项不得带 id,修改时回传已有事项 id(未回传的事项会被删除) * - assetReturns(入职领用物品归还清单)移动端暂不提交 */ export function buildHandoverPayload(form = {}) { const handoverType = form.handoverType ? String(form.handoverType).toUpperCase() : "ONLINE"; const isOffline = handoverType === "OFFLINE"; let attachmentIds = form.attachmentIds; if (Array.isArray(attachmentIds)) attachmentIds = attachmentIds.filter(Boolean).join(","); attachmentIds = attachmentIds ? String(attachmentIds) : undefined; if (!isOffline) attachmentIds = undefined; const items = (Array.isArray(form.items) ? form.items : []) .filter((item) => item && String(item.itemName || "").trim()) .map((item) => compactHandover({ id: item.id, itemCategory: item.itemCategory || item.category || "WORK", itemName: String(item.itemName || item.name).trim(), itemContent: item.itemContent ? String(item.itemContent).trim() : undefined, receiverUserId: item.receiverUserId, }) ); // 资产归还清单:创建时按「归还名称 + 状态」新增(不得带主键); // 编辑时仅回传已进入清单(有主键)的行,修改接口不接受新增明细(与 PC 一致) const assetReturns = (Array.isArray(form.assetReturns) ? form.assetReturns : []) .map((row) => { const name = String(row.returnName || row.itemName || "").trim(); if (!name) return null; const status = String(row.returnStatus || "PENDING").toUpperCase() === "RETURNED" ? "RETURNED" : "PENDING"; return compactHandover({ id: row.id, returnName: name, returnStatus: status, returnRemark: row.returnRemark ? String(row.returnRemark).trim() : undefined, }); }) .filter(Boolean); return compactHandover({ handoverType, attachmentIds, handoverDate: dateOnly(form.handoverDate) || undefined, handoverUserId: form.handoverUserId, items: items.length ? items : undefined, assetReturns: assetReturns.length ? assetReturns : undefined, remark: form.remark ? String(form.remark).trim() : undefined, resignationId: form.resignationId, }); } /** 创建离职交接单 POST /main/handovers(data 为新建交接单主键) */ export async function createHandover(form, loding = true) { const payload = buildHandoverPayload(form); if (!payload.resignationId) return Promise.reject(new Error("缺少关联的离职申请")); if (!payload.handoverDate) return Promise.reject(new Error("请选择交接日期")); if (!payload.handoverUserId) return Promise.reject(new Error("请选择交接人")); if (payload.handoverType === "OFFLINE" && !payload.attachmentIds) { return Promise.reject(new Error("线下交接必须上传交接附件")); } if (!payload.items || !payload.items.length) { return Promise.reject(new Error("请至少填写一条交接事项")); } const res = await postJ(Vue.prototype.apiUrl + "/main/handovers", payload, loding); if (res && res.code == 0) return res.data; return Promise.reject(new Error((res && res.message) || "交接单创建失败")); } /** 修改离职交接单 PUT /main/handovers/{id}(items 采用全量回传,未回传的事项会被删除) */ export async function updateHandover(id, form, loding = true) { if (!id) return Promise.reject(new Error("缺少交接单主键")); const payload = buildHandoverPayload(form); if (!payload.handoverDate) return Promise.reject(new Error("请选择交接日期")); if (!payload.handoverUserId) return Promise.reject(new Error("请选择交接人")); if (payload.handoverType === "OFFLINE" && !payload.attachmentIds) { return Promise.reject(new Error("线下交接必须上传交接附件")); } if (!payload.items || !payload.items.length) { return Promise.reject(new Error("请至少填写一条交接事项")); } const res = await putJ(Vue.prototype.apiUrl + `/main/handovers/${id}`, payload, loding); if (res && res.code == 0) return true; return Promise.reject(new Error((res && res.message) || "交接单保存失败")); } // —— 流程动作 —— export async function actSubmitResignation(row) { if (!row || !row.id) throw new Error("缺少离职单主键"); return submitResignation(row.id); } export async function actWithdrawResignation(row, reason = "") { if (!row || !row.id) throw new Error("缺少离职单主键"); return withdrawResignation(row.id, reason); } export async function actCancelResignation(row, reason = "") { if (!row || !row.id) throw new Error("缺少离职单主键"); return cancelResignation(row.id, reason); } export async function actGenerateHandover(row) { if (!row || !row.id) throw new Error("缺少离职单主键"); return generateResignationHandover(row.id); } export async function actCompleteHandoverItem(resignationId, itemId, remark = "") { return completeResignationHandoverItem(resignationId, itemId, remark); } export async function actArchiveResignation(row) { if (!row || !row.id) throw new Error("缺少离职单主键"); return archiveResignation(row.id); } export async function actForceCompleteResignation(row, reason) { if (!row || !row.id) throw new Error("缺少离职单主键"); return forceCompleteResignation(row.id, reason); } /** 提交离职单 POST /main/resignations/{id}/submit */ export async function submitResignation(id, loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/resignations/${id}/submit`, {}, loding); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 撤回离职单 POST /main/resignations/{id}/withdraw */ export async function withdrawResignation(id, reason = "", loding = true) { const res = await postJ( Vue.prototype.apiUrl + `/main/resignations/${id}/withdraw`, { reason }, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 撤销离职单 POST /main/resignations/cancel/{id} */ export async function cancelResignation(id, reason = "", loding = true) { const res = await postJ( Vue.prototype.apiUrl + `/main/resignations/cancel/${id}`, { reason }, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 归档离职单 POST /main/resignations/{id}/archive */ export async function archiveResignation(id, loding = true) { const res = await postJ(Vue.prototype.apiUrl + `/main/resignations/${id}/archive`, {}, loding); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 强制完成离职 POST /main/resignations/{id}/force-complete */ export async function forceCompleteResignation(id, reason = "", loding = true) { const res = await postJ( Vue.prototype.apiUrl + `/main/resignations/${id}/force-complete`, { reason }, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 生成交接单 POST /main/resignations/{id}/generate-handover */ export async function generateResignationHandover(id, loding = true) { const res = await postJ( Vue.prototype.apiUrl + `/main/resignations/${id}/generate-handover`, {}, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 完成交接项 PUT /main/resignations/{resignationId}/handover/items/{itemId}/complete */ export async function completeResignationHandoverItem(resignationId, itemId, loding = true) { const res = await putJ( Vue.prototype.apiUrl + `/main/resignations/${resignationId}/handover/items/${itemId}/complete`, {}, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 回退交接项 PUT /main/handovers/{handoverId}/items/{itemId}/rollback */ export async function rollbackResignationHandoverItem(handoverId, itemId, reason = "", loding = true) { if (!handoverId || !itemId) throw new Error("缺少交接单或事项主键"); const res = await putJ( Vue.prototype.apiUrl + `/main/handovers/${handoverId}/items/${itemId}/rollback`, { rollbackReason: reason }, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 回退交接项(流程动作封装,handoverId 取自离职详情 handover.id) */ export async function actRollbackHandoverItem(handoverId, itemId, reason = "") { return rollbackResignationHandoverItem(handoverId, itemId, reason); } /** 删除交接单 DELETE /main/handovers/{id} */ export async function deleteHandover(id, loding = true) { const res = await deleteApi(Vue.prototype.apiUrl + `/main/handovers/${id}`, {}, loding); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 删除交接单(流程动作封装) */ export async function actDeleteHandover(id) { if (!id) throw new Error("缺少交接单主键"); return deleteHandover(id); } /** 交接单分页 POST /main/handovers/page */ export async function getHandoverPage(params = {}, loding = true) { const res = await postJ(Vue.prototype.apiUrl + "/main/handovers/page", params, loding); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } /** 交接单详情 GET /main/handovers/{id} */ export async function getHandoverDetail(id, loding = true) { const res = await get(Vue.prototype.apiUrl + `/main/handovers/${id}`, {}, loding); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message)); } // ============ 员工端离职申请(对齐 PC 端 /main/resignations)============ /** 离职申请 BPM 流程标识(与 PC 端 RESIGNATION_APPLY_BUSINESS_KEY 一致) */ export const RESIGNATION_APPROVAL_BUSINESS_KEY = "main_resignation_approval"; /** 离职交接单 BPM 流程标识(与 PC 端 HANDOVER_APPROVAL_BUSINESS_KEY 一致) */ export const HANDOVER_APPROVAL_BUSINESS_KEY = "main_handover_approval"; /** * 办理方式:ONLINE 线上模板 / OFFLINE 线下附件。 * 移动端页面不再提供切换,统一按 OFFLINE(线下附件)提交;此表仅用于回显旧数据。 */ export const RESIGN_HANDLING_MODE_LABELS = { ONLINE: "线上模板", OFFLINE: "线下附件", }; /** 离职申请表单 → 后端 resignationType 枚举(表单直接存码,此处仅做兜底映射) */ export const RESIGN_TYPE_CODE_LABELS = { VOLUNTARY: "主动离职", NEGOTIATED: "协商解除", PROBATION_FAILED: "试用不合格", PERFORMANCE_FAILED: "绩效不合格", TERMINATED: "辞退", DISMISSED: "辞退", CONTRACT_EXPIRED: "合同到期", RETIRE: "退休", RETIREMENT: "退休", OTHER: "其他", }; /** 需要展示在下拉里的离职类型(对齐 PC 端 RESIGNATION_TYPE_OPTIONS,去掉重复的辞退/退休别名) */ export const RESIGN_TYPE_OPTIONS = Object.keys(RESIGNATION_TYPE_LABELS) .filter((code) => code !== "TERMINATED" && code !== "RETIRE") .map((code) => ({ value: code, label: RESIGNATION_TYPE_LABELS[code] })); function ok(res) { return res && Number(res.code) === 0; } 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 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 firstFileIdOf(value) { if (value == null || value === "") return undefined; if (Array.isArray(value)) return firstFileIdOf(value.find((item) => item !== undefined && item !== null && item !== "")); if (typeof value === "object") return value.id != null ? value.id : value.fileId; return value; } function todayIso() { const d = new Date(); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } function extractId(result) { if (result == null) return null; if (typeof result === "object") { return result.id != null ? result.id : result.resignationId != null ? result.resignationId : null; } return result; } /** 办理方式规范化:老数据无该字段时按线上单据字段推断,默认线下 */ export function normalizeResignHandlingMode(raw = {}) { const mode = String(raw.handlingMode || raw.handlingType || "").trim().toUpperCase(); if (mode === "ONLINE" || mode === "OFFLINE") return mode; return raw.onlineDocumentTemplateId != null && raw.onlineDocumentTemplateId !== "" ? "ONLINE" : raw.onlineDocumentId ? "ONLINE" : "OFFLINE"; } /** * 组装新建 / 修改离职单参数,严格对齐 PC 端 buildResignationPayload(POST /main/resignations PO): * - 机构 / 部门 / 岗位序列 / 岗位名称 / 岗位层级 主键 + 名称快照 * - 办理方式:ONLINE 传模板主键;OFFLINE 传附件主键(取表单文件数组首个) */ export function buildResignationPayload(form = {}) { const source = form || {}; const handlingMode = normalizeResignHandlingMode(source); const online = handlingMode === "ONLINE"; return compact({ id: keepBigIntId(source.resignationId || source.id), userId: keepBigIntId(source.userId || source.employeeId), employmentPeriodId: keepBigIntId(source.employmentPeriodId), // 机构 / 部门 / 岗位主键 groupId: keepBigIntId(source.groupId), deptId: keepBigIntId(source.deptId), positionId: keepBigIntId(source.positionId), positionName: source.position || source.positionName || undefined, positionSequenceId: keepBigIntId(source.positionSequenceId), positionSequence: source.positionSequence || undefined, positionLevelId: keepBigIntId(source.positionLevelId), positionLevel: source.positionLevel || undefined, positionType: source.positionType || undefined, positionTypeName: source.positionTypeName || undefined, phone: source.phone || undefined, applyDate: dateOnly(source.applyDate) || todayIso(), plannedLastWorkDate: dateOnly(source.plannedLastWorkDate || source.expectedLeaveDate), effectiveDate: dateOnly(source.effectiveDate), reason: source.reason, resignationType: source.resignationType || "VOLUNTARY", userComment: source.userComment || undefined, hrComment: source.hrComment || undefined, // 办理方式:ONLINE 线上模板 / OFFLINE 线下附件 handlingMode, // 线下附件:接口字段为单个 fileId,取表单文件数组首个 approvalDocumentFileId: online ? undefined : keepBigIntId(source.approvalDocumentFileId || firstFileIdOf(source.attachmentIds)), // 线上模板:仅线上办理时传模板主键 onlineDocumentTemplateId: online ? keepBigIntId(source.onlineDocumentTemplateId) : undefined, onlineDocumentContent: source.onlineDocumentContent, onlineDocumentVariableValuesJson: source.onlineDocumentVariableValuesJson, }); } /** 新建离职草稿 POST /main/resignations(返回离职单主键) */ export async function createResignation(payload, loding = true) { const res = await postJ(Vue.prototype.apiUrl + "/main/resignations", payload || {}, loding); if (!ok(res)) throw new Error(res.message || "离职申请创建失败"); const id = extractId(res.data); if (id != null) rememberResignId(String(id)); return id; } /** 修改离职草稿 PUT /main/resignations */ export async function updateResignation(payload, loding = true) { const id = payload && (payload.id || payload.resignationId); if (id === undefined || id === null || id === "") throw new Error("缺少离职单主键"); const res = await putJ(Vue.prototype.apiUrl + "/main/resignations", payload || {}, loding); if (!ok(res)) throw new Error(res.message || "离职申请修改失败"); rememberResignId(String(id)); return extractId(res.data) != null ? extractId(res.data) : id; } /** * 保存离职单(新建 / 修改统一入口)。 * @returns {Promise} 离职单主键 */ export async function saveResignationDraft(form, loding = true) { const payload = buildResignationPayload(form); if (!payload.userId) throw new Error("请选择离职人员"); if (!payload.resignationType) throw new Error("请选择离职类型"); if (!payload.plannedLastWorkDate) throw new Error("请选择计划最后工作日"); if (!payload.reason) throw new Error("请填写离职原因"); if (payload.handlingMode === "OFFLINE" && !payload.approvalDocumentFileId) { throw new Error("请上传离职申请附件"); } if (payload.id != null && payload.id !== "") { await updateResignation(payload, loding); return payload.id; } return createResignation(payload, loding); } /** 审批流程 variables(与 PC 端 processSubmitDialog 的入参形状一致) */ export function buildResignationVariables(form = {}, resignationId = null) { const row = form || {}; const id = resignationId != null && resignationId !== "" ? resignationId : row.resignationId || row.id; return compact({ businessCode: row.resignationNo || row.businessCode || (id != null ? String(id) : ""), businessName: row.name || row.employeeName || row.userName || "", businessType: "离职申请", userId: row.userId != null ? row.userId : row.employeeId, groupId: row.groupId || "", groupName: row.company || "", deptId: row.deptId || "", departmentName: row.department || "", positionId: row.positionId || "", positionName: row.position || "", adjustHeadcount: true, }); } /** * 发起离职申请审批流程。 * businessKey 固定 main_resignation_approval,businessId 为离职单主键。 */ export async function startResignationApproval(businessId, variables = {}, loding = false) { if (businessId == null || businessId === "") throw new Error("缺少离职单主键,无法发起审批"); const page = await processInstancePage( { pageNo: 1, pageSize: 1, reset: true, key: RESIGNATION_APPROVAL_BUSINESS_KEY }, loding ); const first = page && page.list && page.list[0]; const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : ""; if (!processDefinitionId) throw new Error("未匹配到离职审批流程,请联系管理员配置流程定义"); return processInstanceCreateAPI( { businessId, businessKey: RESIGNATION_APPROVAL_BUSINESS_KEY, formCreateUserId: variables.userId || "", processDefinitionId, variables, }, false ); } /** * 提交离职申请:创建(或更新)离职单后直接发起审批。 * 审批发起失败不阻断主流程(单据已落库,避免重复创建)。 * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>} */ export async function submitResignationFlow({ form, existingId = null, loding = true } = {}) { if (!form) throw new Error("申请参数为空"); const id0 = existingId != null && existingId !== "" ? existingId : form.resignationId || form.id || null; const payload = buildResignationPayload(Object.assign({}, form, { id: id0 })); let resignationId = id0; if (id0 != null && id0 !== "") { await updateResignation(Object.assign({}, payload, { id: resignationId }), loding); } else { resignationId = await createResignation(payload, loding); if (resignationId == null) throw new Error("离职申请已提交但未返回主键,请联系管理员"); } const variables = buildResignationVariables(Object.assign({}, form, { id: resignationId }), resignationId); let approvalStarted = false; let approvalError = ""; try { await startResignationApproval(resignationId, variables); approvalStarted = true; } catch (error) { approvalError = (error && error.message) || "审批流程发起失败"; } return { id: resignationId, approvalStarted, approvalError }; } /** * 离职单 → 离职申请表单回填(只读详情 / 编辑态共用) * @param {object} raw GET /main/resignations/{id} 的原始 VO * @param {object} extras 列表行 / 员工档案补出的姓名、工号等展示字段 */ export function hydrateResignationForm(raw = {}, extras = {}) { const vo = raw || {}; const ext = extras || {}; const handlingMode = normalizeResignHandlingMode(vo); const attachmentId = vo.approvalDocumentFileId; return { resignationId: vo.id != null ? vo.id : null, userId: vo.userId != null ? String(vo.userId) : ext.userId != null ? String(ext.userId) : "", name: ext.name || ext.employeeName || vo.employeeName || vo.userName || "", employeeNo: ext.employeeNo || vo.employeeNo || "", phone: vo.phone || ext.phone || "", hireDate: dateOnly(vo.hireDate || vo.joinDate || ext.hireDate), // —— 员工信息(级联字段)—— groupId: vo.groupId != null ? String(vo.groupId) : "", company: vo.groupName || vo.companyName || vo.legalEntityName || ext.company || "", deptId: vo.deptId != null ? String(vo.deptId) : "", department: vo.departmentName || vo.deptName || ext.department || "", positionId: vo.positionId != null ? String(vo.positionId) : "", position: vo.positionName || ext.position || "", positionSequenceId: vo.positionSequenceId != null ? String(vo.positionSequenceId) : "", positionSequence: vo.positionSequence || vo.positionSequenceName || "", positionLevelId: vo.positionLevelId != null ? String(vo.positionLevelId) : "", positionLevel: vo.positionLevel || "", positionType: vo.positionType != null ? String(vo.positionType) : "", positionTypeName: vo.positionTypeName || "", employmentPeriodId: vo.employmentPeriodId != null ? vo.employmentPeriodId : null, // —— 离职信息 —— resignationType: String(vo.resignationType || "VOLUNTARY").toUpperCase(), applyDate: dateOnly(vo.applyDate) || dateOnly(vo.createTime) || todayIso(), plannedLastWorkDate: dateOnly(vo.plannedLastWorkDate) || dateOnly(vo.leaveDate), effectiveDate: dateOnly(vo.effectiveDate), handlingMode, onlineDocumentTemplateId: vo.onlineDocumentTemplateId != null ? vo.onlineDocumentTemplateId : null, // 附件主键可能是逗号拼接的多附件字符串 attachmentIds: attachmentId != null && attachmentId !== "" ? String(attachmentId) .split(",") .map((id) => id.trim()) .filter(Boolean) : [], reason: vo.reason || "", userComment: vo.userComment || "", hrComment: vo.hrComment || "", // —— 状态(只读详情展示用)—— resignationNo: vo.resignationNo || "", approvalStatus: vo.approvalStatus || "", createTime: vo.createTime || "", }; } /* ---------------- 离职交接单:物品归还清单 / 审批 ---------------- */ /** 领用单状态 */ export const RECEIPT_STATUS_LABELS = { DRAFT: "草稿", APPROVING: "审批中", COMPLETED: "已完成", }; /** 入职领用明细 → 交接单可编辑行(默认全部按「已归还」预选) */ export function decorateReceiptItems(receipt) { const details = Array.isArray(receipt && receipt.details) ? receipt.details : []; return details.map((item) => ({ id: item.id, itemName: item.itemName || "", itemId: item.itemId != null ? String(item.itemId) : "", itemTypeText: item.itemTypeText || item.itemType || "", model: item.model || "", specification: item.specification || "", quantity: item.quantity != null ? item.quantity : "", // 逐行标记的归还状态会随交接单一并提交为物品归还清单 returnStatus: "RETURNED", returnRemark: item.remark || "", substanceId: item.substanceId, })); } /** * 按员工取最新入职物品领用单 GET /main/onboarding-item-receipts/latest/{employeeId} * 后端可能返回 null / {}(该员工无领用单),此时视为「无记录」。 */ export async function getLatestItemReceipt(employeeId, loding = false) { if (employeeId === undefined || employeeId === null || employeeId === "") return null; try { const res = await get( Vue.prototype.apiUrl + `/main/onboarding-item-receipts/latest/${encodeURIComponent(String(employeeId))}`, {}, loding ); if (!ok(res)) return null; const data = res.data; if (!data || typeof data !== "object") return null; const hasId = data.id != null && data.id !== ""; const details = Array.isArray(data.details) ? data.details : []; if (!hasId && !details.length) return null; return { id: data.id != null ? data.id : "", documentCode: data.documentCode || "", receiptStatus: String(data.receiptStatus || "DRAFT").toUpperCase(), employeeId: data.employeeId, details: details.map((item) => ({ id: item.id, itemName: item.itemName || "", itemId: item.itemId != null ? String(item.itemId) : "", itemTypeText: item.itemType || item.categoryName || "", model: item.itemModel || item.model || "", specification: item.specifications || item.specification || "", quantity: item.quantity != null ? Number(item.quantity) : 1, remark: item.remark || "", substanceId: item.substanceId != null && item.substanceId !== "" ? item.substanceId : undefined, })), }; } catch (error) { return null; } } /** * 可发起交接单的离职申请:审批通过 + 未归档 + 尚未生成交接单 * POST /main/resignations/page */ export async function loadApprovedResignations(params = {}, loding = false) { const res = await postJ( Vue.prototype.apiUrl + "/main/resignations/page", compact({ pageNum: params.pageNum || 1, size: params.size || 100, approvalStatus: "APPROVED", archiveStatus: "NOT_ARCHIVED", }), loding ); if (!ok(res)) return []; const seen = new Set(); const rows = []; // /main/resignations/page 的 VO 常不带 employeeName / departmentName / 计划最后工作日, // 直接 adapt 会得到「离职单号 · · 」这种空白选项(PC 端用 withEmployeeInfo 补全,这里对齐) for (const vo of asList(res.data)) { const brief = await fetchEmployeeBrief(vo && vo.userId); const row = adaptResignation(vo, extrasOf(brief)); if (row) rows.push(row); } return rows .filter((row) => { const code = String(row.handoverStatus || "").toUpperCase(); const created = row.handover && row.handover.id ? true : Boolean(code) && code !== "NOT_CREATED"; return !created; }) .filter((row) => { const key = String(row.resignationNo || row.id || ""); if (!key) return true; if (seen.has(key)) return false; seen.add(key); return true; }); } /** 交接单审批流程 variables(对齐 PC 端 launchApproval 入参) */ export function buildHandoverVariables(form = {}, handoverId = null) { const row = form || {}; const id = handoverId != null && handoverId !== "" ? handoverId : row.handoverId || row.id; return compact({ businessCode: row.documentCode || row.resignationNo || (id != null ? String(id) : ""), businessName: row.person || row.handoverUserName || row.employee || "", businessType: "离职交接单", userId: row.userId, groupId: row.groupId || row.deptId || "", groupName: row.deptName || row.department || "", }); } /** 发起离职交接单审批流程(businessKey 固定 main_handover_approval) */ export async function startHandoverApproval(businessId, variables = {}, loding = false) { if (businessId == null || businessId === "") throw new Error("缺少交接单主键,无法发起审批"); const page = await processInstancePage( { pageNo: 1, pageSize: 1, reset: true, key: HANDOVER_APPROVAL_BUSINESS_KEY }, loding ); const first = page && page.list && page.list[0]; const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : ""; if (!processDefinitionId) throw new Error("未匹配到离职交接单审批流程,请联系管理员配置流程定义"); return processInstanceCreateAPI( { businessId, businessKey: HANDOVER_APPROVAL_BUSINESS_KEY, formCreateUserId: variables.userId || "", processDefinitionId, variables, }, false ); } /** * 提交离职交接单:创建(或更新)交接单后直接发起审批。 * @returns {Promise<{id:number|string, approvalStarted:boolean, approvalError:string}>} */ export async function submitHandoverFlow({ form, existingId = null } = {}) { if (!form) throw new Error("交接单参数为空"); const id0 = existingId != null && existingId !== "" ? existingId : form.handoverId || null; let handoverId = id0; if (id0 != null && id0 !== "") { await updateHandover(id0, form, true); } else { handoverId = await createHandover(form, true); if (handoverId == null) throw new Error("交接单已创建但未返回主键,请联系管理员"); } const variables = buildHandoverVariables( Object.assign({}, form, { handoverId, documentCode: form.documentCode, resignationNo: form.resignationNo }), handoverId ); let approvalStarted = false; let approvalError = ""; try { await startHandoverApproval(handoverId, variables); approvalStarted = true; } catch (error) { approvalError = (error && error.message) || "审批流程发起失败"; } return { id: handoverId, approvalStarted, approvalError }; }