import { Search } from "@element-plus/icons-vue"; import dayjs from "dayjs"; import { deleteFinReimbursement, getFinReimbursementDetail, listFinReimbursementPage, persistFinReimbursement, } from "@/api/officeProcessAutomation/finReimbursement.js"; import { ElMessageBox } from "element-plus"; 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, mapFinReimbursementDetailRow, resolveReimbursementDeleteId, unwrapFinReimbursementDetail, unwrapFinReimbursementPage, validateReimbursementApprovalNodes, validateReimbursementPersistDto, } from "../shared/finReimbursementMappers.js"; import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; import { EXPENSE_CATEGORY_OPTIONS, CATEGORY_TEMPLATES, EXPENSE_SUBJECT_OPTIONS, expenseCategoryLabel, expenseSubjectLabel, statusLabel, statusTagType, formatApprovalFlowSummary, buildAutoApprovalFlow, getApprovalRuleHint, createEmptyExpenseDetail, createEmptyForm, applyCategoryTemplate, initApprovalFlowNodes, advanceApprovalFlow, rejectApprovalFlow, normalizeImportedRow, } from "./costReimburseUtils.js"; function unwrapArray(payload) { if (Array.isArray(payload)) return payload; if (payload?.data && Array.isArray(payload.data)) return payload.data; if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; return []; } function isActiveUser(u) { if (u.delFlag === "2" || u.delFlag === 2) return false; if (u.status == null) return true; return String(u.status) === "0"; } function demoFlowNodes(amount = 1200, category = "transport") { return buildAutoApprovalFlow(amount, category); } export function useCostReimburse() { const { proxy } = getCurrentInstance(); const allRows = ref([]); const searchForm = reactive({ applicantKeyword: "", deptId: undefined, expenseCategory: "", applyTimeRange: [], }); const tableLoading = ref(false); const page = reactive({ current: 1, size: 10, total: 0 }); const importInputRef = ref(null); 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 }); const detailDialog = reactive({ visible: false }); const detailLoading = ref(false); const detailRow = ref({}); const approveDialog = reactive({ visible: false, row: null }); const approveOpinion = ref(""); const submitSaving = ref(false); const tableData = computed(() => allRows.value.map((r) => ({ ...r, approvalFlowSummary: formatApprovalFlowSummary(r), })) ); async function fetchList() { tableLoading.value = true; try { const res = await listFinReimbursementPage( buildFinReimbursementListParams({ page, searchForm, reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, }) ); const { records, total } = unwrapFinReimbursementPage(res); const filtered = filterRowsByReimbursementType( records, FIN_REIMBURSEMENT_TYPE.COST ); 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; proxy?.$modal?.msgError?.("费用报销列表加载失败"); } finally { tableLoading.value = false; } } const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); const detailTotalAmount = computed(() => { const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); return Math.round(sum * 100) / 100; }); const approvalRuleHint = computed(() => getApprovalRuleHint(form.applyAmount ?? detailTotalAmount.value, form.expenseCategory) ); const tableColumn = ref([ { 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 }, { label: "创建时间", prop: "createTime", width: 165 }, { label: "报销状态", prop: "approvalResult", width: 100, dataType: "tag", formatData: (v) => statusLabel(v), formatType: (v) => statusTagType(v), }, { label: "审批流程", prop: "approvalFlowSummary", minWidth: 200, showOverflowTooltip: true, }, { dataType: "action", label: "操作", align: "center", fixed: "right", width: 220, operation: [ { name: "编辑", type: "text", disabled: (row) => !canEditReimbursementRow(row), clickFun: (row) => openFormDialog("edit", row), }, { name: "详情", type: "text", clickFun: (row) => openDetail(row) }, { name: "删除", type: "danger", disabled: (row) => !canDeleteReimbursementRow(row), clickFun: (row) => confirmRemoveRow(row), }, ], }, ]); 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" }], applyAmount: [{ required: true, message: "请填写报销金额", trigger: "blur" }], payee: [{ required: true, message: "请填写收款人", trigger: "blur" }], payeeAccount: [{ required: true, message: "请填写收款账号", trigger: "blur" }], bankBranch: [{ required: true, message: "请填写开户支行", trigger: "blur" }], approvalFlowNodes: [ { validator: (_r, _v, cb) => { const nodes = form.approvalFlowNodes || []; if (!nodes.length) { cb(new Error("请至少配置一个审批节点")); return; } if (nodes.some((n) => n.approverId == null || n.approverId === "")) { cb(new Error("每个节点须选择审批人")); return; } cb(); }, trigger: "change", }, ], }; async function loadUserPool() { try { 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 || ""; const empNo = employeeNoFromUser(u); return `${nick || name || "用户"} - ${empNo}`; } function userById(id) { return applicantFormOptions.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) : ""); } async function remoteSearchApplicantForm(query) { applicantFormSearchLoading.value = true; try { 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; } } function onApplicantChange(uid) { const u = userById(uid); if (u) { form.employeeName = u.nickName || u.userName || ""; form.employeeNo = employeeNoFromUser(u); form.payee = form.payee || form.employeeName; 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 ); nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); } function onExpenseCategoryChange(val) { if (val && !(form.expenseDetails || []).length) { applyCategoryTemplate(form, val); syncApplyAmountFromDetails(); } autoAssignApprovalFlow(); } function applyTemplate(category) { applyCategoryTemplate(form, category); syncApplyAmountFromDetails(); autoAssignApprovalFlow(); proxy?.$modal?.msgSuccess?.(`已应用「${CATEGORY_TEMPLATES[category]?.label || category}」填报模板`); } function onDetailAmountChange() { syncApplyAmountFromDetails(); autoAssignApprovalFlow(); } function onApprovalFlowChange() { nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); } function addExpenseDetail() { form.expenseDetails.push(createEmptyExpenseDetail()); } function removeExpenseDetail(index) { form.expenseDetails.splice(index, 1); syncApplyAmountFromDetails(); autoAssignApprovalFlow(); } function syncApplyAmountFromDetails() { form.applyAmount = detailTotalAmount.value; } function handleQuery() { page.current = 1; return fetchList(); } function resetSearch() { searchForm.applicantKeyword = ""; handleQuery(); } function pagination(obj) { page.current = obj.page; page.size = obj.limit; return fetchList(); } async function loadCostDetailRow(row) { const id = resolveReimbursementDeleteId(row); if (id == null) { throw new Error("missing id"); } const res = await getFinReimbursementDetail(id); const raw = unwrapFinReimbursementDetail(res); return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.COST); } async function openDetail(row) { const id = resolveReimbursementDeleteId(row); if (id == null) { proxy?.$modal?.msgWarning?.("无法查看详情:缺少报销单 ID"); return; } detailDialog.visible = true; detailLoading.value = true; detailRow.value = { ...row }; try { detailRow.value = await loadCostDetailRow(row); } catch { proxy?.$modal?.msgError?.("加载详情失败"); detailDialog.visible = false; } finally { detailLoading.value = false; } } async function confirmRemoveRow(row) { const id = resolveReimbursementDeleteId(row); if (id == null) { proxy?.$modal?.msgWarning?.("无法删除:缺少报销单 ID"); return; } const title = row.reimburseNo || row.billNo || row.reimburseReason || "该报销单"; try { await ElMessageBox.confirm( `确定要删除「${title}」吗?删除后不可恢复。`, "删除确认", { type: "warning", confirmButtonText: "确定删除", cancelButtonText: "取消", distinguishCancelAndClose: true, autofocus: false, } ); } catch { return; } try { await deleteFinReimbursement([id]); proxy?.$modal?.msgSuccess?.("删除成功"); if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) { detailDialog.visible = false; } await handleQuery(); } catch { proxy?.$modal?.msgError?.("删除失败"); } } function openApprove(row) { approveDialog.row = { ...row }; approveDialog.visible = true; } function approvalActionLabel(v) { if (v === "approved") return "通过"; if (v === "rejected") return "驳回"; return "提交"; } async function openFormDialog(mode, row) { formDialog.mode = mode; formDialog.readonly = false; formDialog.title = mode === "add" ? "新增费用报销" : "编辑费用报销"; if (!allUsersCache.value.length) await loadUserPool(); Object.assign(form, createEmptyForm()); if (mode === "edit" && row) { let editRow = row; try { editRow = await loadCostDetailRow(row); } catch { proxy?.$modal?.msgError?.("加载报销详情失败"); return; } Object.assign(form, buildCostReimbursementSaveDto(editRow)); formDialog.title = "编辑费用报销"; await remoteSearchApplicantForm(""); const u = userById(editRow.applicantId); if (!u) { applicantFormOptions.value.unshift({ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.applicantCode }); } } else { form.approvalFlowNodes = buildAutoApprovalFlow(0, "other"); remoteSearchApplicantForm(""); } formDialog.visible = true; nextTick(() => { formRef.value?.clearValidate?.(); }); } function onFormClosed() { formRef.value?.resetFields?.(); } async function submitForm() { try { await formRef.value?.validate?.(); } catch { return; } if (!(form.expenseDetails || []).length) { proxy?.$modal?.msgWarning?.("请至少添加一条报销明细"); return; } syncApplyAmountFromDetails(); if (submitSaving.value) return; const isEdit = formDialog.mode === "edit"; const dto = buildCostReimbursementSaveDto(form); 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; try { await persistFinReimbursement(dto, isEdit); proxy?.$modal?.msgSuccess?.(isEdit ? "保存成功" : "提交成功"); formDialog.visible = false; await handleQuery(); } catch { proxy?.$modal?.msgError?.(isEdit ? "保存失败" : "提交失败"); } finally { submitSaving.value = false; } } async function submitApprove(result) { const row = approveDialog.row; if (!row) return; if (result === "rejected" && !(approveOpinion.value || "").trim()) { proxy?.$modal?.msgWarning?.("驳回须填写审批意见(如:发票模糊需重传)"); return; } const idx = allRows.value.findIndex((r) => r.id === row.id); if (idx === -1) return; const cur = allRows.value[idx]; const operatorName = "当前审批人"; const record = { operatorName, result, opinion: approveOpinion.value || (result === "approved" ? "同意" : "驳回"), time: dayjs().format("YYYY-MM-DD HH:mm:ss"), }; const records = [...(cur.approvalRecords || []), record]; let flowUpdate; if (result === "approved") { flowUpdate = advanceApprovalFlow(cur, approveOpinion.value); } else { flowUpdate = rejectApprovalFlow(cur, approveOpinion.value); } allRows.value[idx] = { ...cur, approvalFlowNodes: flowUpdate.nodes, currentNodeIndex: flowUpdate.currentNodeIndex, approvalResult: flowUpdate.approvalResult, rejectReason: flowUpdate.rejectReason ?? cur.rejectReason, approvalRecords: records, }; proxy?.$modal?.msgSuccess?.(result === "approved" ? "已通过" : "已驳回"); approveDialog.visible = false; handleQuery(); } function handleExport() { 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() { importInputRef.value?.click?.(); } function onImportFile(e) { const file = e.target.files?.[0]; e.target.value = ""; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = JSON.parse(String(reader.result || "")); const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data; if (!Array.isArray(arr) || !arr.length) { proxy?.$modal?.msgWarning?.("导入格式须为费用报销 JSON 数组"); return; } arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i))); proxy?.$modal?.msgSuccess?.(`成功导入 ${arr.length} 条`); handleQuery(); } catch { proxy?.$modal?.msgError?.("解析失败"); } }; reader.readAsText(file, "utf-8"); } onMounted(async () => { loadUserPool(); loadDeptOptions(); await fetchList(); const editPayload = consumeReimburseEditFromApprove(); if (editPayload?.reimbursementId != null) { await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); } }); return { Search, EXPENSE_CATEGORY_OPTIONS, CATEGORY_TEMPLATES, EXPENSE_SUBJECT_OPTIONS, expenseCategoryLabel, expenseSubjectLabel, searchForm, tableLoading, page, tableData, tableColumn, importInputRef, formRef, form, formDialog, formRules, detailDialog, detailLoading, detailRow, approveDialog, approveOpinion, applicantFormSearchLoading, applicantFormOptions, deptOptions, flowUserOptions, detailTotalAmount, approvalRuleHint, handleQuery, resetSearch, pagination, remoteSearchApplicantForm, userSelectLabel, onApplicantChange, onDeptChange, onExpenseCategoryChange, applyTemplate, onDetailAmountChange, onApprovalFlowChange, addExpenseDetail, removeExpenseDetail, syncApplyAmountFromDetails, autoAssignApprovalFlow, openFormDialog, onFormClosed, submitForm, submitSaving, openDetail, approvalActionLabel, submitApprove, handleExport, handleImportClick, onImportFile, }; }