hsy
18 小时以前 2d3530a7c11557a40807ad7c0b9e302ce6ce3a9f
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
@@ -7,13 +7,16 @@
  persistFinReimbursement,
} from "@/api/officeProcessAutomation/finReimbursement.js";
import { ElMessageBox } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { userListNoPageByTenantId, deptTreeSelect } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
import {
  buildCostReimbursementSaveDto,
  buildFinReimbursementListParams,
  filterReimbursementRowsBySearch,
  hasActiveReimbursementSearch,
  canDeleteReimbursementRow,
  canEditReimbursementRow,
  enrichReimbursementListRowsWithApprovalFlow,
  filterRowsByReimbursementType,
  FIN_REIMBURSEMENT_TYPE,
  mapCostReimbursementRow,
@@ -21,6 +24,7 @@
  resolveReimbursementDeleteId,
  unwrapFinReimbursementDetail,
  unwrapFinReimbursementPage,
  validateReimbursementApprovalNodes,
  validateReimbursementPersistDto,
} from "../shared/finReimbursementMappers.js";
import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
@@ -68,8 +72,9 @@
  const searchForm = reactive({
    applicantKeyword: "",
    applyTimeFrom: "",
    applyTimeTo: "",
    deptId: undefined,
    expenseCategory: "",
    applyTimeRange: [],
  });
  const tableLoading = ref(false);
  const page = reactive({ current: 1, size: 10, total: 0 });
@@ -77,6 +82,7 @@
  const allUsersCache = ref([]);
  const applicantFormSearchLoading = ref(false);
  const applicantFormOptions = ref([]);
  const deptOptions = ref([]);
  const formRef = ref();
  const form = reactive(createEmptyForm());
  const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
@@ -105,11 +111,26 @@
        })
      );
      const { records, total } = unwrapFinReimbursementPage(res);
      allRows.value = filterRowsByReimbursementType(
      const filtered = filterRowsByReimbursementType(
        records,
        FIN_REIMBURSEMENT_TYPE.COST
      ).map(mapCostReimbursementRow);
      page.total = total;
      );
      let mapped = filtered.map(mapCostReimbursementRow);
      mapped = await enrichReimbursementListRowsWithApprovalFlow(
        mapped,
        FIN_REIMBURSEMENT_TYPE.COST
      );
      if (hasActiveReimbursementSearch(searchForm)) {
        mapped = filterReimbursementRowsBySearch(mapped, searchForm);
      }
      allRows.value = mapped;
      const dropped = records.length - filtered.length;
      let nextTotal =
        dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
      if (hasActiveReimbursementSearch(searchForm)) {
        nextTotal = mapped.length;
      }
      page.total = nextTotal;
    } catch {
      allRows.value = [];
      page.total = 0;
@@ -134,6 +155,9 @@
    { label: "报销单号", prop: "reimburseNo", width: 150 },
    { label: "申请人编号", prop: "applicantNo", width: 110 },
    { label: "申请人", prop: "applicantName", minWidth: 90 },
    { label: "部门", prop: "applicantDeptName", minWidth: 100 },
    { label: "费用类型", prop: "expenseCategory", minWidth: 90, formatData: (v) => expenseCategoryLabel(v) },
    { label: "付款方", prop: "payer", minWidth: 100 },
    { label: "报销金额(元)", prop: "applyAmount", width: 110 },
    { label: "报销原因", prop: "reimburseReason", minWidth: 160, showOverflowTooltip: true },
    { label: "申请时间", prop: "applyTime", width: 165 },
@@ -177,6 +201,7 @@
  ]);
  const formRules = {
    deptId: [{ required: true, message: "请选择归属部门", trigger: "change" }],
    applicantId: [{ required: true, message: "请选择员工", trigger: "change" }],
    expenseCategory: [{ required: true, message: "请选择费用类型", trigger: "change" }],
    reimburseReason: [{ required: true, message: "请填写报销原因", trigger: "blur" }],
@@ -205,21 +230,39 @@
  async function loadUserPool() {
    try {
      allUsersCache.value = unwrapArray(await userListNoPageByTenantId());
      allUsersCache.value = unwrapArray(await userListNoPageByTenantId({}));
    } catch {
      allUsersCache.value = [];
    }
  }
  function formatDeptOptions(list) {
    return (list || []).map((d) => ({
      ...d,
      rawLabel: d.label,
      label: d.label, // 部门下拉不要显示id
      children: d.children && d.children.length ? formatDeptOptions(d.children) : undefined,
    }));
  }
  async function loadDeptOptions() {
    try {
      const res = await deptTreeSelect();
      deptOptions.value = formatDeptOptions(res.data || []);
    } catch {
      deptOptions.value = [];
    }
  }
  function userSelectLabel(u) {
    const nick = u.nickName || "";
    const name = u.userName || "";
    if (nick && name && nick !== name) return `${nick}(${name})`;
    return nick || name || `用户${u.userId ?? u.id ?? ""}`;
    const empNo = employeeNoFromUser(u);
    return `${nick || name || "用户"} - ${empNo}`;
  }
  function userById(id) {
    return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
    return applicantFormOptions.value.find((u) => String(u.userId ?? u.id) === String(id));
  }
  function employeeNoFromUser(u) {
@@ -227,22 +270,21 @@
    return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
  }
  function filterUsersByQuery(query) {
    const list = allUsersCache.value.filter(isActiveUser);
    const q = (query || "").trim().toLowerCase();
    if (!q) return [...list];
    return list.filter((u) => {
      const nick = (u.nickName || "").toLowerCase();
      const uname = (u.userName || "").toLowerCase();
      return nick.includes(q) || uname.includes(q);
    });
  }
  async function remoteSearchApplicantForm(query) {
    applicantFormSearchLoading.value = true;
    try {
      if (!allUsersCache.value.length) await loadUserPool();
      applicantFormOptions.value = filterUsersByQuery(query);
      const q = (query || "").trim();
      const params = {};
      if (form.deptId) {
        params.deptId = form.deptId;
      }
      if (q) {
        params.nickName = q; // You can pass nickName to backend
      }
      const res = await userListNoPageByTenantId(params);
      applicantFormOptions.value = unwrapArray(res).filter(isActiveUser);
    } catch {
      applicantFormOptions.value = [];
    } finally {
      applicantFormSearchLoading.value = false;
    }
@@ -254,17 +296,56 @@
      form.employeeName = u.nickName || u.userName || "";
      form.employeeNo = employeeNoFromUser(u);
      form.payee = form.payee || form.employeeName;
      form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
      form.deptName = u.dept?.deptName ?? u.deptName ?? "";
      const userDeptId = String(u.deptId ?? u.sysDeptId ?? "");
      if (userDeptId && form.deptId !== userDeptId) {
        form.deptId = userDeptId;
        const name = findDeptLabel(deptOptions.value, userDeptId);
        form.deptName = name || (u.dept?.deptName ?? u.deptName ?? "");
      }
    } else {
      form.employeeName = "";
      form.employeeNo = "";
    }
  }
  function findDeptLabel(nodes, id) {
    if (!nodes) return null;
    for (const node of nodes) {
      if (String(node.id) === String(id)) return node.rawLabel || node.label;
      if (node.children) {
        const hit = findDeptLabel(node.children, id);
        if (hit) return hit;
      }
    }
    return null;
  }
  function onDeptChange(deptId) {
    if (deptId) {
      const name = findDeptLabel(deptOptions.value, deptId);
      if (name) form.deptName = name;
      remoteSearchApplicantForm("");
      if (form.applicantId) {
        const u = userById(form.applicantId);
        if (u && String(u.deptId ?? u.sysDeptId) !== String(deptId)) {
          form.applicantId = "";
          onApplicantChange("");
        }
      }
    } else {
      form.deptName = "";
      remoteSearchApplicantForm("");
    }
  }
  function autoAssignApprovalFlow() {
    const amount = Number(form.applyAmount) || detailTotalAmount.value || 0;
    form.approvalFlowNodes = buildAutoApprovalFlow(amount, form.expenseCategory || "other");
    form.approvalFlowNodes = buildAutoApprovalFlow(
      amount,
      form.expenseCategory || "other",
      form.approvalFlowNodes
    );
    nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
  }
@@ -313,8 +394,6 @@
  function resetSearch() {
    searchForm.applicantKeyword = "";
    searchForm.applyTimeFrom = "";
    searchForm.applyTimeTo = "";
    handleQuery();
  }
@@ -412,17 +491,13 @@
        proxy?.$modal?.msgError?.("加载报销详情失败");
        return;
      }
      Object.assign(form, {
        ...JSON.parse(JSON.stringify(editRow)),
        reimbursementId: editRow.reimbursementId ?? editRow.id,
        attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])),
        approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])),
        expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])),
      });
      Object.assign(form, buildCostReimbursementSaveDto(editRow));
      formDialog.title = "编辑费用报销";
      await remoteSearchApplicantForm("");
      const u = userById(editRow.applicantId);
      applicantFormOptions.value = u
        ? [u]
        : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }];
      if (!u) {
        applicantFormOptions.value.unshift({ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.applicantCode });
      }
    } else {
      form.approvalFlowNodes = buildAutoApprovalFlow(0, "other");
      remoteSearchApplicantForm("");
@@ -448,7 +523,6 @@
      return;
    }
    syncApplyAmountFromDetails();
    autoAssignApprovalFlow();
    if (submitSaving.value) return;
    const isEdit = formDialog.mode === "edit";
@@ -456,6 +530,11 @@
    const check = validateReimbursementPersistDto(dto, isEdit);
    if (!check.ok) {
      proxy?.$modal?.msgWarning?.(check.message);
      return;
    }
    const nodeCheck = validateReimbursementApprovalNodes(dto);
    if (!nodeCheck.ok) {
      proxy?.$modal?.msgWarning?.(nodeCheck.message);
      return;
    }
    submitSaving.value = true;
@@ -509,15 +588,17 @@
  }
  function handleExport() {
    const data = allRows.value;
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `费用报销导出_${dayjs().format("YYYYMMDDHHmmss")}.json`;
    a.click();
    URL.revokeObjectURL(url);
    proxy?.$modal?.msgSuccess?.(`已导出 ${data.length} 条`);
    const params = buildFinReimbursementListParams({
      page: { current: 1, size: 99999 }, // Just so the function doesn't crash, backend export ignores pagination anyway
      searchForm,
      reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
    });
    // Remove pagination parameters since export doesn't need them
    delete params.current;
    delete params.size;
    delete params["page.current"];
    delete params["page.size"];
    proxy.download("/finReimbursement/export", params, `费用报销导出_${dayjs().format("YYYYMMDDHHmmss")}.xlsx`);
  }
  function handleImportClick() {
@@ -549,6 +630,7 @@
  onMounted(async () => {
    loadUserPool();
    loadDeptOptions();
    await fetchList();
    const editPayload = consumeReimburseEditFromApprove();
    if (editPayload?.reimbursementId != null) {
@@ -580,6 +662,7 @@
    approveOpinion,
    applicantFormSearchLoading,
    applicantFormOptions,
    deptOptions,
    flowUserOptions,
    detailTotalAmount,
    approvalRuleHint,
@@ -589,6 +672,7 @@
    remoteSearchApplicantForm,
    userSelectLabel,
    onApplicantChange,
    onDeptChange,
    onExpenseCategoryChange,
    applyTemplate,
    onDetailAmountChange,