| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 ?? "", |
| | | }; |
| | | } |