yyb
12 小时以前 8bba0a2d08c7abc07604a0654661efc884e5d751
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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 ?? "",
  };
}