refactor: enforce 800 line source limits
This commit is contained in:
495
web/src/views/scripts/travelRequestDetailSetup.js
Normal file
495
web/src/views/scripts/travelRequestDetailSetup.js
Normal file
@@ -0,0 +1,495 @@
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import {
|
||||
canApproveBudgetExpenseApplications,
|
||||
canApproveLeaderExpenseClaims,
|
||||
canManageExpenseClaims,
|
||||
canReturnExpenseClaims,
|
||||
isCurrentDirectManagerForRequest,
|
||||
isCurrentRequestApplicant,
|
||||
isFinanceUser,
|
||||
isPlatformAdminUser
|
||||
} from '../../utils/accessControl.js'
|
||||
import {
|
||||
buildLeaderApprovalEvents,
|
||||
buildLeaderApprovalInfo
|
||||
} from '../../utils/applicationApproval.js'
|
||||
import {
|
||||
buildApplicationDetailFactItems,
|
||||
buildRelatedApplicationFactItems
|
||||
} from '../../utils/expenseApplicationDetail.js'
|
||||
import { buildRiskViewerContext } from '../../utils/riskVisibility.js'
|
||||
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
||||
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
||||
import {
|
||||
EXPENSE_TYPE_OPTIONS,
|
||||
buildFallbackExpenseItems,
|
||||
buildFallbackProgressSteps,
|
||||
isApplicationDocumentRequest,
|
||||
rebuildExpenseItems,
|
||||
resolveExpenseReasonHelper,
|
||||
resolveExpenseReasonPlaceholder
|
||||
} from './travelRequestDetailExpenseModel.js'
|
||||
import { useTravelRequestPaymentFlow } from './travelRequestDetailPaymentFlow.js'
|
||||
import { useTravelRequestDetailApprovalFlow } from './useTravelRequestDetailApprovalFlow.js'
|
||||
import { useTravelRequestDetailAttachmentPreview } from './useTravelRequestDetailAttachmentPreview.js'
|
||||
import { useTravelRequestDetailExpenseEditor } from './useTravelRequestDetailExpenseEditor.js'
|
||||
import { useTravelRequestDetailRiskSubmit } from './useTravelRequestDetailRiskSubmit.js'
|
||||
|
||||
export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
const { toast } = useToast()
|
||||
const { currentUser } = useSystemState()
|
||||
const expenseItems = ref([])
|
||||
const expenseAttachmentMeta = reactive({})
|
||||
const riskFlagPreviewSnapshot = ref(null)
|
||||
let actionBusy = { value: false }
|
||||
const getActionBusy = () => Boolean(actionBusy?.value)
|
||||
|
||||
const request = computed(() => {
|
||||
const normalized = normalizeRequestForUi(props.request)
|
||||
|
||||
return (
|
||||
normalized || {
|
||||
id: 'EXP-202605-000',
|
||||
claimId: '',
|
||||
reason: '待补充报销事由',
|
||||
typeLabel: '其他费用',
|
||||
typeCode: 'other',
|
||||
detailVariant: 'general',
|
||||
sceneTarget: '待补充',
|
||||
location: '待补充',
|
||||
occurredDisplay: '待补充',
|
||||
applyTime: '待补充',
|
||||
amountDisplay: '¥0',
|
||||
amountValue: 0,
|
||||
node: '待提交',
|
||||
approval: '草稿',
|
||||
approvalKey: 'draft',
|
||||
approvalTone: 'draft',
|
||||
secondaryStatusLabel: '票据状态',
|
||||
secondaryStatusValue: '待补充',
|
||||
secondaryStatusTone: 'warning',
|
||||
relatedCustomer: '待补充',
|
||||
attachmentSummary: '待补充',
|
||||
riskSummary: '待补充',
|
||||
note: '',
|
||||
profileIdentity: '员工',
|
||||
profilePosition: '待补充',
|
||||
profileGrade: '待补充',
|
||||
profileManager: '待补充',
|
||||
profileName: '当前申请人',
|
||||
profileDepartment: '待补充部门',
|
||||
profileAvatar: '申'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const isApplicationDocument = computed(() => isApplicationDocumentRequest(request.value))
|
||||
const isTravelRequest = computed(() => request.value.detailVariant === 'travel' && !isApplicationDocument.value)
|
||||
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
|
||||
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
||||
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
||||
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
||||
const canModifyReturnedApplication = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& isEditableRequest.value
|
||||
&& isCurrentApplicant.value
|
||||
&& String(request.value.status || '').trim().toLowerCase() === 'returned'
|
||||
))
|
||||
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
||||
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
||||
const isApplicantDeletableRequest = computed(() => {
|
||||
if (!isCurrentApplicant.value) {
|
||||
return false
|
||||
}
|
||||
const status = String(request.value.status || request.value.approvalKey || '').trim().toLowerCase()
|
||||
return ['draft', 'supplement', 'returned'].includes(status)
|
||||
})
|
||||
const canDeleteRequest = computed(() => {
|
||||
if (isPlatformAdminUser(currentUser.value)) {
|
||||
return true
|
||||
}
|
||||
return isApplicantDeletableRequest.value
|
||||
})
|
||||
const isDirectManagerApprovalStage = computed(() => {
|
||||
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
||||
return node === '直属领导审批'
|
||||
})
|
||||
const isFinanceApprovalStage = computed(() => {
|
||||
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
||||
return node === '财务审批'
|
||||
})
|
||||
const isBudgetApprovalStage = computed(() => {
|
||||
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
||||
return node === '预算管理者审批'
|
||||
})
|
||||
const isCurrentDirectManagerApprover = computed(() => (
|
||||
canApproveLeaderExpenseClaims(currentUser.value)
|
||||
&& isCurrentDirectManagerForRequest(request.value, currentUser.value)
|
||||
))
|
||||
const canProcessFinanceApprovalStage = computed(() => (
|
||||
!isApplicationDocument.value
|
||||
&& isFinanceApprovalStage.value
|
||||
&& isFinanceUser(currentUser.value)
|
||||
&& !isCurrentApplicant.value
|
||||
))
|
||||
const canProcessBudgetApprovalStage = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& isBudgetApprovalStage.value
|
||||
&& canApproveBudgetExpenseApplications(currentUser.value, request.value)
|
||||
&& !isCurrentApplicant.value
|
||||
))
|
||||
const showBudgetAnalysis = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& isBudgetApprovalStage.value
|
||||
&& canApproveBudgetExpenseApplications(currentUser.value, request.value)
|
||||
&& !isCurrentApplicant.value
|
||||
))
|
||||
const canProcessCurrentApprovalStage = computed(() => {
|
||||
if (isDirectManagerApprovalStage.value) {
|
||||
return isCurrentDirectManagerApprover.value
|
||||
}
|
||||
if (isBudgetApprovalStage.value) {
|
||||
return canProcessBudgetApprovalStage.value
|
||||
}
|
||||
return canProcessFinanceApprovalStage.value
|
||||
})
|
||||
const canReturnRequest = computed(() => {
|
||||
if (request.value.approvalKey !== 'in_progress' || !request.value.claimId || !canReturnExpenseClaims(currentUser.value)) {
|
||||
return false
|
||||
}
|
||||
return canProcessCurrentApprovalStage.value
|
||||
})
|
||||
const canApproveRequest = computed(() =>
|
||||
request.value.approvalKey === 'in_progress'
|
||||
&& Boolean(request.value.claimId)
|
||||
&& canProcessCurrentApprovalStage.value
|
||||
)
|
||||
const canViewApprovalRiskAdvice = computed(() => (
|
||||
Boolean(request.value.claimId)
|
||||
&& !isDraftRequest.value
|
||||
&& !isCurrentApplicant.value
|
||||
&& (canReturnRequest.value || canApproveRequest.value)
|
||||
))
|
||||
const showStageRiskAdvice = computed(() => canViewApprovalRiskAdvice.value)
|
||||
const riskViewerContext = computed(() => buildRiskViewerContext({
|
||||
request: request.value,
|
||||
currentUser: currentUser.value,
|
||||
businessStage: isApplicationDocument.value ? 'expense_application' : 'reimbursement',
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
isCurrentApplicant: isCurrentApplicant.value,
|
||||
isBudgetReviewer: canProcessBudgetApprovalStage.value,
|
||||
isDirectManagerReviewer: isCurrentDirectManagerApprover.value,
|
||||
isFinanceReviewer: canProcessFinanceApprovalStage.value,
|
||||
isAdminViewer: canManageCurrentClaim.value,
|
||||
canViewApprovalRiskAdvice: canViewApprovalRiskAdvice.value
|
||||
}))
|
||||
|
||||
const paymentFlow = useTravelRequestPaymentFlow({
|
||||
request,
|
||||
currentUser,
|
||||
isApplicationDocument,
|
||||
isCurrentApplicant,
|
||||
toast,
|
||||
emit
|
||||
})
|
||||
const attachmentPreview = useTravelRequestDetailAttachmentPreview({
|
||||
request,
|
||||
expenseItems,
|
||||
expenseAttachmentMeta
|
||||
})
|
||||
const riskSubmit = useTravelRequestDetailRiskSubmit({
|
||||
request,
|
||||
expenseItems,
|
||||
expenseAttachmentMeta,
|
||||
riskFlagPreviewSnapshot,
|
||||
isApplicationDocument,
|
||||
isEditableRequest,
|
||||
isDraftRequest,
|
||||
isCurrentApplicant,
|
||||
canViewApprovalRiskAdvice,
|
||||
riskViewerContext,
|
||||
getActionBusy,
|
||||
toast,
|
||||
emit
|
||||
})
|
||||
const approvalFlow = useTravelRequestDetailApprovalFlow({
|
||||
request,
|
||||
isApplicationDocument,
|
||||
isDraftRequest,
|
||||
isArchivedRequest,
|
||||
isFinanceApprovalStage,
|
||||
isBudgetApprovalStage,
|
||||
canDeleteRequest,
|
||||
canReturnRequest,
|
||||
canApproveRequest,
|
||||
approvalRiskConfirmItems: riskSubmit.approvalRiskConfirmItems,
|
||||
canViewApprovalRiskAdvice,
|
||||
toast,
|
||||
emit
|
||||
})
|
||||
const expenseEditor = useTravelRequestDetailExpenseEditor({
|
||||
request,
|
||||
expenseItems,
|
||||
expenseAttachmentMeta,
|
||||
isEditableRequest,
|
||||
getActionBusy,
|
||||
toast,
|
||||
emit,
|
||||
attachmentPreviewOpen: attachmentPreview.attachmentPreviewOpen,
|
||||
buildAttachmentRiskNotice: attachmentPreview.buildAttachmentRiskNotice,
|
||||
closeAttachmentPreview: attachmentPreview.closeAttachmentPreview,
|
||||
refreshExpenseAttachmentMeta: attachmentPreview.refreshExpenseAttachmentMeta,
|
||||
resolveAttachmentMeta: attachmentPreview.resolveAttachmentMeta,
|
||||
resolveClaimRiskFlags: riskSubmit.resolveClaimRiskFlags,
|
||||
applyClaimRiskFlagsPayload: riskSubmit.applyClaimRiskFlagsPayload
|
||||
})
|
||||
const {
|
||||
deletingAttachmentId,
|
||||
deletingExpenseId,
|
||||
savingExpenseId,
|
||||
smartEntryRecognitionBusy,
|
||||
uploadingExpenseId
|
||||
} = expenseEditor
|
||||
|
||||
actionBusy = computed(() =>
|
||||
Boolean(savingExpenseId.value)
|
||||
|| riskSubmit.submitBusy.value
|
||||
|| approvalFlow.deleteBusy.value
|
||||
|| approvalFlow.returnBusy.value
|
||||
|| approvalFlow.approveBusy.value
|
||||
|| paymentFlow.payBusy.value
|
||||
|| smartEntryRecognitionBusy.value
|
||||
|| Boolean(uploadingExpenseId.value)
|
||||
|| Boolean(deletingAttachmentId.value)
|
||||
|| Boolean(deletingExpenseId.value)
|
||||
)
|
||||
|
||||
const profile = computed(() => ({
|
||||
name: request.value.profileName,
|
||||
identity: request.value.profileIdentity,
|
||||
position: request.value.profilePosition,
|
||||
department: request.value.profileDepartment,
|
||||
grade: request.value.profileGrade,
|
||||
manager: request.value.profileManager,
|
||||
avatar: request.value.profileAvatar
|
||||
}))
|
||||
const leaderApprovalInfo = computed(() => buildLeaderApprovalInfo(request.value))
|
||||
const leaderApprovalEvents = computed(() => buildLeaderApprovalEvents(request.value))
|
||||
const hasLeaderApprovalEvents = computed(() => leaderApprovalEvents.value.length > 0)
|
||||
const hasSingleLeaderApprovalEvent = computed(() => leaderApprovalEvents.value.length === 1)
|
||||
const leaderApprovalReadonlyMeta = computed(() => {
|
||||
const pieces = hasLeaderApprovalEvents.value ? [`${leaderApprovalEvents.value.length} 条批复记录`] : []
|
||||
if (leaderApprovalInfo.value.generatedDraftClaimNo) {
|
||||
pieces.push(`已生成报销草稿 ${leaderApprovalInfo.value.generatedDraftClaimNo}`)
|
||||
}
|
||||
return pieces.join(' · ')
|
||||
})
|
||||
const showApplicationLeaderOpinion = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& hasLeaderApprovalEvents.value
|
||||
))
|
||||
const heroFactItems = computed(() => [
|
||||
{
|
||||
key: 'document',
|
||||
label: isApplicationDocument.value ? '申请单号' : '报销单号',
|
||||
value: request.value.documentNo || request.value.id,
|
||||
icon: 'mdi mdi-camera-outline',
|
||||
valueClass: ''
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
label: '单据申请日期',
|
||||
value: request.value.applyTime || request.value.occurredDisplay,
|
||||
icon: 'mdi mdi-calendar-month-outline',
|
||||
valueClass: ''
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: isApplicationDocument.value ? '预计金额' : '报销金额',
|
||||
value: request.value.amountDisplay,
|
||||
icon: '',
|
||||
valueClass: 'amount'
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: isApplicationDocument.value ? '申请类型' : isTravelRequest.value ? '差旅类型' : '报销类型',
|
||||
value: request.value.typeLabel,
|
||||
icon: '',
|
||||
valueClass: ''
|
||||
}
|
||||
])
|
||||
const progressSteps = computed(() => {
|
||||
const sourceSteps = Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
||||
? request.value.progressSteps
|
||||
: buildFallbackProgressSteps(request.value)
|
||||
return resolveProgressStepsForViewer(sourceSteps, {
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
isCurrentDirectManagerApprover: isCurrentDirectManagerApprover.value
|
||||
})
|
||||
})
|
||||
const currentProgressRingMotion = {
|
||||
initial: { scale: 1, opacity: 0.34 },
|
||||
enter: {
|
||||
scale: [1, 1.42, 1.78],
|
||||
opacity: [0.34, 0.16, 0],
|
||||
transition: {
|
||||
duration: 3.2,
|
||||
repeat: Infinity,
|
||||
repeatType: 'loop',
|
||||
repeatDelay: 0.85,
|
||||
ease: 'easeOut',
|
||||
times: [0, 0.5, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
const submitConfirmAmountDisplay = computed(() =>
|
||||
isApplicationDocument.value ? (request.value.amountDisplay || expenseEditor.expenseTotal.value) : expenseEditor.expenseTotal.value
|
||||
)
|
||||
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
||||
const relatedApplicationFactItems = computed(() => buildRelatedApplicationFactItems(request.value))
|
||||
|
||||
watch(
|
||||
request,
|
||||
(nextRequest, previousRequest) => {
|
||||
expenseItems.value =
|
||||
Array.isArray(nextRequest.expenseItems)
|
||||
? rebuildExpenseItems(nextRequest.expenseItems, nextRequest)
|
||||
: buildFallbackExpenseItems(nextRequest)
|
||||
if (nextRequest.claimId !== previousRequest?.claimId) {
|
||||
Object.keys(expenseAttachmentMeta).forEach((key) => {
|
||||
delete expenseAttachmentMeta[key]
|
||||
})
|
||||
riskSubmit.resetSubmitWorkState()
|
||||
attachmentPreview.closeAttachmentPreview()
|
||||
}
|
||||
expenseEditor.resetExpenseWorkState()
|
||||
void attachmentPreview.syncExpenseAttachmentMeta()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => request.value.claimId,
|
||||
() => {
|
||||
riskSubmit.clearRiskFlagPreviewSnapshot()
|
||||
expenseEditor.resetSmartEntryRecognitionApplications()
|
||||
expenseEditor.bindSmartEntryRecognitionTask()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function buildApplicationEditPreview() {
|
||||
const factEntries = applicationDetailFactItems.value
|
||||
.map((item) => [String(item?.label || '').trim(), String(item?.value || '').trim()])
|
||||
.filter(([label, value]) => label && value)
|
||||
const facts = new Map(factEntries)
|
||||
const pickFact = (...labels) => {
|
||||
for (const label of labels) {
|
||||
const value = facts.get(label)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const tripStart = pickFact('出发时间')
|
||||
const tripReturn = pickFact('返回时间')
|
||||
const time = tripStart && tripReturn && tripStart !== tripReturn
|
||||
? `${tripStart} 至 ${tripReturn}`
|
||||
: pickFact('行程时间', '申请时间', '招待时间', '发生时间') || tripStart
|
||||
|
||||
return {
|
||||
sourceText: '修改申请',
|
||||
modelReviewStatus: 'template',
|
||||
fields: {
|
||||
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
||||
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
||||
grade: pickFact('职级') || request.value.profileGrade || '',
|
||||
department: request.value.profileDepartment || request.value.departmentName || request.value.department || '',
|
||||
position: request.value.profilePosition || request.value.employeePosition || request.value.position || '',
|
||||
managerName: request.value.profileManager || request.value.managerName || request.value.manager || '',
|
||||
time,
|
||||
location: pickFact('地点') || request.value.location || request.value.city || '',
|
||||
reason: pickFact('事由') || request.value.reason || '',
|
||||
days: pickFact('天数'),
|
||||
transportMode: pickFact('出行方式'),
|
||||
lodgingDailyCap: pickFact('住宿上限/天'),
|
||||
subsidyDailyCap: pickFact('补贴标准/天'),
|
||||
transportPolicy: pickFact('交通费用口径'),
|
||||
policyEstimate: pickFact('规则测算参考'),
|
||||
amount: pickFact('系统预估费用', '用户预估费用', '预计金额') || request.value.amountDisplay || request.value.amount || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModifyApplication() {
|
||||
if (!canModifyReturnedApplication.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const claimId = String(request.value?.claimId || '').trim()
|
||||
emit('openAssistant', {
|
||||
source: 'application',
|
||||
sessionType: 'application',
|
||||
prompt: '',
|
||||
applicationPreview: buildApplicationEditPreview(),
|
||||
request: {
|
||||
...request.value,
|
||||
applicationEditMode: true
|
||||
},
|
||||
restoreLatestConversation: false,
|
||||
initialPromptAutoSubmit: false,
|
||||
scope: claimId
|
||||
? { type: 'claim', claimId }
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
riskSubmit.disposeRiskSubmit()
|
||||
expenseEditor.disposeExpenseEditor()
|
||||
attachmentPreview.closeAttachmentPreview()
|
||||
})
|
||||
|
||||
return {
|
||||
emit,
|
||||
actionBusy,
|
||||
...attachmentPreview,
|
||||
...approvalFlow,
|
||||
...expenseEditor,
|
||||
...paymentFlow,
|
||||
...riskSubmit,
|
||||
applicationDetailFactItems,
|
||||
relatedApplicationFactItems,
|
||||
canDeleteRequest,
|
||||
canManageCurrentClaim,
|
||||
canModifyReturnedApplication,
|
||||
canOpenAiEntry,
|
||||
canApproveRequest,
|
||||
canReturnRequest,
|
||||
currentProgressRingMotion,
|
||||
expenseItems,
|
||||
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
||||
handleModifyApplication,
|
||||
hasLeaderApprovalEvents,
|
||||
hasSingleLeaderApprovalEvent,
|
||||
heroFactItems,
|
||||
isApplicationDocument,
|
||||
isDraftRequest,
|
||||
isEditableRequest,
|
||||
isTravelRequest,
|
||||
leaderApprovalEvents,
|
||||
leaderApprovalReadonlyMeta,
|
||||
profile,
|
||||
progressSteps,
|
||||
request,
|
||||
resolveExpenseReasonHelper,
|
||||
resolveExpenseReasonPlaceholder,
|
||||
showApplicationLeaderOpinion,
|
||||
showBudgetAnalysis,
|
||||
showStageRiskAdvice,
|
||||
submitConfirmAmountDisplay
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user