| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å页æ¥è¯¢è´¢å¡æ¥é GET /finReimbursement/listPage */ |
| | | export function listFinReimbursementPage(params) { |
| | | return request({ |
| | | url: "/finReimbursement/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** 详æ
queryï¼Spring ç»å® finReimbursementDto.idï¼å¿ç¨ finReimbursementDto[id] */ |
| | | function buildFinReimbursementDetailParams(idOrDto) { |
| | | const raw = |
| | | typeof idOrDto === "object" && idOrDto !== null |
| | | ? idOrDto.id ?? idOrDto.reimbursementId |
| | | : idOrDto; |
| | | return { |
| | | "finReimbursementDto.id": raw, |
| | | id: raw, |
| | | }; |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¢å¡æ¥é详æ
GET /finReimbursement/detail */ |
| | | export function getFinReimbursementDetail(idOrDto) { |
| | | return request({ |
| | | url: "/finReimbursement/detail", |
| | | method: "get", |
| | | params: buildFinReimbursementDetailParams(idOrDto), |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢è´¢å¡æ¥é POST /finReimbursement/save */ |
| | | export function saveFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/save", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¢å¡æ¥é POST /finReimbursement/update */ |
| | | export function updateFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/update", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤è´¢å¡æ¥é DELETE /finReimbursement/deleteï¼body 为 ID æ°ç»ï¼ */ |
| | | export function deleteFinReimbursement(ids) { |
| | | const idList = (Array.isArray(ids) ? ids : [ids]).filter( |
| | | (id) => id != null && id !== "" |
| | | ); |
| | | return request({ |
| | | url: "/finReimbursement/delete", |
| | | method: "delete", |
| | | data: idList, |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢èµ° saveï¼ä¿®æ¹èµ° updateï¼ä¸æ¥å£ææ¡£ä¸è´ï¼ */ |
| | | export function persistFinReimbursement(finReimbursementDto, isEdit = false) { |
| | | if (isEdit) { |
| | | return updateFinReimbursement(finReimbursementDto); |
| | | } |
| | | const payload = { ...finReimbursementDto }; |
| | | delete payload.id; |
| | | return saveFinReimbursement(payload); |
| | | } |
| | |
| | | applicantId: row.applicantId, |
| | | applicantNo: row.applicantId != null ? String(row.applicantId) : "", |
| | | applicantName: row.applicantName || "", |
| | | approvalType: row.templateName || "", |
| | | approvalType: row.approvalType || row.templateName || "", |
| | | unread: Boolean(row.isApprove) && approvalStatus === "pending", |
| | | isApprove: Boolean(row.isApprove), |
| | | approvalStatus, |
| | |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å·®æ
/è´¹ç¨æ¥é详æ
ï¼å®¡æ¹åè¡¨ï¼ --> |
| | | <el-dialog |
| | | v-model="reimburseDialog.visible" |
| | | :title="reimburseDialog.mode === 'approve' ? reimburseApproveTitle : reimburseDetailTitle" |
| | | width="1000px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <FinReimburseApprovePanel |
| | | :mode="reimburseDialog.mode" |
| | | :module-key="reimburseDialog.moduleKey" |
| | | :reimburse-row="reimburseDialog.reimburseRow" |
| | | :loading="reimburseDialog.loading" |
| | | v-model:approve-opinion="approveOpinion" |
| | | /> |
| | | <template #footer> |
| | | <template v-if="reimburseDialog.mode === 'approve'"> |
| | | <el-button |
| | | type="success" |
| | | :loading="approveSubmitting" |
| | | @click="onReimburseApprove('approved')" |
| | | > |
| | | é è¿ |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | :loading="approveSubmitting" |
| | | @click="onReimburseApprove('rejected')" |
| | | > |
| | | 驳 å |
| | | </el-button> |
| | | <el-button :disabled="approveSubmitting" @click="reimburseDialog.visible = false"> |
| | | å æ¶ |
| | | </el-button> |
| | | </template> |
| | | <template v-else> |
| | | <el-button |
| | | v-if="reimburseDialog.instanceRow?.approvalStatus === 'pending'" |
| | | @click="openEditFromReimburseDetail" |
| | | > |
| | | ä¿® æ¹ |
| | | </el-button> |
| | | <el-button |
| | | v-if=" |
| | | reimburseDialog.instanceRow?.approvalStatus === 'pending' && |
| | | reimburseDialog.instanceRow?.isApprove |
| | | " |
| | | type="primary" |
| | | @click="openReimburseApproveFromDetail" |
| | | > |
| | | å»å®¡æ¹ |
| | | </el-button> |
| | | <el-button type="primary" @click="reimburseDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å®¡æ¹æä½ --> |
| | | <el-dialog |
| | | v-model="approveDialog.visible" |
| | |
| | | <script setup> |
| | | import { Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted, ref } from "vue"; |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../approve-shared/approvalModuleRegistry.js"; |
| | | import FinReimburseApprovePanel from "../../ReimburseManage/shared/components/FinReimburseApprovePanel.vue"; |
| | | import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue"; |
| | | import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue"; |
| | | import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js"; |
| | |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitReimburseApprove, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "审æ¹å·²æäº¤"); |
| | | } |
| | | |
| | | const reimburseDetailTitle = computed(() => |
| | | reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "è´¹ç¨æ¥é详æ
" |
| | | : "å·®æ
æ¥é详æ
" |
| | | ); |
| | | const reimburseApproveTitle = computed(() => |
| | | reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "è´¹ç¨æ¥é审æ¹" |
| | | : "å·®æ
æ¥é审æ¹" |
| | | ); |
| | | |
| | | async function onApprove(result) { |
| | | const ret = await submitApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("é©³åæ¶è¯·å¡«åå®¡æ¹æè§"); |
| | | return; |
| | | } |
| | | if (ret?.ok) { |
| | | ElMessage.success(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | } |
| | | } |
| | | |
| | | async function onReimburseApprove(result) { |
| | | const ret = await submitReimburseApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("é©³åæ¶è¯·å¡«åå®¡æ¹æè§"); |
| | | return; |
| | |
| | | return formatDisplayTime(time) || "â"; |
| | | } |
| | | |
| | | function openApproveFromDetail() { |
| | | async function openApproveFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openApprove(row); |
| | | await openApprove(row); |
| | | } |
| | | |
| | | function openEditFromDetail() { |
| | |
| | | openEditDialog(row); |
| | | } |
| | | |
| | | function openEditFromReimburseDetail() { |
| | | const row = reimburseDialog.instanceRow; |
| | | reimburseDialog.visible = false; |
| | | if (row) openEditDialog(row); |
| | | } |
| | | |
| | | async function openReimburseApproveFromDetail() { |
| | | const row = reimburseDialog.instanceRow; |
| | | if (!row) return; |
| | | reimburseDialog.mode = "approve"; |
| | | approveOpinion.value = ""; |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadFlowUsers(); |
| | | loadSearchBusinessTypeOptions(); |
| | |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { computed, getCurrentInstance, reactive, ref } from "vue"; |
| | | import { |
| | | inferReimburseModuleKeyFromInstance, |
| | | loadReimburseDetailForInstance, |
| | | navigateToReimburseManageForEdit, |
| | | resolveFinReimbursementIdFromInstance, |
| | | } from "../../ReimburseManage/shared/reimburseApproveBridge.js"; |
| | | import { |
| | | fetchBusinessTypeOptions, |
| | | formatDisplayTime, |
| | |
| | | } from "./approveListConstants.js"; |
| | | |
| | | export function useApproveList() { |
| | | const { proxy } = getCurrentInstance() || {}; |
| | | const userStore = useUserStore(); |
| | | |
| | | const tableData = ref([]); |
| | |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const approveSubmitting = ref(false); |
| | | |
| | | /** å·®æ
/è´¹ç¨æ¥éä¸ç¨è¯¦æ
ã审æ¹å¼¹çª */ |
| | | const reimburseDialog = reactive({ |
| | | visible: false, |
| | | mode: "detail", |
| | | moduleKey: "", |
| | | loading: false, |
| | | reimburseRow: {}, |
| | | instanceRow: null, |
| | | }); |
| | | |
| | | const submitDialog = reactive({ visible: false, step: 1, mode: "add" }); |
| | | const submitEditRow = ref(null); |
| | |
| | | fetchApprovalList(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | async function openReimburseDetail(row, mode) { |
| | | const moduleKey = inferReimburseModuleKeyFromInstance(row); |
| | | if (!moduleKey) return false; |
| | | reimburseDialog.mode = mode; |
| | | reimburseDialog.moduleKey = moduleKey; |
| | | reimburseDialog.instanceRow = row; |
| | | reimburseDialog.visible = true; |
| | | reimburseDialog.loading = true; |
| | | reimburseDialog.reimburseRow = {}; |
| | | try { |
| | | const { reimburseRow, moduleKey: resolvedMk } = |
| | | await loadReimburseDetailForInstance(row, moduleKey); |
| | | reimburseDialog.moduleKey = resolvedMk || moduleKey; |
| | | reimburseDialog.reimburseRow = reimburseRow; |
| | | return true; |
| | | } catch { |
| | | ElMessage.error("å è½½æ¥é详æ
失败"); |
| | | reimburseDialog.visible = false; |
| | | return false; |
| | | } finally { |
| | | reimburseDialog.loading = false; |
| | | } |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | if (isReimburseApprovalInstance(row)) { |
| | | await openReimburseDetail(row, "detail"); |
| | | return; |
| | | } |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openApprove(row) { |
| | | async function openApprove(row) { |
| | | if (inferReimburseModuleKeyFromInstance(row)) { |
| | | approveOpinion.value = ""; |
| | | await openReimburseDetail(row, "approve"); |
| | | return; |
| | | } |
| | | approveDialog.row = { ...row }; |
| | | approveOpinion.value = ""; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function isReimburseApprovalInstance(row) { |
| | | return Boolean(inferReimburseModuleKeyFromInstance(row)); |
| | | } |
| | | |
| | | function resetSubmitDialogState() { |
| | |
| | | loadSubmitTemplates(); |
| | | } |
| | | |
| | | function openEditDialog(row) { |
| | | async function openEditDialog(row) { |
| | | if (row?.approvalStatus !== "pending") { |
| | | ElMessage.warning("ä»
å®¡æ ¸ä¸ç审æ¹å¯ä¿®æ¹"); |
| | | return; |
| | | } |
| | | const moduleKey = inferReimburseModuleKeyFromInstance(row); |
| | | if (moduleKey) { |
| | | const rid = resolveFinReimbursementIdFromInstance(row); |
| | | if (rid == null) { |
| | | ElMessage.warning("æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | try { |
| | | await navigateToReimburseManageForEdit(proxy?.$router, moduleKey, rid); |
| | | } catch { |
| | | ElMessage.warning("æªæ¾å°å·®æ
/è´¹ç¨æ¥éèåè·¯ç±ï¼è¯·ä»å·¦ä¾§èåè¿å
¥ååç¼è¾"); |
| | | } |
| | | return; |
| | | } |
| | | if (!row?.id) { |
| | |
| | | } |
| | | } |
| | | |
| | | async function submitReimburseApprove(result) { |
| | | const row = reimburseDialog.instanceRow; |
| | | if (!row?.id) return { ok: false }; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | return { needOpinion: true }; |
| | | } |
| | | if (approveSubmitting.value) return { ok: false }; |
| | | approveSubmitting.value = true; |
| | | try { |
| | | await approveApprovalInstance( |
| | | buildApproveInstanceDto(row, result, approveOpinion.value) |
| | | ); |
| | | reimburseDialog.visible = false; |
| | | await fetchApprovalList(); |
| | | return { ok: true, result }; |
| | | } catch { |
| | | ElMessage.error("å®¡æ¹æä½å¤±è´¥"); |
| | | return { ok: false }; |
| | | } finally { |
| | | approveSubmitting.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row?.id) return { ok: false }; |
| | |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitReimburseApprove, |
| | | isReimburseApprovalInstance, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | |
| | | }); |
| | | |
| | | const attachmentFiles = computed(() => { |
| | | const list = props.row?.attachmentList?.length ? props.row.attachmentList : props.row?.invoiceAttachments; |
| | | const list = |
| | | props.row?.attachmentList || |
| | | props.row?.storageBlobVOList || |
| | | props.row?.invoiceAttachments; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | |
| | | } |
| | | |
| | | export function statusLabel(v) { |
| | | if (v === "draft") return "è稿"; |
| | | if (v === "approved") return "å·²éè¿"; |
| | | if (v === "paid") return "已仿¬¾"; |
| | | if (v === "rejected") return "已驳å"; |
| | | if (v === "cancelled") return "å·²æ¤å"; |
| | | return "å®¡æ ¸ä¸"; |
| | | } |
| | | |
| | | export function statusTagType(v) { |
| | | if (v === "approved") return "success"; |
| | | if (v === "draft") return "info"; |
| | | if (v === "approved" || v === "paid") return "success"; |
| | | if (v === "rejected") return "danger"; |
| | | if (v === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | |
| | | <!--OA模åï¼è´¹ç¨æ¥é--> |
| | | <!--OA模åï¼è´¹ç¨æ¥éï¼å表 /finReimbursement/listPageï¼reimbursementType=2ï¼--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | |
| | | </el-card> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button v-if="!formDialog.readonly" type="primary" @click="submitForm">æ 交</el-button> |
| | | <el-button |
| | | v-if="!formDialog.readonly" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="submitForm" |
| | | > |
| | | æ 交 |
| | | </el-button> |
| | | <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "å
³ é" : "å æ¶" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog v-model="detailDialog.visible" title="è´¹ç¨æ¥é详æ
" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="detailLoading"> |
| | | <DetailPanel :row="detailRow" /> |
| | | <el-divider content-position="left">å®¡æ¹æµç¨</el-divider> |
| | | <ApprovalFlowProgress :nodes="detailRow.approvalFlowNodes" :current-index="detailRow.currentNodeIndex ?? 0" /> |
| | |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ å®¡æ¹è®°å½" :image-size="60" /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button type="primary" @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | |
| | | 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 } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { |
| | | buildCostReimbursementSaveDto, |
| | | buildFinReimbursementListParams, |
| | | canDeleteReimbursementRow, |
| | | canEditReimbursementRow, |
| | | filterRowsByReimbursementType, |
| | | FIN_REIMBURSEMENT_TYPE, |
| | | mapCostReimbursementRow, |
| | | mapFinReimbursementDetailRow, |
| | | resolveReimbursementDeleteId, |
| | | unwrapFinReimbursementDetail, |
| | | unwrapFinReimbursementPage, |
| | | validateReimbursementPersistDto, |
| | | } from "../shared/finReimbursementMappers.js"; |
| | | import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; |
| | | import { |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | CATEGORY_TEMPLATES, |
| | |
| | | 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 filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | const kw = (searchForm.applicantKeyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const name = (r.applicantName || r.employeeName || "").toLowerCase(); |
| | | const no = (r.applicantNo || r.employeeNo || "").toLowerCase(); |
| | | return name.includes(kw) || no.includes(kw); |
| | | }); |
| | | } |
| | | if (searchForm.applyTimeFrom) { |
| | | list = list.filter((r) => { |
| | | const t = (r.applyTime || r.createTime || "").slice(0, 10); |
| | | return !t || t >= searchForm.applyTimeFrom; |
| | | }); |
| | | } |
| | | if (searchForm.applyTimeTo) { |
| | | list = list.filter((r) => { |
| | | const t = (r.applyTime || r.createTime || "").slice(0, 10); |
| | | return !t || t <= searchForm.applyTimeTo; |
| | | }); |
| | | } |
| | | return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1)); |
| | | }); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size).map((r) => ({ |
| | | 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); |
| | | allRows.value = filterRowsByReimbursementType( |
| | | records, |
| | | FIN_REIMBURSEMENT_TYPE.COST |
| | | ).map(mapCostReimbursementRow); |
| | | page.total = total; |
| | | } catch { |
| | | allRows.value = []; |
| | | page.total = 0; |
| | | proxy?.$modal?.msgError?.("è´¹ç¨æ¥éå表å 载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", |
| | | disabled: (row) => !canEditReimbursementRow(row), |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "审æ¹", |
| | | type: "text", |
| | | disabled: (row) => row.approvalResult !== "pending", |
| | | clickFun: (row) => openApprove(row), |
| | | name: "å é¤", |
| | | type: "danger", |
| | | disabled: (row) => !canDeleteReimbursementRow(row), |
| | | clickFun: (row) => confirmRemoveRow(row), |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | tableLoading.value = true; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 150); |
| | | return fetchList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | return fetchList(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | 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) { |
| | |
| | | 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, { |
| | | ...JSON.parse(JSON.stringify(row)), |
| | | attachmentList: JSON.parse(JSON.stringify(row.attachmentList || row.invoiceAttachments || [])), |
| | | approvalFlowNodes: JSON.parse(JSON.stringify(row.approvalFlowNodes || [])), |
| | | expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])), |
| | | ...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 || [])), |
| | | }); |
| | | const u = userById(row.applicantId); |
| | | const u = userById(editRow.applicantId); |
| | | applicantFormOptions.value = u |
| | | ? [u] |
| | | : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }]; |
| | | : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }]; |
| | | } else { |
| | | form.approvalFlowNodes = buildAutoApprovalFlow(0, "other"); |
| | | remoteSearchApplicantForm(""); |
| | |
| | | syncApplyAmountFromDetails(); |
| | | autoAssignApprovalFlow(); |
| | | |
| | | const payload = { |
| | | reimburseNo: form.reimburseNo || `CR${dayjs().format("YYYYMMDDHHmmss")}`, |
| | | applicantId: form.applicantId, |
| | | employeeNo: form.employeeNo, |
| | | employeeName: form.employeeName, |
| | | applicantNo: form.employeeNo, |
| | | applicantName: form.employeeName, |
| | | expenseCategory: form.expenseCategory, |
| | | reimburseReason: form.reimburseReason, |
| | | applyAmount: form.applyAmount, |
| | | payee: form.payee, |
| | | payeeAccount: form.payeeAccount, |
| | | bankBranch: form.bankBranch, |
| | | expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)), |
| | | attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])), |
| | | invoiceAttachments: (form.attachmentList || []).map((f, i) => ({ |
| | | id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`, |
| | | name: f.name || f.fileName || "æªå½å", |
| | | url: f.url || f.downloadURL || "", |
| | | })), |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | deptId: form.deptId, |
| | | deptName: form.deptName, |
| | | }; |
| | | |
| | | if (formDialog.mode === "add") { |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | allRows.value.unshift({ |
| | | id: `local_${Date.now()}`, |
| | | ...payload, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | applyTime: now, |
| | | createTime: now, |
| | | }); |
| | | proxy?.$modal?.msgSuccess?.("æäº¤æå"); |
| | | } else { |
| | | const idx = allRows.value.findIndex((r) => r.id === form.id); |
| | | if (idx !== -1) { |
| | | const prev = allRows.value[idx]; |
| | | allRows.value[idx] = { |
| | | ...prev, |
| | | ...payload, |
| | | id: form.id, |
| | | approvalResult: prev.approvalResult === "rejected" ? "pending" : prev.approvalResult, |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | rejectReason: prev.approvalResult === "rejected" ? "" : prev.rejectReason, |
| | | applyTime: prev.applyTime, |
| | | createTime: prev.createTime, |
| | | }; |
| | | } |
| | | proxy?.$modal?.msgSuccess?.("ä¿åæå"); |
| | | 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; |
| | | } |
| | | formDialog.visible = false; |
| | | handleQuery(); |
| | | 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) { |
| | |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = filteredList.value; |
| | | 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"); |
| | |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(() => loadUserPool()); |
| | | onMounted(async () => { |
| | | loadUserPool(); |
| | | await fetchList(); |
| | | const editPayload = consumeReimburseEditFromApprove(); |
| | | if (editPayload?.reimbursementId != null) { |
| | | await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | Search, |
| | |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | approvalActionLabel, |
| | | submitApprove, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å·®æ
/è´¹ç¨æ¥éï¼å®¡æ¹å表å
详æ
/审æ¹å¼¹çªå
容ï¼ä¸æ¥é页弹çªä¸è´ï¼ --> |
| | | <template> |
| | | <div v-loading="loading"> |
| | | <TravelDetailPanel v-if="isTravel" :row="reimburseRow" /> |
| | | <CostDetailPanel v-else :row="reimburseRow" /> |
| | | |
| | | <el-divider content-position="left">æµç¨è¿åº¦</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="reimburseRow.approvalFlowNodes" |
| | | :current-index="reimburseRow.currentNodeIndex ?? 0" |
| | | /> |
| | | |
| | | <template v-if="mode === 'detail'"> |
| | | <el-divider content-position="left">审æ¹è®°å½ï¼å
¨æµç¨ççï¼</el-divider> |
| | | <el-timeline v-if="reimburseRow.approvalRecords?.length"> |
| | | <el-timeline-item |
| | | v-for="(rec, i) in reimburseRow.approvalRecords" |
| | | :key="i" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'" |
| | | :timestamp="rec.time" |
| | | > |
| | | {{ rec.operatorName }} â {{ actionLabel(rec.result) }}ï¼{{ rec.opinion || "æ æè§" }} |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ å®¡æ¹è®°å½" :image-size="60" /> |
| | | </template> |
| | | |
| | | <el-form v-else label-width="100px" class="mt16"> |
| | | <el-form-item label="å®¡æ¹æè§"> |
| | | <el-input |
| | | :model-value="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | :placeholder="isTravel ? 'éè¿å¯ç空ï¼é©³å请填ååå ' : 'éè¿å¯ç空ï¼é©³å请填åå
·ä½åå ï¼å¦ï¼å票模ç³ééä¼ ï¼'" |
| | | @update:model-value="$emit('update:approveOpinion', $event)" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { isTravelReimbursementType } from "../finReimbursementMappers.js"; |
| | | import ApprovalFlowProgress from "../../travel-reimburse/components/ApprovalFlowProgress.vue"; |
| | | import CostDetailPanel from "../../cost-reimburse/components/DetailPanel.vue"; |
| | | import TravelDetailPanel from "../../travel-reimburse/components/DetailPanel.vue"; |
| | | |
| | | const props = defineProps({ |
| | | mode: { type: String, default: "detail" }, |
| | | moduleKey: { type: String, default: "" }, |
| | | reimburseRow: { type: Object, default: () => ({}) }, |
| | | loading: { type: Boolean, default: false }, |
| | | approveOpinion: { type: String, default: "" }, |
| | | }); |
| | | |
| | | defineEmits(["update:approveOpinion"]); |
| | | |
| | | const isTravel = computed(() => |
| | | isTravelReimbursementType(props.reimburseRow?.reimbursementType ?? props.moduleKey) |
| | | ); |
| | | |
| | | function actionLabel(v) { |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "rejected") return "驳å"; |
| | | return "æäº¤"; |
| | | } |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { formatDisplayTime } from "../../ApproveManage/approve-template/approveTemplateConstants.js"; |
| | | import { |
| | | mapRecordResultFromApi, |
| | | mapRecordsFromApi, |
| | | mapTasksToFlowNodes, |
| | | } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | |
| | | function taskStatusToNodeStatus(taskStatus) { |
| | | const s = String(taskStatus ?? "").toUpperCase(); |
| | | if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) { |
| | | return "finish"; |
| | | } |
| | | if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) { |
| | | return "error"; |
| | | } |
| | | if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) { |
| | | return "process"; |
| | | } |
| | | return "wait"; |
| | | } |
| | | |
| | | /** storageBlobVOList â 页é¢éä»¶å表 */ |
| | | export function mapReimbursementAttachments(source = {}) { |
| | | const list = |
| | | source.storageBlobVOList || |
| | | source.storageBlobDTOs || |
| | | source.storageBlobDTOS || |
| | | source.storageBlobVOS || |
| | | source.attachmentList || |
| | | source.invoiceAttachments || |
| | | []; |
| | | if (!Array.isArray(list)) return []; |
| | | return list.map((b, i) => ({ |
| | | ...b, |
| | | id: b.id ?? b.blobId ?? `att_${i}`, |
| | | name: |
| | | b.fileName || |
| | | b.originalFilename || |
| | | b.originalFileName || |
| | | b.blobName || |
| | | b.name || |
| | | "éä»¶", |
| | | url: |
| | | b.url || |
| | | b.fileUrl || |
| | | b.downloadUrl || |
| | | b.downloadURL || |
| | | b.previewUrl || |
| | | b.previewURL || |
| | | b.link || |
| | | "", |
| | | })); |
| | | } |
| | | |
| | | /** 审æ¹è®°å½æ¥èª tasksï¼æ¯æ¡ä»»å¡ä¸æ¡ççï¼ */ |
| | | export function mapTasksToApprovalRecords(tasks) { |
| | | const list = Array.isArray(tasks) ? tasks : []; |
| | | return list |
| | | .map((t, index) => ({ |
| | | id: t.id ?? index, |
| | | operatorName: t.approverName || t.operatorName || t.createUserName || "â", |
| | | result: mapRecordResultFromApi( |
| | | t.approveAction ?? t.taskStatus ?? t.status |
| | | ), |
| | | opinion: t.approveComment || t.comment || t.opinion || "", |
| | | time: formatDisplayTime( |
| | | t.approveTime || t.finishTime || t.updateTime || t.createTime || "" |
| | | ), |
| | | levelNo: t.levelNo ?? t.taskLevel, |
| | | raw: t, |
| | | })) |
| | | .sort((a, b) => { |
| | | const la = Number(a.levelNo ?? 0); |
| | | const lb = Number(b.levelNo ?? 0); |
| | | if (la !== lb) return la - lb; |
| | | return String(a.time).localeCompare(String(b.time)); |
| | | }); |
| | | } |
| | | |
| | | /** tasks â ApprovalFlowProgress èç¹ */ |
| | | export function mapTasksToApprovalFlowNodes(tasks) { |
| | | const grouped = mapTasksToFlowNodes(tasks); |
| | | return grouped.map((node, i) => { |
| | | const approvers = node.approvers || []; |
| | | const statuses = approvers.map(a => |
| | | taskStatusToNodeStatus(a.taskStatus ?? a.status) |
| | | ); |
| | | let nodeStatus = "wait"; |
| | | if (statuses.includes("error")) nodeStatus = "error"; |
| | | else if (statuses.length && statuses.every(s => s === "finish")) { |
| | | nodeStatus = "finish"; |
| | | } else if (statuses.includes("process")) nodeStatus = "process"; |
| | | |
| | | const names = approvers.map(a => a.approverName).filter(Boolean).join("ã"); |
| | | const opinions = approvers |
| | | .map(a => a.approveComment) |
| | | .filter(Boolean) |
| | | .join("ï¼"); |
| | | |
| | | return { |
| | | nodeOrder: node.nodeOrder ?? node.levelNo ?? i + 1, |
| | | sortOrder: node.nodeOrder ?? node.levelNo ?? i + 1, |
| | | approverName: names || "â", |
| | | approveOpinion: opinions, |
| | | approveTime: approvers.find(a => a.approveTime)?.approveTime || "", |
| | | nodeStatus, |
| | | signMode: node.signMode, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) { |
| | | const list = approvalFlowNodes || []; |
| | | const processing = list.findIndex(n => n.nodeStatus === "process"); |
| | | if (processing >= 0) return processing; |
| | | const errorIdx = list.findIndex(n => n.nodeStatus === "error"); |
| | | if (errorIdx >= 0) return errorIdx; |
| | | return list.filter(n => n.nodeStatus === "finish").length; |
| | | } |
| | | |
| | | /** 详æ
DTO è¡¥å
tasks / éä»¶ / 审æ¹è®°å½ */ |
| | | export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) { |
| | | if (!mapped || typeof mapped !== "object") return mapped; |
| | | const source = { ...raw, ...mapped }; |
| | | const tasks = Array.isArray(source.tasks) ? source.tasks : []; |
| | | const attachments = mapReimbursementAttachments(source); |
| | | const approvalRecords = tasks.length |
| | | ? mapTasksToApprovalRecords(tasks) |
| | | : mapRecordsFromApi(source.records || source.approvalRecords); |
| | | const approvalFlowNodes = tasks.length |
| | | ? mapTasksToApprovalFlowNodes(tasks) |
| | | : mapped.approvalFlowNodes || []; |
| | | const currentNodeIndex = computeApprovalFlowCurrentIndex(approvalFlowNodes); |
| | | const rejectReason = |
| | | approvalRecords.find(r => r.result === "rejected")?.opinion || |
| | | source.rejectReason || |
| | | ""; |
| | | |
| | | return { |
| | | ...mapped, |
| | | tasks, |
| | | storageBlobVOList: attachments, |
| | | attachmentList: attachments, |
| | | invoiceAttachments: attachments, |
| | | approvalRecords, |
| | | records: tasks.length ? tasks : source.records, |
| | | approvalFlowNodes, |
| | | currentNodeIndex, |
| | | rejectReason, |
| | | flowNodes: tasks.length ? mapTasksToFlowNodes(tasks) : mapped.flowNodes, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | 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 { 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 || {}; |
| | | } |
| | | return { |
| | | ...applyFinReimbursementDetailEnrichment(mapped, raw), |
| | | 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 {}; |
| | | if (/[\u4e00-\u9fa5]/.test(kw)) return { applicantName: kw }; |
| | | return { applicantCode: kw }; |
| | | } |
| | | |
| | | /** ç»è£
listPage æ¥è¯¢åæ°ï¼page + finReimbursementDtoï¼ */ |
| | | export function buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType, |
| | | extraDto = {}, |
| | | }) { |
| | | const dto = { |
| | | reimbursementType, |
| | | ...pickApplicantQuery(searchForm), |
| | | ...(extraDto && typeof extraDto === "object" ? extraDto : {}), |
| | | }; |
| | | |
| | | if (searchForm?.billStatus) { |
| | | dto.billStatus = searchForm.billStatus; |
| | | } |
| | | |
| | | const range = |
| | | searchForm?.createTimeRange ?? |
| | | searchForm?.applyDateRange ?? |
| | | (searchForm?.applyTimeFrom || searchForm?.applyTimeTo |
| | | ? [searchForm.applyTimeFrom, searchForm.applyTimeTo] |
| | | : null); |
| | | |
| | | if (Array.isArray(range) && range[0]) { |
| | | dto.createTimeStart = range[0]; |
| | | } |
| | | if (Array.isArray(range) && range[1]) { |
| | | dto.createTimeEnd = range[1]; |
| | | } |
| | | |
| | | if (reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | if (searchForm?.travelStartFrom) { |
| | | dto.startTimeStart = searchForm.travelStartFrom; |
| | | } |
| | | if (searchForm?.travelEndTo) { |
| | | dto.endTimeEnd = searchForm.travelEndTo; |
| | | } |
| | | } |
| | | |
| | | return { |
| | | page: { |
| | | current: page.current, |
| | | size: page.size, |
| | | }, |
| | | finReimbursementDto: dto, |
| | | }; |
| | | } |
| | | |
| | | 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: row.nodes || [], |
| | | approvalFlowNodes: mapNodesToFormFlow(row.nodes), |
| | | tasks: row.tasks || [], |
| | | }; |
| | | return base; |
| | | } |
| | | |
| | | /** æ¥å£è¡ â è´¹ç¨æ¥éå表è¡ï¼å
¼å®¹ useCostReimburse åæ®µï¼ */ |
| | | export function mapCostReimbursementRow(row) { |
| | | if (!row) return {}; |
| | | 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 || "", |
| | | expenseCategory: row.expenseType || "", |
| | | applyAmount: row.applyAmount, |
| | | applyTime: 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: row.nodes || [], |
| | | approvalFlowNodes: mapNodesToFormFlow(row.nodes), |
| | | tasks: row.tasks || [], |
| | | }; |
| | | } |
| | | |
| | | 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 || ""; |
| | | } |
| | | |
| | | /** æ¥å£ nodes â 页é¢å®¡æ¹æµï¼å审æ¹äººèç¹ï¼ */ |
| | | export function mapNodesToFormFlow(nodes = []) { |
| | | return (Array.isArray(nodes) ? nodes : []).map((n, i) => { |
| | | const first = Array.isArray(n.approvers) ? n.approvers[0] : null; |
| | | return { |
| | | ...n, |
| | | nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1, |
| | | signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign", |
| | | approverId: first?.approverId ?? n.approverId ?? null, |
| | | approverName: first?.approverName ?? n.approverName ?? "", |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | /** 页é¢å®¡æ¹èç¹ â æ¥å£ 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) => ({ |
| | | id: a.id, |
| | | nodeId: a.nodeId, |
| | | templateId: a.templateId ?? templateId, |
| | | approverId: toNumber(a.approverId) ?? a.approverId, |
| | | approverName: a.approverName || "", |
| | | sortNo: a.sortNo ?? idx + 1, |
| | | })); |
| | | } else if (n.approverId != null && n.approverId !== "") { |
| | | approvers = [ |
| | | { |
| | | approverId: toNumber(n.approverId) ?? n.approverId, |
| | | approverName: n.approverName || "", |
| | | sortNo: 1, |
| | | }, |
| | | ]; |
| | | } |
| | | if (!approvers.length) return null; |
| | | |
| | | const node = { |
| | | levelNo: n.levelNo ?? n.nodeOrder ?? 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; |
| | | return node; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¶è¡¥é½ä¸»è¡¨ä¸å表å
³è 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; |
| | | }); |
| | | } |
| | | 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(form.approvalFlowNodes, 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); |
| | | |
| | | 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(form.approvalFlowNodes, 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); |
| | | } |
| | | |
| | | 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" }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { matchBusinessTypeValue } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | import { |
| | | APPROVAL_MODULE_KEYS, |
| | | getApprovalModuleConfig, |
| | | } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { |
| | | getModuleKeyByReimbursementType, |
| | | mapFinReimbursementDetailRow, |
| | | resolveReimbursementType, |
| | | unwrapFinReimbursementDetail, |
| | | } from "./finReimbursementMappers.js"; |
| | | |
| | | export const REIMBURSE_EDIT_FROM_APPROVE_KEY = "oa_reimburse_edit_from_approve"; |
| | | |
| | | const REIMBURSE_MODULE_KEYS = [ |
| | | APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE, |
| | | APPROVAL_MODULE_KEYS.COST_REIMBURSE, |
| | | ]; |
| | | |
| | | /** 审æ¹å®ä¾æ¯å¦å·®æ
/è´¹ç¨æ¥é */ |
| | | export function inferReimburseModuleKeyFromInstance(row) { |
| | | if (!row) return ""; |
| | | for (const moduleKey of REIMBURSE_MODULE_KEYS) { |
| | | const cfg = getApprovalModuleConfig(moduleKey); |
| | | if (!cfg) continue; |
| | | if ( |
| | | cfg.businessType != null && |
| | | cfg.businessType !== "" && |
| | | matchBusinessTypeValue(row.businessType, cfg.businessType) |
| | | ) { |
| | | return moduleKey; |
| | | } |
| | | if (matchBusinessTypeValue(row.businessType, cfg.approvalType)) { |
| | | return moduleKey; |
| | | } |
| | | const text = `${row.templateName || ""}${row.title || ""}${row.businessName || ""}`; |
| | | if ((cfg.typeLabels || []).some((l) => l && text.includes(l))) { |
| | | return moduleKey; |
| | | } |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function isReimburseApprovalInstance(row) { |
| | | return Boolean(inferReimburseModuleKeyFromInstance(row)); |
| | | } |
| | | |
| | | /** 审æ¹å®ä¾å
³èç fin_reimbursement.id */ |
| | | export function resolveFinReimbursementIdFromInstance(row) { |
| | | const raw = row?.businessId ?? row?.formPayload?.reimbursementId; |
| | | if (raw == null || raw === "") return undefined; |
| | | const n = Number(raw); |
| | | return Number.isNaN(n) ? raw : n; |
| | | } |
| | | |
| | | /** æåæ¥é详æ
å¹¶æ å°ä¸ºå·®æ
/è´¹ç¨é¡µé¢è¡ï¼ä»¥æ¥å£ reimbursementType 为åï¼ */ |
| | | export async function loadReimburseDetailForInstance(instanceRow, moduleKey) { |
| | | const mk = moduleKey || inferReimburseModuleKeyFromInstance(instanceRow); |
| | | const id = resolveFinReimbursementIdFromInstance(instanceRow); |
| | | if (id == null) { |
| | | throw new Error("missing reimbursement id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | const reimburseRow = mapFinReimbursementDetailRow(raw, mk); |
| | | const reimbursementType = resolveReimbursementType(raw, mk); |
| | | const resolvedMk = |
| | | getModuleKeyByReimbursementType(reimbursementType) || mk; |
| | | return { |
| | | reimburseRow, |
| | | instanceRow, |
| | | moduleKey: resolvedMk, |
| | | reimbursementType, |
| | | }; |
| | | } |
| | | |
| | | export function stashReimburseEditFromApprove(moduleKey, reimbursementId) { |
| | | sessionStorage.setItem( |
| | | REIMBURSE_EDIT_FROM_APPROVE_KEY, |
| | | JSON.stringify({ moduleKey, reimbursementId }) |
| | | ); |
| | | } |
| | | |
| | | export function consumeReimburseEditFromApprove() { |
| | | const raw = sessionStorage.getItem(REIMBURSE_EDIT_FROM_APPROVE_KEY); |
| | | if (!raw) return null; |
| | | sessionStorage.removeItem(REIMBURSE_EDIT_FROM_APPROVE_KEY); |
| | | try { |
| | | return JSON.parse(raw); |
| | | } catch { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** ä»å·²æ³¨åè·¯ç±è§£æå·®æ
/è´¹ç¨æ¥éèå pathï¼é¿å
åæ» path å¯¼è´ 404ï¼ */ |
| | | export function resolveReimburseManageRoutePath(router, moduleKey) { |
| | | if (!router?.getRoutes) return ""; |
| | | const needle = |
| | | moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE |
| | | ? "travel-reimburse" |
| | | : moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "cost-reimburse" |
| | | : ""; |
| | | if (!needle) return ""; |
| | | const labelHint = |
| | | moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE ? "å·®æ
" : "è´¹ç¨"; |
| | | const hit = router.getRoutes().find((r) => { |
| | | const path = r.path || ""; |
| | | if (path.includes(needle)) return true; |
| | | const title = r.meta?.title || ""; |
| | | return title.includes(labelHint) && title.includes("æ¥é"); |
| | | }); |
| | | return hit?.path || ""; |
| | | } |
| | | |
| | | export async function navigateToReimburseManageForEdit(router, moduleKey, reimbursementId) { |
| | | stashReimburseEditFromApprove(moduleKey, reimbursementId); |
| | | const path = resolveReimburseManageRoutePath(router, moduleKey); |
| | | if (!path) { |
| | | throw new Error("route not found"); |
| | | } |
| | | await router.push(path); |
| | | } |
| | |
| | | }); |
| | | |
| | | const attachmentFiles = computed(() => { |
| | | const list = props.row?.attachmentList?.length |
| | | ? props.row.attachmentList |
| | | : props.row?.invoiceAttachments; |
| | | const list = |
| | | props.row?.attachmentList || |
| | | props.row?.storageBlobVOList || |
| | | props.row?.invoiceAttachments; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | |
| | | <!--OA模åï¼å·®æ
æ¥é--> |
| | | <!--OA模åï¼å·®æ
æ¥éï¼å表 /finReimbursement/listPageï¼reimbursementType=1ï¼--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | |
| | | </el-card> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button v-if="!formDialog.readonly" type="primary" @click="submitForm">æ 交</el-button> |
| | | <el-button |
| | | v-if="!formDialog.readonly" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="submitForm" |
| | | > |
| | | æ 交 |
| | | </el-button> |
| | | <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "å
³ é" : "å æ¶" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog v-model="detailDialog.visible" title="å·®æ
æ¥é详æ
" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="detailLoading"> |
| | | <DetailPanel :row="detailRow" /> |
| | | <ApprovalFlowProgress |
| | | class="mt16" |
| | |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ å®¡æ¹è®°å½" :image-size="60" /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button type="primary" @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | |
| | | } |
| | | |
| | | export function statusLabel(v) { |
| | | if (v === "draft") return "è稿"; |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "paid") return "已仿¬¾"; |
| | | if (v === "rejected") return "驳å"; |
| | | if (v === "cancelled") return "å·²æ¤å"; |
| | | return "å®¡æ ¸ä¸"; |
| | | } |
| | | |
| | | export function statusTagType(v) { |
| | | if (v === "approved") return "success"; |
| | | if (v === "draft") return "info"; |
| | | if (v === "approved" || v === "paid") return "success"; |
| | | if (v === "rejected") return "danger"; |
| | | if (v === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | |
| | | 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 } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { |
| | | buildFinReimbursementListParams, |
| | | buildTravelReimbursementSaveDto, |
| | | canDeleteReimbursementRow, |
| | | canEditReimbursementRow, |
| | | filterRowsByReimbursementType, |
| | | FIN_REIMBURSEMENT_TYPE, |
| | | mapFinReimbursementDetailRow, |
| | | mapTravelReimbursementRow, |
| | | resolveReimbursementDeleteId, |
| | | unwrapFinReimbursementDetail, |
| | | unwrapFinReimbursementPage, |
| | | validateReimbursementPersistDto, |
| | | } from "../shared/finReimbursementMappers.js"; |
| | | import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; |
| | | import { |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | |
| | | 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 filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | const kw = (searchForm.applicantKeyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const name = (r.applicantName || r.employeeName || "").toLowerCase(); |
| | | const no = (r.applicantNo || r.employeeNo || "").toLowerCase(); |
| | | return name.includes(kw) || no.includes(kw); |
| | | }); |
| | | } |
| | | if (searchForm.travelStartFrom) { |
| | | list = list.filter((r) => !r.travelStartTime || r.travelStartTime.slice(0, 10) >= searchForm.travelStartFrom); |
| | | } |
| | | if (searchForm.travelEndTo) { |
| | | list = list.filter((r) => !r.travelEndTime || r.travelEndTime.slice(0, 10) <= searchForm.travelEndTo); |
| | | } |
| | | return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1)); |
| | | }); |
| | | const tableData = computed(() => allRows.value); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | }); |
| | | async function fetchList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listFinReimbursementPage( |
| | | buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | }) |
| | | ); |
| | | const { records, total } = unwrapFinReimbursementPage(res); |
| | | allRows.value = filterRowsByReimbursementType( |
| | | records, |
| | | FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ).map(mapTravelReimbursementRow); |
| | | page.total = total; |
| | | } catch { |
| | | allRows.value = []; |
| | | page.total = 0; |
| | | proxy?.$modal?.msgError?.("å·®æ
æ¥éå表å 载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 200, |
| | | width: 220, |
| | | operation: [ |
| | | { name: "ç¼è¾", type: "text", disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", clickFun: (row) => openFormDialog("edit", row) }, |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => !canEditReimbursementRow(row), |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { name: "审æ¹", type: "text", disabled: (row) => row.approvalResult !== "pending", clickFun: (row) => openApprove(row) }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | disabled: (row) => !canDeleteReimbursementRow(row), |
| | | clickFun: (row) => confirmRemoveRow(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | tableLoading.value = true; |
| | | setTimeout(() => { tableLoading.value = false; }, 150); |
| | | return fetchList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | return fetchList(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | async function loadTravelDetailRow(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.TRAVEL); |
| | | } |
| | | |
| | | 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 loadTravelDetailRow(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) { |
| | |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | Object.assign(form, createEmptyForm()); |
| | | if (mode === "edit" && row) { |
| | | let editRow = row; |
| | | try { |
| | | editRow = await loadTravelDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å è½½æ¥é详æ
失败"); |
| | | return; |
| | | } |
| | | Object.assign(form, { |
| | | ...JSON.parse(JSON.stringify(row)), |
| | | attachmentList: JSON.parse(JSON.stringify(row.attachmentList || row.invoiceAttachments || [])), |
| | | approvalFlowNodes: JSON.parse(JSON.stringify(row.approvalFlowNodes || [])), |
| | | expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])), |
| | | ...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 || [])), |
| | | }); |
| | | const u = userById(row.applicantId); |
| | | applicantFormOptions.value = u ? [u] : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }]; |
| | | const u = userById(editRow.applicantId); |
| | | applicantFormOptions.value = u |
| | | ? [u] |
| | | : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }]; |
| | | } else { |
| | | form.approvalFlowNodes = [{ approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1 }]; |
| | | remoteSearchApplicantForm(""); |
| | |
| | | return; |
| | | } |
| | | } |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | const payload = { |
| | | reimburseNo: form.reimburseNo || `TR${dayjs().format("YYYYMMDDHHmmss")}`, |
| | | applicantId: form.applicantId, |
| | | employeeNo: form.employeeNo, |
| | | employeeName: form.employeeName, |
| | | applicantNo: form.employeeNo, |
| | | applicantName: form.employeeName, |
| | | reimburseReason: form.reimburseReason, |
| | | travelStartTime: form.travelStartTime, |
| | | travelEndTime: form.travelEndTime, |
| | | travelDays: days, |
| | | departurePlace: form.departurePlace, |
| | | destination: form.destination, |
| | | hotelStandard: form.hotelStandard, |
| | | hotelDays: form.hotelDays, |
| | | livingSubsidy: form.livingSubsidy, |
| | | applyAmount: form.applyAmount, |
| | | payee: form.payee, |
| | | expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)), |
| | | attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])), |
| | | invoiceAttachments: mapAttachmentList(form.attachmentList), |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | needSpecialApproval: form.needSpecialApproval, |
| | | deptId: form.deptId, |
| | | deptName: form.deptName, |
| | | travelTier: form.travelTier, |
| | | }; |
| | | if (formDialog.mode === "add") { |
| | | allRows.value.unshift({ |
| | | id: `local_${Date.now()}`, |
| | | ...payload, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | proxy?.$modal?.msgSuccess?.("æäº¤æå"); |
| | | } else { |
| | | const idx = allRows.value.findIndex((r) => r.id === form.id); |
| | | if (idx !== -1) { |
| | | const prev = allRows.value[idx]; |
| | | allRows.value[idx] = { |
| | | ...prev, |
| | | ...payload, |
| | | id: form.id, |
| | | approvalResult: prev.approvalResult === "rejected" ? "pending" : prev.approvalResult, |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | createTime: prev.createTime, |
| | | }; |
| | | } |
| | | proxy?.$modal?.msgSuccess?.("ä¿åæå"); |
| | | if (submitSaving.value) return; |
| | | const isEdit = formDialog.mode === "edit"; |
| | | const dto = buildTravelReimbursementSaveDto(form, { computeTravelDays }); |
| | | const check = validateReimbursementPersistDto(dto, isEdit); |
| | | if (!check.ok) { |
| | | proxy?.$modal?.msgWarning?.(check.message); |
| | | return; |
| | | } |
| | | formDialog.visible = false; |
| | | handleQuery(); |
| | | 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) { |
| | |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = filteredList.value; |
| | | 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"); |
| | |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(() => loadUserPool()); |
| | | onMounted(async () => { |
| | | loadUserPool(); |
| | | await fetchList(); |
| | | const editPayload = consumeReimburseEditFromApprove(); |
| | | if (editPayload?.reimbursementId != null) { |
| | | await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | Search, |
| | |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | confirmRemoveRow, |
| | | openApprove, |
| | | approvalActionLabel, |
| | | submitApprove, |