1
yyb
13 小时以前 69b917fa605be8ccd0984e5c095f24d6476dce95
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,904 @@
import dayjs from "dayjs";
import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js";
import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js";
import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js";
import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js";
/** æŠ¥é”€ç±»åž‹ï¼š1-差旅报销,2-费用报销 */
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 || {};
  }
  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),
  };
}
/** å•据状态 â†’ é¡µé¢ 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 {};
  // å ä½ã€Œå§“名或编号」:姓名走 applicantName;编号另传 applicantCode
  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;
  }
}
/** ç»„装 listPage æŸ¥è¯¢å‚数(扁平 page.* / finReimbursementDto.*,与 detail æŽ¥å£ä¸€è‡´ï¼‰ */
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,
  };
}
/** åˆ—表/详情时间展示(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: resolveRowApiNodes(row),
    approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)),
    tasks: row.tasks || [],
    approvalFlowSummary: buildApprovalFlowSummaryForRow(row),
  };
  return base;
}
/** æŽ¥å£è¡Œ â†’ è´¹ç”¨æŠ¥é”€åˆ—表行(兼容 useCostReimburse å­—段) */
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: row.expenseType || "",
    applyAmount: row.applyAmount,
    applyTime: formatReimbursementDateTime(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: apiNodes,
    approvalFlowNodes,
    tasks: row.tasks || [],
    approvalFlowSummary: buildApprovalFlowSummaryForRow({
      ...row,
      nodes: apiNodes,
      approvalFlowNodes,
    }),
  };
}
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 || "";
}
/** åˆ—表/详情行上的审批节点(listPage å¸¸ä¸è¿”回,需详情补全) */
export function resolveRowApiNodes(row) {
  if (!row || typeof row !== "object") return [];
  const list =
    row.nodes ||
    row.flowNodes ||
    row.approveNodes ||
    row.finReimbursementNodes ||
    row.nodeList ||
    row.reimbursementNodeList ||
    [];
  return Array.isArray(list) ? list : [];
}
function sortFlowNodesByLevel(nodes = []) {
  return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => {
    const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0);
    const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0);
    return la - lb;
  });
}
function formatApiNodeApproverLabel(node, index) {
  if (!node || typeof node !== "object") return "";
  const approvers = Array.isArray(node.approvers) ? node.approvers : [];
  const names = approvers
    .map((a) => (a?.approverName || "").trim())
    .filter(Boolean);
  if (names.length) return names.join("/");
  return (node.approverName || "").trim() || `节点${index + 1}`;
}
/** æŽ¥å£ nodes â†’ é¡µé¢å®¡æ‰¹æµï¼ˆå•审批人节点) */
export function mapNodesToFormFlow(nodes = []) {
  return 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 ??
        null,
      approverName:
        names.join("、") || first?.approverName || n.approverName || "",
      nodeStatus: n.nodeStatus,
    };
  });
}
function formatTasksToFlowSummary(tasks = []) {
  const list = sortFlowNodesByLevel(
    (Array.isArray(tasks) ? tasks : []).map((t, i) => ({
      levelNo: t.levelNo ?? t.taskLevel ?? i + 1,
      approverName:
        (t.approverName || t.operatorName || t.createUserName || "").trim() ||
        "",
    }))
  );
  const parts = list.map((t) => t.approverName).filter(Boolean);
  return parts.length ? parts.join(" â†’ ") : "";
}
function buildApprovalFlowSummaryForRow(row) {
  const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row));
  let flowNodes =
    row?.approvalFlowNodes?.length > 0
      ? sortFlowNodesByLevel(row.approvalFlowNodes)
      : mapNodesToFormFlow(apiNodes);
  if (!flowNodes.length && apiNodes.length) {
    const line = apiNodes
      .map((n, i) => formatApiNodeApproverLabel(n, i))
      .filter(Boolean)
      .join(" â†’ ");
    if (line) return line;
  }
  if (!flowNodes.length) {
    const fromTasks = formatTasksToFlowSummary(row?.tasks);
    if (fromTasks) return fromTasks;
    return "—";
  }
  return flowNodes
    .map((n, i) => {
      const name = (n.approverName || "").trim() || `节点${i + 1}`;
      if (n.nodeStatus === "finish") return `${name}✓`;
      if (n.nodeStatus === "error") return `${name}✗`;
      if (n.nodeStatus === "process") return `${name}…`;
      return name;
    })
    .join(" â†’ ");
}
/** åˆ—表「审批流程」列文案 */
export function formatApprovalFlowSummary(row) {
  return buildApprovalFlowSummaryForRow(row);
}
/** listPage å¸¸ä¸å¸¦å®Œæ•´ nodes,列表加载后统一拉详情补全多级审批流程 */
export async function enrichReimbursementListRowsWithApprovalFlow(
  rows,
  reimbursementType
) {
  const list = Array.isArray(rows) ? rows : [];
  if (!list.length) return list;
  const needIds = list
    .map((r) => resolveReimbursementDeleteId(r))
    .filter((id) => id != null);
  if (!needIds.length) return list;
  const detailById = new Map();
  await Promise.all(
    needIds.map(async (id) => {
      try {
        const res = await getFinReimbursementDetail(id);
        detailById.set(String(id), unwrapFinReimbursementDetail(res));
      } catch {
        /* å•行失败不影响列表 */
      }
    })
  );
  const mapRow =
    reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL
      ? mapTravelReimbursementRow
      : mapCostReimbursementRow;
  return list.map((row) => {
    const id = resolveReimbursementDeleteId(row);
    const detail = id != null ? detailById.get(String(id)) : null;
    if (!detail) return row;
    const merged = {
      ...row,
      ...detail,
      id: row.id ?? detail.id,
      reimbursementId: row.reimbursementId ?? row.id ?? detail.id,
      reimbursementType: detail.reimbursementType ?? row.reimbursementType,
    };
    return mapRow(merged);
  });
}
/** è¡¨å•上的审批流(兼容 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;
}
/** ä¿®æ”¹æ—¶è¡¥é½ä¸»è¡¨ä¸Žå­è¡¨å…³è” 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;
    });
  }
  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;
}
/** å·®æ—…表单 â†’ 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);
}
/** åˆ—表行主键(删除/修改用 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" };
}