feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态 流转,前端完善退回对话框和审批交互组件,新增报销申请关联 模型,优化文档中心行数据和审批收件箱工具函数,增强引导 流程和会话模型,补充单元测试覆盖。
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
canAccessAppView,
|
||||
canDeleteArchivedExpenseClaims,
|
||||
canEditBudgetCenter,
|
||||
isCurrentDirectManagerForRequest,
|
||||
isCurrentRequestApplicant,
|
||||
canManageExpenseClaims,
|
||||
canReturnExpenseClaims,
|
||||
canSwitchBudgetDepartments
|
||||
@@ -87,7 +89,33 @@ test('users with both finance and manager roles can process both relevant stages
|
||||
true
|
||||
)
|
||||
assert.equal(
|
||||
canProcessApprovalRequest({ workflowNode: '直属领导审批', person: '张三' }, financeManagerUser),
|
||||
canProcessApprovalRequest(
|
||||
{ workflowNode: '直属领导审批', person: '张三', managerName: '李经理' },
|
||||
financeManagerUser
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.equal(
|
||||
canProcessApprovalRequest(
|
||||
{ workflowNode: '直属领导审批', person: '李经理', managerName: '王总' },
|
||||
financeManagerUser
|
||||
),
|
||||
false
|
||||
)
|
||||
assert.equal(
|
||||
canProcessApprovalRequest(
|
||||
{ workflowNode: '直属领导审批', person: '张三', managerName: '王总' },
|
||||
financeManagerUser
|
||||
),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('direct-manager approval helpers only match claims pushed to the current user', () => {
|
||||
const managerUser = { roleCodes: ['manager'], name: '李经理', username: 'manager@example.com' }
|
||||
|
||||
assert.equal(isCurrentRequestApplicant({ person: '李经理', managerName: '王总' }, managerUser), true)
|
||||
assert.equal(isCurrentDirectManagerForRequest({ person: '李经理', managerName: '王总' }, managerUser), false)
|
||||
assert.equal(isCurrentDirectManagerForRequest({ person: '张三', managerName: '李经理' }, managerUser), true)
|
||||
assert.equal(isCurrentDirectManagerForRequest({ person: '张三', managerName: '王总' }, managerUser), false)
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildLeaderApprovalEvents,
|
||||
buildLeaderApprovalInfo,
|
||||
resolveGeneratedDraftClaimNo
|
||||
} from '../src/utils/applicationApproval.js'
|
||||
@@ -52,3 +53,56 @@ test('resolveGeneratedDraftClaimNo reads approval response payload', () => {
|
||||
'EXP-202605-0012'
|
||||
)
|
||||
})
|
||||
|
||||
test('buildLeaderApprovalEvents returns leader return and approval timeline in event order', () => {
|
||||
const events = buildLeaderApprovalEvents({
|
||||
profileManager: 'Fallback Manager',
|
||||
riskFlags: [
|
||||
{
|
||||
source: 'manual_approval',
|
||||
event_type: 'expense_application_approval',
|
||||
operator: 'Leader Li',
|
||||
opinion: 'Approved after supplement.',
|
||||
approval_event_id: 'approval-1',
|
||||
created_at: '2026-05-25T11:00:00'
|
||||
},
|
||||
{
|
||||
source: 'manual_return',
|
||||
event_type: 'expense_application_return',
|
||||
operator: 'manager@example.com',
|
||||
operator_name: 'Leader Li',
|
||||
reason: 'Need clearer budget explanation.',
|
||||
return_count: 1,
|
||||
return_event_id: 'return-1',
|
||||
created_at: '2026-05-25T09:00:00'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.deepEqual(events.map((event) => event.id), ['return-1', 'approval-1'])
|
||||
assert.deepEqual(events.map((event) => event.type), ['returned', 'approved'])
|
||||
assert.deepEqual(events.map((event) => event.tone), ['danger', 'success'])
|
||||
assert.equal(events[0].operator, 'Leader Li')
|
||||
assert.equal(events[0].opinion, 'Need clearer budget explanation.')
|
||||
assert.equal(events[0].returnCount, 1)
|
||||
assert.equal(events[0].time, '2026-05-25 09:00')
|
||||
assert.equal(Object.hasOwn(events[0], 'sortAt'), false)
|
||||
})
|
||||
|
||||
test('buildLeaderApprovalEvents hides empty or unrelated return state', () => {
|
||||
assert.deepEqual(buildLeaderApprovalEvents({ riskFlags: [] }), [])
|
||||
assert.deepEqual(
|
||||
buildLeaderApprovalEvents({
|
||||
riskFlags: [
|
||||
{
|
||||
source: 'manual_return',
|
||||
event_type: 'expense_claim_return',
|
||||
return_stage_key: 'finance',
|
||||
reason: 'Finance return should not render as leader application opinion.',
|
||||
created_at: '2026-05-25T09:00:00'
|
||||
}
|
||||
]
|
||||
}),
|
||||
[]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ import test from 'node:test'
|
||||
|
||||
import {
|
||||
excludeArchivedDocumentRows,
|
||||
filterApplicationScopeNewRows,
|
||||
prepareApplicationScopeRows,
|
||||
isArchivedDocumentRow
|
||||
} from '../src/utils/documentCenterRows.js'
|
||||
|
||||
@@ -48,3 +50,34 @@ test('document center all scope excludes archived rows from merged lists', () =>
|
||||
|
||||
assert.deepEqual(rows.map((row) => row.claimId), ['c'])
|
||||
})
|
||||
|
||||
test('application scope does not mark submitted approval application rows as new', () => {
|
||||
const rows = prepareApplicationScopeRows([
|
||||
{
|
||||
claimId: 'draft-application',
|
||||
documentTypeCode: 'application',
|
||||
statusGroup: 'draft',
|
||||
isNewDocument: true
|
||||
},
|
||||
{
|
||||
claimId: 'submitted-application',
|
||||
documentTypeCode: 'application',
|
||||
statusGroup: 'in_progress',
|
||||
isNewDocument: true
|
||||
},
|
||||
{
|
||||
claimId: 'reimbursement',
|
||||
documentTypeCode: 'reimbursement',
|
||||
statusGroup: 'in_progress',
|
||||
isNewDocument: true
|
||||
}
|
||||
])
|
||||
|
||||
assert.deepEqual(rows.map((row) => row.claimId), ['draft-application', 'submitted-application'])
|
||||
assert.equal(rows.find((row) => row.claimId === 'draft-application')?.isNewDocument, true)
|
||||
assert.equal(rows.find((row) => row.claimId === 'submitted-application')?.isNewDocument, false)
|
||||
assert.deepEqual(
|
||||
filterApplicationScopeNewRows(rows).map((row) => row.claimId),
|
||||
['draft-application']
|
||||
)
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ test('documents center category tabs map to the intended row sources', () => {
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/activeScopeTab\.value === DOCUMENT_SCOPE_APPLICATION[\s\S]*nonArchivedRows\.value\.filter\(\(row\) => row\.documentTypeCode === DOCUMENT_TYPE_APPLICATION/
|
||||
/activeScopeTab\.value === DOCUMENT_SCOPE_APPLICATION[\s\S]*return applicationScopeRows\.value/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
@@ -122,7 +122,11 @@ test('documents center category tabs render bubble counts for new documents', ()
|
||||
assert.match(documentsCenterView, /\[DOCUMENT_SCOPE_ALL\]: countNewDocuments\(nonArchivedRows\.value, viewedDocumentKeys\.value\)/)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_APPLICATION\]: countNewDocuments\(nonArchivedRows\.value\.filter\(\(row\) => row\.documentTypeCode === DOCUMENT_TYPE_APPLICATION\), viewedDocumentKeys\.value\)/
|
||||
/const applicationScopeRows = computed\(\(\) => prepareApplicationScopeRows\(ownedRows\.value\)\)/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_APPLICATION\]: countNewDocuments\(filterApplicationScopeNewRows\(applicationScopeRows\.value\), viewedDocumentKeys\.value\)/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
|
||||
@@ -17,6 +17,10 @@ import {
|
||||
shouldUseLocalApplicationPreview
|
||||
} from '../src/utils/expenseApplicationPreview.js'
|
||||
import { renderMarkdown } from '../src/utils/markdown.js'
|
||||
import {
|
||||
createMessage as createConversationMessage,
|
||||
hasMeaningfulSessionMessages
|
||||
} from '../src/views/scripts/travelReimbursementConversationModel.js'
|
||||
|
||||
const submitComposerScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
|
||||
@@ -217,6 +221,23 @@ test('application quick start renders a template without model review', () => {
|
||||
assert.match(buildApplicationPreviewFooterMessage(preview), /当前还需要补充/)
|
||||
})
|
||||
|
||||
test('application quick start template counts as deletable session content', () => {
|
||||
const welcomeMessage = createConversationMessage('assistant', '欢迎语', [], {
|
||||
isWelcome: true,
|
||||
welcomeQuickActions: [{ label: '快速发起申请', action: 'start_guided_application' }]
|
||||
})
|
||||
const templateMessage = createConversationMessage('assistant', '申请模板', [], {
|
||||
applicationPreview: buildApplicationTemplatePreview({
|
||||
name: '测试员工',
|
||||
departmentName: '财务部',
|
||||
grade: 'P5'
|
||||
})
|
||||
})
|
||||
|
||||
assert.equal(hasMeaningfulSessionMessages([welcomeMessage]), false)
|
||||
assert.equal(hasMeaningfulSessionMessages([welcomeMessage, templateMessage]), true)
|
||||
})
|
||||
|
||||
test('application session shows intent flow, persists preview, and supports inline table edit', () => {
|
||||
assert.match(submitComposerScript, /shouldUseLocalApplicationPreview/)
|
||||
assert.match(submitComposerScript, /buildLocalApplicationPreviewMessage/)
|
||||
@@ -228,7 +249,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(submitComposerScript, /startFlowStep\('intent'/)
|
||||
assert.match(submitComposerScript, /startFlowStep\('application-review-preview'/)
|
||||
assert.match(submitComposerScript, /completeFlowStep\('intent'/)
|
||||
assert.match(submitComposerScript, /insightPanelCollapsed\.value = true/)
|
||||
assert.doesNotMatch(submitComposerScript, /insightPanelCollapsed\.value = true/)
|
||||
assert.doesNotMatch(submitComposerScript, /void refineApplicationPreviewWithModel/)
|
||||
assert.match(submitComposerScript, /return null[\s\S]*const hasUnsavedReviewDraft/)
|
||||
assert.ok(
|
||||
@@ -239,7 +260,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(createViewScript, /const isApplicationSession = computed/)
|
||||
assert.match(createViewScript, /insightPanelCollapsed,/)
|
||||
assert.doesNotMatch(createViewScript, /if \(isApplicationSession\.value\) \{\s*return false\s*\}/)
|
||||
assert.match(createViewScript, /flowSteps\.value\.length > 0/)
|
||||
assert.match(createViewScript, /activeFlowSteps\.value\.length > 0/)
|
||||
assert.match(createViewScript, /useApplicationPreviewEditor/)
|
||||
assert.match(createViewScript, /message-bubble-application-preview/)
|
||||
assert.match(createViewScript, /buildApplicationPreviewFooterMessage/)
|
||||
@@ -248,6 +269,8 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(createViewScript, /user_input_text: applicationSubmitText/)
|
||||
assert.match(conversationModelScript, /applicationPreview: null/)
|
||||
assert.match(conversationModelScript, /applicationPreview: message\.applicationPreview \|\| null/)
|
||||
assert.match(conversationModelScript, /\|\| message\.applicationPreview/)
|
||||
assert.match(createViewScript, /hasMeaningfulSessionMessages\(messages\.value\)/)
|
||||
|
||||
assert.match(messageItemTemplate, /class="application-preview-table"/)
|
||||
assert.match(messageItemTemplate, /class="application-preview-footer application-preview-footer-missing"/)
|
||||
|
||||
@@ -3,12 +3,21 @@ import test from 'node:test'
|
||||
|
||||
import { mapExpenseClaimToRequest } from '../src/composables/useRequests.js'
|
||||
|
||||
const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7'
|
||||
const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279'
|
||||
const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
|
||||
const RETURNED = '\u9000\u56de'
|
||||
const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4'
|
||||
const WAIT_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \u6279\u590d'
|
||||
const LEADER_RETURNED_STATUS = '\u9886\u5bfc\u5df2\u9000\u56de\uff0c\u5f85\u91cd\u65b0\u63d0\u4ea4'
|
||||
|
||||
test('application claims are mapped as application documents', () => {
|
||||
const request = mapExpenseClaimToRequest({
|
||||
id: 'claim-application-1',
|
||||
claim_no: 'AP-20260525103045-ABCDEFGH',
|
||||
employee_name: '张三',
|
||||
department_name: '交付部',
|
||||
manager_name: 'Leader Li',
|
||||
expense_type: 'travel_application',
|
||||
reason: '支撑国网服务器上线部署',
|
||||
location: '上海',
|
||||
@@ -32,11 +41,59 @@ test('application claims are mapped as application documents', () => {
|
||||
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
['创建申请', '直属领导审批', '审批完成']
|
||||
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED]
|
||||
)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === '直属领导审批')?.current, true)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === DIRECT_MANAGER_APPROVAL), false)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.rawLabel, DIRECT_MANAGER_APPROVAL)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.current, true)
|
||||
})
|
||||
|
||||
test('returned application claims include leader return node and supplement status', () => {
|
||||
const request = mapExpenseClaimToRequest({
|
||||
id: 'claim-application-returned',
|
||||
claim_no: 'APP-20260525-RETURNED',
|
||||
employee_name: 'Applicant Zhang',
|
||||
department_name: 'Delivery',
|
||||
manager_name: 'Leader Li',
|
||||
expense_type: 'travel_application',
|
||||
reason: 'Project onsite support',
|
||||
location: 'Shanghai',
|
||||
amount: 12000,
|
||||
invoice_count: 0,
|
||||
occurred_at: '2026-05-25T00:00:00.000Z',
|
||||
submitted_at: null,
|
||||
created_at: '2026-05-25T01:30:00.000Z',
|
||||
updated_at: '2026-05-25T04:00:00.000Z',
|
||||
status: 'returned',
|
||||
approval_stage: WAIT_SUBMIT,
|
||||
risk_flags_json: [
|
||||
{
|
||||
source: 'manual_return',
|
||||
event_type: 'expense_application_return',
|
||||
operator: 'Leader Li',
|
||||
opinion: 'Need clearer budget explanation.',
|
||||
return_stage_key: 'direct_manager',
|
||||
next_status: 'returned',
|
||||
next_approval_stage: WAIT_SUBMIT,
|
||||
return_count: 2,
|
||||
created_at: '2026-05-25T04:00:00.000Z'
|
||||
}
|
||||
],
|
||||
items: []
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, RETURNED, WAIT_SUBMIT]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === RETURNED)?.time, 'Leader Li\u9000\u56de')
|
||||
assert.match(request.progressSteps.find((step) => step.label === RETURNED)?.detail, /2026-05-25/)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_SUBMIT)?.current, true)
|
||||
assert.equal(request.secondaryStatusValue, LEADER_RETURNED_STATUS)
|
||||
assert.equal(request.secondaryStatusTone, 'warning')
|
||||
assert.equal(request.progressSteps.some((step) => step.label === APPROVAL_COMPLETED), false)
|
||||
})
|
||||
|
||||
test('approved application claims complete after direct manager approval only', () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
GUIDED_ACTION_START_APPLICATION,
|
||||
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
||||
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
||||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
||||
GUIDED_ACTION_SELECT_QUERY_MODE,
|
||||
GUIDED_ACTION_SELECT_QUERY_STATUS,
|
||||
GUIDED_ACTION_START_REIMBURSEMENT,
|
||||
@@ -42,10 +43,19 @@ import {
|
||||
createGuidedStatusQueryState,
|
||||
isGuidedReimbursementReadyForReview,
|
||||
normalizeGuidedFlowState,
|
||||
selectGuidedRequiredApplication,
|
||||
selectGuidedExpenseType,
|
||||
selectGuidedQueryMode,
|
||||
shouldConfirmGuidedInterruption
|
||||
shouldConfirmGuidedInterruption,
|
||||
waitForGuidedApplicationSelection
|
||||
} from '../src/views/scripts/travelReimbursementGuidedFlowModel.js'
|
||||
import {
|
||||
buildRequiredApplicationActions,
|
||||
buildRequiredApplicationMissingText,
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates,
|
||||
requiresApplicationBeforeReimbursement
|
||||
} from '../src/views/scripts/travelReimbursementApplicationLinkModel.js'
|
||||
import {
|
||||
ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||
resolveAssistantScopeGuard
|
||||
@@ -71,7 +81,7 @@ const submitComposerScript = readFileSync(
|
||||
test('assistant session modes expose independent quick actions', () => {
|
||||
assert.deepEqual(
|
||||
ASSISTANT_SESSION_MODE_OPTIONS.map((item) => item.label),
|
||||
['申请助手', '报销助手', '审核助手', '财务知识助手']
|
||||
['申请助手', '报销助手', '审核助手', '财务知识助手', '预算编制助手']
|
||||
)
|
||||
assert.deepEqual(
|
||||
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
|
||||
@@ -181,6 +191,82 @@ test('guided reimbursement asks type first and walks travel fields in order', ()
|
||||
assert.match(submitOptions.rawText, /出差时间\/天数:2026-05-20 至 2026-05-23,出差 3 天/)
|
||||
})
|
||||
|
||||
test('guided reimbursement requires application selection for travel and entertainment', () => {
|
||||
assert.equal(requiresApplicationBeforeReimbursement('travel'), true)
|
||||
assert.equal(requiresApplicationBeforeReimbursement('meal'), true)
|
||||
assert.equal(requiresApplicationBeforeReimbursement('transport'), false)
|
||||
|
||||
const claimsPayload = {
|
||||
items: [
|
||||
{
|
||||
id: 'app-travel',
|
||||
claim_no: 'AP-202605-001',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'travel_application',
|
||||
reason: '去上海支持项目部署',
|
||||
location: '上海',
|
||||
amount: 1800,
|
||||
status: 'approved',
|
||||
created_at: '2026-05-20T08:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'app-meal',
|
||||
claim_no: 'AP-202605-002',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'expense_application',
|
||||
reason: '客户招待沟通项目',
|
||||
location: '武汉',
|
||||
amount: 600,
|
||||
status: 'submitted',
|
||||
created_at: '2026-05-21T08:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'app-draft',
|
||||
claim_no: 'AP-202605-003',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'travel_application',
|
||||
reason: '草稿出差申请',
|
||||
status: 'draft'
|
||||
},
|
||||
{
|
||||
id: 'app-other-user',
|
||||
claim_no: 'AP-202605-004',
|
||||
employee_name: '李四',
|
||||
expense_type: 'travel_application',
|
||||
reason: '其他员工出差申请',
|
||||
status: 'approved'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const currentUser = { name: '张小青', username: 'xiaoqing.zhang' }
|
||||
const travelApplications = filterRequiredApplicationCandidates(claimsPayload, 'travel', currentUser)
|
||||
assert.deepEqual(travelApplications.map((item) => item.claim_no), ['AP-202605-001'])
|
||||
assert.match(buildRequiredApplicationSelectionText('travel', travelApplications), /需要先关联对应的申请单/)
|
||||
assert.match(buildRequiredApplicationMissingText('meal'), /不能继续这类报销流程/)
|
||||
|
||||
const mealApplications = filterRequiredApplicationCandidates(claimsPayload, 'meal', currentUser)
|
||||
assert.deepEqual(mealApplications.map((item) => item.claim_no), ['AP-202605-002'])
|
||||
|
||||
const actions = buildRequiredApplicationActions(travelApplications, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||||
assert.equal(actions[0].action_type, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||||
assert.equal(actions[0].payload.application_claim_no, 'AP-202605-001')
|
||||
|
||||
let state = waitForGuidedApplicationSelection(createGuidedReimbursementState(), 'travel', travelApplications)
|
||||
assert.equal(state.stepKey, 'application_selection')
|
||||
assert.equal(state.applicationCandidates[0].claim_no, 'AP-202605-001')
|
||||
|
||||
state = selectGuidedRequiredApplication(state, actions[0].payload)
|
||||
assert.equal(state.stepKey, 'reason')
|
||||
assert.equal(state.values.application_claim_no, 'AP-202605-001')
|
||||
assert.match(buildGuidedReimbursementSummaryText(state), /关联申请单:AP-202605-001/)
|
||||
|
||||
const submitOptions = buildGuidedReviewSubmitOptions(state)
|
||||
assert.equal(submitOptions.extraContext.review_form_values.application_claim_no, 'AP-202605-001')
|
||||
assert.equal(submitOptions.extraContext.expense_scene_selection.application_claim_no, 'AP-202605-001')
|
||||
assert.match(submitOptions.rawText, /关联申请单:AP-202605-001/)
|
||||
})
|
||||
|
||||
test('guided reimbursement interrupts suspicious questions before expensive flow', () => {
|
||||
const state = selectGuidedExpenseType(createGuidedReimbursementState(), 'transport')
|
||||
assert.equal(shouldConfirmGuidedInterruption('送客户去机场', state), false)
|
||||
@@ -232,7 +318,8 @@ test('guided flow state is serializable and restored through session state', ()
|
||||
amount: '200',
|
||||
attachment_names: ['a.pdf']
|
||||
},
|
||||
pendingInterruptionText: '查询状态?'
|
||||
pendingInterruptionText: '查询状态?',
|
||||
applicationCandidates: []
|
||||
}
|
||||
)
|
||||
|
||||
@@ -241,7 +328,7 @@ test('guided flow state is serializable and restored through session state', ()
|
||||
assert.match(sessionStateScript, /guidedFlowState,\s*\n\s*insightPanelCollapsed/)
|
||||
assert.match(sessionStateScript, /function refreshWelcomeQuickActions/)
|
||||
assert.match(sessionStateScript, /buildWelcomeQuickActions\(/)
|
||||
assert.match(sessionStateScript, /ASSISTANT_SESSION_TYPES\.reduce/)
|
||||
assert.match(sessionStateScript, /resolveAccessibleSessionTypes\(\)\.reduce/)
|
||||
assert.match(sessionStateScript, /props\.entrySource === 'application' \? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE/)
|
||||
assert.match(sessionStateScript, /const canRestorePersistedInitialState =[\s\S]*shouldPersistLocalSnapshot/)
|
||||
})
|
||||
@@ -250,6 +337,10 @@ test('guided flow is local until final confirmation or collected query handoff',
|
||||
assert.doesNotMatch(guidedFlowScript, /runOrchestrator/)
|
||||
assert.doesNotMatch(guidedFlowScript, /startExpenseClaimDraftFlowStep/)
|
||||
assert.doesNotMatch(guidedFlowScript, /review_action:\s*['"]save_draft['"]/)
|
||||
assert.match(guidedFlowScript, /fetchExpenseClaims/)
|
||||
assert.match(guidedFlowScript, /GUIDED_ACTION_SELECT_REQUIRED_APPLICATION/)
|
||||
assert.match(guidedFlowScript, /handleSceneSelectionApplicationGate/)
|
||||
assert.match(createViewScript, /handleSceneSelectionApplicationGate/)
|
||||
assert.match(createViewScript, /if \(await handleGuidedComposerSubmit\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*return submitComposerInternal\(options\)/)
|
||||
assert.match(createViewScript, /ASSISTANT_SCOPE_ACTION_SWITCH/)
|
||||
assert.match(createViewScript, /actionPayload\.carry_text/)
|
||||
|
||||
@@ -15,6 +15,10 @@ const detailStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/travel-request-detail-view.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const approvalDialog = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/travel/TravelRequestApprovalDialog.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const reimbursementService = readFileSync(
|
||||
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -43,34 +47,57 @@ function extractFunction(source, name) {
|
||||
assert.fail(`${name} body should be closed`)
|
||||
}
|
||||
|
||||
test('approval-mode detail collects leader opinion and confirms approval before API call', () => {
|
||||
test('approval-mode detail collects leader opinion inside confirm dialog before API call', () => {
|
||||
assert.match(detailScript, /approvalMode:/)
|
||||
assert.match(detailScript, /const leaderOpinion = ref\(''\)/)
|
||||
assert.match(detailScript, /const approveConfirmDialogOpen = ref\(false\)/)
|
||||
assert.match(detailScript, /const canApproveRequest = computed/)
|
||||
assert.match(detailScript, /canApproveLeaderExpenseClaims/)
|
||||
assert.match(detailScript, /isCurrentDirectManagerForRequest/)
|
||||
assert.match(detailScript, /isCurrentRequestApplicant/)
|
||||
assert.match(detailScript, /isFinanceApprovalStage/)
|
||||
assert.match(detailScript, /const isCurrentApplicant = computed/)
|
||||
assert.match(detailScript, /const isCurrentDirectManagerApprover = computed/)
|
||||
assert.match(detailScript, /const canProcessFinanceApprovalStage = computed/)
|
||||
assert.match(detailScript, /approvalOpinionTitle/)
|
||||
assert.match(detailScript, /approvalConfirmDescription/)
|
||||
assert.match(detailScript, /approvalNextStage/)
|
||||
assert.match(detailScript, /showApplicationLeaderOpinionInput/)
|
||||
assert.doesNotMatch(detailScript, /showApplicationLeaderOpinionInput/)
|
||||
assert.doesNotMatch(detailScript, /showLeaderApprovalPanel/)
|
||||
assert.match(detailScript, /const requiresApprovalOpinion = computed\(\(\) => isDirectManagerApprovalStage\.value\)/)
|
||||
assert.match(detailScript, /buildLeaderApprovalEvents/)
|
||||
assert.match(detailScript, /buildLeaderApprovalInfo/)
|
||||
assert.match(detailScript, /const leaderApprovalEvents = computed/)
|
||||
assert.match(detailScript, /const hasLeaderApprovalEvents = computed/)
|
||||
assert.match(
|
||||
detailScript,
|
||||
/const showApplicationLeaderOpinion = computed\(\(\) => \(\s*isApplicationDocument\.value\s*&& hasLeaderApprovalEvents\.value\s*\)\)/
|
||||
)
|
||||
assert.match(detailScript, /isDirectManagerApprovalStage\.value\)[\s\S]*return isCurrentDirectManagerApprover\.value/)
|
||||
assert.match(detailScript, /isDirectManagerApprovalStage\.value[\s\S]*&& isCurrentDirectManagerApprover\.value/)
|
||||
assert.match(detailScript, /canProcessFinanceApprovalStage\.value/)
|
||||
assert.doesNotMatch(detailScript, /leaderApprovalReadonlyText/)
|
||||
assert.match(detailScript, /resolveGeneratedDraftClaimNo/)
|
||||
assert.match(detailScript, /approveActionLabel/)
|
||||
assert.match(detailScript, /requiresApprovalOpinion\.value && !leaderOpinion\.value\.trim\(\)/)
|
||||
assert.match(detailScript, /请先填写领导意见,填写后才能确认审核。/)
|
||||
assert.match(detailScript, /approveExpenseClaim\(request\.value\.claimId, \{[\s\S]*opinion: leaderOpinion\.value\.trim\(\)/)
|
||||
assert.match(detailScript, /报销草稿 \$\{generatedDraftClaimNo\} 已生成/)
|
||||
|
||||
assert.match(detailTemplate, /v-if="showLeaderApprovalPanel"/)
|
||||
assert.doesNotMatch(detailTemplate, /v-if="showLeaderApprovalPanel"/)
|
||||
assert.doesNotMatch(detailTemplate, /showApplicationLeaderOpinionInput/)
|
||||
assert.doesNotMatch(detailTemplate, /class="leader-approval-card/)
|
||||
assert.doesNotMatch(detailTemplate, /class="inline-leader-opinion/)
|
||||
assert.match(detailTemplate, /v-if="showApplicationLeaderOpinion"/)
|
||||
assert.match(detailTemplate, /class="application-leader-opinion"/)
|
||||
assert.match(detailTemplate, /v-if="hasLeaderApprovalEvents"/)
|
||||
assert.match(detailTemplate, /class="application-leader-opinion-timeline"/)
|
||||
assert.match(detailTemplate, /v-for="event in leaderApprovalEvents"/)
|
||||
assert.match(detailTemplate, /class="application-leader-opinion-event"/)
|
||||
assert.match(detailTemplate, /event\.type === 'returned'/)
|
||||
assert.doesNotMatch(detailTemplate, /leaderApprovalReadonlyText/)
|
||||
assert.doesNotMatch(detailTemplate, /\u5f85\u76f4\u5c5e\u9886\u5bfc\u586b\u5199\u5ba1\u6279\u610f\u89c1/)
|
||||
assert.match(detailTemplate, /领导意见/)
|
||||
assert.match(detailTemplate, /\{\{ approvalOpinionTitle \}\}/)
|
||||
assert.match(detailTemplate, /v-model="leaderOpinion"/)
|
||||
assert.match(detailTemplate, /maxlength="500"\s+:required="requiresApprovalOpinion"/)
|
||||
assert.match(detailTemplate, /:placeholder="approvalOpinionPlaceholder"/)
|
||||
assert.match(approvalDialog, /\{\{ opinionTitle \}\}/)
|
||||
assert.doesNotMatch(detailTemplate, /v-model="leaderOpinion"/)
|
||||
assert.match(detailTemplate, /@click="handleApproveRequest"/)
|
||||
assert.match(detailTemplate, /\{\{ approveBusy \? approveBusyLabel : approveActionLabel \}\}/)
|
||||
assert.match(detailTemplate, /:open="approveConfirmDialogOpen"/)
|
||||
@@ -78,18 +105,39 @@ test('approval-mode detail collects leader opinion and confirms approval before
|
||||
assert.match(detailTemplate, /:description="approvalConfirmDescription"/)
|
||||
assert.match(detailTemplate, /:confirm-text="approveConfirmText"/)
|
||||
assert.match(detailTemplate, /:busy-text="approveBusyText"/)
|
||||
assert.match(detailTemplate, /\{\{ approvalNextStage \}\}/)
|
||||
assert.match(detailTemplate, /:next-stage="approvalNextStage"/)
|
||||
assert.match(detailTemplate, /v-model:opinion="leaderOpinion"/)
|
||||
assert.match(detailTemplate, /:opinion-placeholder="approvalOpinionPlaceholder"/)
|
||||
assert.match(detailTemplate, /:opinion-hint="approvalOpinionHint"/)
|
||||
assert.match(detailTemplate, /:opinion-required="requiresApprovalOpinion"/)
|
||||
assert.match(detailTemplate, /@confirm="confirmApproveRequest"/)
|
||||
assert.match(detailTemplate, /:description="returnDialogDescription"/)
|
||||
assert.match(detailTemplate, /:application="isApplicationDocument"/)
|
||||
|
||||
const handleApproveRequest = extractFunction(detailScript, 'handleApproveRequest')
|
||||
const confirmApproveRequest = extractFunction(detailScript, 'confirmApproveRequest')
|
||||
assert.doesNotMatch(handleApproveRequest, /approveExpenseClaim/)
|
||||
assert.doesNotMatch(handleApproveRequest, /leaderOpinion\.value\.trim/)
|
||||
assert.match(confirmApproveRequest, /approveExpenseClaim/)
|
||||
assert.match(confirmApproveRequest, /requiresApprovalOpinion\.value && !leaderOpinion\.value\.trim\(\)/)
|
||||
assert.match(confirmApproveRequest, /请先填写领导意见,填写后才能确认审核。/)
|
||||
|
||||
assert.match(approvalDialog, /<textarea/)
|
||||
assert.match(approvalDialog, /update:opinion/)
|
||||
assert.match(approvalDialog, /opinionPlaceholder/)
|
||||
assert.match(approvalDialog, /opinionHint/)
|
||||
assert.match(approvalDialog, /opinionRequired/)
|
||||
assert.match(approvalDialog, /\{\{ currentOpinion\.length \}\}\/500/)
|
||||
|
||||
assert.match(detailStyles, /\.detail-card-title-with-icon \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
||||
assert.match(detailStyles, /\.detail-card-title-with-icon i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/)
|
||||
assert.match(detailStyles, /\.application-leader-opinion-head span \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
||||
assert.doesNotMatch(detailStyles, /\.leader-approval-card/)
|
||||
assert.doesNotMatch(detailStyles, /\.inline-leader-opinion/)
|
||||
assert.match(detailStyles, /\.application-leader-opinion-timeline \{/)
|
||||
assert.match(detailStyles, /\.application-leader-opinion-event \{/)
|
||||
assert.match(detailStyles, /\.application-leader-opinion-event\.danger::before \{/)
|
||||
assert.match(detailStyles, /\.application-leader-opinion-event\.success::before \{/)
|
||||
|
||||
assert.match(reimbursementService, /export function approveExpenseClaim\(claimId, payload = \{\}\)/)
|
||||
assert.match(reimbursementService, /\/approve/)
|
||||
|
||||
@@ -678,10 +678,21 @@ test('transport ticket items no longer generate business location completion adv
|
||||
test('return reason dialog is wired into approval and detail return actions', () => {
|
||||
assert.match(returnReasonDialog, /missing_attachment/)
|
||||
assert.match(returnReasonDialog, /invoice_mismatch/)
|
||||
assert.match(returnReasonDialog, /APPLICATION_RETURN_REASON_OPTIONS/)
|
||||
assert.match(returnReasonDialog, /application_info_incomplete/)
|
||||
assert.match(returnReasonDialog, /application_business_need_unclear/)
|
||||
assert.match(returnReasonDialog, /application_budget_basis_missing/)
|
||||
assert.match(returnReasonDialog, /application_policy_mismatch/)
|
||||
assert.match(returnReasonDialog, /application_attachment_needed/)
|
||||
assert.match(returnReasonDialog, /退单选项/)
|
||||
assert.match(returnReasonDialog, /selectionError/)
|
||||
assert.match(returnReasonDialog, /selectedCodes\.value\.length === 0/)
|
||||
assert.match(returnReasonDialog, /lastAutoReason/)
|
||||
assert.match(returnReasonDialog, /reason_codes/)
|
||||
assert.match(approvalCenterTemplate, /<TravelRequestDetailView/)
|
||||
assert.doesNotMatch(approvalCenterTemplate, /<ReturnReasonDialog/)
|
||||
assert.match(detailViewTemplate, /<ReturnReasonDialog/)
|
||||
assert.match(detailViewTemplate, /<TravelRequestReturnDialog/)
|
||||
assert.match(detailViewTemplate, /:application="isApplicationDocument"/)
|
||||
assert.doesNotMatch(approvalCenterScript, /returnExpenseClaim/)
|
||||
assert.match(detailViewScript, /returnExpenseClaim\(request\.value\.claimId, payload\)/)
|
||||
assert.doesNotMatch(approvalCenterScript, /审批中心退回/)
|
||||
|
||||
Reference in New Issue
Block a user