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);
|
}
|