gongchunyi
2026-05-28 19f2e3bdbe04e7ea79c6a0bdc8c7318d4837b189
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,313 @@
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 APPROVAL_ROLE_LABELS = {
  direct_supervisor: "直属上级",
  dept_manager: "部门经理",
  cfo: "财务总监",
  compliance: "合规审核",
};
/** æŒ‰é‡‘额预设审批链 */
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 === "draft") return "草稿";
  if (v === "approved") return "已通过";
  if (v === "paid") return "已付款";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤回";
  return "审核中";
}
export function statusTagType(v) {
  if (v === "draft") return "info";
  if (v === "approved" || v === "paid") return "success";
  if (v === "rejected") return "danger";
  if (v === "cancelled") return "info";
  return "warning";
}
export { formatApprovalFlowSummary } from "../shared/finReimbursementMappers.js";
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, previousNodes = []) {
  const roles = resolveApprovalRoles(amount, expenseCategory);
  const prevByRole = new Map();
  (previousNodes || []).forEach((n, idx) => {
    if (n?.roleKey) prevByRole.set(n.roleKey, n);
    else if (n?.approverId != null && n.approverId !== "") {
      prevByRole.set(`__idx_${idx}`, n);
    }
  });
  return roles.map((role, i) => {
    const prev = prevByRole.get(role) || prevByRole.get(`__idx_${i}`);
    const hasApprover = prev?.approverId != null && prev.approverId !== "";
    return {
      approverId: hasApprover ? prev.approverId : null,
      approverName: hasApprover
        ? prev.approverName || ""
        : APPROVAL_ROLE_LABELS[role] || role,
      roleKey: role,
      signMode: prev?.signMode || "countersign",
      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) => APPROVAL_ROLE_LABELS[r] || 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 ?? "",
  };
}