import dayjs from "dayjs"; 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 { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js"; /** 报销类型:1-差旅报销,2-费用报销 */ 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; }); } 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; } /** 详情查询参数(query finReimbursementDto) */ export function buildFinReimbursementDetailParams(id) { const raw = id?.id != null ? id.id : id; const n = toNumber(raw); return { finReimbursementDto: { id: n != null ? n : raw } }; } /** 详情 DTO → 页面行(按 reimbursementType 映射,含 tasks / storageBlobVOList) */ 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 || {}; } return { ...applyFinReimbursementDetailEnrichment(mapped, raw), reimbursementType: type, reimbursementTypeLabel: reimbursementTypeLabel(type), moduleKey: getModuleKeyByReimbursementType(type), }; } /** 单据状态 → 页面 approvalResult(兼容 statusLabel) */ export function mapBillStatusToApprovalResult(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 "paid"; return "pending"; } function pickApplicantQuery(searchForm = {}) { const kw = (searchForm.applicantKeyword || "").trim(); if (!kw) return {}; if (/[\u4e00-\u9fa5]/.test(kw)) return { applicantName: kw }; return { applicantCode: kw }; } /** 组装 listPage 查询参数(page + finReimbursementDto) */ export function buildFinReimbursementListParams({ page, searchForm, reimbursementType, extraDto = {}, }) { const dto = { reimbursementType, ...pickApplicantQuery(searchForm), ...(extraDto && typeof extraDto === "object" ? extraDto : {}), }; if (searchForm?.billStatus) { dto.billStatus = searchForm.billStatus; } const range = searchForm?.createTimeRange ?? searchForm?.applyDateRange ?? (searchForm?.applyTimeFrom || searchForm?.applyTimeTo ? [searchForm.applyTimeFrom, searchForm.applyTimeTo] : null); if (Array.isArray(range) && range[0]) { dto.createTimeStart = range[0]; } if (Array.isArray(range) && range[1]) { dto.createTimeEnd = range[1]; } if (reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL) { if (searchForm?.travelStartFrom) { dto.startTimeStart = searchForm.travelStartFrom; } if (searchForm?.travelEndTo) { dto.endTimeEnd = searchForm.travelEndTo; } } return { page: { current: page.current, size: page.size, }, finReimbursementDto: dto, }; } 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, }; } /** 列表/详情时间展示(ISO → YYYY-MM-DD HH:mm:ss) */ 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"); } /** 接口行 → 差旅报销列表行(兼容 useTravelReimburse 字段) */ export function mapTravelReimbursementRow(row) { if (!row) return {}; const travel = pickTravelFromRow(row); const details = Array.isArray(row.details) ? row.details : []; const base = { ...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, approvalResult: mapBillStatusToApprovalResult(row.billStatus), createTime: formatReimbursementDateTime(row.createTime), expenseDetails: details.map((d) => ({ ...d, expenseSubject: d.expenseCategory, })), travel: row.travel && typeof row.travel === "object" && Object.keys(row.travel).length ? row.travel : travel, details, nodes: row.nodes || [], approvalFlowNodes: mapNodesToFormFlow(row.nodes), tasks: row.tasks || [], }; return base; } /** 接口行 → 费用报销列表行(兼容 useCostReimburse 字段) */ export function mapCostReimbursementRow(row) { if (!row) return {}; 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 || "", expenseCategory: row.expenseType || "", applyAmount: row.applyAmount, applyTime: row.createTime || "", payee: row.payeeName || "", payeeAccount: row.payeeAccount || "", bankBranch: row.payeeBank || "", billStatus: row.billStatus, approvalResult: mapBillStatusToApprovalResult(row.billStatus), createTime: formatReimbursementDateTime(row.createTime), expenseDetails: details.map((d) => ({ ...d, expenseSubject: d.expenseCategory, })), details, nodes: row.nodes || [], approvalFlowNodes: mapNodesToFormFlow(row.nodes), tasks: row.tasks || [], }; } function toNumber(val) { if (val == null || val === "") return undefined; const n = Number(val); return Number.isNaN(n) ? undefined : n; } function expenseSubjectToCategory(subject) { const hit = EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === subject); return hit?.label || subject || ""; } function expenseCategoryToType(category) { const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === category); return hit?.label || category || ""; } /** 接口 nodes → 页面审批流(单审批人节点) */ export function mapNodesToFormFlow(nodes = []) { return (Array.isArray(nodes) ? nodes : []).map((n, i) => { const first = Array.isArray(n.approvers) ? n.approvers[0] : null; 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 ?? "", }; }); } /** 页面审批节点 → 接口 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) => ({ id: a.id, nodeId: a.nodeId, templateId: a.templateId ?? templateId, approverId: toNumber(a.approverId) ?? a.approverId, approverName: a.approverName || "", sortNo: a.sortNo ?? idx + 1, })); } else if (n.approverId != null && n.approverId !== "") { approvers = [ { approverId: toNumber(n.approverId) ?? n.approverId, approverName: n.approverName || "", sortNo: 1, }, ]; } if (!approvers.length) return null; const node = { levelNo: n.levelNo ?? n.nodeOrder ?? 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; return node; }) .filter(Boolean); } 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; } /** 修改时补齐主表与子表关联 ID */ 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; }); } 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; } /** 差旅表单 → 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(form.approvalFlowNodes, 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); 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(form.approvalFlowNodes, 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); } return applyReimbursementRelations(dto); } /** 列表行主键(删除/修改用 fin_reimbursement.id) */ export function resolveReimbursementDeleteId(row) { const raw = row?.reimbursementId ?? row?.id; if (raw == null || raw === "" || String(raw).startsWith("local_")) { return undefined; } const n = toNumber(raw); return n != null ? n : raw; } /** 是否允许删除(审批中、已通过、已付款不可删) */ export function canDeleteReimbursementRow(row) { const key = mapBillStatusToApprovalResult( row?.billStatus ?? row?.approvalResult ?? row?.status ); return key !== "pending" && key !== "approved" && key !== "paid"; } /** 是否允许编辑(与删除规则一致) */ export function canEditReimbursementRow(row) { return canDeleteReimbursementRow(row); } /** 修改场景必须带主键 ID */ export function validateReimbursementPersistDto(dto, isEdit) { if (!isEdit) return { ok: true }; if (dto?.id != null && dto.id !== "") return { ok: true }; return { ok: false, message: "无法修改:缺少报销单 ID" }; }