gaoluyang
2026-06-16 2f58128ac3b1c025a32f64016328992bf9bf5b48
src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,448 @@
import { computed, reactive, ref } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user";
import useUserStore from "@/store/modules/user";
import { persistFinReimbursement } from "@/api/oa/finReimbursement.js";
import {
  isActiveUser,
  unwrapUserList,
  userAvatarColor,
  userSelectLabel,
  userSubLabel,
} from "../../_utils/userPickerUtils.js";
import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
import {
  buildCostReimbursementSaveDto,
  buildTravelReimbursementSaveDto,
  fetchFinReimbursementFormDetail,
  getReimbursementTypeByModuleKey,
  validateReimbursementApprovalNodes,
  validateReimbursementPersistDto,
} from "../../_utils/finReimbursementMappers.js";
import {
  applyCategoryTemplate,
  createEmptyCostForm,
  EXPENSE_CATEGORY_OPTIONS,
  EXPENSE_SUBJECT_OPTIONS as COST_SUBJECT_OPTIONS,
  createEmptyExpenseDetail as createCostDetail,
} from "../_utils/costReimburseUtils.js";
import {
  computeTravelDays,
  createEmptyExpenseDetail,
  createEmptyTravelForm,
  detectTravelTier,
  EXPENSE_SUBJECT_OPTIONS,
  getTravelStandardByTier,
} from "../_utils/travelReimburseUtils.js";
// å»¶è¿Ÿåˆå§‹åŒ– userStore,避免在模块加载时调用
let userStore = null;
function getUserStore() {
  if (!userStore) {
    userStore = useUserStore();
  }
  return userStore;
}
function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) {
  const warnings = [];
  const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 };
  (f.expenseDetails || []).forEach(d => {
    const key = d.expenseSubject || "other";
    bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0);
  });
  if (bySubject.transport > transportLimit && transportLimit > 0) {
    warnings.push(`交通费 ${bySubject.transport} å…ƒè¶…出标准 ${transportLimit} å…ƒ`);
  }
  if (bySubject.hotel > hotelLimit && hotelLimit > 0) {
    warnings.push(`住宿费 ${bySubject.hotel} å…ƒè¶…出限额 ${hotelLimit} å…ƒ`);
  }
  if (bySubject.meal > mealLimit && mealLimit > 0) {
    warnings.push(`餐饮费 ${bySubject.meal} å…ƒè¶…出生活补贴建议 ${mealLimit} å…ƒ`);
  }
  const std = getTravelStandardByTier(f.travelTier);
  if (Number(f.hotelStandard) > std.hotelPerNight) {
    warnings.push(`酒店标准 ${f.hotelStandard} å…ƒ/晚高于${std.label}标准 ${std.hotelPerNight} å…ƒ/晚`);
  }
  const apply = Number(f.applyAmount) || detailTotal;
  const standardTotal = transportLimit + hotelLimit + mealLimit;
  if (apply > standardTotal && standardTotal > 0) {
    warnings.push(`申请总额 ${apply} å…ƒé«˜äºŽå·®æ—…标准合计约 ${standardTotal} å…ƒ`);
  }
  return warnings;
}
export function useFinReimburseForm(moduleKeyRef, modeRef) {
  const isTravel = computed(
    () => moduleKeyRef.value === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE
  );
  const form = reactive(
    moduleKeyRef.value === APPROVAL_MODULE_KEYS.COST_REIMBURSE
      ? createEmptyCostForm()
      : createEmptyTravelForm()
  );
  const submitting = ref(false);
  const loading = ref(false);
  const allUsersCache = ref([]);
  const showApplicantPicker = ref(false);
  const applicantDisplaySub = computed(() => {
    if (!form.applicantId) return "点击选择申请人";
    const u = userById(form.applicantId);
    if (u) return userSubLabel(u) || form.employeeNo || "";
    return form.employeeNo ? `工号 ${form.employeeNo}` : "";
  });
  const applicantAvatarColor = computed(() =>
    userAvatarColor(form.employeeName || form.employeeNo || "")
  );
  const showCategorySheet = ref(false);
  const showSubjectSheet = ref(false);
  const editingDetailIndex = ref(-1);
  const pickApplicantId = ref("");
  const pickCategoryValue = ref("");
  const pickSubjectValue = ref("");
  const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
  const travelDaysDisplay = computed(() => {
    if (!isTravel.value) return "";
    const d = computeTravelDays(form.travelStartTime, form.travelEndTime);
    return d == null ? "" : String(d);
  });
  const travelTierLabel = computed(() => {
    if (!isTravel.value) return "";
    const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination));
    return `按${std.label}标准`;
  });
  const suggestedLivingSubsidy = computed(() => {
    const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0;
    const std = getTravelStandardByTier(form.travelTier);
    return Math.round(std.mealPerDay * days * 100) / 100;
  });
  const suggestedTransportSubsidy = computed(() => {
    const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0;
    const std = getTravelStandardByTier(form.travelTier);
    return Math.round(std.transportPerDay * days * 100) / 100;
  });
  const suggestedHotelLimit = computed(() => {
    const nights = form.hotelDays || 0;
    const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight;
    return Math.round(perNight * nights * 100) / 100;
  });
  const detailTotalAmount = computed(() => {
    const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
    return Math.round(sum * 100) / 100;
  });
  const overBudgetWarnings = computed(() => {
    if (!isTravel.value) return [];
    return buildOverBudgetWarnings(
      form,
      detailTotalAmount.value,
      suggestedHotelLimit.value,
      suggestedTransportSubsidy.value,
      suggestedLivingSubsidy.value
    );
  });
  const expenseSubjectOptions = computed(() =>
    isTravel.value ? EXPENSE_SUBJECT_OPTIONS : COST_SUBJECT_OPTIONS
  );
  const categoryActions = computed(() =>
    EXPENSE_CATEGORY_OPTIONS.map(x => ({ name: x.label, value: x.value }))
  );
  const categoryLabel = computed(() => {
    const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.value === form.expenseCategory);
    return hit?.label || "请选择费用类型";
  });
  async function loadUserPool() {
    try {
      allUsersCache.value = unwrapUserList(await userListNoPageByTenantId());
    } catch {
      allUsersCache.value = [];
    }
  }
  function userLabel(u) {
    return userSelectLabel(u);
  }
  function userById(id) {
    return allUsersCache.value.find(u => String(u.userId ?? u.id) === String(id));
  }
  function employeeNoFromUser(u) {
    if (!u) return "";
    return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
  }
  function fillApplicantFromUser(u) {
    if (!u) return;
    form.applicantId = u.userId ?? u.id;
    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 ?? "";
  }
  function onApplicantPicked(uidOrUser) {
    const u =
      typeof uidOrUser === "object" && uidOrUser
        ? uidOrUser
        : userById(uidOrUser);
    fillApplicantFromUser(u);
  }
  /** æ–°å¢žæ—¶é»˜è®¤å¸¦å‡ºå½“前登录人,减少选人步骤 */
  function tryApplyCurrentUser() {
    if (modeRef.value === "edit" || form.applicantId) return;
    const store = getUserStore();
    const id = store.id;
    if (!id) return;
    let u = userById(id);
    if (!u) {
      u = {
        userId: id,
        nickName: store.nickName,
        userName: store.name,
      };
    }
    fillApplicantFromUser(u);
  }
  function recalcTravelStandards() {
    if (!isTravel.value) return;
    form.travelTier = detectTravelTier(form.destination);
    const std = getTravelStandardByTier(form.travelTier);
    if (form.hotelStandard == null || form.hotelStandard === "" || form.hotelStandard === 0) {
      form.hotelStandard = std.hotelPerNight;
    }
    const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
    if (days != null) {
      form.travelDays = days;
      if (form.hotelDays == null || form.hotelDays === "") {
        form.hotelDays = Math.max(0, days - 1);
      }
      if (form.livingSubsidy == null || form.livingSubsidy === "" || form.livingSubsidy === 0) {
        form.livingSubsidy = suggestedLivingSubsidy.value;
      }
    }
    form.needSpecialApproval = overBudgetWarnings.value.length > 0;
  }
  function syncApplyAmountFromDetails() {
    form.applyAmount = detailTotalAmount.value;
    recalcTravelStandards();
  }
  function addExpenseDetail() {
    const row = isTravel.value ? createEmptyExpenseDetail() : createCostDetail();
    form.expenseDetails.push(row);
  }
  function removeExpenseDetail(index) {
    form.expenseDetails.splice(index, 1);
    recalcTravelStandards();
  }
  function applyTemplate(category) {
    applyCategoryTemplate(form, category);
    syncApplyAmountFromDetails();
  }
  function resetFormForModule() {
    const empty = isTravel.value ? createEmptyTravelForm() : createEmptyCostForm();
    Object.keys(form).forEach(k => delete form[k]);
    Object.assign(form, empty);
    if (!form.approvalFlowNodes?.length) {
      form.approvalFlowNodes = [
        { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
      ];
    }
  }
  async function loadEdit(reimbursementId) {
    loading.value = true;
    try {
      if (!allUsersCache.value.length) await loadUserPool();
      const row = await fetchFinReimbursementFormDetail(
        { reimbursementId },
        moduleKeyRef.value
      );
      if (row?.moduleKey && row.moduleKey !== moduleKeyRef.value) {
        moduleKeyRef.value = row.moduleKey;
      }
      Object.assign(form, JSON.parse(JSON.stringify(row)), {
        reimbursementId: row.reimbursementId ?? row.id,
        expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])),
        approvalFlowNodes: JSON.parse(
          JSON.stringify(
            row.approvalFlowNodes?.length
              ? row.approvalFlowNodes
              : [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }]
          )
        ),
        attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
      });
      if (!isTravel.value && form.expenseCategory) {
        /* å·²ç”± mapCost è½¬ä¸º value */
      }
      recalcTravelStandards();
    } finally {
      loading.value = false;
    }
  }
  async function initForm() {
    resetFormForModule();
    if (!allUsersCache.value.length) await loadUserPool();
    if (modeRef.value !== "edit") {
      form.approvalFlowNodes = [
        { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
      ];
      tryApplyCurrentUser();
    }
  }
  function validateForm() {
    if (!form.applicantId) {
      uni.showToast({ title: "请选择员工", icon: "none" });
      return false;
    }
    if (!(form.reimburseReason || "").trim()) {
      uni.showToast({ title: "请填写报销原因", icon: "none" });
      return false;
    }
    if (isTravel.value) {
      if (!form.travelStartTime) {
        uni.showToast({ title: "请选择出差开始时间", icon: "none" });
        return false;
      }
      if (!form.travelEndTime) {
        uni.showToast({ title: "请选择出差结束时间", icon: "none" });
        return false;
      }
      if (computeTravelDays(form.travelStartTime, form.travelEndTime) == null) {
        uni.showToast({ title: "结束时间须晚于开始时间", icon: "none" });
        return false;
      }
      if (!(form.departurePlace || "").trim()) {
        uni.showToast({ title: "请填写出差地", icon: "none" });
        return false;
      }
      if (!(form.destination || "").trim()) {
        uni.showToast({ title: "请填写目的地", icon: "none" });
        return false;
      }
    } else if (!form.expenseCategory) {
      uni.showToast({ title: "请选择费用类型", icon: "none" });
      return false;
    }
    if (form.applyAmount === "" || form.applyAmount == null) {
      uni.showToast({ title: "请填写申请金额", icon: "none" });
      return false;
    }
    if (!(form.payee || "").trim()) {
      uni.showToast({ title: "请填写收款人", icon: "none" });
      return false;
    }
    if (!(form.expenseDetails || []).length) {
      uni.showToast({ title: "请至少添加一条报销明细", icon: "none" });
      return false;
    }
    const nodes = form.approvalFlowNodes || [];
    if (!nodes.length || nodes.some(n => n.approverId == null || n.approverId === "")) {
      uni.showToast({ title: "每个审批节点须选择审批人", icon: "none" });
      return false;
    }
    return true;
  }
  async function submitForm() {
    if (!validateForm()) return;
    recalcTravelStandards();
    if (isTravel.value && form.needSpecialApproval) {
      const ok = await new Promise(resolve => {
        uni.showModal({
          title: "超支提醒",
          content: "存在超支项,提交后将标记为需特批,是否继续?",
          success: r => resolve(!!r.confirm),
        });
      });
      if (!ok) return;
    }
    const isEdit = modeRef.value === "edit";
    const dto = isTravel.value
      ? buildTravelReimbursementSaveDto(form, { computeTravelDays })
      : buildCostReimbursementSaveDto(form);
    const check = validateReimbursementPersistDto(dto, isEdit);
    if (!check.ok) {
      uni.showToast({ title: check.message, icon: "none" });
      return;
    }
    const nodeCheck = validateReimbursementApprovalNodes(dto);
    if (!nodeCheck.ok) {
      uni.showToast({ title: nodeCheck.message, icon: "none" });
      return;
    }
    submitting.value = true;
    try {
      await persistFinReimbursement(dto, isEdit);
      uni.showToast({ title: isEdit ? "保存成功" : "提交成功", icon: "success" });
      return true;
    } catch {
      uni.showToast({ title: isEdit ? "保存失败" : "提交失败", icon: "none" });
      return false;
    } finally {
      submitting.value = false;
    }
  }
  return {
    form,
    isTravel,
    submitting,
    loading,
    flowUserOptions,
    travelDaysDisplay,
    travelTierLabel,
    suggestedLivingSubsidy,
    suggestedTransportSubsidy,
    suggestedHotelLimit,
    detailTotalAmount,
    overBudgetWarnings,
    expenseSubjectOptions,
    categoryActions,
    categoryLabel,
    showApplicantPicker,
    applicantDisplaySub,
    applicantAvatarColor,
    showCategorySheet,
    showSubjectSheet,
    editingDetailIndex,
    pickCategoryValue,
    pickSubjectValue,
    loadUserPool,
    userLabel,
    onApplicantPicked,
    tryApplyCurrentUser,
    recalcTravelStandards,
    syncApplyAmountFromDetails,
    addExpenseDetail,
    removeExpenseDetail,
    applyTemplate,
    initForm,
    loadEdit,
    submitForm,
    getReimbursementTypeByModuleKey,
  };
}