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 ?? "",
|
};
|
}
|