| | |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | deleteFinReimbursement, |
| | | getFinReimbursementDetail, |
| | | persistFinReimbursement, |
| | | } from "@/api/oa/finReimbursement.js"; |
| | | import { deleteFinReimbursement, getFinReimbursementDetail, persistFinReimbursement } from "@/api/oa/finReimbursement.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "./approvalModuleRegistry.js"; |
| | | import { businessStatusClass, normalizeApprovalStatusKey } from "./approveListUtils.js"; |
| | | import { |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | expenseTypeToCategory, |
| | | } from "../ReimburseManage/_utils/costReimburseUtils.js"; |
| | | import { EXPENSE_CATEGORY_OPTIONS, expenseTypeToCategory } from "../ReimburseManage/_utils/costReimburseUtils.js"; |
| | | import { EXPENSE_SUBJECT_OPTIONS as TRAVEL_EXPENSE_SUBJECTS } from "../ReimburseManage/_utils/travelReimburseUtils.js"; |
| | | import { EXPENSE_SUBJECT_OPTIONS as COST_EXPENSE_SUBJECTS } from "../ReimburseManage/_utils/costReimburseUtils.js"; |
| | | import { resolveExpenseSubjectLabel } from "../ReimburseManage/_utils/expenseDetailDisplay.js"; |
| | |
| | | export function resolveReimbursementType(raw, fallback) { |
| | | const fromApi = normalizeReimbursementType(raw?.reimbursementType); |
| | | if (fromApi) return fromApi; |
| | | return ( |
| | | normalizeReimbursementType(fallback) || |
| | | getReimbursementTypeByModuleKey(fallback) || |
| | | "" |
| | | ); |
| | | return normalizeReimbursementType(fallback) || getReimbursementTypeByModuleKey(fallback) || ""; |
| | | } |
| | | |
| | | export function isTravelReimbursementType(type) { |
| | |
| | | } |
| | | |
| | | export function mapBillStatusToApprovalKey(billStatus) { |
| | | const upper = String(billStatus ?? "").trim().toUpperCase(); |
| | | const upper = String(billStatus ?? "") |
| | | .trim() |
| | | .toUpperCase(); |
| | | if (upper === "DRAFT") return "draft"; |
| | | if (upper === "IN_APPROVAL") return "pending"; |
| | | if (upper === "APPROVED") return "approved"; |
| | |
| | | } |
| | | |
| | | export function billStatusLabel(billStatus) { |
| | | const upper = String(billStatus ?? "").trim().toUpperCase(); |
| | | const upper = String(billStatus ?? "") |
| | | .trim() |
| | | .toUpperCase(); |
| | | if (BILL_STATUS_LABEL[upper]) return BILL_STATUS_LABEL[upper]; |
| | | const key = mapBillStatusToApprovalKey(billStatus); |
| | | if (key === "draft") return "草稿"; |
| | |
| | | } |
| | | |
| | | export function billStatusCssClass(item) { |
| | | return businessStatusClass( |
| | | mapBillStatusToApprovalKey(item?.billStatus ?? item?.status) |
| | | ); |
| | | return businessStatusClass(mapBillStatusToApprovalKey(item?.billStatus ?? item?.status)); |
| | | } |
| | | |
| | | function pickApplicantQuery(searchForm = {}) { |
| | |
| | | const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase(); |
| | | if (!kw) return list; |
| | | |
| | | return list.filter((row) => { |
| | | const parts = [ |
| | | row.applicantName, |
| | | row.employeeName, |
| | | row.applicantNo, |
| | | row.applicantCode, |
| | | row.employeeNo, |
| | | ] |
| | | .filter((v) => v != null && v !== "") |
| | | .map((v) => String(v).toLowerCase()); |
| | | return parts.some((p) => p.includes(kw)); |
| | | return list.filter(row => { |
| | | const parts = [row.applicantName, row.employeeName, row.applicantNo, row.applicantCode, row.employeeNo].filter(v => v != null && v !== "").map(v => String(v).toLowerCase()); |
| | | return parts.some(p => p.includes(kw)); |
| | | }); |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | export function buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType, |
| | | extraDto = {}, |
| | | }) { |
| | | export function buildFinReimbursementListParams({ page, searchForm, reimbursementType, extraDto = {} }) { |
| | | const dto = { |
| | | reimbursementType, |
| | | ...pickApplicantQuery(searchForm), |
| | |
| | | /** 兼容 list/detail 多种差旅子对象结构 */ |
| | | export function pickTravelFromRow(row) { |
| | | if (!row || typeof row !== "object") return {}; |
| | | const nested = |
| | | (row.travel && typeof row.travel === "object" ? row.travel : null) || |
| | | row.finReimbursementTravel || |
| | | row.finReimbursementTravelDto || |
| | | row.travelDto || |
| | | row.travelVO || |
| | | {}; |
| | | const src = |
| | | nested && typeof nested === "object" && Object.keys(nested).length |
| | | ? nested |
| | | : row; |
| | | const nested = (row.travel && typeof row.travel === "object" ? row.travel : null) || row.finReimbursementTravel || row.finReimbursementTravelDto || row.travelDto || row.travelVO || {}; |
| | | const src = nested && typeof nested === "object" && Object.keys(nested).length ? nested : row; |
| | | return { |
| | | startTime: pickTravelField(src, [ |
| | | "startTime", |
| | | "travelStartTime", |
| | | "startDate", |
| | | "travelStartDate", |
| | | "departureTime", |
| | | ]), |
| | | endTime: pickTravelField(src, [ |
| | | "endTime", |
| | | "travelEndTime", |
| | | "endDate", |
| | | "travelEndDate", |
| | | "returnTime", |
| | | ]), |
| | | startTime: pickTravelField(src, ["startTime", "travelStartTime", "startDate", "travelStartDate", "departureTime"]), |
| | | endTime: pickTravelField(src, ["endTime", "travelEndTime", "endDate", "travelEndDate", "returnTime"]), |
| | | travelDays: src.travelDays, |
| | | departureCity: pickTravelField(src, [ |
| | | "departureCity", |
| | | "departurePlace", |
| | | "departure", |
| | | ]), |
| | | destinationCity: pickTravelField(src, [ |
| | | "destinationCity", |
| | | "destination", |
| | | "destinationPlace", |
| | | ]), |
| | | departureCity: pickTravelField(src, ["departureCity", "departurePlace", "departure"]), |
| | | destinationCity: pickTravelField(src, ["destinationCity", "destination", "destinationPlace"]), |
| | | hotelStandard: src.hotelStandard, |
| | | lodgingDays: src.lodgingDays ?? src.hotelDays, |
| | | mealAllowance: src.mealAllowance ?? src.livingSubsidy, |
| | |
| | | |
| | | export function mapFinReimbursementFromApi(row, { reimbursementType, moduleKey } = {}) { |
| | | if (!row) return {}; |
| | | const type = resolveReimbursementType( |
| | | row, |
| | | reimbursementType || getReimbursementTypeByModuleKey(moduleKey) |
| | | ); |
| | | const type = resolveReimbursementType(row, reimbursementType || getReimbursementTypeByModuleKey(moduleKey)); |
| | | const isTravel = type === FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | const travel = isTravel ? pickTravelFromRow(row) : {}; |
| | | const apiNodes = resolveRowApiNodes(row); |
| | |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | }); |
| | | const instanceId = row.approvalInstanceId ?? row.id; |
| | | const instanceId = row.id ?? row.approvalInstanceId; |
| | | const reimbursementId = row.reimbursementId ?? row.id; |
| | | |
| | | return { |
| | | ...row, |
| | | reimbursementId: row.id, |
| | | reimbursementId, |
| | | id: instanceId, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | instanceNo: row.billNo || "", |
| | |
| | | reason: row.reason, |
| | | approvalFlowSummary: flowSummary, |
| | | }, |
| | | type |
| | | type, |
| | | ), |
| | | }; |
| | | } |
| | |
| | | { label: "单据状态", value: billStatusLabel(item.billStatus) }, |
| | | ]; |
| | | if (isTravel) { |
| | | rows.splice( |
| | | 1, |
| | | 0, |
| | | { label: "出差地", value: item.departurePlace }, |
| | | { label: "目的地", value: item.destination } |
| | | ); |
| | | rows.splice(1, 0, { label: "出差地", value: item.departurePlace }, { label: "目的地", value: item.destination }); |
| | | } else { |
| | | rows.splice(1, 0, { label: "费用类型", value: item.expenseType }); |
| | | } |
| | |
| | | |
| | | /** 列表行主键(删除/修改用 fin_reimbursement.id,勿用 item.id 审批实例 ID) */ |
| | | export function resolveReimbursementDeleteId(row) { |
| | | const raw = row?.reimbursementId; |
| | | const raw = row?.reimbursementId ?? row?.reimbursementID ?? row?.finReimbursementId; |
| | | if (raw == null || raw === "" || String(raw).startsWith("local_")) { |
| | | return undefined; |
| | | } |
| | |
| | | |
| | | /** 是否允许删除(审批中、已通过、已付款不可删) */ |
| | | export function canDeleteReimbursementRow(row) { |
| | | const upper = String(row?.billStatus ?? row?.status ?? "").trim().toUpperCase(); |
| | | const upper = String(row?.billStatus ?? row?.status ?? "") |
| | | .trim() |
| | | .toUpperCase(); |
| | | if (upper === "PAID") return false; |
| | | const key = mapBillStatusToApprovalKey( |
| | | row?.billStatus ?? row?.approvalStatus ?? row?.status |
| | | ); |
| | | const key = mapBillStatusToApprovalKey(row?.billStatus ?? row?.approvalStatus ?? row?.status); |
| | | return key !== "pending" && key !== "approved"; |
| | | } |
| | | |
| | |
| | | expenseType: row.expenseCategory || row.expenseType, |
| | | reason: row.reimburseReason || row.reason, |
| | | }, |
| | | type |
| | | type, |
| | | ), |
| | | }; |
| | | } |
| | |
| | | } |
| | | |
| | | function expenseSubjectToCategory(subject) { |
| | | const hit = |
| | | TRAVEL_EXPENSE_SUBJECTS.find(x => x.value === subject) || |
| | | COST_EXPENSE_SUBJECTS.find(x => x.value === subject); |
| | | const hit = TRAVEL_EXPENSE_SUBJECTS.find(x => x.value === subject) || COST_EXPENSE_SUBJECTS.find(x => x.value === subject); |
| | | return hit?.label || subject || ""; |
| | | } |
| | | |
| | | function mapDetailRowFromApi(d, reimbursementType) { |
| | | const type = normalizeReimbursementType(reimbursementType); |
| | | const raw = d.expenseCategory ?? d.expenseSubject ?? ""; |
| | | const opts = |
| | | type === FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ? TRAVEL_EXPENSE_SUBJECTS |
| | | : COST_EXPENSE_SUBJECTS; |
| | | const opts = type === FIN_REIMBURSEMENT_TYPE.TRAVEL ? TRAVEL_EXPENSE_SUBJECTS : COST_EXPENSE_SUBJECTS; |
| | | const label = resolveExpenseSubjectLabel(raw, { |
| | | isTravel: type === FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | subjectOptions: opts, |
| | |
| | | |
| | | export function resolveRowApiNodes(row) { |
| | | if (!row || typeof row !== "object") return []; |
| | | const list = |
| | | row.nodes || |
| | | row.flowNodes || |
| | | row.approveNodes || |
| | | row.finReimbursementNodes || |
| | | row.nodeList || |
| | | []; |
| | | const list = row.nodes || row.flowNodes || row.approveNodes || row.finReimbursementNodes || row.nodeList || []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | |
| | | 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); |
| | | const names = approvers.map(a => (a?.approverName || "").trim()).filter(Boolean); |
| | | if (names.length) return names.join("/"); |
| | | return (node.approverName || "").trim() || `节点${index + 1}`; |
| | | } |
| | |
| | | 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); |
| | | 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: |
| | | toNumber(first?.approverId ?? n.approverId) ?? |
| | | first?.approverId ?? |
| | | n.approverId ?? |
| | | "", |
| | | approverName: |
| | | names.join("、") || first?.approverName || n.approverName || "", |
| | | approverId: toNumber(first?.approverId ?? n.approverId) ?? first?.approverId ?? n.approverId ?? "", |
| | | approverName: names.join("、") || first?.approverName || n.approverName || "", |
| | | nodeStatus: n.nodeStatus, |
| | | }; |
| | | }); |
| | |
| | | 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() || |
| | | "", |
| | | })) |
| | | 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); |
| | | let flowNodes = row?.approvalFlowNodes?.length > 0 ? sortFlowNodesByLevel(row.approvalFlowNodes) : mapNodesToFormFlow(apiNodes); |
| | | |
| | | if (!flowNodes.length && apiNodes.length) { |
| | | const line = apiNodes |
| | |
| | | } |
| | | |
| | | /** listPage 常不带完整 nodes,列表加载后统一拉详情补全多级审批流程 */ |
| | | export async function enrichReimbursementListRowsWithApprovalFlow( |
| | | rows, |
| | | reimbursementType |
| | | ) { |
| | | 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); |
| | | const needIds = list.map(r => resolveReimbursementDeleteId(r)).filter(id => id != null); |
| | | |
| | | if (!needIds.length) return list; |
| | | |
| | |
| | | } catch { |
| | | /* ignore */ |
| | | } |
| | | }) |
| | | }), |
| | | ); |
| | | |
| | | const mapRow = |
| | | reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ? mapTravelReimbursementRow |
| | | : mapCostReimbursementRow; |
| | | const mapRow = reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL ? mapTravelReimbursementRow : mapCostReimbursementRow; |
| | | |
| | | return list.map(row => { |
| | | const id = resolveReimbursementDeleteId(row); |
| | |
| | | |
| | | /** 表单上的审批流(兼容 approvalFlowNodes / nodes / flowNodes) */ |
| | | export function resolveFormApprovalFlowNodes(form) { |
| | | const list = |
| | | form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? []; |
| | | const list = form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | |
| | | |
| | | /** 表单附件列表(兼容多种字段名) */ |
| | | export function resolveFormAttachmentList(form) { |
| | | const list = |
| | | form?.attachmentList ?? |
| | | form?.storageBlobDTOs ?? |
| | | form?.storageBlobVOList ?? |
| | | form?.invoiceAttachments ?? |
| | | []; |
| | | 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; |
| | | 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 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)); |
| | | const isTempId = idRaw != null && /^(inv_|att_|ed_|local_)/.test(String(idRaw)); |
| | | |
| | | if (!url && (idRaw == null || isTempId)) return null; |
| | | |
| | |
| | | } |
| | | |
| | | function applyStorageBlobsToSaveDto(dto, form) { |
| | | const blobs = mapFormAttachmentsToApi( |
| | | resolveFormAttachmentList(form), |
| | | dto?.id ?? form?.reimbursementId ?? form?.id |
| | | ); |
| | | const blobs = mapFormAttachmentsToApi(resolveFormAttachmentList(form), dto?.id ?? form?.reimbursementId ?? form?.id); |
| | | if (blobs.length) { |
| | | dto.storageBlobVOList = blobs; |
| | | dto.storageBlobDTOs = blobs; |
| | |
| | | payeeAccount: row.payeeAccount || "", |
| | | payeeBank: row.payeeBank || "", |
| | | billStatus: row.billStatus, |
| | | expenseDetails: details.map(d => |
| | | mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.TRAVEL) |
| | | ), |
| | | travel: |
| | | row.travel && typeof row.travel === "object" && Object.keys(row.travel).length |
| | | ? row.travel |
| | | : travel, |
| | | expenseDetails: details.map(d => mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.TRAVEL)), |
| | | travel: row.travel && typeof row.travel === "object" && Object.keys(row.travel).length ? row.travel : travel, |
| | | details, |
| | | nodes: resolveRowApiNodes(row), |
| | | approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)), |
| | |
| | | bankBranch: row.payeeBank || "", |
| | | payeeBank: row.payeeBank || "", |
| | | billStatus: row.billStatus, |
| | | expenseDetails: details.map(d => |
| | | mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.COST) |
| | | ), |
| | | expenseDetails: details.map(d => mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.COST)), |
| | | details, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | |
| | | const enriched = applyFinReimbursementDetailEnrichment(mapped, raw); |
| | | return { |
| | | ...enriched, |
| | | approvalFlowNodes: formApprovalFlowNodes.length |
| | | ? formApprovalFlowNodes |
| | | : enriched.approvalFlowNodes, |
| | | approvalFlowNodes: formApprovalFlowNodes.length ? formApprovalFlowNodes : enriched.approvalFlowNodes, |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | |
| | | const details = mapDetailsToApi(form.expenseDetails); |
| | | const detailTotal = sumDetailAmount(form.expenseDetails); |
| | | const applyAmount = toNumber(form.applyAmount) ?? detailTotal; |
| | | const travelDays = |
| | | form.travelDays != null |
| | | ? toNumber(form.travelDays) |
| | | : computeTravelDays?.(form.travelStartTime, form.travelEndTime); |
| | | const travelDays = form.travelDays != null ? toNumber(form.travelDays) : computeTravelDays?.(form.travelStartTime, form.travelEndTime); |
| | | |
| | | const dto = { |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | |
| | | withinStandard: form.needSpecialApproval ? "0" : "1", |
| | | }, |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | nodes: mapApprovalFlowNodesToApi(resolveFormApprovalFlowNodes(form), form.templateId), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |
| | |
| | | billStatus: "IN_APPROVAL", |
| | | deptId: toNumber(form.deptId), |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | nodes: mapApprovalFlowNodesToApi(resolveFormApprovalFlowNodes(form), form.templateId), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |