| | |
| | | import dayjs from "dayjs"; |
| | | import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js"; |
| | | import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js"; |
| | | import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js"; |
| | | import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js"; |
| | | |
| | | /** 报销类型:1-差旅报销,2-费用报销 */ |
| | |
| | | } else { |
| | | mapped = raw || {}; |
| | | } |
| | | |
| | | let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw)); |
| | | if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) { |
| | | formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks)); |
| | | } |
| | | |
| | | const enriched = applyFinReimbursementDetailEnrichment(mapped, raw); |
| | | return { |
| | | ...applyFinReimbursementDetailEnrichment(mapped, raw), |
| | | ...enriched, |
| | | approvalFlowNodes: formApprovalFlowNodes.length |
| | | ? formApprovalFlowNodes |
| | | : enriched.approvalFlowNodes, |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | |
| | | ? row.travel |
| | | : travel, |
| | | details, |
| | | nodes: row.nodes || [], |
| | | approvalFlowNodes: mapNodesToFormFlow(row.nodes), |
| | | nodes: resolveRowApiNodes(row), |
| | | approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)), |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: buildApprovalFlowSummaryForRow(row), |
| | | }; |
| | | return base; |
| | | } |
| | |
| | | export function mapCostReimbursementRow(row) { |
| | | if (!row) return {}; |
| | | const details = Array.isArray(row.details) ? row.details : []; |
| | | const apiNodes = resolveRowApiNodes(row); |
| | | const approvalFlowNodes = mapNodesToFormFlow(apiNodes); |
| | | |
| | | return { |
| | | ...row, |
| | |
| | | reimburseReason: row.reason || "", |
| | | expenseCategory: row.expenseType || "", |
| | | applyAmount: row.applyAmount, |
| | | applyTime: row.createTime || "", |
| | | applyTime: formatReimbursementDateTime(row.createTime), |
| | | payee: row.payeeName || "", |
| | | payeeAccount: row.payeeAccount || "", |
| | | bankBranch: row.payeeBank || "", |
| | |
| | | expenseSubject: d.expenseCategory, |
| | | })), |
| | | details, |
| | | nodes: row.nodes || [], |
| | | approvalFlowNodes: mapNodesToFormFlow(row.nodes), |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: buildApprovalFlowSummaryForRow({ |
| | | ...row, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | }), |
| | | }; |
| | | } |
| | | |
| | |
| | | return hit?.label || category || ""; |
| | | } |
| | | |
| | | /** 列表/详情行上的审批节点(listPage 常不返回,需详情补全) */ |
| | | export function resolveRowApiNodes(row) { |
| | | if (!row || typeof row !== "object") return []; |
| | | const list = |
| | | row.nodes || |
| | | row.flowNodes || |
| | | row.approveNodes || |
| | | row.finReimbursementNodes || |
| | | row.nodeList || |
| | | row.reimbursementNodeList || |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | function sortFlowNodesByLevel(nodes = []) { |
| | | return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => { |
| | | const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0); |
| | | const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0); |
| | | return la - lb; |
| | | }); |
| | | } |
| | | |
| | | function formatApiNodeApproverLabel(node, index) { |
| | | if (!node || typeof node !== "object") return ""; |
| | | const approvers = Array.isArray(node.approvers) ? node.approvers : []; |
| | | const names = approvers |
| | | .map((a) => (a?.approverName || "").trim()) |
| | | .filter(Boolean); |
| | | if (names.length) return names.join("/"); |
| | | return (node.approverName || "").trim() || `节点${index + 1}`; |
| | | } |
| | | |
| | | /** 接口 nodes → 页面审批流(单审批人节点) */ |
| | | export function mapNodesToFormFlow(nodes = []) { |
| | | return (Array.isArray(nodes) ? nodes : []).map((n, i) => { |
| | | const first = Array.isArray(n.approvers) ? n.approvers[0] : null; |
| | | return sortFlowNodesByLevel(nodes).map((n, i) => { |
| | | const approvers = Array.isArray(n.approvers) ? n.approvers : []; |
| | | const first = approvers[0] || null; |
| | | const names = approvers |
| | | .map((a) => (a?.approverName || "").trim()) |
| | | .filter(Boolean); |
| | | return { |
| | | ...n, |
| | | nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1, |
| | | signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign", |
| | | approverId: first?.approverId ?? n.approverId ?? null, |
| | | approverName: first?.approverName ?? n.approverName ?? "", |
| | | approverId: |
| | | toNumber(first?.approverId ?? n.approverId) ?? |
| | | first?.approverId ?? |
| | | n.approverId ?? |
| | | null, |
| | | approverName: |
| | | names.join("、") || first?.approverName || n.approverName || "", |
| | | nodeStatus: n.nodeStatus, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | function formatTasksToFlowSummary(tasks = []) { |
| | | const list = sortFlowNodesByLevel( |
| | | (Array.isArray(tasks) ? tasks : []).map((t, i) => ({ |
| | | levelNo: t.levelNo ?? t.taskLevel ?? i + 1, |
| | | approverName: |
| | | (t.approverName || t.operatorName || t.createUserName || "").trim() || |
| | | "", |
| | | })) |
| | | ); |
| | | const parts = list.map((t) => t.approverName).filter(Boolean); |
| | | return parts.length ? parts.join(" → ") : ""; |
| | | } |
| | | |
| | | function buildApprovalFlowSummaryForRow(row) { |
| | | const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row)); |
| | | let flowNodes = |
| | | row?.approvalFlowNodes?.length > 0 |
| | | ? sortFlowNodesByLevel(row.approvalFlowNodes) |
| | | : mapNodesToFormFlow(apiNodes); |
| | | |
| | | if (!flowNodes.length && apiNodes.length) { |
| | | const line = apiNodes |
| | | .map((n, i) => formatApiNodeApproverLabel(n, i)) |
| | | .filter(Boolean) |
| | | .join(" → "); |
| | | if (line) return line; |
| | | } |
| | | |
| | | if (!flowNodes.length) { |
| | | const fromTasks = formatTasksToFlowSummary(row?.tasks); |
| | | if (fromTasks) return fromTasks; |
| | | return "—"; |
| | | } |
| | | |
| | | return flowNodes |
| | | .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 formatApprovalFlowSummary(row) { |
| | | return buildApprovalFlowSummaryForRow(row); |
| | | } |
| | | |
| | | /** listPage 常不带完整 nodes,列表加载后统一拉详情补全多级审批流程 */ |
| | | export async function enrichReimbursementListRowsWithApprovalFlow( |
| | | rows, |
| | | reimbursementType |
| | | ) { |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | if (!list.length) return list; |
| | | |
| | | const needIds = list |
| | | .map((r) => resolveReimbursementDeleteId(r)) |
| | | .filter((id) => id != null); |
| | | |
| | | if (!needIds.length) return list; |
| | | |
| | | const detailById = new Map(); |
| | | await Promise.all( |
| | | needIds.map(async (id) => { |
| | | try { |
| | | const res = await getFinReimbursementDetail(id); |
| | | detailById.set(String(id), unwrapFinReimbursementDetail(res)); |
| | | } catch { |
| | | /* 单行失败不影响列表 */ |
| | | } |
| | | }) |
| | | ); |
| | | |
| | | const mapRow = |
| | | reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ? mapTravelReimbursementRow |
| | | : mapCostReimbursementRow; |
| | | |
| | | return list.map((row) => { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | const detail = id != null ? detailById.get(String(id)) : null; |
| | | if (!detail) return row; |
| | | const merged = { |
| | | ...row, |
| | | ...detail, |
| | | id: row.id ?? detail.id, |
| | | reimbursementId: row.reimbursementId ?? row.id ?? detail.id, |
| | | reimbursementType: detail.reimbursementType ?? row.reimbursementType, |
| | | }; |
| | | return mapRow(merged); |
| | | }); |
| | | } |
| | | |
| | |
| | | return Math.round(sum * 100) / 100; |
| | | } |
| | | |
| | | /** 表单附件列表(兼容多种字段名) */ |
| | | export function resolveFormAttachmentList(form) { |
| | | const list = |
| | | form?.attachmentList ?? |
| | | form?.storageBlobDTOs ?? |
| | | form?.storageBlobVOList ?? |
| | | form?.invoiceAttachments ?? |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | /** 页面附件 → 保存 DTO(storageBlobVOList / storageBlobDTOs) */ |
| | | export function mapFormAttachmentsToApi(list = [], reimbursementId) { |
| | | const rid = |
| | | reimbursementId != null |
| | | ? toNumber(reimbursementId) ?? reimbursementId |
| | | : undefined; |
| | | |
| | | return (list || []) |
| | | .map((item, i) => { |
| | | if (!item) return null; |
| | | const url = |
| | | item.url || |
| | | item.fileUrl || |
| | | item.downloadUrl || |
| | | item.downloadURL || |
| | | item.previewUrl || |
| | | item.previewURL || |
| | | item.link || |
| | | ""; |
| | | const name = |
| | | item.fileName || |
| | | item.originalFilename || |
| | | item.originalFileName || |
| | | item.blobName || |
| | | item.name || |
| | | `附件${i + 1}`; |
| | | |
| | | const idRaw = item.id ?? item.blobId; |
| | | const isTempId = |
| | | idRaw != null && |
| | | /^(inv_|att_|ed_|local_)/.test(String(idRaw)); |
| | | |
| | | if (!url && (idRaw == null || isTempId)) return null; |
| | | |
| | | const blob = { |
| | | fileName: name, |
| | | originalFilename: name, |
| | | fileUrl: url || undefined, |
| | | url: url || undefined, |
| | | }; |
| | | |
| | | if (idRaw != null && !isTempId) { |
| | | const n = toNumber(idRaw); |
| | | blob.id = n != null ? n : idRaw; |
| | | blob.blobId = blob.id; |
| | | } |
| | | if (rid != null) blob.reimbursementId = rid; |
| | | return blob; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | function applyStorageBlobsToSaveDto(dto, form) { |
| | | const blobs = mapFormAttachmentsToApi( |
| | | resolveFormAttachmentList(form), |
| | | dto?.id ?? form?.reimbursementId ?? form?.id |
| | | ); |
| | | if (blobs.length) { |
| | | dto.storageBlobVOList = blobs; |
| | | dto.storageBlobDTOs = blobs; |
| | | } |
| | | return dto; |
| | | } |
| | | |
| | | /** 修改时补齐主表与子表关联 ID */ |
| | | function applyReimbursementRelations(dto) { |
| | | const rid = dto?.id; |
| | |
| | | d.reimbursementId = rid; |
| | | }); |
| | | } |
| | | const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray); |
| | | blobLists.forEach((list) => { |
| | | list.forEach((b) => { |
| | | b.reimbursementId = rid; |
| | | }); |
| | | }); |
| | | return dto; |
| | | } |
| | | |
| | |
| | | } |
| | | if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id); |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |
| | |
| | | dto.approveProcessId = toNumber(form.approveProcessId); |
| | | } |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |