From 19f2e3bdbe04e7ea79c6a0bdc8c7318d4837b189 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 28 五月 2026 17:36:45 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_pro_山西_晋和园

---
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js |  313 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 313 insertions(+), 0 deletions(-)

diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
new file mode 100644
index 0000000..1736b3e
--- /dev/null
+++ b/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 ?? "",
+  };
+}

--
Gitblit v1.9.3