| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | 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_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"; |
| | | import { mapTasksToFlowNodes } from "./approveListUtils.js"; |
| | | import { applyFinReimbursementDetailEnrichment } from "../ReimburseManage/_utils/finReimbursementDetailExtras.js"; |
| | | |
| | | export const FIN_REIMBURSEMENT_TYPE = { |
| | | TRAVEL: "1", |
| | | COST: "2", |
| | | }; |
| | | |
| | | const REIMBURSEMENT_TYPE_LABEL = { |
| | | [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "å·®æ
æ¥é", |
| | | [FIN_REIMBURSEMENT_TYPE.COST]: "è´¹ç¨æ¥é", |
| | | }; |
| | | |
| | | /** å½ä¸åæ¥éç±»åï¼1-å·®æ
ï¼2-è´¹ç¨ */ |
| | | export function normalizeReimbursementType(val) { |
| | | const s = String(val ?? "").trim(); |
| | | if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | return FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | } |
| | | if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | return FIN_REIMBURSEMENT_TYPE.COST; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function reimbursementTypeLabel(type) { |
| | | return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "â"; |
| | | } |
| | | |
| | | export function getModuleKeyByReimbursementType(type) { |
| | | const t = normalizeReimbursementType(type); |
| | | if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE; |
| | | } |
| | | if (t === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | return APPROVAL_MODULE_KEYS.COST_REIMBURSE; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | /** ä¼å
æ¥å£ reimbursementTypeï¼å
¶æ¬¡é¡µé¢ moduleKey / å
¥å */ |
| | | export function resolveReimbursementType(raw, fallback) { |
| | | const fromApi = normalizeReimbursementType(raw?.reimbursementType); |
| | | if (fromApi) return fromApi; |
| | | return ( |
| | | normalizeReimbursementType(fallback) || |
| | | getReimbursementTypeByModuleKey(fallback) || |
| | | "" |
| | | ); |
| | | } |
| | | |
| | | export function isTravelReimbursementType(type) { |
| | | return resolveReimbursementType({ reimbursementType: type }, type) === FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | } |
| | | |
| | | export function filterRowsByReimbursementType(rows, expectedType) { |
| | | const expected = normalizeReimbursementType(expectedType); |
| | | if (!expected) return rows || []; |
| | | return (rows || []).filter(row => { |
| | | const t = resolveReimbursementType(row, expected); |
| | | return t === expected; |
| | | }); |
| | | } |
| | | |
| | | const BILL_STATUS_LABEL = { |
| | | DRAFT: "è稿", |
| | | IN_APPROVAL: "审æ¹ä¸", |
| | | APPROVED: "审æ¹éè¿", |
| | | REJECTED: "审æ¹é©³å", |
| | | WITHDRAWN: "å·²æ¤å", |
| | | PAID: "已仿¬¾", |
| | | }; |
| | | |
| | | export function getReimbursementTypeByModuleKey(moduleKey) { |
| | | if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) { |
| | | return FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | } |
| | | if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) { |
| | | return FIN_REIMBURSEMENT_TYPE.COST; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function unwrapFinReimbursementPage(res) { |
| | | const data = res?.data ?? res; |
| | | if (!data || typeof data !== "object") { |
| | | return { records: [], total: 0 }; |
| | | } |
| | | if (Array.isArray(data.records)) { |
| | | return { records: data.records, total: Number(data.total ?? 0) }; |
| | | } |
| | | const nested = data.data; |
| | | if (nested && typeof nested === "object" && Array.isArray(nested.records)) { |
| | | return { records: nested.records, total: Number(nested.total ?? 0) }; |
| | | } |
| | | return { records: [], total: 0 }; |
| | | } |
| | | |
| | | /** 详æ
æ¥å£ data è§£å
*/ |
| | | export function unwrapFinReimbursementDetail(res) { |
| | | const data = res?.data ?? res; |
| | | if (!data || typeof data !== "object") return {}; |
| | | if (data.billNo != null || data.id != null || data.reimbursementType != null) { |
| | | return data; |
| | | } |
| | | const nested = data.data; |
| | | if (nested && typeof nested === "object" && !Array.isArray(nested)) { |
| | | return nested; |
| | | } |
| | | if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") { |
| | | return data.finReimbursementDto; |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | export function mapBillStatusToApprovalKey(billStatus) { |
| | | const upper = String(billStatus ?? "").trim().toUpperCase(); |
| | | if (upper === "DRAFT") return "draft"; |
| | | if (upper === "IN_APPROVAL") return "pending"; |
| | | if (upper === "APPROVED") return "approved"; |
| | | if (upper === "REJECTED") return "rejected"; |
| | | if (upper === "WITHDRAWN") return "cancelled"; |
| | | if (upper === "PAID") return "approved"; |
| | | return normalizeApprovalStatusKey(billStatus); |
| | | } |
| | | |
| | | export function billStatusLabel(billStatus) { |
| | | const upper = String(billStatus ?? "").trim().toUpperCase(); |
| | | if (BILL_STATUS_LABEL[upper]) return BILL_STATUS_LABEL[upper]; |
| | | const key = mapBillStatusToApprovalKey(billStatus); |
| | | if (key === "draft") return "è稿"; |
| | | if (key === "approved") return "已宿"; |
| | | if (key === "rejected") return "已驳å"; |
| | | if (key === "cancelled") return "å·²æ¤å"; |
| | | return "è¿è¡ä¸"; |
| | | } |
| | | |
| | | export function billStatusCssClass(item) { |
| | | return businessStatusClass( |
| | | mapBillStatusToApprovalKey(item?.billStatus ?? item?.status) |
| | | ); |
| | | } |
| | | |
| | | function pickApplicantQuery(searchForm = {}) { |
| | | const kw = (searchForm.applicantKeyword || "").trim(); |
| | | if (!kw) return {}; |
| | | const out = { applicantName: kw }; |
| | | if (!/[\u4e00-\u9fa5]/.test(kw)) { |
| | | out.applicantCode = kw; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | export function hasActiveReimbursementSearch(searchForm = {}) { |
| | | return Boolean((searchForm?.applicantKeyword || "").trim()); |
| | | } |
| | | |
| | | export function filterReimbursementRowsBySearch(rows, searchForm = {}) { |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | 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)); |
| | | }); |
| | | } |
| | | |
| | | /** æå¹³å为 Spring GET å¯ç»å®ç queryï¼finReimbursementDto.xxxï¼å¿ç¨æ¹æ¬å·ï¼ */ |
| | | function appendDotNotationQuery(target, prefix, fields) { |
| | | if (!fields || typeof fields !== "object") return; |
| | | for (const [key, value] of Object.entries(fields)) { |
| | | if (value == null || value === "") continue; |
| | | target[`${prefix}.${key}`] = value; |
| | | } |
| | | } |
| | | |
| | | export function buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType, |
| | | extraDto = {}, |
| | | }) { |
| | | const dto = { |
| | | reimbursementType, |
| | | ...pickApplicantQuery(searchForm), |
| | | ...(extraDto && typeof extraDto === "object" ? extraDto : {}), |
| | | }; |
| | | |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | "page.current": page.current, |
| | | "page.size": page.size, |
| | | ...dto, |
| | | }; |
| | | appendDotNotationQuery(params, "finReimbursementDto", dto); |
| | | return params; |
| | | } |
| | | |
| | | function pickTravelField(obj, keys) { |
| | | if (!obj || typeof obj !== "object") return ""; |
| | | for (const key of keys) { |
| | | const v = obj[key]; |
| | | if (v != null && v !== "") return v; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | /** å
¼å®¹ 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; |
| | | return { |
| | | 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", |
| | | ]), |
| | | hotelStandard: src.hotelStandard, |
| | | lodgingDays: src.lodgingDays ?? src.hotelDays, |
| | | mealAllowance: src.mealAllowance ?? src.livingSubsidy, |
| | | transportAllowance: src.transportAllowance ?? src.transportSubsidy, |
| | | lodgingLimit: src.lodgingLimit, |
| | | withinStandard: src.withinStandard, |
| | | standardTag: src.standardTag || "", |
| | | id: src.id, |
| | | reimbursementId: src.reimbursementId, |
| | | }; |
| | | } |
| | | |
| | | export function formatReimbursementDateTime(val) { |
| | | if (val == null || val === "") return ""; |
| | | const d = dayjs(val); |
| | | if (!d.isValid()) return String(val); |
| | | const raw = String(val); |
| | | const hasTime = raw.includes("T") || /:\d{2}/.test(raw); |
| | | return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD"); |
| | | } |
| | | |
| | | export function mapFinReimbursementFromApi(row, { reimbursementType, moduleKey } = {}) { |
| | | if (!row) return {}; |
| | | const type = resolveReimbursementType( |
| | | row, |
| | | reimbursementType || getReimbursementTypeByModuleKey(moduleKey) |
| | | ); |
| | | const isTravel = type === FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | const travel = isTravel ? pickTravelFromRow(row) : {}; |
| | | const apiNodes = resolveRowApiNodes(row); |
| | | const approvalFlowNodes = mapNodesToFormFlow(apiNodes); |
| | | const flowSummary = formatApprovalFlowSummary({ |
| | | ...row, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | }); |
| | | const instanceId = row.approvalInstanceId ?? row.id; |
| | | |
| | | return { |
| | | ...row, |
| | | reimbursementId: row.id, |
| | | id: instanceId, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | instanceNo: row.billNo || "", |
| | | billNo: row.billNo || "", |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | | applicantNo: row.applicantCode || "", |
| | | applicantCode: row.applicantCode || "", |
| | | applicantName: row.applicantName || "", |
| | | reason: row.reason || "", |
| | | expenseType: row.expenseType || "", |
| | | applyAmount: row.applyAmount, |
| | | billStatus: row.billStatus, |
| | | status: row.billStatus, |
| | | approvalStatus: mapBillStatusToApprovalKey(row.billStatus), |
| | | title: row.reason || row.billNo || "", |
| | | summary: row.reason || row.billNo || "", |
| | | createTime: formatReimbursementDateTime(row.createTime), |
| | | departurePlace: travel.departureCity || "", |
| | | destination: travel.destinationCity || "", |
| | | travelStartTime: formatReimbursementDateTime(travel.startTime), |
| | | travelEndTime: formatReimbursementDateTime(travel.endTime), |
| | | travel, |
| | | details: row.details || [], |
| | | nodes: apiNodes, |
| | | flowNodes: apiNodes, |
| | | approvalFlowNodes, |
| | | approvalFlowSummary: flowSummary, |
| | | displayRows: buildFinReimbursementDisplayRows( |
| | | { |
| | | billNo: row.billNo, |
| | | applyAmount: row.applyAmount, |
| | | billStatus: row.billStatus, |
| | | departurePlace: travel.departureCity, |
| | | destination: travel.destinationCity, |
| | | expenseType: row.expenseType, |
| | | reason: row.reason, |
| | | approvalFlowSummary: flowSummary, |
| | | }, |
| | | type |
| | | ), |
| | | }; |
| | | } |
| | | |
| | | export function buildFinReimbursementDisplayRows(item, reimbursementType) { |
| | | const type = normalizeReimbursementType(reimbursementType); |
| | | const isTravel = type === FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | const rows = [ |
| | | { label: "æ¥éåå·", value: item.billNo }, |
| | | { |
| | | label: "ç³è¯·éé¢", |
| | | value: item.applyAmount != null ? `${item.applyAmount} å
` : "", |
| | | }, |
| | | { label: "åæ®ç¶æ", value: billStatusLabel(item.billStatus) }, |
| | | ]; |
| | | if (isTravel) { |
| | | rows.splice( |
| | | 1, |
| | | 0, |
| | | { label: "åºå·®å°", value: item.departurePlace }, |
| | | { label: "ç®çå°", value: item.destination } |
| | | ); |
| | | } else { |
| | | rows.splice(1, 0, { label: "è´¹ç¨ç±»å", value: item.expenseType }); |
| | | } |
| | | if (item.reason) { |
| | | rows.push({ label: "æ¥éåå ", value: item.reason }); |
| | | } |
| | | if (item.approvalFlowSummary && item.approvalFlowSummary !== "â") { |
| | | rows.push({ label: "å®¡æ¹æµç¨", value: item.approvalFlowSummary }); |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | /** ä¿®æ¹åºæ¯å¿
é¡»å¸¦ä¸»é® IDï¼ä¸ Web ä¸è´ï¼ */ |
| | | export function validateReimbursementPersistDto(dto, isEdit) { |
| | | if (!isEdit) return { ok: true }; |
| | | if (dto?.id != null && dto.id !== "") return { ok: true }; |
| | | return { ok: false, message: "æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID" }; |
| | | } |
| | | |
| | | export { deleteFinReimbursement, getFinReimbursementDetail, persistFinReimbursement }; |
| | | |
| | | /** å表è¡ä¸»é®ï¼å é¤/ä¿®æ¹ç¨ fin_reimbursement.idï¼å¿ç¨ item.id 审æ¹å®ä¾ IDï¼ */ |
| | | export function resolveReimbursementDeleteId(row) { |
| | | const raw = row?.reimbursementId; |
| | | if (raw == null || raw === "" || String(raw).startsWith("local_")) { |
| | | return undefined; |
| | | } |
| | | const n = Number(raw); |
| | | return Number.isNaN(n) ? raw : n; |
| | | } |
| | | |
| | | /** æ¯å¦å
许å é¤ï¼å®¡æ¹ä¸ãå·²éè¿ã已仿¬¾ä¸å¯å ï¼ */ |
| | | export function canDeleteReimbursementRow(row) { |
| | | const upper = String(row?.billStatus ?? row?.status ?? "").trim().toUpperCase(); |
| | | if (upper === "PAID") return false; |
| | | const key = mapBillStatusToApprovalKey( |
| | | row?.billStatus ?? row?.approvalStatus ?? row?.status |
| | | ); |
| | | return key !== "pending" && key !== "approved"; |
| | | } |
| | | |
| | | export function canEditReimbursementRow(row) { |
| | | return canDeleteReimbursementRow(row); |
| | | } |
| | | |
| | | /** æåæ¥é详æ
ï¼å«æç»ã审æ¹èç¹ï¼ä¸ Web mapFinReimbursementDetailRow ä¸è´ï¼ */ |
| | | export async function fetchFinReimbursementListItemDetail(item, reimbursementTypeOrModuleKey) { |
| | | const id = resolveReimbursementDeleteId(item); |
| | | if (id == null) { |
| | | throw new Error("missing reimbursement id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey); |
| | | const row = mapFinReimbursementDetailRow(raw, type); |
| | | return { |
| | | ...row, |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | | displayRows: buildFinReimbursementDisplayRows( |
| | | { |
| | | billNo: row.billNo || row.reimburseNo, |
| | | applyAmount: row.applyAmount, |
| | | billStatus: row.billStatus, |
| | | departurePlace: row.departurePlace, |
| | | destination: row.destination, |
| | | expenseType: row.expenseCategory || row.expenseType, |
| | | reason: row.reimburseReason || row.reason, |
| | | }, |
| | | type |
| | | ), |
| | | }; |
| | | } |
| | | |
| | | function toNumber(val) { |
| | | if (val == null || val === "") return undefined; |
| | | const n = Number(val); |
| | | return Number.isNaN(n) ? undefined : n; |
| | | } |
| | | |
| | | function mapSignModeToApi(signMode) { |
| | | return signMode === "or_sign" ? "OR" : "AND"; |
| | | } |
| | | |
| | | function expenseSubjectToCategory(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 label = resolveExpenseSubjectLabel(raw, { |
| | | isTravel: type === FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | subjectOptions: opts, |
| | | }); |
| | | const hit = opts.find(x => x.value === raw || x.label === raw || x.label === label); |
| | | return { |
| | | ...d, |
| | | expenseSubject: hit?.value || raw, |
| | | }; |
| | | } |
| | | |
| | | function expenseCategoryToType(category) { |
| | | const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.value === category); |
| | | return hit?.label || category || ""; |
| | | } |
| | | |
| | | export function resolveRowApiNodes(row) { |
| | | if (!row || typeof row !== "object") return []; |
| | | const list = |
| | | row.nodes || |
| | | row.flowNodes || |
| | | row.approveNodes || |
| | | row.finReimbursementNodes || |
| | | row.nodeList || |
| | | []; |
| | | 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 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: |
| | | toNumber(first?.approverId ?? n.approverId) ?? |
| | | first?.approverId ?? |
| | | n.approverId ?? |
| | | "", |
| | | 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 { |
| | | /* ignore */ |
| | | } |
| | | }) |
| | | ); |
| | | |
| | | 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; |
| | | return mapRow({ |
| | | ...row, |
| | | ...detail, |
| | | id: row.id ?? detail.id, |
| | | reimbursementId: row.reimbursementId ?? row.id ?? detail.id, |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** 表åä¸çå®¡æ¹æµï¼å
¼å®¹ approvalFlowNodes / nodes / flowNodesï¼ */ |
| | | export function resolveFormApprovalFlowNodes(form) { |
| | | const list = |
| | | form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | /** 页é¢å®¡æ¹èç¹ â æ¥å£ nodes */ |
| | | export function mapApprovalFlowNodesToApi(nodes = [], templateId) { |
| | | const list = Array.isArray(nodes) ? nodes : []; |
| | | return list |
| | | .map((n, i) => { |
| | | let approvers = []; |
| | | if (Array.isArray(n.approvers) && n.approvers.length) { |
| | | approvers = n.approvers |
| | | .filter(a => a?.approverId != null && a.approverId !== "") |
| | | .map((a, idx) => { |
| | | const item = { |
| | | approverId: toNumber(a.approverId) ?? a.approverId, |
| | | approverName: a.approverName || "", |
| | | sortNo: a.sortNo ?? idx + 1, |
| | | }; |
| | | if (a.id != null) item.id = a.id; |
| | | if (a.nodeId != null) item.nodeId = a.nodeId; |
| | | if (a.templateId != null) item.templateId = a.templateId; |
| | | else if (templateId != null) item.templateId = templateId; |
| | | if (a.roleKey) item.roleKey = a.roleKey; |
| | | return item; |
| | | }); |
| | | } else if (n.approverId != null && n.approverId !== "") { |
| | | const item = { |
| | | approverId: toNumber(n.approverId) ?? n.approverId, |
| | | approverName: n.approverName || "", |
| | | sortNo: 1, |
| | | }; |
| | | if (n.roleKey) item.roleKey = n.roleKey; |
| | | approvers = [item]; |
| | | } |
| | | if (!approvers.length) return null; |
| | | const node = { |
| | | levelNo: n.levelNo ?? n.nodeOrder ?? n.sortOrder ?? i + 1, |
| | | approveType: n.approveType || mapSignModeToApi(n.signMode), |
| | | approvers, |
| | | }; |
| | | if (n.id != null) node.id = n.id; |
| | | if (n.templateId != null) node.templateId = n.templateId; |
| | | else if (templateId != null) node.templateId = templateId; |
| | | if (n.roleKey) node.roleKey = n.roleKey; |
| | | return node; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | /** ä¿ååæ ¡éª nodes å·²é
ç½® */ |
| | | export function validateReimbursementApprovalNodes(dto) { |
| | | if (Array.isArray(dto?.nodes) && dto.nodes.length > 0) { |
| | | return { ok: true }; |
| | | } |
| | | return { ok: false, message: "请é
ç½®å®¡æ¹æµç¨å¹¶éæ©å®¡æ¹äºº" }; |
| | | } |
| | | |
| | | function mapDetailsToApi(details = []) { |
| | | return (details || []).map((d, i) => { |
| | | const item = { |
| | | rowNo: d.rowNo ?? i + 1, |
| | | invoiceDate: d.invoiceDate || undefined, |
| | | expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory), |
| | | amount: toNumber(d.amount), |
| | | description: d.description || "", |
| | | invoiceNo: d.invoiceNo || undefined, |
| | | invoiceType: d.invoiceType || undefined, |
| | | invoiceAmount: toNumber(d.invoiceAmount), |
| | | taxRate: toNumber(d.taxRate), |
| | | taxAmount: toNumber(d.taxAmount), |
| | | remark: d.remark || undefined, |
| | | }; |
| | | if (d.id != null && !String(d.id).startsWith("ed_")) { |
| | | item.id = toNumber(d.id) ?? d.id; |
| | | } |
| | | if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId); |
| | | return item; |
| | | }); |
| | | } |
| | | |
| | | function sumDetailAmount(details = []) { |
| | | const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | 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; |
| | | } |
| | | |
| | | function applyReimbursementRelations(dto) { |
| | | const rid = dto?.id; |
| | | if (rid == null) return dto; |
| | | if (dto.travel && typeof dto.travel === "object") { |
| | | dto.travel.reimbursementId = rid; |
| | | } |
| | | if (Array.isArray(dto.details)) { |
| | | dto.details.forEach(d => { |
| | | d.reimbursementId = rid; |
| | | }); |
| | | } |
| | | const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray); |
| | | blobLists.forEach(list => { |
| | | list.forEach(b => { |
| | | b.reimbursementId = rid; |
| | | }); |
| | | }); |
| | | return dto; |
| | | } |
| | | |
| | | function resolveReimbursementId(form) { |
| | | const rawId = form?.reimbursementId ?? form?.id; |
| | | if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) { |
| | | return undefined; |
| | | } |
| | | return toNumber(rawId) ?? rawId; |
| | | } |
| | | |
| | | /** æ¥å£è¡ â å·®æ
æ¥é表åè¡ */ |
| | | export function mapTravelReimbursementRow(row) { |
| | | if (!row) return {}; |
| | | const travel = pickTravelFromRow(row); |
| | | const details = Array.isArray(row.details) ? row.details : []; |
| | | return { |
| | | ...row, |
| | | id: row.id, |
| | | reimbursementId: row.id, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | reimburseNo: row.billNo || "", |
| | | applicantId: row.applicantId, |
| | | applicantNo: row.applicantCode || "", |
| | | applicantName: row.applicantName || "", |
| | | employeeNo: row.applicantCode || "", |
| | | employeeName: row.applicantName || "", |
| | | applicantDeptName: row.applicantDeptName || "", |
| | | reimburseReason: row.reason || "", |
| | | travelStartTime: formatReimbursementDateTime(travel.startTime), |
| | | travelEndTime: formatReimbursementDateTime(travel.endTime), |
| | | travelDays: travel.travelDays, |
| | | departurePlace: travel.departureCity || "", |
| | | destination: travel.destinationCity || "", |
| | | hotelStandard: travel.hotelStandard, |
| | | hotelDays: travel.lodgingDays, |
| | | livingSubsidy: travel.mealAllowance, |
| | | transportSubsidy: travel.transportAllowance, |
| | | lodgingLimit: travel.lodgingLimit, |
| | | needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0, |
| | | standardTag: travel.standardTag || "", |
| | | applyAmount: row.applyAmount, |
| | | payee: row.payeeName || "", |
| | | 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, |
| | | details, |
| | | nodes: resolveRowApiNodes(row), |
| | | approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)), |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: formatApprovalFlowSummary(row), |
| | | attachmentList: row.attachmentList || row.invoiceAttachments || [], |
| | | }; |
| | | } |
| | | |
| | | /** æ¥å£è¡ â è´¹ç¨æ¥é表åè¡ */ |
| | | 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, |
| | | id: row.id, |
| | | reimbursementId: row.id, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | reimburseNo: row.billNo || "", |
| | | applicantId: row.applicantId, |
| | | applicantNo: row.applicantCode || "", |
| | | applicantName: row.applicantName || "", |
| | | employeeNo: row.applicantCode || "", |
| | | employeeName: row.applicantName || "", |
| | | applicantDeptName: row.applicantDeptName || "", |
| | | reimburseReason: row.reason || "", |
| | | expenseCategory: expenseTypeToCategory(row.expenseType), |
| | | applyAmount: row.applyAmount, |
| | | applyTime: formatReimbursementDateTime(row.createTime), |
| | | createTime: formatReimbursementDateTime(row.createTime), |
| | | payee: row.payeeName || "", |
| | | payeeAccount: row.payeeAccount || "", |
| | | bankBranch: row.payeeBank || "", |
| | | payeeBank: row.payeeBank || "", |
| | | billStatus: row.billStatus, |
| | | expenseDetails: details.map(d => |
| | | mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.COST) |
| | | ), |
| | | details, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: formatApprovalFlowSummary({ |
| | | ...row, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | }), |
| | | attachmentList: row.attachmentList || row.invoiceAttachments || [], |
| | | }; |
| | | } |
| | | |
| | | export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) { |
| | | const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey); |
| | | let mapped = {}; |
| | | if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | mapped = mapTravelReimbursementRow(raw); |
| | | } else if (type === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | mapped = mapCostReimbursementRow(raw); |
| | | } 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 { |
| | | ...enriched, |
| | | approvalFlowNodes: formApprovalFlowNodes.length |
| | | ? formApprovalFlowNodes |
| | | : enriched.approvalFlowNodes, |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | | }; |
| | | } |
| | | |
| | | /** å·®æ
表å â FinReimbursementDto */ |
| | | export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) { |
| | | 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 dto = { |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | expenseType: "å·®æ
è´¹", |
| | | applicantId: toNumber(form.applicantId), |
| | | applicantCode: form.employeeNo || form.applicantNo || "", |
| | | applicantName: form.employeeName || form.applicantName || "", |
| | | applicantDeptId: toNumber(form.applicantDeptId), |
| | | applicantDeptName: form.applicantDeptName || form.deptName || "", |
| | | reason: (form.reimburseReason || "").trim(), |
| | | applyAmount, |
| | | detailTotalAmount: detailTotal, |
| | | payeeName: form.payee || "", |
| | | payeeAccount: form.payeeAccount || undefined, |
| | | payeeBank: form.payeeBank || undefined, |
| | | billStatus: "IN_APPROVAL", |
| | | deptId: toNumber(form.deptId), |
| | | travel: { |
| | | startTime: form.travelStartTime || undefined, |
| | | endTime: form.travelEndTime || undefined, |
| | | travelDays, |
| | | departureCity: form.departurePlace || "", |
| | | destinationCity: form.destination || "", |
| | | hotelStandard: toNumber(form.hotelStandard), |
| | | lodgingDays: toNumber(form.hotelDays), |
| | | mealAllowance: toNumber(form.livingSubsidy), |
| | | transportAllowance: toNumber(form.transportSubsidy), |
| | | lodgingLimit: toNumber(form.lodgingLimit), |
| | | standardTag: form.standardTag || (form.needSpecialApproval ? "è¶
æ ç¹æ¹" : "卿 åèå´å
"), |
| | | withinStandard: form.needSpecialApproval ? "0" : "1", |
| | | }, |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |
| | | if (id != null) dto.id = id; |
| | | if (form.billNo || form.reimburseNo) dto.billNo = form.billNo || form.reimburseNo; |
| | | if (form.approvalInstanceId != null) dto.approvalInstanceId = toNumber(form.approvalInstanceId); |
| | | if (form.approveProcessId != null) dto.approveProcessId = toNumber(form.approveProcessId); |
| | | if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id); |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |
| | | /** è´¹ç¨è¡¨å â FinReimbursementDto */ |
| | | export function buildCostReimbursementSaveDto(form) { |
| | | const details = mapDetailsToApi(form.expenseDetails); |
| | | const detailTotal = sumDetailAmount(form.expenseDetails); |
| | | const applyAmount = toNumber(form.applyAmount) ?? detailTotal; |
| | | |
| | | const dto = { |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, |
| | | expenseType: expenseCategoryToType(form.expenseCategory), |
| | | applicantId: toNumber(form.applicantId), |
| | | applicantCode: form.employeeNo || form.applicantNo || "", |
| | | applicantName: form.employeeName || form.applicantName || "", |
| | | applicantDeptId: toNumber(form.applicantDeptId), |
| | | applicantDeptName: form.applicantDeptName || form.deptName || "", |
| | | reason: (form.reimburseReason || "").trim(), |
| | | applyAmount, |
| | | detailTotalAmount: detailTotal, |
| | | payeeName: form.payee || "", |
| | | payeeAccount: form.payeeAccount || "", |
| | | payeeBank: form.bankBranch || form.payeeBank || "", |
| | | billStatus: "IN_APPROVAL", |
| | | deptId: toNumber(form.deptId), |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |
| | | if (id != null) dto.id = id; |
| | | if (form.billNo || form.reimburseNo) dto.billNo = form.billNo || form.reimburseNo; |
| | | if (form.approvalInstanceId != null) dto.approvalInstanceId = toNumber(form.approvalInstanceId); |
| | | if (form.approveProcessId != null) dto.approveProcessId = toNumber(form.approveProcessId); |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |
| | | /** å¡«æ¥é¡µå 载详æ
ï¼ä¸ Web openFormDialog edit ä¸è´ï¼ */ |
| | | export async function fetchFinReimbursementFormDetail(item, moduleKey) { |
| | | const id = resolveReimbursementDeleteId(item); |
| | | if (id == null) throw new Error("missing reimbursement id"); |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | return mapFinReimbursementDetailRow(raw, moduleKey); |
| | | } |