import dayjs from "dayjs"; /** 费用报销大类 */ export const EXPENSE_CATEGORY_OPTIONS = [ { label: "差旅", value: "travel" }, { label: "办公采购", value: "office_procurement" }, { label: "业务招待", value: "business_entertainment" }, { label: "交通费", value: "transport" }, { label: "通讯费", value: "communication" }, { label: "其他", value: "other" }, ]; /** 明细费用科目 */ export const EXPENSE_SUBJECT_OPTIONS = [ { label: "交通费", value: "transport" }, { label: "住宿费", value: "hotel" }, { label: "餐饮费", value: "meal" }, { label: "办公用品", value: "office_supply" }, { label: "招待费", value: "entertainment" }, { label: "通讯费", value: "phone" }, { label: "其他", value: "other" }, ]; /** 分类填报模板(一键调用) */ export const CATEGORY_TEMPLATES = { travel: { label: "差旅费用", reason: "因公出差产生的交通、住宿、餐饮等费用报销。", details: [ { expenseSubject: "transport", description: "往返交通费" }, { expenseSubject: "hotel", description: "住宿费" }, { expenseSubject: "meal", description: "出差餐饮" }, ], }, office_procurement: { label: "办公采购", reason: "部门日常办公用品、耗材采购报销。", details: [ { expenseSubject: "office_supply", description: "办公用品采购" }, { expenseSubject: "office_supply", description: "打印耗材" }, ], }, business_entertainment: { label: "业务招待", reason: "客户接待、商务宴请等费用报销。", details: [ { expenseSubject: "entertainment", description: "客户接待餐费" }, { expenseSubject: "entertainment", description: "商务礼品" }, ], }, transport: { label: "交通费", reason: "市内通勤、打车、停车等交通费用报销。", details: [{ expenseSubject: "transport", description: "市内交通" }], }, communication: { label: "通讯费", reason: "因公通讯、流量、话费补贴报销。", details: [{ expenseSubject: "phone", description: "话费/流量" }], }, other: { label: "其他费用", reason: "其他因公支出费用报销。", details: [{ expenseSubject: "other", description: "其他费用" }], }, }; /** 审批角色与模拟审批人 */ export const MOCK_APPROVERS_BY_ROLE = { direct_supervisor: { approverId: "mock_supervisor", approverName: "直属上级" }, dept_manager: { approverId: "mock_manager", approverName: "部门经理" }, cfo: { approverId: "mock_cfo", approverName: "财务总监" }, compliance: { approverId: "mock_compliance", approverName: "合规审核" }, }; /** 按金额预设审批链 */ export const APPROVAL_AMOUNT_RULES = [ { maxAmount: 500, description: "500元以内:直属上级审批", roles: ["direct_supervisor"], }, { maxAmount: 5000, description: "500~5000元:直属上级 + 部门经理", roles: ["direct_supervisor", "dept_manager"], }, { maxAmount: Infinity, description: "超5000元:直属上级 + 部门经理 + 财务总监复核", roles: ["direct_supervisor", "dept_manager", "cfo"], }, ]; /** 部分品类额外审批节点 */ export const CATEGORY_EXTRA_APPROVAL = { business_entertainment: ["compliance"], office_procurement: [], }; export function expenseCategoryLabel(v) { return EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === v)?.label || "—"; } export function expenseSubjectLabel(v) { return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "—"; } export function statusLabel(v) { if (v === "approved") return "已通过"; if (v === "rejected") return "已驳回"; return "审核中"; } export function statusTagType(v) { if (v === "approved") return "success"; if (v === "rejected") return "danger"; return "warning"; } export function formatApprovalFlowSummary(row) { const nodes = row?.approvalFlowNodes || []; if (!nodes.length) return "—"; return nodes .map((n, i) => { const name = (n.approverName || "").trim() || `节点${i + 1}`; if (n.nodeStatus === "finish") return `${name}✓`; if (n.nodeStatus === "error") return `${name}✗`; if (n.nodeStatus === "process") return `${name}…`; return name; }) .join(" → "); } export function resolveApprovalRoles(amount, expenseCategory) { const amt = Number(amount) || 0; let roles = []; for (const rule of APPROVAL_AMOUNT_RULES) { if (amt <= rule.maxAmount) { roles = [...rule.roles]; break; } } if (!roles.length) roles = ["direct_supervisor"]; const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || []; extra.forEach((r) => { if (!roles.includes(r)) roles.push(r); }); return roles; } export function buildAutoApprovalFlow(amount, expenseCategory) { const roles = resolveApprovalRoles(amount, expenseCategory); return roles.map((role, i) => { const mock = MOCK_APPROVERS_BY_ROLE[role] || { approverId: `mock_${role}`, approverName: role }; return { approverId: mock.approverId, approverName: mock.approverName, roleKey: role, sortOrder: i + 1, nodeOrder: i + 1, nodeStatus: i === 0 ? "process" : "wait", approveOpinion: "", approveTime: "", }; }); } export function getApprovalRuleHint(amount, expenseCategory) { const amt = Number(amount) || 0; const rule = APPROVAL_AMOUNT_RULES.find((r) => amt <= r.maxAmount) || APPROVAL_AMOUNT_RULES[APPROVAL_AMOUNT_RULES.length - 1]; const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || []; const extraText = extra.length ? `;${expenseCategoryLabel(expenseCategory)}类另需:${extra.map((r) => MOCK_APPROVERS_BY_ROLE[r]?.approverName || r).join("、")}` : ""; return `${rule.description}${extraText}`; } export function createEmptyExpenseDetail() { return { id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, invoiceDate: "", expenseSubject: "", amount: undefined, description: "", }; } export function createEmptyForm() { return { id: undefined, reimburseNo: "", applicantId: "", employeeNo: "", employeeName: "", expenseCategory: "", reimburseReason: "", applyAmount: undefined, payee: "", payeeAccount: "", bankBranch: "", expenseDetails: [], attachmentList: [], approvalFlowNodes: [], currentNodeIndex: 0, approvalResult: "pending", rejectReason: "", deptId: "", deptName: "", }; } export function applyCategoryTemplate(form, category) { const tpl = CATEGORY_TEMPLATES[category]; if (!tpl) return; form.expenseCategory = category; if (!form.reimburseReason?.trim()) form.reimburseReason = tpl.reason; form.expenseDetails = (tpl.details || []).map((d) => ({ ...createEmptyExpenseDetail(), expenseSubject: d.expenseSubject, description: d.description, invoiceDate: dayjs().format("YYYY-MM-DD"), })); } export function initApprovalFlowNodes(nodes) { return (nodes || []).map((n, i) => ({ ...n, sortOrder: i + 1, nodeOrder: i + 1, nodeStatus: i === 0 ? "process" : "wait", approveOpinion: n.approveOpinion || "", approveTime: n.approveTime || "", })); } export function advanceApprovalFlow(row, opinion) { const nodes = [...(row.approvalFlowNodes || [])]; const idx = row.currentNodeIndex ?? 0; if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult }; const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); nodes[idx] = { ...nodes[idx], nodeStatus: "finish", approveOpinion: opinion || "同意", approveTime: now, }; const next = idx + 1; if (next >= nodes.length) { return { nodes, currentNodeIndex: idx, approvalResult: "approved", rejectReason: "" }; } nodes[next] = { ...nodes[next], nodeStatus: "process" }; return { nodes, currentNodeIndex: next, approvalResult: "pending", rejectReason: "" }; } export function rejectApprovalFlow(row, opinion) { const nodes = [...(row.approvalFlowNodes || [])]; const idx = row.currentNodeIndex ?? 0; const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); const reason = (opinion || "").trim() || "驳回"; if (nodes[idx]) { nodes[idx] = { ...nodes[idx], nodeStatus: "error", approveOpinion: reason, approveTime: now, }; } return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: reason }; } export function normalizeImportedRow(raw, idx) { const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`; const expenseDetails = Array.isArray(raw.expenseDetails) ? raw.expenseDetails : []; const applyAmount = raw.applyAmount ?? expenseDetails.reduce((s, d) => s + (Number(d.amount) || 0), 0); const expenseCategory = raw.expenseCategory || "other"; const approvalFlowNodes = Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length ? raw.approvalFlowNodes : buildAutoApprovalFlow(applyAmount, expenseCategory); return { id, reimburseNo: raw.reimburseNo || `CR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`, applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`, employeeNo: raw.employeeNo ?? raw.applicantNo ?? "", employeeName: raw.employeeName ?? raw.applicantName ?? "未知", applicantNo: raw.employeeNo ?? raw.applicantNo ?? "", applicantName: raw.employeeName ?? raw.applicantName ?? "未知", expenseCategory, reimburseReason: raw.reimburseReason ?? "", applyAmount, payee: raw.payee ?? "", payeeAccount: raw.payeeAccount ?? "", bankBranch: raw.bankBranch ?? "", expenseDetails, attachmentList: Array.isArray(raw.attachmentList) ? raw.attachmentList : [], invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [], approvalFlowNodes, currentNodeIndex: raw.currentNodeIndex ?? 0, approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending", rejectReason: raw.rejectReason ?? "", approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [], applyTime: raw.applyTime || raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), deptId: raw.deptId ?? "", deptName: raw.deptName ?? "", }; }